fix(offline): address PR #120 review (comment 2513)
CHANGELOG: stop presenting the service-worker API cache as an active offline store (/api is NetworkOnly) — describe it as a defensive purge of the legacy api-get-cache from older clients; add an explicit upgrade note that the new CORS allowlist rejects previously-allowed cross-domain REST clients until their origin is added to CORS_ALLOWED_ORIGINS. test(offline): cover make-offline ancestor-walk + dedup — a real-ancestor case exercising the ancestorId===pageId guard (page warmed once), the dedup of repeated tree failures into a single "tree" label, and the "breadcrumbs" label when the breadcrumbs lookup rejects. test(auth): cover clearOfflineCache in handleLogout — purged exactly once before window.location.replace, and a thrown purge error does not block the redirect. conventions: use pageKeys.detail() instead of raw ["pages", …] literals in title-editor and use-page-collab-providers. cleanup: remove the dead emit() in title-editor (the gateway ignores it; the cross-user tree refresh is server-side via the Yjs title fragment); drop the trivial Array.isArray(tiptapExtensions) test (schema is exercised transitively). refactor: extract the shared page.<id> Yjs doc-name convention into pageYdocName()/PAGE_YDOC_NAME_PREFIX so the editor providers, offline warm, and offline purge can no longer drift apart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ import { useParams } from "react-router-dom";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { FIVE_MINUTES } from "@/lib/constants.ts";
|
||||
import { collabTokenNeedsRefresh } from "@/features/editor/hooks/collab-token";
|
||||
import { pageYdocName } from "@/features/editor/page-ydoc-name";
|
||||
import { pageKeys } from "@/features/page/queries/page-query";
|
||||
|
||||
export interface PageCollabProviders {
|
||||
ydoc: Y.Doc | null;
|
||||
@@ -72,7 +74,7 @@ export function usePageCollabProviders(pageId: string): PageCollabProviders {
|
||||
|
||||
useEffect(() => {
|
||||
if (!providersRef.current) {
|
||||
const documentName = `page.${pageId}`;
|
||||
const documentName = pageYdocName(pageId);
|
||||
const ydoc = new Y.Doc();
|
||||
const local = new IndexeddbPersistence(documentName, ydoc);
|
||||
const socket = new HocuspocusProviderWebsocket({
|
||||
@@ -91,9 +93,11 @@ export function usePageCollabProviders(pageId: string): PageCollabProviders {
|
||||
try {
|
||||
const message = JSON.parse(payload);
|
||||
if (message?.type !== "page.updated" || !message.updatedAt) return;
|
||||
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
|
||||
const pageData = queryClient.getQueryData<IPage>(
|
||||
pageKeys.detail(slugId),
|
||||
);
|
||||
if (pageData) {
|
||||
queryClient.setQueryData(["pages", slugId], {
|
||||
queryClient.setQueryData(pageKeys.detail(slugId), {
|
||||
...pageData,
|
||||
updatedAt: message.updatedAt,
|
||||
...(message.lastUpdatedBy && {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Single source of truth for the IndexedDB / Hocuspocus document name of a
|
||||
* page's collaborative Yjs doc.
|
||||
*
|
||||
* The `page.<id>` convention is shared knowledge across three call sites: the
|
||||
* live editor providers (`use-page-collab-providers`), the offline warm path
|
||||
* (`make-offline`), and the offline purge (`clear-offline-cache`, which matches
|
||||
* the databases to delete by this prefix). Centralizing it here stops those
|
||||
* sites from silently drifting apart.
|
||||
*/
|
||||
export const PAGE_YDOC_NAME_PREFIX = "page.";
|
||||
|
||||
export const pageYdocName = (pageId: string): string =>
|
||||
`${PAGE_YDOC_NAME_PREFIX}${pageId}`;
|
||||
@@ -11,10 +11,9 @@ import {
|
||||
pageEditorAtom,
|
||||
titleEditorAtom,
|
||||
} from "@/features/editor/atoms/editor-atoms";
|
||||
import { updatePageData } from "@/features/page/queries/page-query";
|
||||
import { pageKeys, updatePageData } from "@/features/page/queries/page-query";
|
||||
import { useDebouncedCallback, getHotkeyHandler } from "@mantine/hooks";
|
||||
import { useAtom } from "jotai";
|
||||
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
||||
import { Collaboration } from "@tiptap/extension-collaboration";
|
||||
import { shouldPropagateTitleChange } from "@/features/editor/title-collab";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
@@ -48,7 +47,6 @@ export function TitleEditor({
|
||||
const { t } = useTranslation();
|
||||
const pageEditor = useAtomValue(pageEditorAtom);
|
||||
const [, setTitleEditor] = useAtom(titleEditorAtom);
|
||||
const emit = useQueryEmit();
|
||||
const navigate = useNavigate();
|
||||
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
||||
|
||||
@@ -145,8 +143,8 @@ export function TitleEditor({
|
||||
});
|
||||
|
||||
const page =
|
||||
queryClient.getQueryData<IPage>(["pages", slugId]) ??
|
||||
queryClient.getQueryData<IPage>(["pages", pageId]);
|
||||
queryClient.getQueryData<IPage>(pageKeys.detail(slugId)) ??
|
||||
queryClient.getQueryData<IPage>(pageKeys.detail(pageId));
|
||||
if (!page) return;
|
||||
|
||||
const updatedPage: IPage = { ...page, title: titleText };
|
||||
@@ -165,8 +163,11 @@ export function TitleEditor({
|
||||
};
|
||||
|
||||
updatePageData(updatedPage);
|
||||
// Drive the local (same-tab) tree/breadcrumb update. The cross-user tree
|
||||
// refresh is handled server-side: the collab process extracts the renamed
|
||||
// 'title' Yjs fragment and broadcasts a treeUpdate. The previous socket
|
||||
// `emit(event)` here was a no-op (the gateway ignores it) and was removed.
|
||||
localEmitter.emit("message", event);
|
||||
emit(event);
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user