@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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.groupPendingChangeItems = groupPendingChangeItems;
|
|
4
|
+
exports.createPendingChangeSnapshot = createPendingChangeSnapshot;
|
|
5
|
+
exports.createPendingChangeTracker = createPendingChangeTracker;
|
|
6
|
+
const recoveryPolicy_1 = require("./recoveryPolicy");
|
|
7
|
+
/** Groups host-scoped rows without imposing account or domain semantics. */
|
|
8
|
+
function groupPendingChangeItems(items) {
|
|
9
|
+
const groups = new Map();
|
|
10
|
+
for (const item of items) {
|
|
11
|
+
let group = groups.get(item.groupKey);
|
|
12
|
+
if (!group) {
|
|
13
|
+
group = { key: item.groupKey, title: item.groupTitle, items: [] };
|
|
14
|
+
groups.set(item.groupKey, group);
|
|
15
|
+
}
|
|
16
|
+
group.items.push(item);
|
|
17
|
+
}
|
|
18
|
+
return [...groups.values()];
|
|
19
|
+
}
|
|
20
|
+
/** Same-ID captured edits replace durable rows; insertion order remains stable. */
|
|
21
|
+
function createPendingChangeSnapshot({ accountKey, items, isOffline = false, loading = false, readError = null, }) {
|
|
22
|
+
const unique = new Map();
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
if (accountKey !== null && item.accountKey === accountKey)
|
|
25
|
+
unique.set(item.id, { ...item });
|
|
26
|
+
}
|
|
27
|
+
const rows = [...unique.values()];
|
|
28
|
+
const groups = groupPendingChangeItems(rows);
|
|
29
|
+
const draftCount = rows.filter((item) => item.status === "draft").length;
|
|
30
|
+
const readyCount = rows.filter((item) => item.status === "waiting" || item.status === "sending").length;
|
|
31
|
+
const attentionCount = rows.filter((item) => item.needsAttention || item.status === "attention").length;
|
|
32
|
+
const storageErrorCount = rows.filter((item) => item.storageError || item.status === "storage-error").length;
|
|
33
|
+
return {
|
|
34
|
+
accountKey,
|
|
35
|
+
items: rows,
|
|
36
|
+
groups,
|
|
37
|
+
count: rows.length,
|
|
38
|
+
draftCount,
|
|
39
|
+
readyCount,
|
|
40
|
+
attentionCount,
|
|
41
|
+
storageErrorCount,
|
|
42
|
+
sendingCount: rows.filter((item) => item.sending || item.status === "sending").length,
|
|
43
|
+
guidance: readError !== null
|
|
44
|
+
? "Pending changes could not be read. Your last known changes are preserved."
|
|
45
|
+
: (0, recoveryPolicy_1.getPendingChangesGuidance)({
|
|
46
|
+
hasReadyChanges: readyCount > 0,
|
|
47
|
+
hasDrafts: draftCount > 0,
|
|
48
|
+
hasErrors: attentionCount > 0 || storageErrorCount > 0 || readError !== null,
|
|
49
|
+
isOffline,
|
|
50
|
+
}),
|
|
51
|
+
loading,
|
|
52
|
+
readError,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Observes existing host queues, never persists or replays changes. Reads fail
|
|
57
|
+
* closed across accounts but retain the current account's last known rows.
|
|
58
|
+
* Hosts must notify for draft, command-store and connectivity source changes.
|
|
59
|
+
*/
|
|
60
|
+
function createPendingChangeTracker(options) {
|
|
61
|
+
let accountKey = options.accountKey ?? null;
|
|
62
|
+
let isOffline = options.isOffline ?? false;
|
|
63
|
+
let durable = [];
|
|
64
|
+
let captured = [];
|
|
65
|
+
let durableReadError = null;
|
|
66
|
+
let captureReadError = null;
|
|
67
|
+
let loading = false;
|
|
68
|
+
let generation = 0;
|
|
69
|
+
let closed = false;
|
|
70
|
+
const listeners = new Set();
|
|
71
|
+
let snapshot = createPendingChangeSnapshot({
|
|
72
|
+
accountKey,
|
|
73
|
+
items: [],
|
|
74
|
+
isOffline,
|
|
75
|
+
});
|
|
76
|
+
const report = (error) => {
|
|
77
|
+
try {
|
|
78
|
+
if (options.onError)
|
|
79
|
+
options.onError(error);
|
|
80
|
+
else
|
|
81
|
+
console.error("Pending-change tracker could not read host changes; retaining the last known account snapshot.", error);
|
|
82
|
+
}
|
|
83
|
+
catch (callbackError) {
|
|
84
|
+
console.error("Pending-change tracker error reporter failed while reporting a host source failure.", callbackError, error);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const publish = () => {
|
|
88
|
+
const publishingGeneration = generation;
|
|
89
|
+
snapshot = createPendingChangeSnapshot({
|
|
90
|
+
accountKey,
|
|
91
|
+
items: [...durable, ...captured],
|
|
92
|
+
isOffline,
|
|
93
|
+
loading,
|
|
94
|
+
readError: durableReadError ?? captureReadError,
|
|
95
|
+
});
|
|
96
|
+
for (const listener of [...listeners]) {
|
|
97
|
+
if (closed || publishingGeneration !== generation)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
listener();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error("Pending-change tracker subscriber failed; other subscribers will still receive updates.", error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const refresh = async () => {
|
|
108
|
+
if (closed)
|
|
109
|
+
return;
|
|
110
|
+
const request = ++generation;
|
|
111
|
+
const requestedAccount = accountKey;
|
|
112
|
+
const current = () => !closed && request === generation && requestedAccount === accountKey;
|
|
113
|
+
if (requestedAccount === null)
|
|
114
|
+
return;
|
|
115
|
+
// Capture callbacks may notify sources or switch accounts synchronously.
|
|
116
|
+
const capture = () => {
|
|
117
|
+
try {
|
|
118
|
+
const rows = options.capture?.(requestedAccount) ?? [];
|
|
119
|
+
if (!current())
|
|
120
|
+
return;
|
|
121
|
+
captured = rows;
|
|
122
|
+
captureReadError = null;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (!current())
|
|
126
|
+
return;
|
|
127
|
+
captureReadError = error;
|
|
128
|
+
report(error);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
capture();
|
|
132
|
+
if (!current())
|
|
133
|
+
return;
|
|
134
|
+
loading = true;
|
|
135
|
+
publish();
|
|
136
|
+
if (!current())
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
const rows = await options.load(requestedAccount);
|
|
140
|
+
if (!current())
|
|
141
|
+
return;
|
|
142
|
+
// A successful capture cannot prove durable storage recovered.
|
|
143
|
+
durableReadError = null;
|
|
144
|
+
durable = rows;
|
|
145
|
+
// A durable read may retire host acceptance receipts without emitting an
|
|
146
|
+
// event. Recapture after proof so drained receipts cannot become ghosts.
|
|
147
|
+
capture();
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (!current())
|
|
151
|
+
return;
|
|
152
|
+
durableReadError = error;
|
|
153
|
+
report(error);
|
|
154
|
+
}
|
|
155
|
+
if (!current())
|
|
156
|
+
return;
|
|
157
|
+
loading = false;
|
|
158
|
+
publish();
|
|
159
|
+
};
|
|
160
|
+
const unsubscribe = options.subscribe?.(() => {
|
|
161
|
+
void refresh();
|
|
162
|
+
});
|
|
163
|
+
const tracker = {
|
|
164
|
+
getSnapshot: () => snapshot,
|
|
165
|
+
subscribe: (listener) => {
|
|
166
|
+
if (closed)
|
|
167
|
+
return () => { };
|
|
168
|
+
listeners.add(listener);
|
|
169
|
+
return () => {
|
|
170
|
+
listeners.delete(listener);
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
refresh,
|
|
174
|
+
setAccountKey: (next) => {
|
|
175
|
+
if (closed || next === accountKey)
|
|
176
|
+
return;
|
|
177
|
+
++generation;
|
|
178
|
+
accountKey = next;
|
|
179
|
+
durable = [];
|
|
180
|
+
captured = [];
|
|
181
|
+
durableReadError = null;
|
|
182
|
+
captureReadError = null;
|
|
183
|
+
loading = false;
|
|
184
|
+
publish();
|
|
185
|
+
void refresh();
|
|
186
|
+
},
|
|
187
|
+
setOffline: (next) => {
|
|
188
|
+
if (closed || next === isOffline)
|
|
189
|
+
return;
|
|
190
|
+
isOffline = next;
|
|
191
|
+
publish();
|
|
192
|
+
},
|
|
193
|
+
close: () => {
|
|
194
|
+
if (closed)
|
|
195
|
+
return;
|
|
196
|
+
closed = true;
|
|
197
|
+
++generation;
|
|
198
|
+
unsubscribe?.();
|
|
199
|
+
listeners.clear();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
void refresh();
|
|
203
|
+
return tracker;
|
|
204
|
+
}
|
|
@@ -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);
|
package/dist-cjs/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>;
|