File size: 2,149 Bytes
cd99321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
 * 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
  }
}