Address the Feature 06 integration review: - Offline bootstrap: the API client keeps a device-local verified-session marker (localStorage "1" only, never the /me payload or CSRF token), set when /me answers with a member and cleared on a 401 or a not-in-household 403. When /me fails with a network error and the marker is set, AuthBootstrap opens the saved pages read-only (writes are refused for want of a CSRF token) and loads /me again when the browser comes back online. /me runs with networkMode 'always' so an offline load fails fast instead of pausing on the skeleton. - Recipe reads (/api/v1/recipes) join the network-first API cache; /auth, settings and jobs stay network-only, including encoded and dot-segment forms. - The network-first strategy marks a response answered from its cache with X-MealQuest-Cache-Fallback; the client counts it as offline, so the indicator shows while navigator.onLine is still true. Tests: offline reload of /plan and /dinner/:id after an online visit, no saved pages without a verified session or after a 401, reconnect reloads /me; the real Workbox NetworkFirst strategy against an in-memory cache opens a previously viewed recipe offline, marked; the pill shows for a cache fallback while online. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
// NetworkWindow is the part of window the network status reads: the browser's
|
|
// online flag and its online/offline events.
|
|
export type NetworkWindow = Pick<Window, 'navigator' | 'addEventListener' | 'removeEventListener'>
|
|
|
|
export type NetworkStatus = {
|
|
// offline reports whether the browser says it is offline or the last API
|
|
// request failed with a network error or was answered from the cache.
|
|
offline(): boolean
|
|
// subscribe is notified when offline may have changed. It listens for the
|
|
// window's online/offline events until the returned function is called.
|
|
subscribe(listener: () => void): () => void
|
|
// requestFailed records a fetch that rejected with a network error or that
|
|
// the service worker answered from its cache.
|
|
requestFailed(): void
|
|
// requestSucceeded records a fetch the server answered.
|
|
requestSucceeded(): void
|
|
}
|
|
|
|
// createNetworkStatus tracks whether MealQuest can reach the server. A failed
|
|
// request counts as offline until a later request gets a response or the
|
|
// browser reports that it is back online.
|
|
export function createNetworkStatus(win: NetworkWindow): NetworkStatus {
|
|
let failed = false
|
|
const listeners = new Set<() => void>()
|
|
|
|
function setFailed(value: boolean) {
|
|
if (failed === value) return
|
|
failed = value
|
|
listeners.forEach((listener) => listener())
|
|
}
|
|
|
|
return {
|
|
offline: () => !win.navigator.onLine || failed,
|
|
subscribe(listener) {
|
|
const online = () => {
|
|
failed = false
|
|
listener()
|
|
}
|
|
listeners.add(listener)
|
|
win.addEventListener('online', online)
|
|
win.addEventListener('offline', listener)
|
|
return () => {
|
|
listeners.delete(listener)
|
|
win.removeEventListener('online', online)
|
|
win.removeEventListener('offline', listener)
|
|
}
|
|
},
|
|
requestFailed: () => setFailed(true),
|
|
requestSucceeded: () => setFailed(false),
|
|
}
|
|
}
|