@estiva-app/platform 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/refresh.d.ts +2 -0
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +60 -11
- package/dist/refresh.js.map +1 -1
- package/dist/retrying-read.d.ts +62 -0
- package/dist/retrying-read.d.ts.map +1 -0
- package/dist/retrying-read.js +141 -0
- package/dist/retrying-read.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +10 -0
- package/src/refresh.ts +67 -9
- package/src/retrying-read.ts +194 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @estiva-app/platform
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
One read a tab holds on to, and reads again when it fails (FOL-5). Additive.
|
|
6
|
+
|
|
7
|
+
`createRetryingRead` is Peek's FOL-42 `useFolders` without React or a relay:
|
|
8
|
+
a failure is read again on a backoff (5 s doubling to 60 s, or the relay's own
|
|
9
|
+
`retry in Ns` for a quota refusal), a failure never replaces a good answer, and
|
|
10
|
+
the answer is held past the component that asked. Ship's Folder listing read
|
|
11
|
+
once and turned one failure into an empty list for the session, which dropped
|
|
12
|
+
every project out of its team until a reload.
|
|
13
|
+
|
|
14
|
+
- `createRetryingRead<T>({ retryable?, firstRetryMs?, maxRetryMs?, freshMs?, now?, timers? })`
|
|
15
|
+
→ `{ snapshot, subscribe, ensure(load), reload(load), dispose, clear }`.
|
|
16
|
+
Wrap it with `useSyncExternalStore(read.subscribe, read.snapshot)`. Retries
|
|
17
|
+
run only while something is subscribed; `clear()` forgets the answer, and a
|
|
18
|
+
read in flight across it writes nothing.
|
|
19
|
+
- `retryDelayMs(failures, error)`, `RETRY_FIRST_MS`, `RETRY_MAX_MS`.
|
|
20
|
+
|
|
21
|
+
## 0.3.1
|
|
22
|
+
|
|
23
|
+
Subscribers on one interval share one timer (PER-6). No API change.
|
|
24
|
+
|
|
25
|
+
0.3.0 started a `setInterval` per subscriber, at the moment it subscribed, so
|
|
26
|
+
three views on one page on the same 10 s woke at three unrelated phases — Ship's
|
|
27
|
+
project page read every ~3 s, and never inside the 1 s merge. Now there is one
|
|
28
|
+
timer per distinct `intervalMs`, and every subscriber on it wakes in the same
|
|
29
|
+
tick.
|
|
30
|
+
|
|
31
|
+
- A subscriber that joined less than half an interval ago sits out a tick: its
|
|
32
|
+
caller has just done its own first read. Its first scheduled refresh comes
|
|
33
|
+
between 0.5× and 1.5× the interval after subscribing, then on the shared beat.
|
|
34
|
+
- Different intervals still have separate timers, and separate phases.
|
|
35
|
+
|
|
3
36
|
## 0.3.0
|
|
4
37
|
|
|
5
38
|
When a tab re-reads, and when the relay has asked it to wait (PER-13). Additive.
|
package/dist/index.d.ts
CHANGED
|
@@ -22,5 +22,6 @@
|
|
|
22
22
|
export { MAX_FOLDERS_PER_SUBSCRIPTION, createLiveClient, createLiveClientHolder, type FolderWatch, type LiveClient, type LiveClientHolder, type LiveClientObservers, type LiveClientOptions, } from './live-client.js';
|
|
23
23
|
export { createFolderActivity, type FolderActivity, type FolderListener, } from './folder-activity.js';
|
|
24
24
|
export { assumeOnline, browserOnlineSource, type OnlineSource, type OnlineTarget } from './online.js';
|
|
25
|
+
export { RETRY_FIRST_MS, RETRY_MAX_MS, createRetryingRead, retryDelayMs, type RetryTimers, type RetryingRead, type RetryingReadOptions, type RetryingReadSnapshot, } from './retrying-read.js';
|
|
25
26
|
export { DEFAULT_BACKOFF_MS, DEFAULT_MERGE_MS, MIN_BACKOFF_MS, createRefreshScheduler, createRelayBudget, isRateLimited, retryHintMs, type BudgetChannel, type FocusTarget, type RefreshScheduler, type RefreshSchedulerOptions, type RefreshSubscription, type RelayBudget, type RelayBudgetOptions, type SchedulerClock, type VisibilityTarget, } from './refresh.js';
|
|
26
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,4BAA4B,EAC5B,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AACrG,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,4BAA4B,EAC5B,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AACrG,OAAO,EACL,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -22,5 +22,6 @@
|
|
|
22
22
|
export { MAX_FOLDERS_PER_SUBSCRIPTION, createLiveClient, createLiveClientHolder, } from './live-client.js';
|
|
23
23
|
export { createFolderActivity, } from './folder-activity.js';
|
|
24
24
|
export { assumeOnline, browserOnlineSource } from './online.js';
|
|
25
|
+
export { RETRY_FIRST_MS, RETRY_MAX_MS, createRetryingRead, retryDelayMs, } from './retrying-read.js';
|
|
25
26
|
export { DEFAULT_BACKOFF_MS, DEFAULT_MERGE_MS, MIN_BACKOFF_MS, createRefreshScheduler, createRelayBudget, isRateLimited, retryHintMs, } from './refresh.js';
|
|
26
27
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,4BAA4B,EAC5B,gBAAgB,EAChB,sBAAsB,GAMvB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,oBAAoB,GAGrB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAwC,MAAM,aAAa,CAAA;AACrG,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,WAAW,GAUZ,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,4BAA4B,EAC5B,gBAAgB,EAChB,sBAAsB,GAMvB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,oBAAoB,GAGrB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAwC,MAAM,aAAa,CAAA;AACrG,OAAO,EACL,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,YAAY,GAKb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,WAAW,GAUZ,MAAM,cAAc,CAAA"}
|
package/dist/refresh.d.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface RefreshScheduler {
|
|
|
153
153
|
/**
|
|
154
154
|
* Re-run `refresh` on focus, on becoming visible, and every `intervalMs`
|
|
155
155
|
* while visible. Never on subscribe — the caller owns its first read.
|
|
156
|
+
* Every subscriber on one `intervalMs` shares one timer and wakes in the same
|
|
157
|
+
* tick; the first scheduled refresh comes 0.5–1.5 intervals after subscribing.
|
|
156
158
|
* Returns an unsubscribe; calling it twice is harmless.
|
|
157
159
|
*/
|
|
158
160
|
subscribe(refresh: () => void | Promise<void>, options?: RefreshSubscription): () => void;
|
package/dist/refresh.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../src/refresh.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../src/refresh.ts"],"names":[],"mappings":"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"}
|
package/dist/refresh.js
CHANGED
|
@@ -143,6 +143,7 @@ export function createRefreshScheduler(options) {
|
|
|
143
143
|
clearInterval: (handle) => host.clearInterval(handle),
|
|
144
144
|
};
|
|
145
145
|
const subscribers = new Set();
|
|
146
|
+
const groups = new Map();
|
|
146
147
|
let attached = false;
|
|
147
148
|
const visible = () => document.visibilityState === 'visible';
|
|
148
149
|
const tick = (subscriber) => {
|
|
@@ -173,18 +174,67 @@ export function createRefreshScheduler(options) {
|
|
|
173
174
|
if (visible())
|
|
174
175
|
fanOut();
|
|
175
176
|
};
|
|
177
|
+
/*
|
|
178
|
+
One timer per interval, not per subscriber (PER-6).
|
|
179
|
+
|
|
180
|
+
0.3.0 gave each subscriber its own `setInterval`, started when it
|
|
181
|
+
subscribed. Three views on one page, each on the same 10 s, therefore woke
|
|
182
|
+
at three unrelated phases — a read every three seconds or so, none of them
|
|
183
|
+
close enough to merge — and a page with a widget per section read "every
|
|
184
|
+
couple of seconds" at an interval nobody had chosen. Sharing the timer
|
|
185
|
+
puts every subscriber on an interval into the same wake-up.
|
|
186
|
+
|
|
187
|
+
A subscriber that joined less than half an interval ago sits the tick out:
|
|
188
|
+
its caller has just done its own first read, and a shared phase would
|
|
189
|
+
otherwise re-read it moments later. So the first scheduled refresh comes
|
|
190
|
+
between a half and one and a half intervals after subscribing, and every
|
|
191
|
+
one after that on the shared beat.
|
|
192
|
+
*/
|
|
193
|
+
const intervalTick = (group, intervalMs) => {
|
|
194
|
+
// Checked at fire time rather than started and stopped on every
|
|
195
|
+
// visibility change: a hidden tab has nothing on screen to be stale.
|
|
196
|
+
if (!visible())
|
|
197
|
+
return;
|
|
198
|
+
const now = clock.now();
|
|
199
|
+
for (const subscriber of [...group.members]) {
|
|
200
|
+
if (now - subscriber.joinedAt < intervalMs / 2)
|
|
201
|
+
continue;
|
|
202
|
+
tick(subscriber);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const join = (subscriber, intervalMs) => {
|
|
206
|
+
let group = groups.get(intervalMs);
|
|
207
|
+
if (!group) {
|
|
208
|
+
const created = { members: new Set(), timer: undefined };
|
|
209
|
+
created.timer = clock.setInterval(() => intervalTick(created, intervalMs), intervalMs);
|
|
210
|
+
groups.set(intervalMs, created);
|
|
211
|
+
group = created;
|
|
212
|
+
}
|
|
213
|
+
group.members.add(subscriber);
|
|
214
|
+
};
|
|
215
|
+
const leave = (subscriber) => {
|
|
216
|
+
if (subscriber.intervalMs === undefined)
|
|
217
|
+
return;
|
|
218
|
+
const group = groups.get(subscriber.intervalMs);
|
|
219
|
+
if (!group)
|
|
220
|
+
return;
|
|
221
|
+
group.members.delete(subscriber);
|
|
222
|
+
if (group.members.size === 0) {
|
|
223
|
+
clock.clearInterval(group.timer);
|
|
224
|
+
groups.delete(subscriber.intervalMs);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
176
227
|
return {
|
|
177
228
|
subscribe(refresh, { interval = true, intervalMs = defaultIntervalMs } = {}) {
|
|
178
|
-
const subscriber = {
|
|
229
|
+
const subscriber = {
|
|
230
|
+
refresh,
|
|
231
|
+
lastAt: 0,
|
|
232
|
+
joinedAt: clock.now(),
|
|
233
|
+
intervalMs: interval ? intervalMs : undefined,
|
|
234
|
+
};
|
|
179
235
|
subscribers.add(subscriber);
|
|
180
|
-
if (interval)
|
|
181
|
-
|
|
182
|
-
// visibility change: a hidden tab has nothing on screen to be stale.
|
|
183
|
-
subscriber.timer = clock.setInterval(() => {
|
|
184
|
-
if (visible())
|
|
185
|
-
tick(subscriber);
|
|
186
|
-
}, intervalMs);
|
|
187
|
-
}
|
|
236
|
+
if (interval)
|
|
237
|
+
join(subscriber, intervalMs);
|
|
188
238
|
if (!attached) {
|
|
189
239
|
// On first use and never at module scope, so importing registers
|
|
190
240
|
// nothing — and one pair of listeners however many subscribe.
|
|
@@ -195,8 +245,7 @@ export function createRefreshScheduler(options) {
|
|
|
195
245
|
return () => {
|
|
196
246
|
if (!subscribers.delete(subscriber))
|
|
197
247
|
return;
|
|
198
|
-
|
|
199
|
-
clock.clearInterval(subscriber.timer);
|
|
248
|
+
leave(subscriber);
|
|
200
249
|
if (subscribers.size === 0 && attached) {
|
|
201
250
|
document.removeEventListener('visibilitychange', onVisibility);
|
|
202
251
|
window.removeEventListener('focus', fanOut);
|
package/dist/refresh.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh.js","sourceRoot":"","sources":["../src/refresh.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"refresh.js","sourceRoot":"","sources":["../src/refresh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** The first wait before reading a failed read again. Peek's `FOLDERS_RETRY_FIRST_MS`. */
|
|
2
|
+
export declare const RETRY_FIRST_MS = 5000;
|
|
3
|
+
/** The longest wait between re-reads while a read keeps failing. */
|
|
4
|
+
export declare const RETRY_MAX_MS = 60000;
|
|
5
|
+
/** Declared here because this package compiles with neither the DOM nor the Node lib. */
|
|
6
|
+
export interface RetryTimers {
|
|
7
|
+
setTimeout(run: () => void, ms: number): unknown;
|
|
8
|
+
clearTimeout(handle: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
export interface RetryingReadOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Whether this failure could answer differently next time. Default: every
|
|
13
|
+
* failure is. A caller that can tell a refusal (`not_a_member`, `refused`)
|
|
14
|
+
* from an outage says so, and the refusal is not asked again.
|
|
15
|
+
*/
|
|
16
|
+
retryable?: (error: unknown) => boolean;
|
|
17
|
+
firstRetryMs?: number;
|
|
18
|
+
maxRetryMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* How long an answer is fresh enough that {@link RetryingRead.ensure} does
|
|
21
|
+
* not ask again. Default: for ever — read once per tab, which is what a
|
|
22
|
+
* navigation listing wants; {@link RetryingRead.reload} always asks.
|
|
23
|
+
*/
|
|
24
|
+
freshMs?: number;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
/** Looked up on every call when absent, so a test's fake timers apply. */
|
|
27
|
+
timers?: RetryTimers;
|
|
28
|
+
}
|
|
29
|
+
/** Replaced, never mutated: a stable snapshot for `useSyncExternalStore` between changes. */
|
|
30
|
+
export interface RetryingReadSnapshot<T> {
|
|
31
|
+
/** The last answer, once any read has succeeded. */
|
|
32
|
+
value?: T;
|
|
33
|
+
/** Why the last read failed — only while there is no value to keep. */
|
|
34
|
+
error?: unknown;
|
|
35
|
+
/** A re-read is scheduled: a failure shown now is not the final word. */
|
|
36
|
+
retrying: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface RetryingRead<T> {
|
|
39
|
+
snapshot(): RetryingReadSnapshot<T>;
|
|
40
|
+
subscribe(listener: () => void): () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Read, unless an answer fresher than `freshMs` is held, a read is in
|
|
43
|
+
* flight, or a retry is already scheduled. What a mount calls.
|
|
44
|
+
*
|
|
45
|
+
* Retries run only while something is subscribed: the last unsubscribe
|
|
46
|
+
* cancels a scheduled one, and the next `ensure` reads in its place.
|
|
47
|
+
*/
|
|
48
|
+
ensure(load: () => Promise<T>): Promise<void>;
|
|
49
|
+
/** Read now, whatever is held; a scheduled retry is replaced by this read. */
|
|
50
|
+
reload(load: () => Promise<T>): Promise<void>;
|
|
51
|
+
/** Cancel a scheduled retry. The held answer stays. */
|
|
52
|
+
dispose(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Forget everything held — for a new viewer, or between test cases. A read
|
|
55
|
+
* in flight across it neither writes its answer nor schedules a retry.
|
|
56
|
+
*/
|
|
57
|
+
clear(): void;
|
|
58
|
+
}
|
|
59
|
+
/** The wait before the next attempt, after `failures` consecutive retryable failures. */
|
|
60
|
+
export declare function retryDelayMs(failures: number, error: unknown, first?: number, max?: number): number;
|
|
61
|
+
export declare function createRetryingRead<T>(options?: RetryingReadOptions): RetryingRead<T>;
|
|
62
|
+
//# sourceMappingURL=retrying-read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retrying-read.d.ts","sourceRoot":"","sources":["../src/retrying-read.ts"],"names":[],"mappings":"AA2BA,0FAA0F;AAC1F,eAAO,MAAM,cAAc,OAAQ,CAAA;AACnC,oEAAoE;AACpE,eAAO,MAAM,YAAY,QAAS,CAAA;AAElC,yFAAyF;AACzF,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAA;IAChD,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;CACpC;AAID,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;IACvC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED,6FAA6F;AAC7F,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,oDAAoD;IACpD,KAAK,CAAC,EAAE,CAAC,CAAA;IACT,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,QAAQ,IAAI,oBAAoB,CAAC,CAAC,CAAC,CAAA;IACnC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;IAC3C;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,8EAA8E;IAC9E,MAAM,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,uDAAuD;IACvD,OAAO,IAAI,IAAI,CAAA;IACf;;;OAGG;IACH,KAAK,IAAI,IAAI,CAAA;CACd;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,SAAiB,EAAE,GAAG,SAAe,GAAG,MAAM,CAOjH;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAE,mBAAwB,GAAG,YAAY,CAAC,CAAC,CAAC,CA2FxF"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One read a tab holds on to, and reads again when it fails (FOL-5).
|
|
3
|
+
*
|
|
4
|
+
* Came from Peek's `useFolders` (FOL-42), which Ship needed word for word.
|
|
5
|
+
* Ship's read the Folder listing once and turned a failure into an empty list
|
|
6
|
+
* for the rest of the session: on production one refused read dropped every
|
|
7
|
+
* project out of its team until a reload. Peek had already paid for that
|
|
8
|
+
* lesson — a refused read left a project's channel unplaced and its team's dot
|
|
9
|
+
* dark — and fixed it with three rules, which are the whole of this object:
|
|
10
|
+
*
|
|
11
|
+
* 1. **A failure is read again**, on a backoff from `firstRetryMs` doubling to
|
|
12
|
+
* `maxRetryMs`, until one succeeds. A quota refusal waits the relay's own
|
|
13
|
+
* stated time instead ({@link retryHintMs}), never less than
|
|
14
|
+
* {@link MIN_BACKOFF_MS}. A failure the caller says will answer the same
|
|
15
|
+
* way (`retryable`) is not retried.
|
|
16
|
+
* 2. **A failure never replaces a good answer.** The last value stays; the
|
|
17
|
+
* failure is reported only while there is nothing else to show.
|
|
18
|
+
* 3. **Held past the component that asked.** Every page mounts its own
|
|
19
|
+
* sidebar; one of these per tab means a page change draws the answer the
|
|
20
|
+
* tab already has rather than a skeleton (Peek's SID-2).
|
|
21
|
+
*
|
|
22
|
+
* No React, no relay: the app hands in the read and wraps the object in its
|
|
23
|
+
* own hook — `useSyncExternalStore(read.subscribe, read.snapshot)` — so the
|
|
24
|
+
* package stays framework-free (ADR 0002 §10), as `createRefreshScheduler` is.
|
|
25
|
+
*/
|
|
26
|
+
import { MIN_BACKOFF_MS, isRateLimited, retryHintMs } from './refresh.js';
|
|
27
|
+
/** The first wait before reading a failed read again. Peek's `FOLDERS_RETRY_FIRST_MS`. */
|
|
28
|
+
export const RETRY_FIRST_MS = 5_000;
|
|
29
|
+
/** The longest wait between re-reads while a read keeps failing. */
|
|
30
|
+
export const RETRY_MAX_MS = 60_000;
|
|
31
|
+
const host = globalThis;
|
|
32
|
+
/** The wait before the next attempt, after `failures` consecutive retryable failures. */
|
|
33
|
+
export function retryDelayMs(failures, error, first = RETRY_FIRST_MS, max = RETRY_MAX_MS) {
|
|
34
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
35
|
+
if (isRateLimited(text)) {
|
|
36
|
+
const hint = retryHintMs(text);
|
|
37
|
+
if (hint !== undefined)
|
|
38
|
+
return Math.max(MIN_BACKOFF_MS, hint);
|
|
39
|
+
}
|
|
40
|
+
return Math.min(first * 2 ** Math.max(0, failures - 1), max);
|
|
41
|
+
}
|
|
42
|
+
export function createRetryingRead(options = {}) {
|
|
43
|
+
const retryable = options.retryable ?? (() => true);
|
|
44
|
+
const first = options.firstRetryMs ?? RETRY_FIRST_MS;
|
|
45
|
+
const max = options.maxRetryMs ?? RETRY_MAX_MS;
|
|
46
|
+
const freshMs = options.freshMs ?? Number.POSITIVE_INFINITY;
|
|
47
|
+
// Taken when the object is made, as Peek's listing cache did: freshness is
|
|
48
|
+
// wall-clock, and a test's fake `Date` must not make a held answer look
|
|
49
|
+
// fresh (or stale) to the next test. Pass `now` to control it.
|
|
50
|
+
const now = options.now ?? Date.now;
|
|
51
|
+
const timers = () => options.timers ?? host;
|
|
52
|
+
let snapshot = { retrying: false };
|
|
53
|
+
let answeredAt = 0;
|
|
54
|
+
let failures = 0;
|
|
55
|
+
let inflight;
|
|
56
|
+
let timer;
|
|
57
|
+
const listeners = new Set();
|
|
58
|
+
const set = (next) => {
|
|
59
|
+
snapshot = next;
|
|
60
|
+
for (const listener of [...listeners])
|
|
61
|
+
listener();
|
|
62
|
+
};
|
|
63
|
+
const cancel = () => {
|
|
64
|
+
if (timer === undefined)
|
|
65
|
+
return;
|
|
66
|
+
timers().clearTimeout(timer);
|
|
67
|
+
timer = undefined;
|
|
68
|
+
};
|
|
69
|
+
/* Bumped by `clear`, so a read in flight across it cannot write the old
|
|
70
|
+
answer back or schedule a retry for it. */
|
|
71
|
+
let generation = 0;
|
|
72
|
+
const run = (load) => {
|
|
73
|
+
if (inflight)
|
|
74
|
+
return inflight;
|
|
75
|
+
cancel();
|
|
76
|
+
const started = generation;
|
|
77
|
+
inflight = (async () => {
|
|
78
|
+
try {
|
|
79
|
+
const value = await load();
|
|
80
|
+
if (started !== generation)
|
|
81
|
+
return;
|
|
82
|
+
failures = 0;
|
|
83
|
+
answeredAt = now();
|
|
84
|
+
set({ value, retrying: false });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (started !== generation)
|
|
88
|
+
return;
|
|
89
|
+
const again = retryable(error);
|
|
90
|
+
if (again) {
|
|
91
|
+
failures += 1;
|
|
92
|
+
timer = timers().setTimeout(() => {
|
|
93
|
+
timer = undefined;
|
|
94
|
+
void run(load);
|
|
95
|
+
}, retryDelayMs(failures, error, first, max));
|
|
96
|
+
}
|
|
97
|
+
// A good answer outlives any failure after it.
|
|
98
|
+
set(snapshot.value !== undefined ? { value: snapshot.value, retrying: again } : { error, retrying: again });
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (started === generation)
|
|
102
|
+
inflight = undefined;
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
return inflight;
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
snapshot: () => snapshot,
|
|
109
|
+
subscribe(listener) {
|
|
110
|
+
listeners.add(listener);
|
|
111
|
+
return () => {
|
|
112
|
+
listeners.delete(listener);
|
|
113
|
+
// Nobody is looking: a retry would spend the relay's budget on an
|
|
114
|
+
// answer no one draws. The snapshot still says a retry is due, and
|
|
115
|
+
// the next `ensure` makes it.
|
|
116
|
+
if (listeners.size === 0)
|
|
117
|
+
cancel();
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
ensure(load) {
|
|
121
|
+
if (inflight)
|
|
122
|
+
return inflight;
|
|
123
|
+
if (timer !== undefined)
|
|
124
|
+
return Promise.resolve();
|
|
125
|
+
if (snapshot.value !== undefined && now() - answeredAt < freshMs)
|
|
126
|
+
return Promise.resolve();
|
|
127
|
+
return run(load);
|
|
128
|
+
},
|
|
129
|
+
reload: run,
|
|
130
|
+
dispose: cancel,
|
|
131
|
+
clear() {
|
|
132
|
+
generation += 1;
|
|
133
|
+
cancel();
|
|
134
|
+
inflight = undefined;
|
|
135
|
+
failures = 0;
|
|
136
|
+
answeredAt = 0;
|
|
137
|
+
set({ retrying: false });
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=retrying-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retrying-read.js","sourceRoot":"","sources":["../src/retrying-read.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estiva-app/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Browser lifecycle two Estiva apps share: the one live relay socket a tab should hold, its credential read per connect, the network coming back, and when a tab re-reads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,16 @@ export {
|
|
|
35
35
|
type FolderListener,
|
|
36
36
|
} from './folder-activity.js'
|
|
37
37
|
export { assumeOnline, browserOnlineSource, type OnlineSource, type OnlineTarget } from './online.js'
|
|
38
|
+
export {
|
|
39
|
+
RETRY_FIRST_MS,
|
|
40
|
+
RETRY_MAX_MS,
|
|
41
|
+
createRetryingRead,
|
|
42
|
+
retryDelayMs,
|
|
43
|
+
type RetryTimers,
|
|
44
|
+
type RetryingRead,
|
|
45
|
+
type RetryingReadOptions,
|
|
46
|
+
type RetryingReadSnapshot,
|
|
47
|
+
} from './retrying-read.js'
|
|
38
48
|
export {
|
|
39
49
|
DEFAULT_BACKOFF_MS,
|
|
40
50
|
DEFAULT_MERGE_MS,
|
package/src/refresh.ts
CHANGED
|
@@ -238,6 +238,8 @@ export interface RefreshScheduler {
|
|
|
238
238
|
/**
|
|
239
239
|
* Re-run `refresh` on focus, on becoming visible, and every `intervalMs`
|
|
240
240
|
* while visible. Never on subscribe — the caller owns its first read.
|
|
241
|
+
* Every subscriber on one `intervalMs` shares one timer and wakes in the same
|
|
242
|
+
* tick; the first scheduled refresh comes 0.5–1.5 intervals after subscribing.
|
|
241
243
|
* Returns an unsubscribe; calling it twice is harmless.
|
|
242
244
|
*/
|
|
243
245
|
subscribe(refresh: () => void | Promise<void>, options?: RefreshSubscription): () => void
|
|
@@ -249,6 +251,15 @@ interface Subscriber {
|
|
|
249
251
|
refresh: () => void | Promise<void>
|
|
250
252
|
/** When this subscriber last ran. Merge and pause are judged per subscriber. */
|
|
251
253
|
lastAt: number
|
|
254
|
+
/** When it subscribed — its caller's own first read, which this never sees. */
|
|
255
|
+
joinedAt: number
|
|
256
|
+
/** The interval it ticks on, or `undefined` for focus and visibility only. */
|
|
257
|
+
intervalMs: number | undefined
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Every subscriber on one interval, and the one timer they share (PER-6). */
|
|
261
|
+
interface IntervalGroup {
|
|
262
|
+
members: Set<Subscriber>
|
|
252
263
|
timer: unknown
|
|
253
264
|
}
|
|
254
265
|
|
|
@@ -263,6 +274,7 @@ export function createRefreshScheduler(options: RefreshSchedulerOptions): Refres
|
|
|
263
274
|
clearInterval: (handle) => host.clearInterval(handle),
|
|
264
275
|
}
|
|
265
276
|
const subscribers = new Set<Subscriber>()
|
|
277
|
+
const groups = new Map<number, IntervalGroup>()
|
|
266
278
|
let attached = false
|
|
267
279
|
|
|
268
280
|
const visible = () => document.visibilityState === 'visible'
|
|
@@ -291,17 +303,63 @@ export function createRefreshScheduler(options: RefreshSchedulerOptions): Refres
|
|
|
291
303
|
if (visible()) fanOut()
|
|
292
304
|
}
|
|
293
305
|
|
|
306
|
+
/*
|
|
307
|
+
One timer per interval, not per subscriber (PER-6).
|
|
308
|
+
|
|
309
|
+
0.3.0 gave each subscriber its own `setInterval`, started when it
|
|
310
|
+
subscribed. Three views on one page, each on the same 10 s, therefore woke
|
|
311
|
+
at three unrelated phases — a read every three seconds or so, none of them
|
|
312
|
+
close enough to merge — and a page with a widget per section read "every
|
|
313
|
+
couple of seconds" at an interval nobody had chosen. Sharing the timer
|
|
314
|
+
puts every subscriber on an interval into the same wake-up.
|
|
315
|
+
|
|
316
|
+
A subscriber that joined less than half an interval ago sits the tick out:
|
|
317
|
+
its caller has just done its own first read, and a shared phase would
|
|
318
|
+
otherwise re-read it moments later. So the first scheduled refresh comes
|
|
319
|
+
between a half and one and a half intervals after subscribing, and every
|
|
320
|
+
one after that on the shared beat.
|
|
321
|
+
*/
|
|
322
|
+
const intervalTick = (group: IntervalGroup, intervalMs: number) => {
|
|
323
|
+
// Checked at fire time rather than started and stopped on every
|
|
324
|
+
// visibility change: a hidden tab has nothing on screen to be stale.
|
|
325
|
+
if (!visible()) return
|
|
326
|
+
const now = clock.now()
|
|
327
|
+
for (const subscriber of [...group.members]) {
|
|
328
|
+
if (now - subscriber.joinedAt < intervalMs / 2) continue
|
|
329
|
+
tick(subscriber)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const join = (subscriber: Subscriber, intervalMs: number) => {
|
|
333
|
+
let group = groups.get(intervalMs)
|
|
334
|
+
if (!group) {
|
|
335
|
+
const created: IntervalGroup = { members: new Set(), timer: undefined }
|
|
336
|
+
created.timer = clock.setInterval(() => intervalTick(created, intervalMs), intervalMs)
|
|
337
|
+
groups.set(intervalMs, created)
|
|
338
|
+
group = created
|
|
339
|
+
}
|
|
340
|
+
group.members.add(subscriber)
|
|
341
|
+
}
|
|
342
|
+
const leave = (subscriber: Subscriber) => {
|
|
343
|
+
if (subscriber.intervalMs === undefined) return
|
|
344
|
+
const group = groups.get(subscriber.intervalMs)
|
|
345
|
+
if (!group) return
|
|
346
|
+
group.members.delete(subscriber)
|
|
347
|
+
if (group.members.size === 0) {
|
|
348
|
+
clock.clearInterval(group.timer)
|
|
349
|
+
groups.delete(subscriber.intervalMs)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
294
353
|
return {
|
|
295
354
|
subscribe(refresh, { interval = true, intervalMs = defaultIntervalMs } = {}) {
|
|
296
|
-
const subscriber: Subscriber = {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
subscriber.timer = clock.setInterval(() => {
|
|
302
|
-
if (visible()) tick(subscriber)
|
|
303
|
-
}, intervalMs)
|
|
355
|
+
const subscriber: Subscriber = {
|
|
356
|
+
refresh,
|
|
357
|
+
lastAt: 0,
|
|
358
|
+
joinedAt: clock.now(),
|
|
359
|
+
intervalMs: interval ? intervalMs : undefined,
|
|
304
360
|
}
|
|
361
|
+
subscribers.add(subscriber)
|
|
362
|
+
if (interval) join(subscriber, intervalMs)
|
|
305
363
|
if (!attached) {
|
|
306
364
|
// On first use and never at module scope, so importing registers
|
|
307
365
|
// nothing — and one pair of listeners however many subscribe.
|
|
@@ -311,7 +369,7 @@ export function createRefreshScheduler(options: RefreshSchedulerOptions): Refres
|
|
|
311
369
|
}
|
|
312
370
|
return () => {
|
|
313
371
|
if (!subscribers.delete(subscriber)) return
|
|
314
|
-
|
|
372
|
+
leave(subscriber)
|
|
315
373
|
if (subscribers.size === 0 && attached) {
|
|
316
374
|
document.removeEventListener('visibilitychange', onVisibility)
|
|
317
375
|
window.removeEventListener('focus', fanOut)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One read a tab holds on to, and reads again when it fails (FOL-5).
|
|
3
|
+
*
|
|
4
|
+
* Came from Peek's `useFolders` (FOL-42), which Ship needed word for word.
|
|
5
|
+
* Ship's read the Folder listing once and turned a failure into an empty list
|
|
6
|
+
* for the rest of the session: on production one refused read dropped every
|
|
7
|
+
* project out of its team until a reload. Peek had already paid for that
|
|
8
|
+
* lesson — a refused read left a project's channel unplaced and its team's dot
|
|
9
|
+
* dark — and fixed it with three rules, which are the whole of this object:
|
|
10
|
+
*
|
|
11
|
+
* 1. **A failure is read again**, on a backoff from `firstRetryMs` doubling to
|
|
12
|
+
* `maxRetryMs`, until one succeeds. A quota refusal waits the relay's own
|
|
13
|
+
* stated time instead ({@link retryHintMs}), never less than
|
|
14
|
+
* {@link MIN_BACKOFF_MS}. A failure the caller says will answer the same
|
|
15
|
+
* way (`retryable`) is not retried.
|
|
16
|
+
* 2. **A failure never replaces a good answer.** The last value stays; the
|
|
17
|
+
* failure is reported only while there is nothing else to show.
|
|
18
|
+
* 3. **Held past the component that asked.** Every page mounts its own
|
|
19
|
+
* sidebar; one of these per tab means a page change draws the answer the
|
|
20
|
+
* tab already has rather than a skeleton (Peek's SID-2).
|
|
21
|
+
*
|
|
22
|
+
* No React, no relay: the app hands in the read and wraps the object in its
|
|
23
|
+
* own hook — `useSyncExternalStore(read.subscribe, read.snapshot)` — so the
|
|
24
|
+
* package stays framework-free (ADR 0002 §10), as `createRefreshScheduler` is.
|
|
25
|
+
*/
|
|
26
|
+
import { MIN_BACKOFF_MS, isRateLimited, retryHintMs } from './refresh.js'
|
|
27
|
+
|
|
28
|
+
/** The first wait before reading a failed read again. Peek's `FOLDERS_RETRY_FIRST_MS`. */
|
|
29
|
+
export const RETRY_FIRST_MS = 5_000
|
|
30
|
+
/** The longest wait between re-reads while a read keeps failing. */
|
|
31
|
+
export const RETRY_MAX_MS = 60_000
|
|
32
|
+
|
|
33
|
+
/** Declared here because this package compiles with neither the DOM nor the Node lib. */
|
|
34
|
+
export interface RetryTimers {
|
|
35
|
+
setTimeout(run: () => void, ms: number): unknown
|
|
36
|
+
clearTimeout(handle: unknown): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const host = globalThis as unknown as RetryTimers
|
|
40
|
+
|
|
41
|
+
export interface RetryingReadOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Whether this failure could answer differently next time. Default: every
|
|
44
|
+
* failure is. A caller that can tell a refusal (`not_a_member`, `refused`)
|
|
45
|
+
* from an outage says so, and the refusal is not asked again.
|
|
46
|
+
*/
|
|
47
|
+
retryable?: (error: unknown) => boolean
|
|
48
|
+
firstRetryMs?: number
|
|
49
|
+
maxRetryMs?: number
|
|
50
|
+
/**
|
|
51
|
+
* How long an answer is fresh enough that {@link RetryingRead.ensure} does
|
|
52
|
+
* not ask again. Default: for ever — read once per tab, which is what a
|
|
53
|
+
* navigation listing wants; {@link RetryingRead.reload} always asks.
|
|
54
|
+
*/
|
|
55
|
+
freshMs?: number
|
|
56
|
+
now?: () => number
|
|
57
|
+
/** Looked up on every call when absent, so a test's fake timers apply. */
|
|
58
|
+
timers?: RetryTimers
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Replaced, never mutated: a stable snapshot for `useSyncExternalStore` between changes. */
|
|
62
|
+
export interface RetryingReadSnapshot<T> {
|
|
63
|
+
/** The last answer, once any read has succeeded. */
|
|
64
|
+
value?: T
|
|
65
|
+
/** Why the last read failed — only while there is no value to keep. */
|
|
66
|
+
error?: unknown
|
|
67
|
+
/** A re-read is scheduled: a failure shown now is not the final word. */
|
|
68
|
+
retrying: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface RetryingRead<T> {
|
|
72
|
+
snapshot(): RetryingReadSnapshot<T>
|
|
73
|
+
subscribe(listener: () => void): () => void
|
|
74
|
+
/**
|
|
75
|
+
* Read, unless an answer fresher than `freshMs` is held, a read is in
|
|
76
|
+
* flight, or a retry is already scheduled. What a mount calls.
|
|
77
|
+
*
|
|
78
|
+
* Retries run only while something is subscribed: the last unsubscribe
|
|
79
|
+
* cancels a scheduled one, and the next `ensure` reads in its place.
|
|
80
|
+
*/
|
|
81
|
+
ensure(load: () => Promise<T>): Promise<void>
|
|
82
|
+
/** Read now, whatever is held; a scheduled retry is replaced by this read. */
|
|
83
|
+
reload(load: () => Promise<T>): Promise<void>
|
|
84
|
+
/** Cancel a scheduled retry. The held answer stays. */
|
|
85
|
+
dispose(): void
|
|
86
|
+
/**
|
|
87
|
+
* Forget everything held — for a new viewer, or between test cases. A read
|
|
88
|
+
* in flight across it neither writes its answer nor schedules a retry.
|
|
89
|
+
*/
|
|
90
|
+
clear(): void
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The wait before the next attempt, after `failures` consecutive retryable failures. */
|
|
94
|
+
export function retryDelayMs(failures: number, error: unknown, first = RETRY_FIRST_MS, max = RETRY_MAX_MS): number {
|
|
95
|
+
const text = error instanceof Error ? error.message : String(error)
|
|
96
|
+
if (isRateLimited(text)) {
|
|
97
|
+
const hint = retryHintMs(text)
|
|
98
|
+
if (hint !== undefined) return Math.max(MIN_BACKOFF_MS, hint)
|
|
99
|
+
}
|
|
100
|
+
return Math.min(first * 2 ** Math.max(0, failures - 1), max)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createRetryingRead<T>(options: RetryingReadOptions = {}): RetryingRead<T> {
|
|
104
|
+
const retryable = options.retryable ?? (() => true)
|
|
105
|
+
const first = options.firstRetryMs ?? RETRY_FIRST_MS
|
|
106
|
+
const max = options.maxRetryMs ?? RETRY_MAX_MS
|
|
107
|
+
const freshMs = options.freshMs ?? Number.POSITIVE_INFINITY
|
|
108
|
+
// Taken when the object is made, as Peek's listing cache did: freshness is
|
|
109
|
+
// wall-clock, and a test's fake `Date` must not make a held answer look
|
|
110
|
+
// fresh (or stale) to the next test. Pass `now` to control it.
|
|
111
|
+
const now = options.now ?? Date.now
|
|
112
|
+
const timers = () => options.timers ?? host
|
|
113
|
+
|
|
114
|
+
let snapshot: RetryingReadSnapshot<T> = { retrying: false }
|
|
115
|
+
let answeredAt = 0
|
|
116
|
+
let failures = 0
|
|
117
|
+
let inflight: Promise<void> | undefined
|
|
118
|
+
let timer: unknown
|
|
119
|
+
const listeners = new Set<() => void>()
|
|
120
|
+
|
|
121
|
+
const set = (next: RetryingReadSnapshot<T>) => {
|
|
122
|
+
snapshot = next
|
|
123
|
+
for (const listener of [...listeners]) listener()
|
|
124
|
+
}
|
|
125
|
+
const cancel = () => {
|
|
126
|
+
if (timer === undefined) return
|
|
127
|
+
timers().clearTimeout(timer)
|
|
128
|
+
timer = undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Bumped by `clear`, so a read in flight across it cannot write the old
|
|
132
|
+
answer back or schedule a retry for it. */
|
|
133
|
+
let generation = 0
|
|
134
|
+
|
|
135
|
+
const run = (load: () => Promise<T>): Promise<void> => {
|
|
136
|
+
if (inflight) return inflight
|
|
137
|
+
cancel()
|
|
138
|
+
const started = generation
|
|
139
|
+
inflight = (async () => {
|
|
140
|
+
try {
|
|
141
|
+
const value = await load()
|
|
142
|
+
if (started !== generation) return
|
|
143
|
+
failures = 0
|
|
144
|
+
answeredAt = now()
|
|
145
|
+
set({ value, retrying: false })
|
|
146
|
+
} catch (error: unknown) {
|
|
147
|
+
if (started !== generation) return
|
|
148
|
+
const again = retryable(error)
|
|
149
|
+
if (again) {
|
|
150
|
+
failures += 1
|
|
151
|
+
timer = timers().setTimeout(() => {
|
|
152
|
+
timer = undefined
|
|
153
|
+
void run(load)
|
|
154
|
+
}, retryDelayMs(failures, error, first, max))
|
|
155
|
+
}
|
|
156
|
+
// A good answer outlives any failure after it.
|
|
157
|
+
set(snapshot.value !== undefined ? { value: snapshot.value, retrying: again } : { error, retrying: again })
|
|
158
|
+
} finally {
|
|
159
|
+
if (started === generation) inflight = undefined
|
|
160
|
+
}
|
|
161
|
+
})()
|
|
162
|
+
return inflight
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
snapshot: () => snapshot,
|
|
167
|
+
subscribe(listener) {
|
|
168
|
+
listeners.add(listener)
|
|
169
|
+
return () => {
|
|
170
|
+
listeners.delete(listener)
|
|
171
|
+
// Nobody is looking: a retry would spend the relay's budget on an
|
|
172
|
+
// answer no one draws. The snapshot still says a retry is due, and
|
|
173
|
+
// the next `ensure` makes it.
|
|
174
|
+
if (listeners.size === 0) cancel()
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
ensure(load) {
|
|
178
|
+
if (inflight) return inflight
|
|
179
|
+
if (timer !== undefined) return Promise.resolve()
|
|
180
|
+
if (snapshot.value !== undefined && now() - answeredAt < freshMs) return Promise.resolve()
|
|
181
|
+
return run(load)
|
|
182
|
+
},
|
|
183
|
+
reload: run,
|
|
184
|
+
dispose: cancel,
|
|
185
|
+
clear() {
|
|
186
|
+
generation += 1
|
|
187
|
+
cancel()
|
|
188
|
+
inflight = undefined
|
|
189
|
+
failures = 0
|
|
190
|
+
answeredAt = 0
|
|
191
|
+
set({ retrying: false })
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|