mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
bcd2c8c959
Repos gated reads on raw navigator.onLine and the online branch had no try/catch, so a captive portal or connected-but-no-internet (navigator.onLine lying "true") threw a network error instead of serving the good cached copy — blanking the trip even though Dexie held it. - new onlineThenCache(onlineFn, cacheFn) helper: reads the cache when offline, and on a network-level failure (Axios error with no HTTP response). A genuine HTTP error (4xx/5xx — the server responded) is rethrown so callers still set error state / navigate, not masked by a stale cache. - gates only on navigator.onLine, NOT the connectivity probe: the probe is a coarse global flag and one failed health check would otherwise divert every read to the (possibly empty) cache even when the request would succeed. - every repo list/get read path routed through it (reads only — writes still go through the mutation queue so failures surface) - tests: captive-portal fallback, HTTP-error rethrow, non-Axios rethrow
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
/**
|
|
* True when an error means the request never reached the server — a network-level
|
|
* failure (offline, captive portal, proxy auth wall, dropped connection, CORS).
|
|
* Axios sets `response` only when the server actually replied; its absence (on an
|
|
* Axios error) means we never got one. A real HTTP error (4xx/5xx) HAS a response
|
|
* and must NOT be treated as a network failure — the server spoke, so the caller
|
|
* needs to see it. Non-Axios errors are surfaced too.
|
|
*/
|
|
function isNetworkError(err: unknown): boolean {
|
|
const e = err as { isAxiosError?: boolean; response?: unknown } | null
|
|
return !!e && e.isAxiosError === true && e.response == null
|
|
}
|
|
|
|
/**
|
|
* Read-through cache pattern shared by every repo's read methods.
|
|
*
|
|
* Reads degrade to the local Dexie cache in two situations:
|
|
* 1. The browser reports it is offline (`navigator.onLine` false) — skip the
|
|
* doomed request entirely.
|
|
* 2. The browser *thinks* it is online but the request fails at the network
|
|
* level — a lying `navigator.onLine` on a captive portal, a dropped
|
|
* connection (H2). Rather than surfacing that (which blanks the trip even
|
|
* though a good cached copy exists), we fall back to the cache.
|
|
*
|
|
* We intentionally gate only on `navigator.onLine`, NOT the connectivity probe:
|
|
* the probe is a coarse global flag, and a single failed health check would
|
|
* otherwise force every read to the (possibly empty) cache even when the request
|
|
* itself would succeed. The network-error catch below covers the captive-portal
|
|
* case the probe was meant to.
|
|
*
|
|
* A genuine HTTP error (404/403/500 — the server responded) is NOT swallowed: it
|
|
* is rethrown so callers can set error state, navigate away, etc.
|
|
*
|
|
* Writes must NOT use this — they go through the mutation queue so failures are
|
|
* surfaced and retried, not silently swallowed.
|
|
*/
|
|
export async function onlineThenCache<T>(
|
|
onlineFn: () => Promise<T>,
|
|
cacheFn: () => Promise<T>,
|
|
): Promise<T> {
|
|
if (!navigator.onLine) return cacheFn()
|
|
try {
|
|
return await onlineFn()
|
|
} catch (err) {
|
|
if (isNetworkError(err)) return cacheFn()
|
|
throw err
|
|
}
|
|
}
|