@bayonai/offline 0.11.0 → 0.13.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 +13 -0
- package/README.md +44 -1
- package/RECOVERY.md +13 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pendingChangeTracker.d.ts +71 -0
- package/dist/pendingChangeTracker.js +199 -0
- package/dist/pendingWriteConsolidation.js +2 -1
- package/dist/readCache.d.ts +31 -8
- package/dist/readCache.js +290 -37
- package/dist-cjs/index.d.ts +1 -0
- package/dist-cjs/index.js +1 -0
- package/dist-cjs/pendingChangeTracker.d.ts +71 -0
- package/dist-cjs/pendingChangeTracker.js +204 -0
- package/dist-cjs/pendingWriteConsolidation.js +2 -1
- package/dist-cjs/readCache.d.ts +31 -8
- package/dist-cjs/readCache.js +290 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 - 2026-11-06
|
|
4
|
+
|
|
5
|
+
- Add opt-in host-classified read failure handling, conditional terminal
|
|
6
|
+
retirement, source-required reads and layered provenance without renewing
|
|
7
|
+
retained cache age. Snapshot identifiers protect newer cross-instance writes;
|
|
8
|
+
existing version-one envelopes remain readable without a database upgrade.
|
|
9
|
+
|
|
10
|
+
## 0.12.0 - 2026-11-06
|
|
11
|
+
|
|
12
|
+
- Add framework-neutral pending-change projection and observable tracker, with host-supplied existing queues/drafts, account and stale-read fencing, stable external-store snapshots, grouping/counts/guidance, and retained last known records on read failures.
|
|
13
|
+
|
|
14
|
+
- Keep durable read failures visible throughout a retry, independently of capture failures; only current durable proof clears the failed read.
|
|
15
|
+
|
|
3
16
|
## 0.11.0
|
|
4
17
|
|
|
5
18
|
- Add opt-in `projectPendingWriteFinalState` and store `consolidate` for final absolute setters. IndexedDB and memory retain winners and remove competing records atomically. Unknown effects, claims and cancellation deadlines remain ordering barriers. Legacy custom drivers retain records safely.
|
package/README.md
CHANGED
|
@@ -74,6 +74,15 @@ exceptions are logged and isolated from the read.
|
|
|
74
74
|
|
|
75
75
|
When refresh fails, `refreshFailure: "use-retained"` returns retained data
|
|
76
76
|
with `status: "stale"` and the error; `"throw"` propagates the loader error.
|
|
77
|
+
An opt-in callback can classify the original failure as `"use-retained"`,
|
|
78
|
+
`"throw"`, or `"retire"`. Retire propagates the original failure and
|
|
79
|
+
conditionally removes the captured snapshot; a later confirmation is preserved.
|
|
80
|
+
Built-in memory and IndexedDB drivers support atomic snapshot comparison.
|
|
81
|
+
Custom adapters must implement `deleteIfVersion` with equivalent atomic
|
|
82
|
+
ownership checks; without it, retirement is logged and cannot be guaranteed.
|
|
83
|
+
New envelopes carry an opaque snapshot identifier. Existing version-one
|
|
84
|
+
envelopes remain readable; legacy comparison is limited to serializable values
|
|
85
|
+
and cannot distinguish identical writes by older, identifier-unaware clients.
|
|
77
86
|
This also applies to forced refresh. Set `forceFresh: true` on
|
|
78
87
|
`loadReadCacheResource` or `readThroughCache` to bypass freshness, retain
|
|
79
88
|
eligible data as the failure fallback, and advance the cache generation so an
|
|
@@ -81,6 +90,19 @@ older in-flight load cannot overwrite the refresh. `readThroughCache` returns
|
|
|
81
90
|
the `status` field alongside its existing `fromCache`, `stale`, and `error`
|
|
82
91
|
fields.
|
|
83
92
|
|
|
93
|
+
Set `includeProvenance: true` to receive `cache-hit`, `source-confirmed`, or
|
|
94
|
+
`retained-fallback` provenance. Cache-derived results include their original
|
|
95
|
+
`savedAt`; fallback also carries `originalError` without normalizing it. Layered
|
|
96
|
+
adapters may use `loadFreshResult` to pass a provenance-bearing
|
|
97
|
+
`readThroughCache` result. Cached and fallback values from an inner layer are
|
|
98
|
+
not persisted as fresh outer confirmations. `requireSource: true` rejects
|
|
99
|
+
cached or fallback results and preserves a fallback's original failure. The
|
|
100
|
+
host still owns authorization, account scoping and retention durations.
|
|
101
|
+
Source confirmations include `confirmedAt` when provenance is requested;
|
|
102
|
+
the built-in cache preserves that time on outer writes. Custom adapters may
|
|
103
|
+
implement `setIfVersionAt` to preserve it. Without that capability, layered
|
|
104
|
+
confirmations are delivered but not persisted by the outer adapter.
|
|
105
|
+
|
|
84
106
|
For compatibility, callers that omit `policy` retain the previous `maxAgeMs`
|
|
85
107
|
freshness behavior and have no age-based fallback expiry. New callers should
|
|
86
108
|
use `policy`; when supplied it takes precedence over the legacy `maxAgeMs`.
|
|
@@ -383,7 +405,7 @@ pnpm --filter @bayonai/offline pack:smoke
|
|
|
383
405
|
2. Run the 100% coverage gate and production dependency audit.
|
|
384
406
|
3. Run `pnpm --filter @bayonai/offline pack:smoke` and confirm the tarball
|
|
385
407
|
includes only `package.json`, `dist`, `dist-cjs`, `README.md`,
|
|
386
|
-
`CHANGELOG.md`, and `LICENSE`.
|
|
408
|
+
`CHANGELOG.md`, `RECOVERY.md`, and `LICENSE`.
|
|
387
409
|
4. Install the generated tarball into an isolated consumer and verify ESM,
|
|
388
410
|
CommonJS, and TypeScript declaration imports.
|
|
389
411
|
5. Confirm `package.json` metadata, version, repository, license, and
|
|
@@ -464,3 +486,24 @@ cancel with superseded intent (for example, an award for a cancelled completion)
|
|
|
464
486
|
Independent effects can opt out of being barriers with `isBarrier`; they stay in
|
|
465
487
|
the queue. Claims and cancellation deadlines always fence both sides regardless
|
|
466
488
|
of these callbacks. All callbacks are synchronous, pure host policy.
|
|
489
|
+
|
|
490
|
+
### Observe pending changes without owning another queue
|
|
491
|
+
|
|
492
|
+
`createPendingChangeTracker<T>()` observes host-supplied `load(accountKey)` and
|
|
493
|
+
optional synchronous `capture(accountKey)` sources. Supply `subscribe(notify)`
|
|
494
|
+
for existing command-store, draft and browser source notifications. It refreshes
|
|
495
|
+
immediately, exposes stable `getSnapshot`/`subscribe` methods for external-store
|
|
496
|
+
consumers, and releases its source subscription with `close()`.
|
|
497
|
+
|
|
498
|
+
Hosts project real rows into `PendingChangeItem` (account, group, label, status
|
|
499
|
+
and attention/storage/sending flags); generic host fields survive projection.
|
|
500
|
+
`createPendingChangeSnapshot` also supports standalone synchronous consumers.
|
|
501
|
+
Captured rows replace same-ID durable rows, groups preserve host order, and all
|
|
502
|
+
counts and guidance derive from the resulting account-scoped rows. IDs must be
|
|
503
|
+
unique across the host's sources unless replacement is intended.
|
|
504
|
+
|
|
505
|
+
`setAccountKey` clears the previous account immediately and fences late reads.
|
|
506
|
+
Read failures retain the current account's last known rows and expose `readError`;
|
|
507
|
+
provide `onError` for host diagnostics (otherwise the tracker logs to console).
|
|
508
|
+
Call `setOffline` when connectivity changes. The tracker never persists, executes,
|
|
509
|
+
consolidates or retires operations, and adds no schema, jobs or provider imports.
|
package/RECOVERY.md
CHANGED
|
@@ -63,5 +63,17 @@ No queue record is discarded or reclassified merely because the app reopened.
|
|
|
63
63
|
- Additional regression coverage: `src/recoveryRemediation.test.ts` exercises the
|
|
64
64
|
four audited failure compositions with real IndexedDB reads and safe cleanup.
|
|
65
65
|
|
|
66
|
-
|
|
67
66
|
Firestore server-only reads may report either exact “Failed to get documents from server.” or the exact SDK cache-retrieval guidance. These known messages classify as transient only when no structured code is present; terminal and unknown structured codes remain authoritative. Arbitrary messages containing connectivity words do not qualify.
|
|
67
|
+
|
|
68
|
+
### Shared inspection source uncertainty
|
|
69
|
+
|
|
70
|
+
`pendingChangeTracker` retains a failed durable read throughout a deferred retry,
|
|
71
|
+
including when synchronous capture succeeds. Only current-generation durable
|
|
72
|
+
proof clears that error. Capture failures remain independent and survive a
|
|
73
|
+
successful durable read until capture itself succeeds. Last known account rows
|
|
74
|
+
stay inspectable; account changes fence prior rows/errors and stale completions.
|
|
75
|
+
Hosts must also project command-provider loading/hydration failures separately:
|
|
76
|
+
an independent successful draft read does not establish an empty command queue.
|
|
77
|
+
Inspection never acquires replay ownership. Coverage:
|
|
78
|
+
`src/pendingChangeTracker.test.ts` and host
|
|
79
|
+
`src/components/offline/PendingChangesIndicator.providers.test.tsx`.
|
package/dist/index.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export * from "./readBoundary.js";
|
|
|
9
9
|
export * from "./recoveryPolicy.js";
|
|
10
10
|
export * from "./pendingWriteSupersession.js";
|
|
11
11
|
export { projectPendingWriteFinalState, type PendingWriteSupersessionKey, type PendingWriteConsolidationOptions, } from "./pendingWriteConsolidation.js";
|
|
12
|
+
export * from "./pendingChangeTracker.js";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type PendingChangeStatus = "draft" | "waiting" | "sending" | "attention" | "storage-error";
|
|
2
|
+
/** Hosts own domain labels and recovery actions; this is only a queue projection. */
|
|
3
|
+
export type PendingChangeItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
accountKey: string;
|
|
6
|
+
groupKey: string;
|
|
7
|
+
groupTitle: string;
|
|
8
|
+
label: string;
|
|
9
|
+
status: PendingChangeStatus;
|
|
10
|
+
attemptedText?: string;
|
|
11
|
+
needsAttention: boolean;
|
|
12
|
+
storageError: boolean;
|
|
13
|
+
sending: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type PendingChangeGroup<T extends PendingChangeItem = PendingChangeItem> = {
|
|
16
|
+
key: string;
|
|
17
|
+
title: string;
|
|
18
|
+
items: readonly T[];
|
|
19
|
+
};
|
|
20
|
+
export type PendingChangeSnapshot<T extends PendingChangeItem = PendingChangeItem> = {
|
|
21
|
+
accountKey: string | null;
|
|
22
|
+
items: readonly T[];
|
|
23
|
+
groups: readonly PendingChangeGroup<T>[];
|
|
24
|
+
count: number;
|
|
25
|
+
draftCount: number;
|
|
26
|
+
readyCount: number;
|
|
27
|
+
attentionCount: number;
|
|
28
|
+
storageErrorCount: number;
|
|
29
|
+
sendingCount: number;
|
|
30
|
+
guidance: string;
|
|
31
|
+
loading: boolean;
|
|
32
|
+
readError: unknown | null;
|
|
33
|
+
};
|
|
34
|
+
/** Groups host-scoped rows without imposing account or domain semantics. */
|
|
35
|
+
export declare function groupPendingChangeItems<T extends {
|
|
36
|
+
groupKey: string;
|
|
37
|
+
groupTitle: string;
|
|
38
|
+
}>(items: readonly T[]): Array<{
|
|
39
|
+
key: string;
|
|
40
|
+
title: string;
|
|
41
|
+
items: T[];
|
|
42
|
+
}>;
|
|
43
|
+
/** Same-ID captured edits replace durable rows; insertion order remains stable. */
|
|
44
|
+
export declare function createPendingChangeSnapshot<T extends PendingChangeItem>({ accountKey, items, isOffline, loading, readError, }: {
|
|
45
|
+
accountKey: string | null;
|
|
46
|
+
items: readonly T[];
|
|
47
|
+
isOffline?: boolean;
|
|
48
|
+
loading?: boolean;
|
|
49
|
+
readError?: unknown;
|
|
50
|
+
}): PendingChangeSnapshot<T>;
|
|
51
|
+
export type PendingChangeTracker<T extends PendingChangeItem = PendingChangeItem> = {
|
|
52
|
+
getSnapshot: () => PendingChangeSnapshot<T>;
|
|
53
|
+
subscribe: (listener: () => void) => () => void;
|
|
54
|
+
refresh: () => Promise<void>;
|
|
55
|
+
setAccountKey: (accountKey: string | null) => void;
|
|
56
|
+
setOffline: (isOffline: boolean) => void;
|
|
57
|
+
close: () => void;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Observes existing host queues, never persists or replays changes. Reads fail
|
|
61
|
+
* closed across accounts but retain the current account's last known rows.
|
|
62
|
+
* Hosts must notify for draft, command-store and connectivity source changes.
|
|
63
|
+
*/
|
|
64
|
+
export declare function createPendingChangeTracker<T extends PendingChangeItem>(options: {
|
|
65
|
+
accountKey?: string | null;
|
|
66
|
+
isOffline?: boolean;
|
|
67
|
+
load: (accountKey: string) => Promise<readonly T[]>;
|
|
68
|
+
capture?: (accountKey: string) => readonly T[];
|
|
69
|
+
subscribe?: (onChange: () => void) => () => void;
|
|
70
|
+
onError?: (error: unknown) => void;
|
|
71
|
+
}): PendingChangeTracker<T>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { getPendingChangesGuidance } from "./recoveryPolicy.js";
|
|
2
|
+
/** Groups host-scoped rows without imposing account or domain semantics. */
|
|
3
|
+
export function groupPendingChangeItems(items) {
|
|
4
|
+
const groups = new Map();
|
|
5
|
+
for (const item of items) {
|
|
6
|
+
let group = groups.get(item.groupKey);
|
|
7
|
+
if (!group) {
|
|
8
|
+
group = { key: item.groupKey, title: item.groupTitle, items: [] };
|
|
9
|
+
groups.set(item.groupKey, group);
|
|
10
|
+
}
|
|
11
|
+
group.items.push(item);
|
|
12
|
+
}
|
|
13
|
+
return [...groups.values()];
|
|
14
|
+
}
|
|
15
|
+
/** Same-ID captured edits replace durable rows; insertion order remains stable. */
|
|
16
|
+
export function createPendingChangeSnapshot({ accountKey, items, isOffline = false, loading = false, readError = null, }) {
|
|
17
|
+
const unique = new Map();
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
if (accountKey !== null && item.accountKey === accountKey)
|
|
20
|
+
unique.set(item.id, { ...item });
|
|
21
|
+
}
|
|
22
|
+
const rows = [...unique.values()];
|
|
23
|
+
const groups = groupPendingChangeItems(rows);
|
|
24
|
+
const draftCount = rows.filter((item) => item.status === "draft").length;
|
|
25
|
+
const readyCount = rows.filter((item) => item.status === "waiting" || item.status === "sending").length;
|
|
26
|
+
const attentionCount = rows.filter((item) => item.needsAttention || item.status === "attention").length;
|
|
27
|
+
const storageErrorCount = rows.filter((item) => item.storageError || item.status === "storage-error").length;
|
|
28
|
+
return {
|
|
29
|
+
accountKey,
|
|
30
|
+
items: rows,
|
|
31
|
+
groups,
|
|
32
|
+
count: rows.length,
|
|
33
|
+
draftCount,
|
|
34
|
+
readyCount,
|
|
35
|
+
attentionCount,
|
|
36
|
+
storageErrorCount,
|
|
37
|
+
sendingCount: rows.filter((item) => item.sending || item.status === "sending").length,
|
|
38
|
+
guidance: readError !== null
|
|
39
|
+
? "Pending changes could not be read. Your last known changes are preserved."
|
|
40
|
+
: getPendingChangesGuidance({
|
|
41
|
+
hasReadyChanges: readyCount > 0,
|
|
42
|
+
hasDrafts: draftCount > 0,
|
|
43
|
+
hasErrors: attentionCount > 0 || storageErrorCount > 0 || readError !== null,
|
|
44
|
+
isOffline,
|
|
45
|
+
}),
|
|
46
|
+
loading,
|
|
47
|
+
readError,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Observes existing host queues, never persists or replays changes. Reads fail
|
|
52
|
+
* closed across accounts but retain the current account's last known rows.
|
|
53
|
+
* Hosts must notify for draft, command-store and connectivity source changes.
|
|
54
|
+
*/
|
|
55
|
+
export function createPendingChangeTracker(options) {
|
|
56
|
+
let accountKey = options.accountKey ?? null;
|
|
57
|
+
let isOffline = options.isOffline ?? false;
|
|
58
|
+
let durable = [];
|
|
59
|
+
let captured = [];
|
|
60
|
+
let durableReadError = null;
|
|
61
|
+
let captureReadError = null;
|
|
62
|
+
let loading = false;
|
|
63
|
+
let generation = 0;
|
|
64
|
+
let closed = false;
|
|
65
|
+
const listeners = new Set();
|
|
66
|
+
let snapshot = createPendingChangeSnapshot({
|
|
67
|
+
accountKey,
|
|
68
|
+
items: [],
|
|
69
|
+
isOffline,
|
|
70
|
+
});
|
|
71
|
+
const report = (error) => {
|
|
72
|
+
try {
|
|
73
|
+
if (options.onError)
|
|
74
|
+
options.onError(error);
|
|
75
|
+
else
|
|
76
|
+
console.error("Pending-change tracker could not read host changes; retaining the last known account snapshot.", error);
|
|
77
|
+
}
|
|
78
|
+
catch (callbackError) {
|
|
79
|
+
console.error("Pending-change tracker error reporter failed while reporting a host source failure.", callbackError, error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const publish = () => {
|
|
83
|
+
const publishingGeneration = generation;
|
|
84
|
+
snapshot = createPendingChangeSnapshot({
|
|
85
|
+
accountKey,
|
|
86
|
+
items: [...durable, ...captured],
|
|
87
|
+
isOffline,
|
|
88
|
+
loading,
|
|
89
|
+
readError: durableReadError ?? captureReadError,
|
|
90
|
+
});
|
|
91
|
+
for (const listener of [...listeners]) {
|
|
92
|
+
if (closed || publishingGeneration !== generation)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
listener();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error("Pending-change tracker subscriber failed; other subscribers will still receive updates.", error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const refresh = async () => {
|
|
103
|
+
if (closed)
|
|
104
|
+
return;
|
|
105
|
+
const request = ++generation;
|
|
106
|
+
const requestedAccount = accountKey;
|
|
107
|
+
const current = () => !closed && request === generation && requestedAccount === accountKey;
|
|
108
|
+
if (requestedAccount === null)
|
|
109
|
+
return;
|
|
110
|
+
// Capture callbacks may notify sources or switch accounts synchronously.
|
|
111
|
+
const capture = () => {
|
|
112
|
+
try {
|
|
113
|
+
const rows = options.capture?.(requestedAccount) ?? [];
|
|
114
|
+
if (!current())
|
|
115
|
+
return;
|
|
116
|
+
captured = rows;
|
|
117
|
+
captureReadError = null;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!current())
|
|
121
|
+
return;
|
|
122
|
+
captureReadError = error;
|
|
123
|
+
report(error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
capture();
|
|
127
|
+
if (!current())
|
|
128
|
+
return;
|
|
129
|
+
loading = true;
|
|
130
|
+
publish();
|
|
131
|
+
if (!current())
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
const rows = await options.load(requestedAccount);
|
|
135
|
+
if (!current())
|
|
136
|
+
return;
|
|
137
|
+
// A successful capture cannot prove durable storage recovered.
|
|
138
|
+
durableReadError = null;
|
|
139
|
+
durable = rows;
|
|
140
|
+
// A durable read may retire host acceptance receipts without emitting an
|
|
141
|
+
// event. Recapture after proof so drained receipts cannot become ghosts.
|
|
142
|
+
capture();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (!current())
|
|
146
|
+
return;
|
|
147
|
+
durableReadError = error;
|
|
148
|
+
report(error);
|
|
149
|
+
}
|
|
150
|
+
if (!current())
|
|
151
|
+
return;
|
|
152
|
+
loading = false;
|
|
153
|
+
publish();
|
|
154
|
+
};
|
|
155
|
+
const unsubscribe = options.subscribe?.(() => {
|
|
156
|
+
void refresh();
|
|
157
|
+
});
|
|
158
|
+
const tracker = {
|
|
159
|
+
getSnapshot: () => snapshot,
|
|
160
|
+
subscribe: (listener) => {
|
|
161
|
+
if (closed)
|
|
162
|
+
return () => { };
|
|
163
|
+
listeners.add(listener);
|
|
164
|
+
return () => {
|
|
165
|
+
listeners.delete(listener);
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
refresh,
|
|
169
|
+
setAccountKey: (next) => {
|
|
170
|
+
if (closed || next === accountKey)
|
|
171
|
+
return;
|
|
172
|
+
++generation;
|
|
173
|
+
accountKey = next;
|
|
174
|
+
durable = [];
|
|
175
|
+
captured = [];
|
|
176
|
+
durableReadError = null;
|
|
177
|
+
captureReadError = null;
|
|
178
|
+
loading = false;
|
|
179
|
+
publish();
|
|
180
|
+
void refresh();
|
|
181
|
+
},
|
|
182
|
+
setOffline: (next) => {
|
|
183
|
+
if (closed || next === isOffline)
|
|
184
|
+
return;
|
|
185
|
+
isOffline = next;
|
|
186
|
+
publish();
|
|
187
|
+
},
|
|
188
|
+
close: () => {
|
|
189
|
+
if (closed)
|
|
190
|
+
return;
|
|
191
|
+
closed = true;
|
|
192
|
+
++generation;
|
|
193
|
+
unsubscribe?.();
|
|
194
|
+
listeners.clear();
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
void refresh();
|
|
198
|
+
return tracker;
|
|
199
|
+
}
|
|
@@ -35,7 +35,8 @@ export function projectPendingWriteFinalState(records, getKey, { isBarrier = ()
|
|
|
35
35
|
const previous = latest.get(key);
|
|
36
36
|
if (previous) {
|
|
37
37
|
superseded.add(previous);
|
|
38
|
-
|
|
38
|
+
// Every latest entry creates its matching dependency list below.
|
|
39
|
+
for (const effect of dependents.get(key))
|
|
39
40
|
superseded.add(effect);
|
|
40
41
|
}
|
|
41
42
|
latest.set(key, record);
|
package/dist/readCache.d.ts
CHANGED
|
@@ -13,12 +13,14 @@ 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 = {
|
|
24
26
|
/**
|
|
@@ -29,12 +31,15 @@ export type ReadCache = {
|
|
|
29
31
|
clear: () => Promise<void>;
|
|
30
32
|
close: () => Promise<void>;
|
|
31
33
|
delete: (key: string) => Promise<void>;
|
|
34
|
+
/** Retire only a snapshot still owned by the captured generation. */
|
|
35
|
+
deleteIfVersion?: (key: string, version: string, snapshot: ReadCacheHit<unknown>) => Promise<boolean>;
|
|
32
36
|
/** Advances a key's generation without deleting its retained value. */
|
|
33
37
|
invalidate?: (key: string) => void;
|
|
34
38
|
get: <T>(key: string) => Promise<ReadCacheHit<T> | null>;
|
|
35
39
|
getVersion: (key: string) => string;
|
|
36
40
|
set: <T>(key: string, value: T) => Promise<void>;
|
|
37
|
-
setIfVersion: <T>(key: string, value: T, version: string) => Promise<boolean>;
|
|
41
|
+
setIfVersion: <T>(key: string, value: T, version: string, snapshot?: ReadCacheHit<unknown> | null, confirmedAt?: string) => Promise<boolean>;
|
|
42
|
+
setIfVersionAt?: <T>(key: string, value: T, version: string, snapshot: ReadCacheHit<unknown> | null, confirmedAt: string) => Promise<boolean>;
|
|
38
43
|
};
|
|
39
44
|
export type ReadCachePolicy = {
|
|
40
45
|
/** Time that a cached value can suppress a remote read. */
|
|
@@ -42,9 +47,10 @@ export type ReadCachePolicy = {
|
|
|
42
47
|
/** Maximum age at which cached data remains eligible as an offline fallback. */
|
|
43
48
|
retainForMs: number;
|
|
44
49
|
/** Behavior when revalidation fails while retained data is available. */
|
|
45
|
-
refreshFailure: "use-retained" | "throw";
|
|
50
|
+
refreshFailure: "use-retained" | "throw" | ((error: unknown) => "use-retained" | "throw" | "retire");
|
|
46
51
|
};
|
|
47
52
|
export type ReadCacheStatus = "fresh" | "stale" | "miss";
|
|
53
|
+
export type ReadCacheProvenance = "cache-hit" | "source-confirmed" | "retained-fallback";
|
|
48
54
|
/** Payload-free cache lifecycle events suitable for host-owned telemetry. */
|
|
49
55
|
export type ReadCacheOutcome = "fresh-hit" | "stale-hit" | "cache-miss" | "forced-refresh" | "loader-success" | "loader-failure";
|
|
50
56
|
export type ReadThroughCacheResult<T> = {
|
|
@@ -53,12 +59,27 @@ export type ReadThroughCacheResult<T> = {
|
|
|
53
59
|
fromCache: boolean;
|
|
54
60
|
stale: boolean;
|
|
55
61
|
status: ReadCacheStatus;
|
|
62
|
+
provenance?: ReadCacheProvenance;
|
|
63
|
+
confirmedAt?: string;
|
|
64
|
+
savedAt?: string;
|
|
65
|
+
originalError?: unknown;
|
|
56
66
|
};
|
|
57
67
|
export type ReadCacheResourceUpdate<T> = {
|
|
58
68
|
data: T;
|
|
59
69
|
error?: Error;
|
|
60
70
|
fromCache: boolean;
|
|
61
71
|
status: ReadCacheStatus;
|
|
72
|
+
provenance?: ReadCacheProvenance;
|
|
73
|
+
confirmedAt?: string;
|
|
74
|
+
savedAt?: string;
|
|
75
|
+
originalError?: unknown;
|
|
76
|
+
};
|
|
77
|
+
export type ReadCacheLoader<T> = {
|
|
78
|
+
loadFresh: () => Promise<T>;
|
|
79
|
+
loadFreshResult?: never;
|
|
80
|
+
} | {
|
|
81
|
+
loadFresh?: never;
|
|
82
|
+
loadFreshResult: () => Promise<ReadThroughCacheResult<T>>;
|
|
62
83
|
};
|
|
63
84
|
export type BrowserReadCacheOptions = Partial<Pick<BrowserKeyValueDriverOptions, "databaseVersion" | "indexedDB">> & {
|
|
64
85
|
databaseName: string;
|
|
@@ -84,29 +105,31 @@ export declare function pruneReadCache({ cache, maxAgeMs, now, }: {
|
|
|
84
105
|
now?: () => number;
|
|
85
106
|
}): Promise<number>;
|
|
86
107
|
export declare function createReadCacheKey({ namespace, params, prefix, scope, }: ReadCacheKeyOptions): string;
|
|
87
|
-
export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, }: {
|
|
108
|
+
export declare function readThroughCache<T>({ cache, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, requireSource, }: {
|
|
88
109
|
cache: ReadCache;
|
|
89
110
|
/**
|
|
90
111
|
* Revalidates after advancing the key generation, retaining the prior value
|
|
91
112
|
* as a possible fallback if refresh fails.
|
|
92
113
|
*/
|
|
93
114
|
forceFresh?: boolean;
|
|
115
|
+
includeProvenance?: boolean;
|
|
94
116
|
key: string;
|
|
95
|
-
loadFresh: () => Promise<T>;
|
|
96
117
|
maxAgeMs?: number;
|
|
97
118
|
onOutcome?: (outcome: ReadCacheOutcome) => void;
|
|
98
119
|
policy?: ReadCachePolicy;
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
requireSource?: boolean;
|
|
121
|
+
} & ReadCacheLoader<T>): Promise<ReadThroughCacheResult<T>>;
|
|
122
|
+
export declare function loadReadCacheResource<T>({ cache, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, requireSource, }: {
|
|
101
123
|
cache: ReadCache;
|
|
102
124
|
/** Revalidate while retaining eligible data as fallback on loader failure. */
|
|
103
125
|
forceFresh?: boolean;
|
|
126
|
+
includeProvenance?: boolean;
|
|
104
127
|
key: string;
|
|
105
|
-
loadFresh: () => Promise<T>;
|
|
106
128
|
maxAgeMs?: number;
|
|
107
129
|
onOutcome?: (outcome: ReadCacheOutcome) => void;
|
|
108
130
|
policy?: ReadCachePolicy;
|
|
109
131
|
onCacheWriteError?: (error: Error) => void;
|
|
110
132
|
onUpdate: (update: ReadCacheResourceUpdate<T>) => void;
|
|
111
133
|
skipCache?: boolean;
|
|
112
|
-
|
|
134
|
+
requireSource?: boolean;
|
|
135
|
+
} & ReadCacheLoader<T>): Promise<T>;
|