3d6f48c3bd
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>
53 lines
1.8 KiB
TypeScript
53 lines
1.8 KiB
TypeScript
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
|
import { getCollabToken, verifyUserToken } from "../services/auth-service";
|
|
import { ICollabToken, IVerifyUserToken } from "../types/auth.types";
|
|
import { isAxiosError } from "axios";
|
|
|
|
/**
|
|
* Retry predicate for the collab-token query.
|
|
*
|
|
* Offline (or any network failure) the POST rejects as an axios NETWORK error:
|
|
* `isAxiosError(error) === true` but `error.response === undefined`. Reading
|
|
* `error.response.status` without a guard threw an uncaught TypeError inside the
|
|
* React Query retryer BEFORE React mounted, white-screening the whole app on an
|
|
* offline cold boot (#237/#238). Optional-chaining `error.response?.status`
|
|
* keeps the predicate total: a network error (no response) is retryable, a real
|
|
* 404 is not. Extracted (and exported) so it can be unit-tested in isolation.
|
|
*/
|
|
export function collabTokenRetry(
|
|
_failureCount: number,
|
|
error: Error,
|
|
): boolean {
|
|
if (isAxiosError(error) && error.response?.status === 404) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function useVerifyUserTokenQuery(
|
|
verify: IVerifyUserToken,
|
|
): UseQueryResult<any, Error> {
|
|
return useQuery({
|
|
queryKey: ["verify-token", verify],
|
|
queryFn: () => verifyUserToken(verify),
|
|
enabled: !!verify.token,
|
|
staleTime: 0,
|
|
});
|
|
}
|
|
|
|
export function useCollabToken(): UseQueryResult<ICollabToken, Error> {
|
|
return useQuery({
|
|
queryKey: ["collab-token"],
|
|
queryFn: () => getCollabToken(),
|
|
staleTime: 20 * 60 * 60 * 1000, //20hrs
|
|
//refetchInterval: 12 * 60 * 60 * 1000, // 12hrs
|
|
//refetchIntervalInBackground: true,
|
|
refetchOnMount: true,
|
|
retry: collabTokenRetry,
|
|
retryDelay: (retryAttempt) => {
|
|
// Exponential backoff: 5s, 10s, 20s, etc.
|
|
return 5000 * Math.pow(2, retryAttempt - 1);
|
|
},
|
|
});
|
|
}
|