@bayonai/offline 0.12.0 → 0.15.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 +28 -1
- package/README.md +98 -1
- package/RECOVERY.md +3 -0
- package/dist/browserReadLifecycle.d.ts +5 -0
- package/dist/browserReadLifecycle.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/pendingWriteConsolidation.js +2 -1
- package/dist/pendingWrites.d.ts +3 -3
- package/dist/pendingWrites.js +20 -15
- package/dist/readCache.d.ts +38 -8
- package/dist/readCache.js +396 -41
- package/dist/readResourceBatch.d.ts +13 -0
- package/dist/readResourceBatch.js +116 -0
- package/dist/readResourceObserver.d.ts +57 -0
- package/dist/readResourceObserver.js +108 -0
- package/dist-cjs/browserReadLifecycle.d.ts +5 -0
- package/dist-cjs/browserReadLifecycle.js +27 -0
- package/dist-cjs/index.d.ts +3 -0
- package/dist-cjs/index.js +3 -0
- package/dist-cjs/pendingWriteConsolidation.js +2 -1
- package/dist-cjs/pendingWrites.d.ts +3 -3
- package/dist-cjs/pendingWrites.js +20 -15
- package/dist-cjs/readCache.d.ts +38 -8
- package/dist-cjs/readCache.js +396 -41
- package/dist-cjs/readResourceBatch.d.ts +13 -0
- package/dist-cjs/readResourceBatch.js +119 -0
- package/dist-cjs/readResourceObserver.d.ts +57 -0
- package/dist-cjs/readResourceObserver.js +111 -0
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** A mounted presentation barrier; child tasks retain sole network/cache ownership. */
|
|
2
|
+
export function observeReadResourceBatch({ count, observe, freshForMs, now, current, canRetain, retiredError, subscribe, onUpdate }) {
|
|
3
|
+
let disposed = false;
|
|
4
|
+
let retired = false;
|
|
5
|
+
let presented = false;
|
|
6
|
+
let strict = false;
|
|
7
|
+
let initialRetry;
|
|
8
|
+
let pending;
|
|
9
|
+
let failure;
|
|
10
|
+
let stop = () => { };
|
|
11
|
+
const children = [];
|
|
12
|
+
const disposedChildren = new Set();
|
|
13
|
+
const detach = () => { const cleanup = stop; stop = () => { }; cleanup(); };
|
|
14
|
+
const disposeChildren = () => children.forEach(child => {
|
|
15
|
+
if (!disposedChildren.has(child)) {
|
|
16
|
+
disposedChildren.add(child);
|
|
17
|
+
child.dispose();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const values = new Array(count);
|
|
21
|
+
const received = new Set();
|
|
22
|
+
const ages = new Array(count).fill(-Infinity);
|
|
23
|
+
const active = () => !disposed && !retired && current();
|
|
24
|
+
const retire = () => { retired = true; detach(); disposeChildren(); };
|
|
25
|
+
for (let index = 0; index < count; index++) {
|
|
26
|
+
const child = observe(index, update => {
|
|
27
|
+
if (disposed || retired)
|
|
28
|
+
return;
|
|
29
|
+
if (update.status === "invalidated" || update.status === "expired") {
|
|
30
|
+
onUpdate({ status: "invalidated" });
|
|
31
|
+
retire();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!current())
|
|
35
|
+
return;
|
|
36
|
+
if ("data" in update) {
|
|
37
|
+
received.add(index);
|
|
38
|
+
values[index] = update.data;
|
|
39
|
+
if (!canRetain(update.data))
|
|
40
|
+
strict = true;
|
|
41
|
+
}
|
|
42
|
+
// Denial retires the group even before every initial source has settled.
|
|
43
|
+
if (update.status === "failed") {
|
|
44
|
+
onUpdate(update);
|
|
45
|
+
presented = false;
|
|
46
|
+
retire();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (update.status === "retry") {
|
|
50
|
+
if (presented)
|
|
51
|
+
failure = update;
|
|
52
|
+
else
|
|
53
|
+
initialRetry = { error: update.error };
|
|
54
|
+
}
|
|
55
|
+
}, createdAt => { ages[index] = createdAt; });
|
|
56
|
+
children.push(child);
|
|
57
|
+
if (retired) {
|
|
58
|
+
disposeChildren();
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const refresh = (force = false) => {
|
|
63
|
+
if (!active() || !presented || strict)
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
if (pending)
|
|
66
|
+
return pending;
|
|
67
|
+
if (!force && ages.every((age, index) => now() - age < freshForMs[index]))
|
|
68
|
+
return Promise.resolve();
|
|
69
|
+
// Claim the barrier before a presentation callback can request it again.
|
|
70
|
+
pending = Promise.resolve().then(async () => {
|
|
71
|
+
if (!active() || strict)
|
|
72
|
+
return;
|
|
73
|
+
failure = undefined;
|
|
74
|
+
onUpdate({ status: "refreshing" });
|
|
75
|
+
if (!active() || strict)
|
|
76
|
+
return;
|
|
77
|
+
await Promise.all(children.map(child => child.refresh?.({ force })));
|
|
78
|
+
if (!active())
|
|
79
|
+
return;
|
|
80
|
+
if (failure)
|
|
81
|
+
onUpdate(failure);
|
|
82
|
+
else {
|
|
83
|
+
// Never expose a successful sibling beside an older failed member.
|
|
84
|
+
onUpdate({ status: "fresh", data: [...values] });
|
|
85
|
+
if (strict)
|
|
86
|
+
detach();
|
|
87
|
+
}
|
|
88
|
+
}).finally(() => { pending = undefined; });
|
|
89
|
+
return pending;
|
|
90
|
+
};
|
|
91
|
+
if (!retired)
|
|
92
|
+
stop = subscribe(() => refresh());
|
|
93
|
+
const promise = Promise.allSettled(children.map(child => child.promise)).then(results => {
|
|
94
|
+
if (!active())
|
|
95
|
+
throw retiredError();
|
|
96
|
+
const rejected = results.find(result => result.status === "rejected");
|
|
97
|
+
if (rejected?.status === "rejected")
|
|
98
|
+
throw rejected.reason;
|
|
99
|
+
const data = results.map(result => result.value);
|
|
100
|
+
data.forEach((value, index) => { if (!received.has(index))
|
|
101
|
+
values[index] = value; });
|
|
102
|
+
presented = true;
|
|
103
|
+
// Presentation follows accepted canonical updates, while the promise keeps
|
|
104
|
+
// each shared source caller's original result (including duplicate positions).
|
|
105
|
+
onUpdate({ status: initialRetry === undefined ? "fresh" : "retained", data: [...values] });
|
|
106
|
+
if (initialRetry !== undefined && active())
|
|
107
|
+
onUpdate({ status: "retry", error: initialRetry.error });
|
|
108
|
+
if (strict)
|
|
109
|
+
detach();
|
|
110
|
+
return data;
|
|
111
|
+
});
|
|
112
|
+
return { promise,
|
|
113
|
+
refresh: ({ force = true } = {}) => active() && presented && !strict ? refresh(force) : undefined,
|
|
114
|
+
dispose() { disposed = true; detach(); disposeChildren(); },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ReadResourceUpdate<T> = {
|
|
2
|
+
status: "retained" | "fresh";
|
|
3
|
+
data: T;
|
|
4
|
+
} | {
|
|
5
|
+
status: "refreshing";
|
|
6
|
+
} | {
|
|
7
|
+
status: "retry";
|
|
8
|
+
error: unknown;
|
|
9
|
+
} | {
|
|
10
|
+
status: "failed";
|
|
11
|
+
error: unknown;
|
|
12
|
+
} | {
|
|
13
|
+
status: "expired";
|
|
14
|
+
} | {
|
|
15
|
+
status: "invalidated";
|
|
16
|
+
};
|
|
17
|
+
export type ReadResourceSnapshot<T> = {
|
|
18
|
+
data: T;
|
|
19
|
+
confirmedAt: number;
|
|
20
|
+
};
|
|
21
|
+
export type ReadResourceObservation<T> = {
|
|
22
|
+
promise: Promise<T>;
|
|
23
|
+
refresh?: (options?: {
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}) => Promise<void> | undefined;
|
|
26
|
+
dispose: () => void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Mounted presentation only: hosts retain cache, transport, identity and initial
|
|
30
|
+
* read policy. Accepting a snapshot preserves its source age; a successful
|
|
31
|
+
* refresh does not require persistence to have survived an unrelated write.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createReadResourceObserver<T, Snapshot>({ enabled, current, now, freshForMs, canRetain, isConnected, canonical, captureSnapshot, read, retireSnapshot, classifyFailure, subscribeWake, onUpdate, onData, onError, }: {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
current: () => boolean;
|
|
36
|
+
now: () => number;
|
|
37
|
+
freshForMs: number;
|
|
38
|
+
canRetain: (data: T) => boolean;
|
|
39
|
+
isConnected?: () => boolean;
|
|
40
|
+
canonical: () => ReadResourceSnapshot<T> | undefined;
|
|
41
|
+
captureSnapshot: () => Snapshot;
|
|
42
|
+
read: () => Promise<ReadResourceSnapshot<T>>;
|
|
43
|
+
retireSnapshot: (snapshot: Snapshot) => Promise<void>;
|
|
44
|
+
classifyFailure: (error: unknown) => "transient" | "terminal" | "superseded" | "retired";
|
|
45
|
+
subscribeWake?: (listener: () => Promise<void>) => () => void;
|
|
46
|
+
onUpdate: (update: ReadResourceUpdate<T>) => void;
|
|
47
|
+
onData?: (data: T) => void;
|
|
48
|
+
onError?: (error: unknown) => void;
|
|
49
|
+
}): {
|
|
50
|
+
accept: (update: ReadResourceUpdate<T>, age?: number) => void;
|
|
51
|
+
canRefresh: () => boolean;
|
|
52
|
+
refresh: ({ force }?: {
|
|
53
|
+
force?: boolean | undefined;
|
|
54
|
+
}) => Promise<void> | undefined;
|
|
55
|
+
retire(): void;
|
|
56
|
+
dispose(): void;
|
|
57
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounted presentation only: hosts retain cache, transport, identity and initial
|
|
3
|
+
* read policy. Accepting a snapshot preserves its source age; a successful
|
|
4
|
+
* refresh does not require persistence to have survived an unrelated write.
|
|
5
|
+
*/
|
|
6
|
+
export function createReadResourceObserver({ enabled = true, current, now, freshForMs, canRetain, isConnected = () => true, canonical, captureSnapshot, read, retireSnapshot, classifyFailure, subscribeWake, onUpdate, onData, onError, }) {
|
|
7
|
+
let disposed = false;
|
|
8
|
+
let retired = false;
|
|
9
|
+
let confirmedAt;
|
|
10
|
+
let retainable = true;
|
|
11
|
+
let pending;
|
|
12
|
+
let stop = () => { };
|
|
13
|
+
const detach = () => { const cleanup = stop; stop = () => { }; cleanup(); };
|
|
14
|
+
const active = () => !disposed && !retired && current();
|
|
15
|
+
const canRefresh = () => enabled && active() && retainable && confirmedAt !== undefined;
|
|
16
|
+
const accept = (update, age = now()) => {
|
|
17
|
+
if (!active())
|
|
18
|
+
return;
|
|
19
|
+
if ("data" in update) {
|
|
20
|
+
confirmedAt = age;
|
|
21
|
+
retainable = canRetain(update.data);
|
|
22
|
+
onData?.(update.data);
|
|
23
|
+
}
|
|
24
|
+
if (active())
|
|
25
|
+
onUpdate(update);
|
|
26
|
+
};
|
|
27
|
+
const adoptCanonical = () => {
|
|
28
|
+
const snapshot = canonical();
|
|
29
|
+
if (!snapshot || !active() || now() - snapshot.confirmedAt >= freshForMs)
|
|
30
|
+
return false;
|
|
31
|
+
accept({ status: "fresh", data: snapshot.data }, snapshot.confirmedAt);
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
const run = async (force) => {
|
|
35
|
+
if (!canRefresh())
|
|
36
|
+
return;
|
|
37
|
+
if (!force && now() - confirmedAt < freshForMs)
|
|
38
|
+
return;
|
|
39
|
+
if (!isConnected()) {
|
|
40
|
+
accept({ status: "retry", error: new Error("Resource unavailable while disconnected") });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!force && adoptCanonical())
|
|
44
|
+
return;
|
|
45
|
+
accept({ status: "refreshing" });
|
|
46
|
+
if (!canRefresh())
|
|
47
|
+
return;
|
|
48
|
+
const snapshot = captureSnapshot();
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
result = await read();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!active())
|
|
55
|
+
return;
|
|
56
|
+
const failure = classifyFailure(error);
|
|
57
|
+
if (failure === "superseded") {
|
|
58
|
+
adoptCanonical();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (failure === "retired")
|
|
62
|
+
return;
|
|
63
|
+
if (onError)
|
|
64
|
+
onError(error);
|
|
65
|
+
else
|
|
66
|
+
console.error("[Bayon offline] Mounted resource refresh failed.", { error });
|
|
67
|
+
if (failure === "transient")
|
|
68
|
+
accept({ status: "retry", error });
|
|
69
|
+
else {
|
|
70
|
+
try {
|
|
71
|
+
await retireSnapshot(snapshot);
|
|
72
|
+
}
|
|
73
|
+
catch (storageError) {
|
|
74
|
+
// Storage cleanup cannot turn a known denial into retained presentation.
|
|
75
|
+
console.error("[Bayon offline] Unable to retire a mounted read snapshot.", { error: storageError });
|
|
76
|
+
}
|
|
77
|
+
if (!active())
|
|
78
|
+
return;
|
|
79
|
+
confirmedAt = undefined;
|
|
80
|
+
accept({ status: "failed", error });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
accept({ status: "fresh", data: result.data }, result.confirmedAt);
|
|
85
|
+
};
|
|
86
|
+
const refresh = ({ force = true } = {}) => {
|
|
87
|
+
if (!canRefresh())
|
|
88
|
+
return;
|
|
89
|
+
// Install ownership before callbacks can synchronously request another read.
|
|
90
|
+
if (!pending)
|
|
91
|
+
pending = Promise.resolve().then(() => run(force)).finally(() => { pending = undefined; });
|
|
92
|
+
return pending;
|
|
93
|
+
};
|
|
94
|
+
if (enabled && subscribeWake)
|
|
95
|
+
stop = subscribeWake(() => refresh({ force: false }) ?? Promise.resolve());
|
|
96
|
+
return {
|
|
97
|
+
accept, canRefresh, refresh,
|
|
98
|
+
retire() {
|
|
99
|
+
if (disposed || retired)
|
|
100
|
+
return;
|
|
101
|
+
retired = true;
|
|
102
|
+
detach();
|
|
103
|
+
// Clearing protected presentation is safe even after the host changed scope.
|
|
104
|
+
onUpdate({ status: "invalidated" });
|
|
105
|
+
},
|
|
106
|
+
dispose() { disposed = true; detach(); },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bindBrowserReadLifecycle = bindBrowserReadLifecycle;
|
|
4
|
+
/** Native wake binding only; hosts own visibility, eligibility and request sharing. */
|
|
5
|
+
function bindBrowserReadLifecycle(targets, notify) {
|
|
6
|
+
const report = (error) => console.error("[Bayon offline] Browser read refresh failed.", { error });
|
|
7
|
+
const wake = () => {
|
|
8
|
+
try {
|
|
9
|
+
void notify().catch(report);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
report(error);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
targets.window.addEventListener("focus", wake);
|
|
16
|
+
targets.window.addEventListener("online", wake);
|
|
17
|
+
targets.document.addEventListener("visibilitychange", wake);
|
|
18
|
+
let stopped = false;
|
|
19
|
+
return () => {
|
|
20
|
+
if (stopped)
|
|
21
|
+
return;
|
|
22
|
+
stopped = true;
|
|
23
|
+
targets.window.removeEventListener("focus", wake);
|
|
24
|
+
targets.window.removeEventListener("online", wake);
|
|
25
|
+
targets.document.removeEventListener("visibilitychange", wake);
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist-cjs/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from "./readCache.js";
|
|
2
|
+
export * from "./readResourceObserver.js";
|
|
3
|
+
export * from "./readResourceBatch.js";
|
|
4
|
+
export * from "./browserReadLifecycle.js";
|
|
2
5
|
export * from "./pendingWrites.js";
|
|
3
6
|
export * from "./browserRecovery.js";
|
|
4
7
|
export * from "./browserCoordination.js";
|
package/dist-cjs/index.js
CHANGED
|
@@ -16,6 +16,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.projectPendingWriteFinalState = exports.OfflineStorageError = void 0;
|
|
18
18
|
__exportStar(require("./readCache"), exports);
|
|
19
|
+
__exportStar(require("./readResourceObserver"), exports);
|
|
20
|
+
__exportStar(require("./readResourceBatch"), exports);
|
|
21
|
+
__exportStar(require("./browserReadLifecycle"), exports);
|
|
19
22
|
__exportStar(require("./pendingWrites"), exports);
|
|
20
23
|
__exportStar(require("./browserRecovery"), exports);
|
|
21
24
|
__exportStar(require("./browserCoordination"), exports);
|
|
@@ -38,7 +38,8 @@ function projectPendingWriteFinalState(records, getKey, { isBarrier = () => true
|
|
|
38
38
|
const previous = latest.get(key);
|
|
39
39
|
if (previous) {
|
|
40
40
|
superseded.add(previous);
|
|
41
|
-
|
|
41
|
+
// Every latest entry creates its matching dependency list below.
|
|
42
|
+
for (const effect of dependents.get(key))
|
|
42
43
|
superseded.add(effect);
|
|
43
44
|
}
|
|
44
45
|
latest.set(key, record);
|
|
@@ -151,9 +151,9 @@ export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
|
|
|
151
151
|
sync: PendingWriteSync<Payload>;
|
|
152
152
|
}): Promise<SyncPendingWriteResult>;
|
|
153
153
|
/**
|
|
154
|
-
* Replays selected writes oldest first
|
|
155
|
-
*
|
|
156
|
-
* mutation semantics.
|
|
154
|
+
* Replays selected writes oldest first. Identical overlapping calls coalesce;
|
|
155
|
+
* calls with different options run serially for the same store and scope.
|
|
156
|
+
* Callers own the selection predicate and idempotent remote mutation semantics.
|
|
157
157
|
*/
|
|
158
158
|
export declare function syncPendingWrites<Payload>({ now, scopeKey, select, stopOnFailure, store, sync, }: {
|
|
159
159
|
now?: () => string;
|
|
@@ -299,30 +299,34 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
|
|
|
299
299
|
: { status: "superseded" };
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
302
|
-
* Replays selected writes oldest first
|
|
303
|
-
*
|
|
304
|
-
* mutation semantics.
|
|
302
|
+
* Replays selected writes oldest first. Identical overlapping calls coalesce;
|
|
303
|
+
* calls with different options run serially for the same store and scope.
|
|
304
|
+
* Callers own the selection predicate and idempotent remote mutation semantics.
|
|
305
305
|
*/
|
|
306
|
-
function syncPendingWrites({ now, scopeKey = "all", select =
|
|
306
|
+
function syncPendingWrites({ now, scopeKey = "all", select = selectAllPendingWrites, stopOnFailure = false, store, sync, }) {
|
|
307
307
|
let inFlightByScope = pendingWriteSyncs.get(store);
|
|
308
308
|
if (!inFlightByScope) {
|
|
309
309
|
inFlightByScope = new Map();
|
|
310
310
|
pendingWriteSyncs.set(store, inFlightByScope);
|
|
311
311
|
}
|
|
312
312
|
const existing = inFlightByScope.get(scopeKey);
|
|
313
|
-
if (existing
|
|
314
|
-
|
|
313
|
+
if (existing &&
|
|
314
|
+
existing.select === select &&
|
|
315
|
+
existing.sync === sync &&
|
|
316
|
+
existing.now === now &&
|
|
317
|
+
existing.stopOnFailure === stopOnFailure) {
|
|
318
|
+
return existing.promise;
|
|
315
319
|
}
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
});
|
|
323
|
-
inFlightByScope.set(scopeKey, syncRun);
|
|
320
|
+
const run = () => performPendingWriteSync({ now, select, stopOnFailure, store, sync });
|
|
321
|
+
// Product decision: distinct filters must never share counts or run in
|
|
322
|
+
// parallel; each waits for the preceding scope run and uses its own policy.
|
|
323
|
+
const syncRun = existing ? existing.promise.then(run, run) : run();
|
|
324
|
+
const entry = { now, select, stopOnFailure, sync, promise: syncRun };
|
|
325
|
+
inFlightByScope.set(scopeKey, entry);
|
|
324
326
|
const clearInFlight = () => {
|
|
325
|
-
inFlightByScope.
|
|
327
|
+
if (inFlightByScope.get(scopeKey) === entry) {
|
|
328
|
+
inFlightByScope.delete(scopeKey);
|
|
329
|
+
}
|
|
326
330
|
};
|
|
327
331
|
void syncRun.then(clearInFlight, clearInFlight);
|
|
328
332
|
return syncRun;
|
|
@@ -360,6 +364,7 @@ function summarizePendingWrites(records) {
|
|
|
360
364
|
return summary;
|
|
361
365
|
}, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
|
|
362
366
|
}
|
|
367
|
+
const selectAllPendingWrites = () => true;
|
|
363
368
|
const pendingWriteSyncs = new WeakMap();
|
|
364
369
|
async function performPendingWriteSync({ now, select, stopOnFailure, store, sync, }) {
|
|
365
370
|
const summary = {
|
package/dist-cjs/readCache.d.ts
CHANGED
|
@@ -13,14 +13,21 @@ export type ReadCacheDriver = {
|
|
|
13
13
|
clear: () => Promise<void>;
|
|
14
14
|
close?: () => Promise<void>;
|
|
15
15
|
delete: (key: string) => Promise<void>;
|
|
16
|
+
deleteIf?: (key: string, predicate: (value: unknown | null) => boolean) => Promise<boolean>;
|
|
16
17
|
get: (key: string) => Promise<unknown | null>;
|
|
17
18
|
listEntries?: () => Promise<Array<{
|
|
18
19
|
key: string;
|
|
19
20
|
value: unknown;
|
|
20
21
|
}>>;
|
|
21
22
|
set: (key: string, value: unknown) => Promise<void>;
|
|
23
|
+
setIf?: (key: string, value: unknown, predicate: (value: unknown | null) => boolean) => Promise<boolean>;
|
|
22
24
|
};
|
|
23
25
|
export type ReadCache = {
|
|
26
|
+
/** Atomic ownership proof for opt-in canonical presentation; version is captured at commit. */
|
|
27
|
+
commitIfVersion?: <T>(key: string, value: T, version: string, snapshot: ReadCacheHit<unknown> | null, confirmedAt?: string) => Promise<{
|
|
28
|
+
written: boolean;
|
|
29
|
+
version: string;
|
|
30
|
+
}>;
|
|
24
31
|
/**
|
|
25
32
|
* Resolves a stable cache for the duration of a resource load. Dynamic
|
|
26
33
|
* adapters use this to pin the active account before asynchronous work.
|
|
@@ -29,12 +36,15 @@ export type ReadCache = {
|
|
|
29
36
|
clear: () => Promise<void>;
|
|
30
37
|
close: () => Promise<void>;
|
|
31
38
|
delete: (key: string) => Promise<void>;
|
|
39
|
+
/** Retire only a snapshot still owned by the captured generation. */
|
|
40
|
+
deleteIfVersion?: (key: string, version: string, snapshot: ReadCacheHit<unknown>) => Promise<boolean>;
|
|
32
41
|
/** Advances a key's generation without deleting its retained value. */
|
|
33
42
|
invalidate?: (key: string) => void;
|
|
34
43
|
get: <T>(key: string) => Promise<ReadCacheHit<T> | null>;
|
|
35
44
|
getVersion: (key: string) => string;
|
|
36
45
|
set: <T>(key: string, value: T) => Promise<void>;
|
|
37
|
-
setIfVersion: <T>(key: string, value: T, version: string) => Promise<boolean>;
|
|
46
|
+
setIfVersion: <T>(key: string, value: T, version: string, snapshot?: ReadCacheHit<unknown> | null, confirmedAt?: string) => Promise<boolean>;
|
|
47
|
+
setIfVersionAt?: <T>(key: string, value: T, version: string, snapshot: ReadCacheHit<unknown> | null, confirmedAt: string) => Promise<boolean>;
|
|
38
48
|
};
|
|
39
49
|
export type ReadCachePolicy = {
|
|
40
50
|
/** Time that a cached value can suppress a remote read. */
|
|
@@ -42,9 +52,10 @@ export type ReadCachePolicy = {
|
|
|
42
52
|
/** Maximum age at which cached data remains eligible as an offline fallback. */
|
|
43
53
|
retainForMs: number;
|
|
44
54
|
/** Behavior when revalidation fails while retained data is available. */
|
|
45
|
-
refreshFailure: "use-retained" | "throw";
|
|
55
|
+
refreshFailure: "use-retained" | "throw" | ((error: unknown) => "use-retained" | "throw" | "retire");
|
|
46
56
|
};
|
|
47
57
|
export type ReadCacheStatus = "fresh" | "stale" | "miss";
|
|
58
|
+
export type ReadCacheProvenance = "cache-hit" | "source-confirmed" | "retained-fallback";
|
|
48
59
|
/** Payload-free cache lifecycle events suitable for host-owned telemetry. */
|
|
49
60
|
export type ReadCacheOutcome = "fresh-hit" | "stale-hit" | "cache-miss" | "forced-refresh" | "loader-success" | "loader-failure";
|
|
50
61
|
export type ReadThroughCacheResult<T> = {
|
|
@@ -53,12 +64,27 @@ export type ReadThroughCacheResult<T> = {
|
|
|
53
64
|
fromCache: boolean;
|
|
54
65
|
stale: boolean;
|
|
55
66
|
status: ReadCacheStatus;
|
|
67
|
+
provenance?: ReadCacheProvenance;
|
|
68
|
+
confirmedAt?: string;
|
|
69
|
+
savedAt?: string;
|
|
70
|
+
originalError?: unknown;
|
|
56
71
|
};
|
|
57
72
|
export type ReadCacheResourceUpdate<T> = {
|
|
58
73
|
data: T;
|
|
59
74
|
error?: Error;
|
|
60
75
|
fromCache: boolean;
|
|
61
76
|
status: ReadCacheStatus;
|
|
77
|
+
provenance?: ReadCacheProvenance;
|
|
78
|
+
confirmedAt?: string;
|
|
79
|
+
savedAt?: string;
|
|
80
|
+
originalError?: unknown;
|
|
81
|
+
};
|
|
82
|
+
export type ReadCacheLoader<T> = {
|
|
83
|
+
loadFresh: () => Promise<T>;
|
|
84
|
+
loadFreshResult?: never;
|
|
85
|
+
} | {
|
|
86
|
+
loadFresh?: never;
|
|
87
|
+
loadFreshResult: () => Promise<ReadThroughCacheResult<T>>;
|
|
62
88
|
};
|
|
63
89
|
export type BrowserReadCacheOptions = Partial<Pick<BrowserKeyValueDriverOptions, "databaseVersion" | "indexedDB">> & {
|
|
64
90
|
databaseName: string;
|
|
@@ -84,29 +110,33 @@ export declare function pruneReadCache({ cache, maxAgeMs, now, }: {
|
|
|
84
110
|
now?: () => number;
|
|
85
111
|
}): Promise<number>;
|
|
86
112
|
export declare function createReadCacheKey({ namespace, params, prefix, scope, }: ReadCacheKeyOptions): string;
|
|
87
|
-
export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, }: {
|
|
113
|
+
export declare function readThroughCache<T>({ cache, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, requireSource, }: {
|
|
88
114
|
cache: ReadCache;
|
|
89
115
|
/**
|
|
90
116
|
* Revalidates after advancing the key generation, retaining the prior value
|
|
91
117
|
* as a possible fallback if refresh fails.
|
|
92
118
|
*/
|
|
93
119
|
forceFresh?: boolean;
|
|
120
|
+
includeProvenance?: boolean;
|
|
94
121
|
key: string;
|
|
95
|
-
loadFresh: () => Promise<T>;
|
|
96
122
|
maxAgeMs?: number;
|
|
97
123
|
onOutcome?: (outcome: ReadCacheOutcome) => void;
|
|
98
124
|
policy?: ReadCachePolicy;
|
|
99
|
-
|
|
100
|
-
|
|
125
|
+
requireSource?: boolean;
|
|
126
|
+
} & ReadCacheLoader<T>): Promise<ReadThroughCacheResult<T>>;
|
|
127
|
+
export declare function loadReadCacheResource<T>({ cache, deliveryPolicy, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, requireSource, }: {
|
|
101
128
|
cache: ReadCache;
|
|
129
|
+
/** Opt in to presenting the eligible canonical cache after conditional persistence. */
|
|
130
|
+
deliveryPolicy?: "immediate" | "cache-current";
|
|
102
131
|
/** Revalidate while retaining eligible data as fallback on loader failure. */
|
|
103
132
|
forceFresh?: boolean;
|
|
133
|
+
includeProvenance?: boolean;
|
|
104
134
|
key: string;
|
|
105
|
-
loadFresh: () => Promise<T>;
|
|
106
135
|
maxAgeMs?: number;
|
|
107
136
|
onOutcome?: (outcome: ReadCacheOutcome) => void;
|
|
108
137
|
policy?: ReadCachePolicy;
|
|
109
138
|
onCacheWriteError?: (error: Error) => void;
|
|
110
139
|
onUpdate: (update: ReadCacheResourceUpdate<T>) => void;
|
|
111
140
|
skipCache?: boolean;
|
|
112
|
-
|
|
141
|
+
requireSource?: boolean;
|
|
142
|
+
} & ReadCacheLoader<T>): Promise<T>;
|