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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ useResolvedCommunityAddress({communityAddress: string, cache: boolean}): {resolv

Pass `{ publicKey, name }` when you have both so `pkc-js` can fetch through the public key and resolve the name in the background. `communityAddress`, `communityAddresses`, and `communityRefs` are no longer accepted by these hooks.

`useCommunity` only exposes community error events that are not superseded by a later `update` event. Transient fetch errors are delayed briefly before surfacing through `error` or `errors`.

#### Authors Hooks

```
Expand Down
8 changes: 7 additions & 1 deletion llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ useResolvedCommunityAddress({communityAddress: string, cache: boolean}): {resolv

Pass `{ publicKey, name }` when you have both so `pkc-js` can fetch through the public key and resolve the name in the background. `communityAddress`, `communityAddresses`, and `communityRefs` are no longer accepted by these hooks.

`useCommunity` only exposes community error events that are not superseded by a later `update` event. Transient fetch errors are delayed briefly before surfacing through `error` or `errors`.

#### Authors Hooks

```
Expand Down Expand Up @@ -3122,12 +3124,16 @@ Avoid GitHub MCP and browser MCP servers for this project because they add signi
Source: https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/CHANGELOG.md

```markdown
## [0.1.11](https://github.com/bitsocialnet/bitsocial-react-hooks/compare/v0.1.10...v0.1.11) (2026-05-20)



## [0.1.10](https://github.com/bitsocialnet/bitsocial-react-hooks/compare/v0.1.9...v0.1.10) (2026-05-15)


### Bug Fixes

* **deps:** upgrade pkc-js to 0.0.33 ([3438c0d](https://github.com/bitsocialnet/bitsocial-react-hooks/commit/3438c0d8ade34180a66b8ddbdc92ba8c970b4e5c))
* **deps:** upgrade pkc-js to 0.0.30 ([9942a82](https://github.com/bitsocialnet/bitsocial-react-hooks/commit/9942a82def8330e49a8252653a7bc16abd6819a4))



Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ This file is generated by `scripts/generate-llms-files.mjs`. Do not hand-edit it

## Optional

- [Changelog](https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/CHANGELOG.md): * **deps:** upgrade pkc-js to 0.0.33 ([3438c0d](https://github.com/bitsocialnet/bitsocial-react-hooks/commit/3438c0d8ade34180a66b8ddbdc92ba8c970b4e5c))
- [Changelog](https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/CHANGELOG.md): * **deps:** upgrade pkc-js to 0.0.30 ([9942a82](https://github.com/bitsocialnet/bitsocial-react-hooks/commit/9942a82def8330e49a8252653a7bc16abd6819a4))
- [TODO](https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/docs/TODO.md): - e2e test to publish to an electron sub - async useAuthorAddress hook (because resolving ETH address synchronously is too slow) - implement sort by active - implement showing your own pending replies in a comment (wh...
58 changes: 30 additions & 28 deletions src/hooks/communities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,41 +322,43 @@ describe("communities", () => {
}
});

test("has error events", async () => {
test("has unsuperseded error events", async () => {
// mock update to save community instance
const communityUpdate = Community.prototype.update;
const updatingCommunities: any = [];
Community.prototype.update = function () {
updatingCommunities.push(this);
return communityUpdate.bind(this)();
return Promise.resolve();
};

const rendered = renderHook<any, any>((communityAddress) =>
useCommunity({ community: toCommunity(communityAddress) }),
);
const waitFor = testUtils.createWaitFor(rendered);
rendered.rerender("community address");

// emit error event
await waitFor(() => updatingCommunities.length > 0);
updatingCommunities[0].emit("error", Error("error 1"));

// first error
await waitFor(() => rendered.result.current.error.message === "error 1");
expect(rendered.result.current.error.message).toBe("error 1");
expect(rendered.result.current.errors[0].message).toBe("error 1");
expect(rendered.result.current.errors.length).toBe(1);

// second error
updatingCommunities[0].emit("error", Error("error 2"));
await waitFor(() => rendered.result.current.error.message === "error 2");
expect(rendered.result.current.error.message).toBe("error 2");
expect(rendered.result.current.errors[0].message).toBe("error 1");
expect(rendered.result.current.errors[1].message).toBe("error 2");
expect(rendered.result.current.errors.length).toBe(2);

// restore mock
Community.prototype.update = communityUpdate;
try {
const rendered = renderHook<any, any>((communityAddress) =>
useCommunity({ community: toCommunity(communityAddress) }),
);
const waitFor = testUtils.createWaitFor(rendered, { timeout: 3000 });
rendered.rerender("community address");

// emit error event
await waitFor(() => updatingCommunities.length > 0);
updatingCommunities[0].emit("error", Error("error 1"));

// first error
await waitFor(() => rendered.result.current.error.message === "error 1");
expect(rendered.result.current.error.message).toBe("error 1");
expect(rendered.result.current.errors[0].message).toBe("error 1");
expect(rendered.result.current.errors.length).toBe(1);

// second error
updatingCommunities[0].emit("error", Error("error 2"));
await waitFor(() => rendered.result.current.error.message === "error 2");
expect(rendered.result.current.error.message).toBe("error 2");
expect(rendered.result.current.errors[0].message).toBe("error 1");
expect(rendered.result.current.errors[1].message).toBe("error 2");
expect(rendered.result.current.errors.length).toBe(2);
} finally {
// restore mock
Community.prototype.update = communityUpdate;
}
});

test("pkc.createCommunity throws adds useCommunity().error", async () => {
Expand Down
82 changes: 82 additions & 0 deletions src/stores/communities/communities-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,88 @@ describe("communities store", () => {
mockAccount.pkc.createCommunity = createOrig;
});

test("community error event waits before reaching store errors", async () => {
const address = "delayed-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 = new Error("fetch failed");
act(() => {
community.emit("error", error);
});

expect(communitiesStore.getState().errors[address]).toBeUndefined();

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

await act(async () => {
await vi.advanceTimersByTimeAsync(1);
});
expect(communitiesStore.getState().errors[address]).toEqual([error]);
} 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();
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 pendingError = new Error("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();

const storedError = new Error("stored fetch failed");
act(() => {
community.emit("error", storedError);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});
expect(communitiesStore.getState().errors[address]).toEqual([storedError]);

act(() => {
community.emit("update", community);
});
expect(communitiesStore.getState().errors[address]).toBeUndefined();
} finally {
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
55 changes: 50 additions & 5 deletions src/stores/communities/communities-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import {

let pkcGetCommunityPending: { [key: string]: boolean } = {};

const COMMUNITY_ERROR_UPDATE_GRACE_MS = 1000;
const pendingCommunityErrorTimers: {
[communityKey: string]: ReturnType<typeof setTimeout>[];
} = {};

const createCommunityWithLookupFallback = async (
pkc: any,
communityLookupOptions: { address?: string; name?: string; publicKey?: string },
Expand All @@ -44,6 +49,43 @@ const createCommunityWithLookupFallback = async (
// reset all event listeners in between tests
const listeners: any = [];

const clearPendingCommunityErrors = (communityKey: string) => {
pendingCommunityErrorTimers[communityKey]?.forEach((timeout) => clearTimeout(timeout));
delete pendingCommunityErrorTimers[communityKey];
};

const clearStoredCommunityErrors = (state: CommunitiesState, communityKey: string) => {
if (!state.errors[communityKey]) {
return state.errors;
}
const nextErrors = { ...state.errors };
delete nextErrors[communityKey];
return nextErrors;
};

const scheduleCommunityError = (setState: Function, communityKey: string, error: Error) => {
const timeout = setTimeout(() => {
pendingCommunityErrorTimers[communityKey] = (
pendingCommunityErrorTimers[communityKey] || []
).filter((pendingTimeout) => pendingTimeout !== timeout);
if ((pendingCommunityErrorTimers[communityKey] || []).length === 0) {
delete pendingCommunityErrorTimers[communityKey];
}
setState((state: CommunitiesState) => {
const communityErrors = state.errors[communityKey] || [];
return {
...state,
errors: { ...state.errors, [communityKey]: [...communityErrors, error] },
};
});
}, COMMUNITY_ERROR_UPDATE_GRACE_MS);

pendingCommunityErrorTimers[communityKey] = [
...(pendingCommunityErrorTimers[communityKey] || []),
timeout,
];
};

export type CommunitiesState = {
communities: Communities;
errors: { [communityAddress: string]: Error[] };
Expand Down Expand Up @@ -170,6 +212,11 @@ const communitiesStore = createStore<CommunitiesState>(

// the community has published new posts
community.on("update", async (updatedCommunity: Community) => {
clearPendingCommunityErrors(communityKey);
setState((state: CommunitiesState) => ({
...state,
errors: clearStoredCommunityErrors(state, communityKey),
}));
updatedCommunity = utils.clone(updatedCommunity);

// add fetchedAt to be able to expire the cache
Expand All @@ -184,6 +231,7 @@ const communitiesStore = createStore<CommunitiesState>(
account,
});
setState((state: any) => ({
...state,
communities: { ...state.communities, [communityKey]: updatedCommunity },
}));

Expand All @@ -206,11 +254,7 @@ const communitiesStore = createStore<CommunitiesState>(
});

community.on("error", (error: Error) => {
setState((state: CommunitiesState) => {
let communityErrors = state.errors[communityKey] || [];
communityErrors = [...communityErrors, error];
return { ...state, errors: { ...state.errors, [communityKey]: communityErrors } };
});
scheduleCommunityError(setState, communityKey, error);
});

// set clients on community so the frontend can display it, dont persist in db because a reload cancels updating
Expand Down Expand Up @@ -399,6 +443,7 @@ const originalState = communitiesStore.getState();
// async function because some stores have async init
export const resetCommunitiesStore = async () => {
pkcGetCommunityPending = {};
Object.keys(pendingCommunityErrorTimers).forEach(clearPendingCommunityErrors);
// remove all event listeners
listeners.forEach((listener: any) => listener.removeAllListeners());
// destroy all component subscriptions to the store
Expand Down
Loading