Fixes the offline-sync defects QA found on PR #120 (#237/#238/#220). Blank-shell / white-screen on offline reload (HIGH): - auth-query.tsx: the useCollabToken retry predicate read `error.response.status` unguarded. Offline the collab-token POST rejects as an axios NETWORK error (isAxiosError true, response undefined), so `.status` threw an uncaught TypeError in the React Query retryer BEFORE React mounted, white-screening every route. Extracted the predicate as `collabTokenRetry` and guarded it with optional chaining (`error.response?.status === 404`). - user-provider.tsx: gated the whole <Layout> on useCurrentUser() and returned a bare `<></>` on any error, blanking every authenticated route offline even when cached data existed. Now renders the cached app when a (stale) user is present and an explicit OfflineFallback when there is no user to fall back on. - query-persister.ts / make-offline.ts: persist and warm the ['currentUser'] query so the auth gate can hydrate offline (pinned pages now survive relaunch). Offline structural create/move/comment silently lost on reload (HIGH): - offline-mutations.ts: register setMutationDefaults (default mutationFns) for stable mutation keys and tag useCreatePageMutation / useMovePageMutation / useCreateCommentMutation with those keys. A paused mutation dehydrated to IndexedDB while offline now has a mutationFn after reload, so resumePausedMutations() replays it on reconnect instead of no-op'ing. Tests (client vitest): collabTokenRetry no longer throws on a no-response network error; UserProvider renders cached children / the offline fallback (not a blank fragment) on a network error; a rehydrated paused create/move is replayable via resumePausedMutations; currentUser persist-root coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
3.0 KiB
TypeScript
65 lines
3.0 KiB
TypeScript
import type { QueryClient } from "@tanstack/react-query";
|
|
import { createPage, movePage } from "@/features/page/services/page-service";
|
|
import { createComment } from "@/features/comment/services/comment-service";
|
|
import { invalidateOnCreatePage } from "@/features/page/queries/page-query";
|
|
import type {
|
|
IMovePage,
|
|
IPage,
|
|
IPageInput,
|
|
} from "@/features/page/types/page.types";
|
|
import type { IComment } from "@/features/comment/types/comment.types";
|
|
|
|
/**
|
|
* Stable mutation keys for the offline-relevant structural mutations.
|
|
*
|
|
* When the browser goes offline, React Query PAUSES these mutations and the
|
|
* PersistQueryClientProvider dehydrates the paused mutation into IndexedDB. On a
|
|
* reload-while-offline the mutation is restored, but a restored mutation has NO
|
|
* observer (no component is mounted) — so its replay relies entirely on the
|
|
* `mutationFn` registered via `setMutationDefaults` for its `mutationKey`.
|
|
* Without that, `resumePausedMutations()` finds a paused mutation with no
|
|
* `mutationFn` and silently no-ops, dropping the offline create/move/comment
|
|
* (#237/#238). Each offline mutation hook tags itself with the matching key so
|
|
* the rehydrated paused mutation can find its default `mutationFn` and replay.
|
|
*/
|
|
export const offlineMutationKeys = {
|
|
createPage: ["create-page"] as const,
|
|
movePage: ["move-page"] as const,
|
|
createComment: ["create-comment"] as const,
|
|
};
|
|
|
|
/**
|
|
* Register default `mutationFn`s (and the minimal success side effects safe to
|
|
* run without a mounted component) for the offline-relevant mutation keys, so a
|
|
* paused mutation restored from IndexedDB after an offline reload is replayable
|
|
* by `resumePausedMutations()` on reconnect.
|
|
*
|
|
* Called once when the QueryClient is created (see main.tsx). The hooks still
|
|
* carry their own inline `mutationFn`/`onSuccess` for the live in-session path;
|
|
* these defaults only take over for a rehydrated paused mutation that lost its
|
|
* observer across the reload.
|
|
*/
|
|
export function registerOfflineMutationDefaults(queryClient: QueryClient): void {
|
|
queryClient.setMutationDefaults(offlineMutationKeys.createPage, {
|
|
mutationFn: (data: Partial<IPageInput>) => createPage(data),
|
|
// Re-converge the sidebar tree / recent-changes from the authoritative
|
|
// create response. Pure cache writes — safe with no component mounted.
|
|
onSuccess: (data: IPage) => {
|
|
invalidateOnCreatePage(data);
|
|
},
|
|
});
|
|
|
|
queryClient.setMutationDefaults(offlineMutationKeys.movePage, {
|
|
// Replay the server-side move. The tree re-converges from the next online
|
|
// sidebar fetch / websocket `moveTreeNode` echo, so no cache write is
|
|
// needed here (the optimistic tree state was local-only anyway).
|
|
mutationFn: (data: IMovePage) => movePage(data),
|
|
});
|
|
|
|
queryClient.setMutationDefaults(offlineMutationKeys.createComment, {
|
|
// Replay the server-side comment create. The comments list refetches on the
|
|
// online reload, so the replay only needs to persist the write.
|
|
mutationFn: (data: Partial<IComment>) => createComment(data),
|
|
});
|
|
}
|