Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/stores/communities/communities-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,87 @@ describe("communities store", () => {
}
});

test("community retriable error event stays out of store errors", async () => {
const address = "retriable-error-address";
const pkc = await PkcJsMock();
const community = await pkc.createCommunity({ address });
const updateSpy = vi.spyOn(community, "update").mockResolvedValue(undefined);
const createOrig = mockAccount.pkc.createCommunity;
mockAccount.pkc.createCommunity = vi.fn().mockResolvedValue(community);

try {
await act(async () => {
await communitiesStore.getState().addCommunityToStore(address, mockAccount);
});

vi.useFakeTimers();
const error = Object.assign(new Error("transient fetch failed"), {
details: { retriableError: true },
});
act(() => {
community.emit("error", error);
});

await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});
expect(communitiesStore.getState().errors[address]).toBeUndefined();
} finally {
vi.useRealTimers();
mockAccount.pkc.createCommunity = createOrig;
updateSpy.mockRestore();
}
});

test("community non-retriable error event still reaches store errors", async () => {
const address = "non-retriable-error-address";
const pkc = await PkcJsMock();
const community = await pkc.createCommunity({ address });
const updateSpy = vi.spyOn(community, "update").mockResolvedValue(undefined);
const createOrig = mockAccount.pkc.createCommunity;
mockAccount.pkc.createCommunity = vi.fn().mockResolvedValue(community);

try {
await act(async () => {
await communitiesStore.getState().addCommunityToStore(address, mockAccount);
});

vi.useFakeTimers();
const error = Object.assign(new Error("final fetch failed"), {
details: { retriableError: false },
});
const laterError = Object.assign(new Error("later final fetch failed"), {
details: { retriableError: false },
});
act(() => {
community.emit("error", error);
});

await act(async () => {
await vi.advanceTimersByTimeAsync(500);
});
expect(communitiesStore.getState().errors[address]).toBeUndefined();

act(() => {
community.emit("error", laterError);
});

await act(async () => {
await vi.advanceTimersByTimeAsync(500);
});
expect(communitiesStore.getState().errors[address]).toEqual([error]);

await act(async () => {
await vi.advanceTimersByTimeAsync(500);
});
expect(communitiesStore.getState().errors[address]).toEqual([error, laterError]);
} finally {
vi.useRealTimers();
mockAccount.pkc.createCommunity = createOrig;
updateSpy.mockRestore();
}
});

test("community update event discards earlier pending and stored errors", async () => {
const address = "stale-error-address";
const pkc = await PkcJsMock();
Expand Down Expand Up @@ -496,6 +577,41 @@ describe("communities store", () => {
}
});

test("community error timeout is ignored when pending entry was already cleared", async () => {
const address = "cleared-pending-error-address";
const pkc = await PkcJsMock();
const community = await pkc.createCommunity({ address });
const updateSpy = vi.spyOn(community, "update").mockResolvedValue(undefined);
const createOrig = mockAccount.pkc.createCommunity;
mockAccount.pkc.createCommunity = vi.fn().mockResolvedValue(community);
let clearTimeoutSpy: { mockRestore: () => void } | undefined;

try {
await act(async () => {
await communitiesStore.getState().addCommunityToStore(address, mockAccount);
});

vi.useFakeTimers();
clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined);
const pendingError = new Error("cleared pending fetch failed");
act(() => {
community.emit("error", pendingError);
community.emit("update", community);
});

await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});

expect(communitiesStore.getState().errors[address]).toBeUndefined();
} finally {
clearTimeoutSpy?.mockRestore();
vi.useRealTimers();
mockAccount.pkc.createCommunity = createOrig;
updateSpy.mockRestore();
}
});

test("createCommunity with no signer asserts address must be undefined", async () => {
const pkc = await PkcJsMock();
const community = await pkc.createCommunity({ address: "new-sub-address" });
Expand Down
29 changes: 25 additions & 4 deletions src/stores/communities/communities-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,34 @@ const clearStoredCommunityErrors = (state: CommunitiesState, communityKey: strin
return nextErrors;
};

const isRetriableCommunityLoadError = (error: Error) => {
const details = (error as Error & { details?: unknown }).details;
return Boolean(
details &&
typeof details === "object" &&
"retriableError" in details &&
(details as { retriableError?: unknown }).retriableError === true,
);
};

const scheduleCommunityError = (setState: Function, communityKey: string, error: Error) => {
if (isRetriableCommunityLoadError(error)) {
return;
}

const timeout = setTimeout(() => {
pendingCommunityErrorTimers[communityKey] = (
pendingCommunityErrorTimers[communityKey] || []
).filter((pendingTimeout) => pendingTimeout !== timeout);
if ((pendingCommunityErrorTimers[communityKey] || []).length === 0) {
const pendingTimeouts = pendingCommunityErrorTimers[communityKey];
if (!pendingTimeouts?.includes(timeout)) {
return;
}

const remainingTimeouts = pendingTimeouts.filter(
(pendingTimeout) => pendingTimeout !== timeout,
);
if (remainingTimeouts.length === 0) {
delete pendingCommunityErrorTimers[communityKey];
} else {
pendingCommunityErrorTimers[communityKey] = remainingTimeouts;
}
setState((state: CommunitiesState) => {
const communityErrors = state.errors[communityKey] || [];
Expand Down
Loading