forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoutboxSync.ts
More file actions
95 lines (84 loc) · 3.62 KB
/
Copy pathoutboxSync.ts
File metadata and controls
95 lines (84 loc) · 3.62 KB
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Flushing the outbox when there is finally signal to flush it with.
//
// The outbox has always queued correctly (lib/outbox.ts) and nothing ever
// emptied it - `flushOutbox` had no production caller at all (#231). This is
// that caller, plus the two conditions worth checking before spending a
// request, plus the guard that stops two flushes overlapping.
import { useEffect } from 'react'
import { flushOutbox, type FlushResult } from './outbox'
import { accessToken, sendReport, permanentFailureReason, API_CONFIGURED } from './api'
/**
* The in-flight flush, if one is running.
*
* `flushOutbox` reads the whole queue, sends each item, then writes back what
* failed. Two overlapping calls therefore both read the same queue and both
* send everything in it - filing every report twice. Its own docstring
* promises "flushing twice cannot file the same report twice", which holds for
* flushes that follow one another and not for flushes that overlap. Coming
* back into signal is exactly when overlap happens: the `online` event and a
* screen mounting can land in the same tick.
*
* Module-level rather than a ref, because the thing being protected is the
* queue in IndexedDB - one per device, not one per component.
*/
let inFlight: Promise<FlushResult | null> | null = null
async function run(): Promise<FlushResult | null> {
// Nothing to send to. Not an error - a build with no backend configured is
// a real state (see api.ts), and one a hiker cannot do anything about.
if (!API_CONFIGURED) return null
// Signed out. Every item would be refused and stay queued, which is the
// correct outcome, so this only avoids spending requests to reach it: a
// report written before signing in waits for an account rather than being
// lost, and goes the moment one exists.
if ((await accessToken()) === null) return null
// The one place transport knowledge (which HTTP statuses are hopeless)
// meets storage (what to do with a report that will never be accepted).
// Neither module imports the other; this introduces them.
return flushOutbox(sendReport, permanentFailureReason)
}
/**
* Sends whatever is queued, or reports that it could not try.
*
* Never rejects: a failed send leaves its item queued (`flushOutbox`), and a
* flush is background work that must not surface as an error over a map
* someone is navigating by.
*/
export async function syncOutbox(): Promise<FlushResult | null> {
if (inFlight !== null) return inFlight
const started = run()
.catch(() => null)
.finally(() => {
inFlight = null
})
inFlight = started
return started
}
/**
* Flushes when `enabled` becomes true - i.e. when the phone has signal.
*
* Deliberately not on a timer. Out here the app spends most of its life with
* no connection, and a poll would wake the radio for nothing on a battery
* that has to last to the next town.
*
* `onSynced` fires only when a flush actually ran, so a caller can record a
* real sync time without mistaking "could not try" for "nothing to send". It
* must be referentially stable, or this re-runs on every render.
*/
export function useOutboxSync(
enabled: boolean,
onSynced: (result: FlushResult) => void,
): void {
useEffect(() => {
if (!enabled) return
let cancelled = false
void syncOutbox().then((result) => {
// Unmounted while in flight. The send itself is not cancelled - those
// reports are gone from the queue and genuinely sent - only the report
// back to a component that is no longer listening.
if (result !== null && !cancelled) onSynced(result)
})
return () => {
cancelled = true
}
}, [enabled, onSynced])
}