@bayonai/offline 0.7.0 → 0.9.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 +38 -1
- package/README.md +81 -8
- package/dist/browserFallbackRecovery.js +7 -2
- package/dist/browserRecovery.d.ts +2 -2
- package/dist/durableCommandStore.d.ts +9 -3
- package/dist/durableCommandStore.js +115 -50
- package/dist/idbDriver.d.ts +3 -0
- package/dist/idbDriver.js +52 -44
- package/dist/index.d.ts +7 -6
- package/dist/index.js +1 -0
- package/dist/pendingWrites.d.ts +16 -6
- package/dist/pendingWrites.js +70 -21
- package/dist/readCache.d.ts +27 -6
- package/dist/readCache.js +134 -23
- package/dist-cjs/browserCoordination.d.ts +23 -0
- package/dist-cjs/browserFallbackRecovery.d.ts +32 -0
- package/dist-cjs/browserFallbackRecovery.js +7 -2
- package/dist-cjs/browserRecovery.d.ts +42 -0
- package/dist-cjs/durableCommandStore.d.ts +49 -0
- package/dist-cjs/durableCommandStore.js +115 -50
- package/dist-cjs/idbDriver.d.ts +26 -0
- package/dist-cjs/idbDriver.js +54 -44
- package/dist-cjs/index.d.ts +7 -0
- package/dist-cjs/index.js +3 -0
- package/dist-cjs/pendingWrites.d.ts +179 -0
- package/dist-cjs/pendingWrites.js +69 -20
- package/dist-cjs/readCache.d.ts +112 -0
- package/dist-cjs/readCache.js +134 -23
- package/package.json +15 -7
package/dist/readCache.js
CHANGED
|
@@ -3,7 +3,9 @@ export const readCacheSchemaVersion = 1;
|
|
|
3
3
|
export const readCacheFreshnessMaxAgeMs = 30_000;
|
|
4
4
|
export const durableReadCacheFreshnessMaxAgeMs = 60 * 60_000;
|
|
5
5
|
export const identityReadCacheFreshnessMaxAgeMs = 24 * 60 * 60_000;
|
|
6
|
-
|
|
6
|
+
// Cache reads are optional and should yield to the fresh loader quickly when
|
|
7
|
+
// IndexedDB is blocked by a browser/WebView lifecycle or another tab.
|
|
8
|
+
export const browserReadCacheOperationTimeoutMs = 1_000;
|
|
7
9
|
const pendingFreshLoads = new WeakMap();
|
|
8
10
|
const cachePruners = new WeakMap();
|
|
9
11
|
export function createReadCache(driver) {
|
|
@@ -14,6 +16,10 @@ export function createReadCache(driver) {
|
|
|
14
16
|
function getVersion(key) {
|
|
15
17
|
return `${globalVersion}:${keyVersions.get(key) ?? 0}`;
|
|
16
18
|
}
|
|
19
|
+
function invalidate(key) {
|
|
20
|
+
pendingLoads.delete(key);
|
|
21
|
+
keyVersions.set(key, (keyVersions.get(key) ?? 0) + 1);
|
|
22
|
+
}
|
|
17
23
|
function enqueue(operation) {
|
|
18
24
|
const next = operationQueue.then(operation, operation);
|
|
19
25
|
operationQueue = next.then(() => undefined, () => undefined);
|
|
@@ -30,10 +36,10 @@ export function createReadCache(driver) {
|
|
|
30
36
|
await enqueue(async () => driver.close?.());
|
|
31
37
|
},
|
|
32
38
|
async delete(key) {
|
|
33
|
-
|
|
34
|
-
keyVersions.set(key, (keyVersions.get(key) ?? 0) + 1);
|
|
39
|
+
invalidate(key);
|
|
35
40
|
await enqueue(() => driver.delete(key));
|
|
36
41
|
},
|
|
42
|
+
invalidate,
|
|
37
43
|
async get(key) {
|
|
38
44
|
const envelope = toReadCacheEnvelope(await driver.get(key));
|
|
39
45
|
return envelope
|
|
@@ -133,8 +139,7 @@ export function createBrowserReadCache({ databaseName, databaseVersion, indexedD
|
|
|
133
139
|
*/
|
|
134
140
|
export function pruneReadCache({ cache, maxAgeMs, now = () => Date.now(), }) {
|
|
135
141
|
const resourceCache = cache.capture?.() ?? cache;
|
|
136
|
-
return cachePruners.get(resourceCache)?.({ maxAgeMs, now }) ??
|
|
137
|
-
Promise.resolve(0);
|
|
142
|
+
return (cachePruners.get(resourceCache)?.({ maxAgeMs, now }) ?? Promise.resolve(0));
|
|
138
143
|
}
|
|
139
144
|
export function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
|
|
140
145
|
const serializedParams = Object.entries(params)
|
|
@@ -151,58 +156,128 @@ export function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
|
|
|
151
156
|
.filter(Boolean)
|
|
152
157
|
.join(":");
|
|
153
158
|
}
|
|
154
|
-
export async function readThroughCache({ cache, forceFresh = false, key, loadFresh, maxAgeMs, }) {
|
|
159
|
+
export async function readThroughCache({ cache, forceFresh = false, key, loadFresh, maxAgeMs, onOutcome, policy, }) {
|
|
155
160
|
const resourceCache = cache.capture?.() ?? cache;
|
|
161
|
+
const resolvedPolicy = resolvePolicy(policy, maxAgeMs);
|
|
162
|
+
const forcedFallback = forceFresh ? await resourceCache.get(key) : null;
|
|
156
163
|
if (forceFresh) {
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
|
|
164
|
+
reportReadCacheOutcome(onOutcome, "forced-refresh");
|
|
165
|
+
// Built-in caches advance generation without deleting fallback data.
|
|
166
|
+
// Legacy custom adapters retain their old delete behavior.
|
|
167
|
+
try {
|
|
168
|
+
if (resourceCache.invalidate) {
|
|
169
|
+
resourceCache.invalidate(key);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await resourceCache.delete(key);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error("[Bayon offline] Unable to invalidate the read cache.", {
|
|
177
|
+
error: normalizeError(error),
|
|
178
|
+
key,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
160
181
|
}
|
|
161
182
|
const version = resourceCache.getVersion(key);
|
|
162
|
-
const cached = await resourceCache.get(key);
|
|
163
|
-
|
|
183
|
+
const cached = forcedFallback ?? (await resourceCache.get(key));
|
|
184
|
+
const retained = cached && isCacheRetained(cached.savedAt, resolvedPolicy);
|
|
185
|
+
if (!forceFresh && retained && isCacheFresh(cached.savedAt, resolvedPolicy)) {
|
|
186
|
+
reportReadCacheOutcome(onOutcome, "fresh-hit");
|
|
164
187
|
return {
|
|
165
188
|
data: cached.value,
|
|
166
189
|
error: null,
|
|
167
190
|
fromCache: true,
|
|
168
191
|
stale: false,
|
|
192
|
+
status: "fresh",
|
|
169
193
|
};
|
|
170
194
|
}
|
|
195
|
+
reportReadCacheOutcome(onOutcome, retained ? "stale-hit" : "cache-miss");
|
|
171
196
|
try {
|
|
172
197
|
const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
|
|
173
|
-
|
|
198
|
+
reportReadCacheOutcome(onOutcome, "loader-success");
|
|
199
|
+
try {
|
|
200
|
+
await resourceCache.setIfVersion(key, fresh, version);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
console.error("[Bayon offline] Unable to refresh the read cache.", {
|
|
204
|
+
error: normalizeError(error),
|
|
205
|
+
key,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
174
208
|
return {
|
|
175
209
|
data: fresh,
|
|
176
210
|
error: null,
|
|
177
211
|
fromCache: false,
|
|
178
212
|
stale: false,
|
|
213
|
+
status: retained ? "fresh" : "miss",
|
|
179
214
|
};
|
|
180
215
|
}
|
|
181
216
|
catch (error) {
|
|
182
|
-
|
|
217
|
+
reportReadCacheOutcome(onOutcome, "loader-failure");
|
|
218
|
+
if (retained && resolvedPolicy.refreshFailure === "use-retained") {
|
|
183
219
|
return {
|
|
184
220
|
data: cached.value,
|
|
185
221
|
error: normalizeError(error),
|
|
186
222
|
fromCache: true,
|
|
187
223
|
stale: true,
|
|
224
|
+
status: "stale",
|
|
188
225
|
};
|
|
189
226
|
}
|
|
190
227
|
throw error;
|
|
191
228
|
}
|
|
192
229
|
}
|
|
193
|
-
export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache = false, }) {
|
|
230
|
+
export async function loadReadCacheResource({ cache, forceFresh = false, key, loadFresh, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache = false, }) {
|
|
194
231
|
const resourceCache = cache.capture?.() ?? cache;
|
|
232
|
+
const resolvedPolicy = resolvePolicy(policy, maxAgeMs);
|
|
233
|
+
const forcedFallback = forceFresh ? await resourceCache.get(key) : null;
|
|
234
|
+
if (forceFresh) {
|
|
235
|
+
reportReadCacheOutcome(onOutcome, "forced-refresh");
|
|
236
|
+
try {
|
|
237
|
+
if (resourceCache.invalidate) {
|
|
238
|
+
resourceCache.invalidate(key);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
await resourceCache.delete(key);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.error("[Bayon offline] Unable to invalidate the read cache.", {
|
|
246
|
+
error: normalizeError(error),
|
|
247
|
+
key,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
195
251
|
const version = resourceCache.getVersion(key);
|
|
196
|
-
const cached = skipCache
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
252
|
+
const cached = skipCache
|
|
253
|
+
? null
|
|
254
|
+
: (forcedFallback ?? (await resourceCache.get(key)));
|
|
255
|
+
const retained = cached && isCacheRetained(cached.savedAt, resolvedPolicy);
|
|
256
|
+
if (retained) {
|
|
257
|
+
const fresh = isCacheFresh(cached.savedAt, resolvedPolicy);
|
|
258
|
+
reportReadCacheOutcome(onOutcome, fresh && !forceFresh ? "fresh-hit" : "stale-hit");
|
|
259
|
+
onUpdate({
|
|
260
|
+
data: cached.value,
|
|
261
|
+
fromCache: true,
|
|
262
|
+
status: fresh && !forceFresh ? "fresh" : "stale",
|
|
263
|
+
});
|
|
264
|
+
if (fresh && !forceFresh) {
|
|
200
265
|
return cached.value;
|
|
201
266
|
}
|
|
202
267
|
}
|
|
268
|
+
else {
|
|
269
|
+
reportReadCacheOutcome(onOutcome, "cache-miss");
|
|
270
|
+
}
|
|
203
271
|
try {
|
|
204
272
|
const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
|
|
205
|
-
|
|
273
|
+
reportReadCacheOutcome(onOutcome, "loader-success");
|
|
274
|
+
if (resourceCache.getVersion(key) === version) {
|
|
275
|
+
onUpdate({
|
|
276
|
+
data: fresh,
|
|
277
|
+
fromCache: false,
|
|
278
|
+
status: retained ? "fresh" : "miss",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
206
281
|
void resourceCache.setIfVersion(key, fresh, version).catch((error) => {
|
|
207
282
|
const normalizedError = normalizeError(error);
|
|
208
283
|
console.error("[Bayon offline] Unable to refresh the read cache.", {
|
|
@@ -214,17 +289,32 @@ export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, o
|
|
|
214
289
|
return fresh;
|
|
215
290
|
}
|
|
216
291
|
catch (error) {
|
|
217
|
-
|
|
292
|
+
reportReadCacheOutcome(onOutcome, "loader-failure");
|
|
293
|
+
if (!retained || resolvedPolicy.refreshFailure === "throw") {
|
|
218
294
|
throw error;
|
|
219
295
|
}
|
|
220
296
|
onUpdate({
|
|
221
297
|
data: cached.value,
|
|
222
298
|
error: normalizeError(error),
|
|
223
299
|
fromCache: true,
|
|
300
|
+
status: "stale",
|
|
224
301
|
});
|
|
225
302
|
return cached.value;
|
|
226
303
|
}
|
|
227
304
|
}
|
|
305
|
+
function reportReadCacheOutcome(onOutcome, outcome) {
|
|
306
|
+
if (!onOutcome)
|
|
307
|
+
return;
|
|
308
|
+
try {
|
|
309
|
+
onOutcome(outcome);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
console.error("[Bayon offline] Read-cache outcome callback failed.", {
|
|
313
|
+
error: normalizeError(error),
|
|
314
|
+
outcome,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
228
318
|
function createNoopReadCacheDriver() {
|
|
229
319
|
return {
|
|
230
320
|
async clear() { },
|
|
@@ -239,12 +329,33 @@ function createNoopReadCacheDriver() {
|
|
|
239
329
|
async set() { },
|
|
240
330
|
};
|
|
241
331
|
}
|
|
242
|
-
function isCacheFresh(savedAt,
|
|
243
|
-
if (
|
|
332
|
+
function isCacheFresh(savedAt, policy) {
|
|
333
|
+
if (policy.freshForMs <= 0) {
|
|
244
334
|
return false;
|
|
245
335
|
}
|
|
246
336
|
const savedTime = Date.parse(savedAt);
|
|
247
|
-
return Number.isFinite(savedTime) && Date.now() - savedTime <=
|
|
337
|
+
return (Number.isFinite(savedTime) && Date.now() - savedTime <= policy.freshForMs);
|
|
338
|
+
}
|
|
339
|
+
function resolvePolicy(policy, maxAgeMs) {
|
|
340
|
+
if (policy) {
|
|
341
|
+
if (!Number.isFinite(policy.freshForMs) ||
|
|
342
|
+
policy.freshForMs < 0 ||
|
|
343
|
+
!Number.isFinite(policy.retainForMs) ||
|
|
344
|
+
policy.retainForMs < policy.freshForMs ||
|
|
345
|
+
!["use-retained", "throw"].includes(policy.refreshFailure)) {
|
|
346
|
+
throw new RangeError("[Bayon offline] Invalid read-cache policy: require finite non-negative durations, retainForMs >= freshForMs, and a supported refreshFailure mode.");
|
|
347
|
+
}
|
|
348
|
+
return policy;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
freshForMs: maxAgeMs ?? 0,
|
|
352
|
+
retainForMs: Number.POSITIVE_INFINITY,
|
|
353
|
+
refreshFailure: "use-retained",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function isCacheRetained(savedAt, policy) {
|
|
357
|
+
const savedTime = Date.parse(savedAt);
|
|
358
|
+
return (Number.isFinite(savedTime) && Date.now() - savedTime <= policy.retainForMs);
|
|
248
359
|
}
|
|
249
360
|
async function loadSharedFreshResource(cache, key, loadFresh) {
|
|
250
361
|
const pendingLoads = getPendingFreshLoads(cache);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type BrowserRecoveryMessageChannel = {
|
|
2
|
+
addEventListener: (type: "message", listener: () => void) => void;
|
|
3
|
+
close: () => void;
|
|
4
|
+
postMessage: (message: "pending-write-recovery") => void;
|
|
5
|
+
removeEventListener: (type: "message", listener: () => void) => void;
|
|
6
|
+
};
|
|
7
|
+
export type BrowserRecoveryLockManager = {
|
|
8
|
+
request: <T>(name: string, options: {
|
|
9
|
+
ifAvailable: true;
|
|
10
|
+
}, callback: (lock: unknown | null) => Promise<T | null>) => Promise<T | null>;
|
|
11
|
+
};
|
|
12
|
+
export type BrowserPendingWriteRecoveryCoordination = {
|
|
13
|
+
close: () => void;
|
|
14
|
+
notify: () => void;
|
|
15
|
+
runExclusive: <T>(attempt: () => Promise<T>) => Promise<T | null>;
|
|
16
|
+
subscribe: (listener: () => void) => () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function createBrowserPendingWriteRecoveryCoordination({ channelName, leaseName, channel, locks, }: {
|
|
19
|
+
channel?: BrowserRecoveryMessageChannel | null;
|
|
20
|
+
channelName: string;
|
|
21
|
+
leaseName: string;
|
|
22
|
+
locks?: BrowserRecoveryLockManager | null;
|
|
23
|
+
}): BrowserPendingWriteRecoveryCoordination;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const defaultOfflineFallbackRetryEvent = "app:network-restored";
|
|
2
|
+
export declare const defaultOfflineFallbackPullThreshold = 72;
|
|
3
|
+
export type BrowserOfflineFallbackRecoveryEnvironment = {
|
|
4
|
+
addEventListener: (type: string, listener: (event: unknown) => void) => void;
|
|
5
|
+
getScrollY: () => number;
|
|
6
|
+
reload: () => void;
|
|
7
|
+
removeEventListener: (type: string, listener: (event: unknown) => void) => void;
|
|
8
|
+
};
|
|
9
|
+
export type OfflineFallbackRetryControl = {
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
textContent: string | null;
|
|
12
|
+
};
|
|
13
|
+
export type BrowserOfflineFallbackRecovery = {
|
|
14
|
+
/** Attempts recovery once. Repeated calls are ignored while navigation starts. */
|
|
15
|
+
retry: () => void;
|
|
16
|
+
start: () => void;
|
|
17
|
+
stop: () => void;
|
|
18
|
+
};
|
|
19
|
+
export type BrowserOfflineFallbackRecoveryOptions = {
|
|
20
|
+
control: OfflineFallbackRetryControl;
|
|
21
|
+
environment?: BrowserOfflineFallbackRecoveryEnvironment;
|
|
22
|
+
/** A host-native validation callback may dispatch this event after connectivity returns. */
|
|
23
|
+
retryEvent?: string;
|
|
24
|
+
retryingText?: string;
|
|
25
|
+
/** Set to `null` to omit touch pull-to-retry. */
|
|
26
|
+
pullThreshold?: number | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Owns generic recovery mechanics for a static offline fallback. Hosts retain
|
|
30
|
+
* all markup, copy, styling, and the choice of a native reconnect event.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createBrowserOfflineFallbackRecovery({ control, environment, pullThreshold, retryEvent, retryingText, }: BrowserOfflineFallbackRecoveryOptions): BrowserOfflineFallbackRecovery;
|
|
@@ -12,9 +12,11 @@ function createBrowserOfflineFallbackRecovery({ control, environment = createDef
|
|
|
12
12
|
let active = false;
|
|
13
13
|
let retrying = false;
|
|
14
14
|
let pullStartY = null;
|
|
15
|
+
const touchThreshold = pullThreshold === null ? Number.POSITIVE_INFINITY : pullThreshold;
|
|
15
16
|
function retry() {
|
|
16
17
|
if (retrying)
|
|
17
18
|
return;
|
|
19
|
+
const previousText = control.textContent;
|
|
18
20
|
retrying = true;
|
|
19
21
|
control.disabled = true;
|
|
20
22
|
control.textContent = retryingText;
|
|
@@ -23,9 +25,12 @@ function createBrowserOfflineFallbackRecovery({ control, environment = createDef
|
|
|
23
25
|
}
|
|
24
26
|
catch (error) {
|
|
25
27
|
// A failed navigation must leave the recovery action visible and usable.
|
|
26
|
-
console.error("[Bayon offline] Offline fallback reload failed.", {
|
|
28
|
+
console.error("[Bayon offline] Offline fallback reload failed.", {
|
|
29
|
+
error,
|
|
30
|
+
});
|
|
27
31
|
retrying = false;
|
|
28
32
|
control.disabled = false;
|
|
33
|
+
control.textContent = previousText;
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
function handleTouchStart(event) {
|
|
@@ -36,7 +41,7 @@ function createBrowserOfflineFallbackRecovery({ control, environment = createDef
|
|
|
36
41
|
const touchY = getSingleTouchY(event, "changedTouches");
|
|
37
42
|
if (pullStartY !== null &&
|
|
38
43
|
touchY !== null &&
|
|
39
|
-
touchY - pullStartY >=
|
|
44
|
+
touchY - pullStartY >= touchThreshold) {
|
|
40
45
|
retry();
|
|
41
46
|
}
|
|
42
47
|
pullStartY = null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PendingWriteRecord, type PendingWriteStore, type PendingWriteSync, type SyncPendingWritesResult } from "./pendingWrites.js";
|
|
2
|
+
import type { BrowserPendingWriteRecoveryCoordination } from "./browserCoordination.js";
|
|
3
|
+
export declare const initialPendingWriteRetryDelayMs = 2000;
|
|
4
|
+
export declare const maxPendingWriteRetryDelayMs = 60000;
|
|
5
|
+
export type BrowserPendingWriteRecoveryEnvironment = {
|
|
6
|
+
addEventListener: (type: "focus" | "offline" | "online", listener: () => void) => void;
|
|
7
|
+
clearTimeout: (timer: ReturnType<typeof setTimeout>) => void;
|
|
8
|
+
isOnline: () => boolean;
|
|
9
|
+
removeEventListener: (type: "focus" | "offline" | "online", listener: () => void) => void;
|
|
10
|
+
setTimeout: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
11
|
+
};
|
|
12
|
+
export type BrowserPendingWriteRecovery<Payload> = {
|
|
13
|
+
/** Signals that a caller has persisted a record outside the store subscription. */
|
|
14
|
+
notifyQueued: () => void;
|
|
15
|
+
/** Runs one immediate, silent recovery attempt when the browser is online. */
|
|
16
|
+
recoverNow: () => Promise<SyncPendingWritesResult | null>;
|
|
17
|
+
start: () => void;
|
|
18
|
+
stop: () => void;
|
|
19
|
+
};
|
|
20
|
+
type BrowserPendingWriteRecoveryOptions<Payload> = {
|
|
21
|
+
/** Optional BroadcastChannel/Web Locks coordination supplied by the package factory. */
|
|
22
|
+
coordination?: BrowserPendingWriteRecoveryCoordination;
|
|
23
|
+
environment?: BrowserPendingWriteRecoveryEnvironment;
|
|
24
|
+
initialRetryDelayMs?: number;
|
|
25
|
+
maxRetryDelayMs?: number;
|
|
26
|
+
/** Diagnostic callback only. Recovery never renders or presents product UI. */
|
|
27
|
+
onError?: (error: unknown) => void;
|
|
28
|
+
/** Diagnostic callback only. Recovery never renders or presents product UI. */
|
|
29
|
+
onResult?: (result: SyncPendingWritesResult) => void;
|
|
30
|
+
scopeKey?: string;
|
|
31
|
+
select?: (record: PendingWriteRecord<Payload>) => boolean;
|
|
32
|
+
store: PendingWriteStore<Payload>;
|
|
33
|
+
} & ({
|
|
34
|
+
/** Runs one complete host-owned recovery pass, such as through an ordered executor. */
|
|
35
|
+
recover: () => Promise<SyncPendingWritesResult>;
|
|
36
|
+
sync?: never;
|
|
37
|
+
} | {
|
|
38
|
+
recover?: never;
|
|
39
|
+
sync: PendingWriteSync<Payload>;
|
|
40
|
+
});
|
|
41
|
+
export declare function createBrowserPendingWriteRecovery<Payload>(options: BrowserPendingWriteRecoveryOptions<Payload>): BrowserPendingWriteRecovery<Payload>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type PendingWriteRecord, type PendingWriteStore } from "./pendingWrites.js";
|
|
2
|
+
export type DurableCommandErrorCategory = "auth" | "conflict" | "connectivity" | "local-storage" | "permission" | "unexpected" | "validation";
|
|
3
|
+
export type DurableCommandOperation<Command> = {
|
|
4
|
+
command: Command;
|
|
5
|
+
error?: DurableCommandErrorCategory;
|
|
6
|
+
id: string;
|
|
7
|
+
status: "accepted" | "failed" | "syncing";
|
|
8
|
+
};
|
|
9
|
+
export type DurableCommandSnapshot<State, Command> = {
|
|
10
|
+
operations: DurableCommandOperation<Command>[];
|
|
11
|
+
state: State;
|
|
12
|
+
};
|
|
13
|
+
export type DurableCommandDispatch = {
|
|
14
|
+
accepted: Promise<void>;
|
|
15
|
+
operationId: string;
|
|
16
|
+
};
|
|
17
|
+
export type DurableCommandExecutionContext = {
|
|
18
|
+
/** Stable across retries so the remote command can be idempotent. */
|
|
19
|
+
operationId: string;
|
|
20
|
+
};
|
|
21
|
+
export type DurableCommandStore<State, Command, Scope> = {
|
|
22
|
+
cancel: (operationId: string) => Promise<boolean>;
|
|
23
|
+
close: () => void;
|
|
24
|
+
dispatch: (command: Command, options?: {
|
|
25
|
+
notBefore?: string;
|
|
26
|
+
}) => DurableCommandDispatch;
|
|
27
|
+
getSnapshot: () => DurableCommandSnapshot<State, Command>;
|
|
28
|
+
recover: () => Promise<void>;
|
|
29
|
+
refresh: (scope: Scope) => Promise<void>;
|
|
30
|
+
subscribe: (listener: () => void) => () => void;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Hosts provide domain projection and transport; this package only owns the
|
|
34
|
+
* durable command lifecycle. A command becomes visible immediately, but is
|
|
35
|
+
* never sent remotely until IndexedDB has accepted it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createDurableCommandStore<State, Command, Scope, Result = unknown>({ canCancel, classifyError, createId, execute, initialState, load, now, project, reconcile, shouldRetry, store, }: {
|
|
38
|
+
canCancel?: (command: Command, now: string) => boolean;
|
|
39
|
+
classifyError?: (error: unknown) => DurableCommandErrorCategory;
|
|
40
|
+
createId?: () => string;
|
|
41
|
+
execute: (command: Command, context: DurableCommandExecutionContext) => Promise<Result>;
|
|
42
|
+
initialState: State;
|
|
43
|
+
load: (scope: Scope) => Promise<State>;
|
|
44
|
+
now?: () => string;
|
|
45
|
+
project: (state: State, commands: readonly PendingWriteRecord<Command>[]) => State;
|
|
46
|
+
reconcile?: (state: State, command: Command, result: Result) => State;
|
|
47
|
+
shouldRetry?: (category: DurableCommandErrorCategory, command: Command) => boolean;
|
|
48
|
+
store: PendingWriteStore<Command>;
|
|
49
|
+
}): DurableCommandStore<State, Command, Scope>;
|