Address six QA findings on the offline-sync feature: 1. HIGH — silent data loss: a paused mutation persisted to IndexedDB and reloaded while still offline never resumed on reconnect. Seed TanStack onlineManager from navigator.onLine at boot (it defaults to online:true and only flips on events, so a cold-boot-offline tab wrongly believed it was online and never got a true online transition), and call resumePausedMutations() in PersistQueryClientProvider onSuccess after the persister rehydrates (defaults are registered before, so the restored mutation has a mutationFn). New offline-resume.test.ts reproduces the full persist -> reload -> reconnect path. 2. MEDIUM (security) — logout did not durably clear gitmost-rq-cache: the throttled persister re-wrote the key ~1s after del() with the still-in-memory snapshot, resurrecting the previous user's data. Freeze the persister (persistClient becomes a no-op) before clearing/deleting so neither the clear()-triggered nor any in-flight write can repopulate the key; re-enable afterwards for the next sign-in session. 3. MEDIUM (UX) — offline create spun forever: the create-note button awaited a mutateAsync that stays pending while paused. Detect offline, fire-and-forget the (queued) mutation, show a "saved offline" notice, and gate the spinner on !isPaused so it no longer hangs. 4. LOW — an uncached page opened offline showed the generic "Error fetching page data." instead of the offline fallback (offline fetch yields no HTTP status). Render OfflineFallback when navigator is offline or the error has no status. 5. LOW — logout teardown threw "Cannot read properties of null (reading 'settings')" in full-editor.tsx: optional-chain the (transiently null) user. 6. Tab title "Untitled": investigated — the tab-title derivation in page.tsx is byte-identical to develop and already reads page.title from REST/cache (the recommended source); live edits keep it in sync via updatePageData. Not a tab-title-derivation regression introduced by this PR; no change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
5.0 KiB
TypeScript
122 lines
5.0 KiB
TypeScript
import "@mantine/core/styles.css";
|
|
import "@mantine/spotlight/styles.css";
|
|
import "@mantine/notifications/styles.css";
|
|
import '@mantine/dates/styles.css';
|
|
import "@/styles/a11y-overrides.css";
|
|
|
|
import ReactDOM from "react-dom/client";
|
|
import App from "./App.tsx";
|
|
import { mantineCssResolver, theme } from "@/theme";
|
|
import { MantineProvider } from "@mantine/core";
|
|
import { BrowserRouter } from "react-router-dom";
|
|
import { ModalsProvider } from "@mantine/modals";
|
|
import { Notifications } from "@mantine/notifications";
|
|
import { QueryClient, onlineManager } from "@tanstack/react-query";
|
|
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
|
import { HelmetProvider } from "react-helmet-async";
|
|
import "./i18n";
|
|
import { PostHogProvider } from "posthog-js/react";
|
|
import {
|
|
getPostHogHost,
|
|
getPostHogKey,
|
|
isCloud,
|
|
isPostHogEnabled,
|
|
} from "@/lib/config.ts";
|
|
import {
|
|
queryPersister,
|
|
shouldDehydrateOfflineQuery,
|
|
} from "@/features/offline/query-persister";
|
|
import { registerOfflineMutationDefaults } from "@/features/offline/offline-mutations";
|
|
import { PwaUpdatePrompt } from "@/pwa/pwa-update-prompt";
|
|
import { isCapacitorNativePlatform } from "@/pwa/is-capacitor";
|
|
import posthog from "posthog-js";
|
|
|
|
export const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnMount: false,
|
|
refetchOnWindowFocus: false,
|
|
retry: false,
|
|
staleTime: 5 * 60 * 1000,
|
|
// Keep cached read data around long enough to be persisted/restored for offline use.
|
|
gcTime: 1000 * 60 * 60 * 24,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Register default mutationFns for the offline-relevant structural mutations so
|
|
// a paused mutation restored from IndexedDB after an offline reload still has a
|
|
// mutationFn and is replayed by resumePausedMutations() on reconnect (instead
|
|
// of silently no-op'ing and dropping the offline create/move/comment). MUST run
|
|
// before any resumePausedMutations() so rehydrated paused mutations have a fn.
|
|
registerOfflineMutationDefaults(queryClient);
|
|
|
|
// Seed TanStack Query's onlineManager from the REAL connectivity state at boot.
|
|
// It defaults to `online: true` and only flips on window online/offline events,
|
|
// so a tab that COLD-BOOTS offline would wrongly believe it is online: paused
|
|
// mutations restored from IndexedDB would never get a later offline->online
|
|
// transition to trigger their replay, and the offline UI affordances could not
|
|
// tell they are offline. Seeding here makes the first real `online` event a true
|
|
// transition that auto-resumes the rehydrated paused mutations (#120 data loss).
|
|
if (typeof navigator !== "undefined" && "onLine" in navigator) {
|
|
onlineManager.setOnline(navigator.onLine);
|
|
}
|
|
|
|
if (isCloud() && isPostHogEnabled) {
|
|
posthog.init(getPostHogKey(), {
|
|
api_host: getPostHogHost(),
|
|
defaults: "2025-05-24",
|
|
disable_session_recording: true,
|
|
capture_pageleave: false,
|
|
});
|
|
}
|
|
|
|
const container = document.getElementById("root") as HTMLElement;
|
|
const root = (container as any).__reactRoot ??= ReactDOM.createRoot(container);
|
|
|
|
root.render(
|
|
<BrowserRouter>
|
|
<MantineProvider theme={theme} cssVariablesResolver={mantineCssResolver}>
|
|
<ModalsProvider>
|
|
<PersistQueryClientProvider
|
|
client={queryClient}
|
|
persistOptions={{
|
|
persister: queryPersister,
|
|
maxAge: 1000 * 60 * 60 * 24,
|
|
buster: APP_VERSION,
|
|
dehydrateOptions: {
|
|
shouldDehydrateQuery: shouldDehydrateOfflineQuery,
|
|
},
|
|
}}
|
|
// After the persister finishes rehydrating, replay any paused
|
|
// mutations restored from IndexedDB. If we are back online this fires
|
|
// them immediately; if still offline they stay paused and TanStack's
|
|
// onlineManager auto-resumes them on the next online transition (which
|
|
// is now a true transition thanks to the onlineManager seeding above).
|
|
// Without this, a paused mutation persisted while offline and then
|
|
// reloaded would never resume and the user's work would be lost (#120).
|
|
onSuccess={() => {
|
|
queryClient.resumePausedMutations();
|
|
}}
|
|
>
|
|
<Notifications position="bottom-center" limit={3} zIndex={10000} />
|
|
{/* Skip SW registration inside the Capacitor native WebView — the
|
|
native shell serves assets itself; a browser SW would conflict. */}
|
|
{!isCapacitorNativePlatform() && <PwaUpdatePrompt />}
|
|
<HelmetProvider>
|
|
<PostHogProvider client={posthog}>
|
|
<App />
|
|
</PostHogProvider>
|
|
</HelmetProvider>
|
|
</PersistQueryClientProvider>
|
|
</ModalsProvider>
|
|
</MantineProvider>
|
|
</BrowserRouter>,
|
|
);
|
|
|
|
// Service worker registration is owned by <PwaUpdatePrompt /> above (via
|
|
// vite-plugin-pwa's useRegisterSW: Workbox precache + prompt-based updates,
|
|
// and skipped inside the Capacitor native WebView). The earlier hand-written
|
|
// /sw.js registration from the mobile bootstrap was removed here to avoid a
|
|
// double registration / competing service worker.
|