Files
cinny/src/app/hooks/useBookmarks.ts
T
jared 4fc3f7a35f DP13: extract shared account-data list-store engine
useBookmarks / useReminders / useUserNotes were near-verbatim copies of a
concurrency-critical engine (module-scoped singleton, per-client
subscribe/teardown, a serialized write-queue that prevents lost-update
clobbering, a listener Set, and the account-data subscription).

Extract it into createAccountDataListStore<T, C>({ eventType, read, write }) in
src/app/hooks/createAccountDataListStore.ts. The write-serialization semantics
are preserved identically (still the lost-update fix). The differing payload
shapes are parameterized via read/write: bookmarks/reminders wrap a list
({bookmarks}/{reminders}); notes is a flat Record passed through unchanged.

The three hooks become thin wrappers with their exact public APIs unchanged
(same exported names, signatures, return shapes, and mutators), so no call site
changes. The DP16 setAccountData helper is used inside the queue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:24:08 -04:00

65 lines
1.9 KiB
TypeScript

import { useCallback } from 'react';
import { useMatrixClient } from './useMatrixClient';
import { createAccountDataListStore } from './createAccountDataListStore';
export type Bookmark = {
roomId: string;
eventId: string;
savedAt: number;
previewText: string;
roomName: string;
};
const BOOKMARKS_KEY = 'io.lotus.bookmarks';
const MAX_BOOKMARKS = 500;
type BookmarksContent = {
bookmarks: Bookmark[];
};
// Shared, concurrency-safe store. See createAccountDataListStore for why the
// snapshot + write queue must be module-scoped (writes are serialized to avoid
// lost updates, since setAccountData replaces the whole content with no merge).
const bookmarksStore = createAccountDataListStore<Bookmark[], BookmarksContent>({
eventType: BOOKMARKS_KEY,
read: (content) => content?.bookmarks ?? [],
write: (bookmarks) => ({ bookmarks }),
});
export function useBookmarks(): {
bookmarks: Bookmark[];
addBookmark: (b: Bookmark) => Promise<void>;
removeBookmark: (eventId: string) => Promise<void>;
isBookmarked: (eventId: string) => boolean;
} {
const mx = useMatrixClient();
const bookmarks = bookmarksStore.useValue(mx);
const addBookmark = useCallback(
(b: Bookmark) =>
bookmarksStore.enqueueWrite(mx, (current) => {
// Avoid duplicates
const filtered = current.filter((bk) => bk.eventId !== b.eventId);
let next = [b, ...filtered];
if (next.length > MAX_BOOKMARKS) {
next = next.slice(0, MAX_BOOKMARKS);
}
return next;
}),
[mx],
);
const removeBookmark = useCallback(
(eventId: string) =>
bookmarksStore.enqueueWrite(mx, (current) => current.filter((bk) => bk.eventId !== eventId)),
[mx],
);
const isBookmarked = useCallback(
(eventId: string) => bookmarks.some((bk) => bk.eventId === eventId),
[bookmarks],
);
return { bookmarks, addBookmark, removeBookmark, isBookmarked };
}