@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/idbDriver.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { wrap, } from "idb";
|
|
2
|
+
export class OfflineStorageError extends Error {
|
|
3
|
+
name = "OfflineStorageError";
|
|
4
|
+
}
|
|
2
5
|
export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB = globalThis.indexedDB, operationTimeoutMs, storeName, }) {
|
|
3
6
|
if (!indexedDB) {
|
|
4
7
|
return null;
|
|
@@ -9,25 +12,33 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
9
12
|
indexedDB,
|
|
10
13
|
storeName,
|
|
11
14
|
});
|
|
15
|
+
function runMutation(operationName, mutate) {
|
|
16
|
+
return runTransactionalStorageOperation({
|
|
17
|
+
failureMode: "throw",
|
|
18
|
+
fallback: undefined,
|
|
19
|
+
operationName,
|
|
20
|
+
storeName,
|
|
21
|
+
operation: async (setTransaction, isCancelled) => {
|
|
22
|
+
const database = await databasePromise;
|
|
23
|
+
if (isCancelled())
|
|
24
|
+
return;
|
|
25
|
+
const transaction = database.transaction(storeName, "readwrite");
|
|
26
|
+
setTransaction(transaction);
|
|
27
|
+
await mutate(transaction.objectStore(storeName));
|
|
28
|
+
await transaction.done;
|
|
29
|
+
},
|
|
30
|
+
timeoutMs: operationTimeoutMs,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
12
33
|
return {
|
|
13
34
|
async clear() {
|
|
14
|
-
await
|
|
15
|
-
fallback: undefined,
|
|
16
|
-
operationName: "clear object store",
|
|
17
|
-
operation: async (setTransaction) => {
|
|
18
|
-
const database = await databasePromise;
|
|
19
|
-
const transaction = database.transaction(storeName, "readwrite");
|
|
20
|
-
setTransaction(transaction);
|
|
21
|
-
await transaction.objectStore(storeName).clear();
|
|
22
|
-
await transaction.done;
|
|
23
|
-
},
|
|
24
|
-
timeoutMs: operationTimeoutMs,
|
|
25
|
-
});
|
|
35
|
+
await runMutation("clear object store", (objectStore) => objectStore.clear());
|
|
26
36
|
},
|
|
27
37
|
async close() {
|
|
28
38
|
await runBrowserStorageOperation({
|
|
29
39
|
fallback: undefined,
|
|
30
40
|
operationName: "close database",
|
|
41
|
+
storeName,
|
|
31
42
|
operation: async () => {
|
|
32
43
|
const database = await databasePromise;
|
|
33
44
|
database.close();
|
|
@@ -36,18 +47,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
36
47
|
});
|
|
37
48
|
},
|
|
38
49
|
async delete(key) {
|
|
39
|
-
await
|
|
40
|
-
fallback: undefined,
|
|
41
|
-
operationName: "delete key",
|
|
42
|
-
operation: async (setTransaction) => {
|
|
43
|
-
const database = await databasePromise;
|
|
44
|
-
const transaction = database.transaction(storeName, "readwrite");
|
|
45
|
-
setTransaction(transaction);
|
|
46
|
-
await transaction.objectStore(storeName).delete(key);
|
|
47
|
-
await transaction.done;
|
|
48
|
-
},
|
|
49
|
-
timeoutMs: operationTimeoutMs,
|
|
50
|
-
});
|
|
50
|
+
await runMutation("delete key", (objectStore) => objectStore.delete(key));
|
|
51
51
|
},
|
|
52
52
|
async deleteIf(key, predicate) {
|
|
53
53
|
return runConditionalStorageOperation({
|
|
@@ -68,6 +68,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
68
68
|
return runBrowserStorageOperation({
|
|
69
69
|
fallback: null,
|
|
70
70
|
operationName: "read key",
|
|
71
|
+
storeName,
|
|
71
72
|
operation: async () => {
|
|
72
73
|
const database = await databasePromise;
|
|
73
74
|
return (await database.get(storeName, key)) ?? null;
|
|
@@ -79,6 +80,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
79
80
|
return runBrowserStorageOperation({
|
|
80
81
|
fallback: [],
|
|
81
82
|
operationName: "list object store",
|
|
83
|
+
storeName,
|
|
82
84
|
operation: async () => {
|
|
83
85
|
const database = await databasePromise;
|
|
84
86
|
return database.getAll(storeName);
|
|
@@ -90,6 +92,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
90
92
|
return runBrowserStorageOperation({
|
|
91
93
|
fallback: [],
|
|
92
94
|
operationName: "list object-store entries",
|
|
95
|
+
storeName,
|
|
93
96
|
operation: async () => {
|
|
94
97
|
const database = await databasePromise;
|
|
95
98
|
// Keys and values must come from one transaction. Separate database
|
|
@@ -111,18 +114,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
111
114
|
});
|
|
112
115
|
},
|
|
113
116
|
async set(key, value) {
|
|
114
|
-
await
|
|
115
|
-
fallback: undefined,
|
|
116
|
-
operationName: "write key",
|
|
117
|
-
operation: async (setTransaction) => {
|
|
118
|
-
const database = await databasePromise;
|
|
119
|
-
const transaction = database.transaction(storeName, "readwrite");
|
|
120
|
-
setTransaction(transaction);
|
|
121
|
-
await transaction.objectStore(storeName).put(value, key);
|
|
122
|
-
await transaction.done;
|
|
123
|
-
},
|
|
124
|
-
timeoutMs: operationTimeoutMs,
|
|
125
|
-
});
|
|
117
|
+
await runMutation("write key", (objectStore) => objectStore.put(value, key));
|
|
126
118
|
},
|
|
127
119
|
async setIf(key, value, predicate) {
|
|
128
120
|
return runConditionalStorageOperation({
|
|
@@ -155,18 +147,27 @@ async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB,
|
|
|
155
147
|
}
|
|
156
148
|
return database;
|
|
157
149
|
}
|
|
158
|
-
async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation, operationName, timeoutMs, }) {
|
|
150
|
+
async function runBrowserStorageOperation({ cancelOnTimeout, failureMode = "fallback", fallback, operation, operationName, storeName, timeoutMs, }) {
|
|
159
151
|
let timeoutId = null;
|
|
152
|
+
const startedAt = Date.now();
|
|
160
153
|
try {
|
|
161
154
|
return await Promise.race([
|
|
162
155
|
operation(),
|
|
163
|
-
new Promise((resolve) => {
|
|
156
|
+
new Promise((resolve, reject) => {
|
|
164
157
|
timeoutId = setTimeout(() => {
|
|
165
|
-
console.
|
|
158
|
+
console.warn("[Bayon offline] IndexedDB operation timed out.", {
|
|
159
|
+
elapsedMs: Date.now() - startedAt,
|
|
160
|
+
operationName,
|
|
161
|
+
storeName,
|
|
166
162
|
timeoutMs,
|
|
167
163
|
});
|
|
168
164
|
if (!cancelOnTimeout || cancelOnTimeout()) {
|
|
169
|
-
|
|
165
|
+
if (failureMode === "throw") {
|
|
166
|
+
reject(new OfflineStorageError(`IndexedDB timed out while attempting to ${operationName} in ${storeName}.`));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
resolve(fallback);
|
|
170
|
+
}
|
|
170
171
|
}
|
|
171
172
|
}, timeoutMs);
|
|
172
173
|
}),
|
|
@@ -174,6 +175,11 @@ async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation
|
|
|
174
175
|
}
|
|
175
176
|
catch (error) {
|
|
176
177
|
console.error(`IndexedDB failed to ${operationName}.`, { error });
|
|
178
|
+
if (failureMode === "throw") {
|
|
179
|
+
if (error instanceof OfflineStorageError)
|
|
180
|
+
throw error;
|
|
181
|
+
throw new OfflineStorageError(`IndexedDB failed to ${operationName} in ${storeName}.`, { cause: error });
|
|
182
|
+
}
|
|
177
183
|
return fallback;
|
|
178
184
|
}
|
|
179
185
|
finally {
|
|
@@ -182,12 +188,14 @@ async function runBrowserStorageOperation({ cancelOnTimeout, fallback, operation
|
|
|
182
188
|
}
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
|
-
async function runTransactionalStorageOperation({ fallback, operation, operationName, timeoutMs, }) {
|
|
191
|
+
async function runTransactionalStorageOperation({ failureMode, fallback, operation, operationName, storeName, timeoutMs, }) {
|
|
186
192
|
let transaction;
|
|
187
193
|
let cancelled = false;
|
|
188
194
|
return runBrowserStorageOperation({
|
|
195
|
+
failureMode,
|
|
189
196
|
fallback,
|
|
190
197
|
operationName,
|
|
198
|
+
storeName,
|
|
191
199
|
timeoutMs,
|
|
192
200
|
cancelOnTimeout() {
|
|
193
201
|
cancelled = true;
|
|
@@ -203,14 +211,12 @@ async function runTransactionalStorageOperation({ fallback, operation, operation
|
|
|
203
211
|
}
|
|
204
212
|
},
|
|
205
213
|
operation: async () => {
|
|
206
|
-
if (cancelled)
|
|
207
|
-
return fallback;
|
|
208
214
|
return operation((nextTransaction) => {
|
|
209
215
|
transaction = nextTransaction;
|
|
210
216
|
// Aborting can reject transaction.done. The operation also awaits it,
|
|
211
217
|
// but observing it here prevents a race from becoming unhandled.
|
|
212
218
|
void transaction.done.catch(() => undefined);
|
|
213
|
-
});
|
|
219
|
+
}, () => cancelled);
|
|
214
220
|
},
|
|
215
221
|
});
|
|
216
222
|
}
|
|
@@ -218,8 +224,10 @@ async function runConditionalStorageOperation({ apply, databasePromise, operatio
|
|
|
218
224
|
let transaction;
|
|
219
225
|
let cancelled = false;
|
|
220
226
|
return runBrowserStorageOperation({
|
|
227
|
+
failureMode: "throw",
|
|
221
228
|
fallback: false,
|
|
222
229
|
operationName,
|
|
230
|
+
storeName,
|
|
223
231
|
timeoutMs,
|
|
224
232
|
cancelOnTimeout() {
|
|
225
233
|
cancelled = true;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export * from "./readCache";
|
|
2
|
-
export * from "./pendingWrites";
|
|
3
|
-
export * from "./browserRecovery";
|
|
4
|
-
export * from "./browserCoordination";
|
|
5
|
-
export * from "./browserFallbackRecovery";
|
|
6
|
-
export * from "./durableCommandStore";
|
|
1
|
+
export * from "./readCache.js";
|
|
2
|
+
export * from "./pendingWrites.js";
|
|
3
|
+
export * from "./browserRecovery.js";
|
|
4
|
+
export * from "./browserCoordination.js";
|
|
5
|
+
export * from "./browserFallbackRecovery.js";
|
|
6
|
+
export * from "./durableCommandStore.js";
|
|
7
|
+
export { OfflineStorageError } from "./idbDriver.js";
|
package/dist/index.js
CHANGED
package/dist/pendingWrites.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BrowserKeyValueDriverOptions } from "./idbDriver";
|
|
1
|
+
import { type BrowserKeyValueDriverOptions } from "./idbDriver.js";
|
|
2
2
|
export declare const pendingWriteSchemaVersion = 1;
|
|
3
3
|
export declare const pendingWriteOperationTimeoutMs = 2000;
|
|
4
4
|
export declare const pendingWriteSyncLeaseTimeoutMs = 60000;
|
|
@@ -22,10 +22,14 @@ export type PendingWritePayloadNormalizer<Payload = unknown> = (payload: unknown
|
|
|
22
22
|
export type PendingWriteDriver = {
|
|
23
23
|
clear: (key: string) => Promise<void>;
|
|
24
24
|
clearIf: (key: string, predicate: PendingWritePredicate) => Promise<boolean>;
|
|
25
|
-
clearAll
|
|
25
|
+
clearAll?: () => Promise<void>;
|
|
26
26
|
close?: () => Promise<void>;
|
|
27
27
|
get: (key: string) => Promise<unknown | null>;
|
|
28
28
|
list: () => Promise<unknown[]>;
|
|
29
|
+
listEntries?: () => Promise<Array<{
|
|
30
|
+
key: string;
|
|
31
|
+
value: unknown;
|
|
32
|
+
}>>;
|
|
29
33
|
set: (key: string, value: unknown) => Promise<void>;
|
|
30
34
|
setIf: (key: string, value: unknown, predicate: PendingWritePredicate) => Promise<boolean>;
|
|
31
35
|
};
|
|
@@ -94,7 +98,11 @@ export type SyncPendingWritesResult = {
|
|
|
94
98
|
superseded: number;
|
|
95
99
|
synced: number;
|
|
96
100
|
};
|
|
97
|
-
export type
|
|
101
|
+
export type PendingWriteSyncContext = {
|
|
102
|
+
/** Stable durable-record identity; safe to use as a remote idempotency key. */
|
|
103
|
+
recordId: string;
|
|
104
|
+
};
|
|
105
|
+
export type PendingWriteSync<Payload, Result = unknown> = (payload: Payload, context: PendingWriteSyncContext) => Promise<Result>;
|
|
98
106
|
export type RunPendingWriteResult<TResult, TQueued> = {
|
|
99
107
|
result: TResult;
|
|
100
108
|
status: "synced";
|
|
@@ -130,7 +138,7 @@ export declare function createMemoryPendingWriteDriver(): PendingWriteDriver;
|
|
|
130
138
|
export declare function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, storeName, }: Omit<BrowserPendingWriteStoreOptions, "keyPrefix" | "normalizePayload" | "schemaVersion">): PendingWriteDriver | null;
|
|
131
139
|
export declare function createBrowserPendingWriteStore<Payload = unknown>({ databaseName, databaseVersion, indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs, schemaVersion, storeName, }: BrowserPendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
|
|
132
140
|
export declare function createPendingWriteKey({ id, prefix }: PendingWriteKeyOptions): string;
|
|
133
|
-
export declare function createPendingWriteSnapshotId():
|
|
141
|
+
export declare function createPendingWriteSnapshotId(): string;
|
|
134
142
|
export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
|
|
135
143
|
id: string;
|
|
136
144
|
now?: () => string;
|
|
@@ -142,7 +150,8 @@ export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
|
|
|
142
150
|
* same store/scope. Callers own the selection predicate and idempotent remote
|
|
143
151
|
* mutation semantics.
|
|
144
152
|
*/
|
|
145
|
-
export declare function syncPendingWrites<Payload>({ scopeKey, select, stopOnFailure, store, sync, }: {
|
|
153
|
+
export declare function syncPendingWrites<Payload>({ now, scopeKey, select, stopOnFailure, store, sync, }: {
|
|
154
|
+
now?: () => string;
|
|
146
155
|
scopeKey?: string;
|
|
147
156
|
select?: (record: PendingWriteRecord<Payload>) => boolean;
|
|
148
157
|
/** Preserve caller ordering by leaving later records for a future retry. */
|
|
@@ -161,7 +170,8 @@ export declare function summarizePendingWrites<Payload>(records: Array<PendingWr
|
|
|
161
170
|
* workspace. Use its `recover` method for background replay and `run` for new
|
|
162
171
|
* writes so a newer remote mutation cannot overtake an older queued mutation.
|
|
163
172
|
*/
|
|
164
|
-
export declare function createOrderedPendingWriteExecutor<Payload>({ scopeKey, select, store, sync, }: {
|
|
173
|
+
export declare function createOrderedPendingWriteExecutor<Payload>({ now, scopeKey, select, store, sync, }: {
|
|
174
|
+
now?: () => string;
|
|
165
175
|
scopeKey?: string;
|
|
166
176
|
select?: (record: PendingWriteRecord<Payload>) => boolean;
|
|
167
177
|
store: PendingWriteStore<Payload>;
|
package/dist/pendingWrites.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createIdbKeyValueDriver, } from "./idbDriver.js";
|
|
1
|
+
import { createIdbKeyValueDriver, OfflineStorageError, } from "./idbDriver.js";
|
|
2
2
|
export const pendingWriteSchemaVersion = 1;
|
|
3
3
|
export const pendingWriteOperationTimeoutMs = 2_000;
|
|
4
4
|
export const pendingWriteSyncLeaseTimeoutMs = 60_000;
|
|
@@ -17,6 +17,29 @@ export function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyReco
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
+
async function listScopedRecords() {
|
|
21
|
+
const readOptions = { normalizeLegacyRecord, normalizePayload };
|
|
22
|
+
const entries = await driver.listEntries?.();
|
|
23
|
+
if (entries) {
|
|
24
|
+
const keyPrefixBoundary = `${keyPrefix}:`;
|
|
25
|
+
return entries
|
|
26
|
+
.filter(({ key }) => key.startsWith(keyPrefixBoundary))
|
|
27
|
+
.map(({ value }) => toPendingWriteEnvelope(value, schemaVersion, readOptions)
|
|
28
|
+
?.record)
|
|
29
|
+
.filter((record) => Boolean(record));
|
|
30
|
+
}
|
|
31
|
+
// Compatibility path for custom drivers created before listEntries was
|
|
32
|
+
// available. Point reads prove that each candidate belongs to this prefix.
|
|
33
|
+
const candidateIds = new Set((await driver.list())
|
|
34
|
+
.map((value) => toPendingWriteEnvelope(value, schemaVersion, readOptions)
|
|
35
|
+
?.record.id)
|
|
36
|
+
.filter((id) => Boolean(id)));
|
|
37
|
+
const records = await Promise.all([...candidateIds].map(async (id) => {
|
|
38
|
+
const value = await driver.get(createPendingWriteKey({ id, prefix: keyPrefix }));
|
|
39
|
+
return toPendingWriteEnvelope(value, schemaVersion, readOptions)?.record;
|
|
40
|
+
}));
|
|
41
|
+
return records.filter((record) => Boolean(record));
|
|
42
|
+
}
|
|
20
43
|
return {
|
|
21
44
|
async claimIfSnapshotId(id, snapshotId, record, now) {
|
|
22
45
|
const claimed = await driver.setIf(createPendingWriteKey({ id, prefix: keyPrefix }), { record, schemaVersion }, (value) => {
|
|
@@ -37,7 +60,20 @@ export function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyReco
|
|
|
37
60
|
notify({ id, type: "clear" });
|
|
38
61
|
},
|
|
39
62
|
async clearAll() {
|
|
40
|
-
await driver.
|
|
63
|
+
const entries = await driver.listEntries?.();
|
|
64
|
+
if (entries) {
|
|
65
|
+
const keyPrefixBoundary = `${keyPrefix}:`;
|
|
66
|
+
await Promise.all(entries
|
|
67
|
+
.filter(({ key }) => key.startsWith(keyPrefixBoundary))
|
|
68
|
+
.map(({ key }) => driver.clear(key)));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const records = await listScopedRecords();
|
|
72
|
+
await Promise.all(records.map((record) => driver.clearIf(createPendingWriteKey({ id: record.id, prefix: keyPrefix }), (value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
73
|
+
normalizeLegacyRecord,
|
|
74
|
+
normalizePayload,
|
|
75
|
+
})?.record.snapshotId === record.snapshotId)));
|
|
76
|
+
}
|
|
41
77
|
notify({ type: "clearAll" });
|
|
42
78
|
},
|
|
43
79
|
async clearIfSnapshotId(id, snapshotId) {
|
|
@@ -57,13 +93,7 @@ export function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyReco
|
|
|
57
93
|
return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizeLegacyRecord, normalizePayload })?.record ?? null);
|
|
58
94
|
},
|
|
59
95
|
async list() {
|
|
60
|
-
return (await
|
|
61
|
-
.map((value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
62
|
-
normalizeLegacyRecord,
|
|
63
|
-
normalizePayload,
|
|
64
|
-
})?.record)
|
|
65
|
-
.filter((record) => Boolean(record))
|
|
66
|
-
.sort((left, right) => left.savedAt.localeCompare(right.savedAt) ||
|
|
96
|
+
return (await listScopedRecords()).sort((left, right) => left.savedAt.localeCompare(right.savedAt) ||
|
|
67
97
|
left.id.localeCompare(right.id));
|
|
68
98
|
},
|
|
69
99
|
async replaceIfSnapshotId(id, snapshotId, record) {
|
|
@@ -118,6 +148,9 @@ export function createMemoryPendingWriteDriver() {
|
|
|
118
148
|
async list() {
|
|
119
149
|
return [...records.values()];
|
|
120
150
|
},
|
|
151
|
+
async listEntries() {
|
|
152
|
+
return [...records].map(([key, value]) => ({ key, value }));
|
|
153
|
+
},
|
|
121
154
|
async set(key, value) {
|
|
122
155
|
records.set(key, value);
|
|
123
156
|
},
|
|
@@ -148,14 +181,24 @@ export function createBrowserPendingWriteStore({ databaseName, databaseVersion,
|
|
|
148
181
|
operationTimeoutMs,
|
|
149
182
|
storeName,
|
|
150
183
|
});
|
|
151
|
-
return createPendingWriteStore(driver ??
|
|
184
|
+
return createPendingWriteStore(driver ?? createUnavailablePendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
|
|
152
185
|
}
|
|
153
186
|
export function createPendingWriteKey({ id, prefix }) {
|
|
154
187
|
return `${prefix}:${encodeURIComponent(id)}`;
|
|
155
188
|
}
|
|
156
189
|
export function createPendingWriteSnapshotId() {
|
|
157
|
-
|
|
190
|
+
const randomId = globalThis.crypto?.randomUUID?.();
|
|
191
|
+
if (randomId)
|
|
192
|
+
return randomId;
|
|
193
|
+
pendingWriteSnapshotSequence += 1;
|
|
194
|
+
return [
|
|
195
|
+
"snapshot",
|
|
196
|
+
Date.now().toString(36),
|
|
197
|
+
pendingWriteSnapshotSequence.toString(36),
|
|
198
|
+
Math.random().toString(36).slice(2),
|
|
199
|
+
].join("-");
|
|
158
200
|
}
|
|
201
|
+
let pendingWriteSnapshotSequence = 0;
|
|
159
202
|
export async function syncPendingWrite({ id, now = () => new Date().toISOString(), store, sync, }) {
|
|
160
203
|
const pending = await store.get(id);
|
|
161
204
|
if (!pending) {
|
|
@@ -163,8 +206,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
|
|
|
163
206
|
}
|
|
164
207
|
// A delayed command remains in its ordered lane until its grace period has
|
|
165
208
|
// elapsed. This makes cancellation survive UI unmounts and app restarts.
|
|
166
|
-
if (pending.notBefore &&
|
|
167
|
-
Date.parse(pending.notBefore) > Date.parse(now())) {
|
|
209
|
+
if (pending.notBefore && Date.parse(pending.notBefore) > Date.parse(now())) {
|
|
168
210
|
return { status: "idle" };
|
|
169
211
|
}
|
|
170
212
|
const claimedSnapshotId = createPendingWriteSnapshotId();
|
|
@@ -179,7 +221,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
|
|
|
179
221
|
return { status: "superseded" };
|
|
180
222
|
}
|
|
181
223
|
try {
|
|
182
|
-
await sync(pending.payload);
|
|
224
|
+
await sync(pending.payload, { recordId: pending.id });
|
|
183
225
|
}
|
|
184
226
|
catch (error) {
|
|
185
227
|
const message = error instanceof Error ? error.message : "Unable to sync pending write.";
|
|
@@ -208,7 +250,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
|
|
|
208
250
|
* same store/scope. Callers own the selection predicate and idempotent remote
|
|
209
251
|
* mutation semantics.
|
|
210
252
|
*/
|
|
211
|
-
export function syncPendingWrites({ scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
|
|
253
|
+
export function syncPendingWrites({ now, scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
|
|
212
254
|
let inFlightByScope = pendingWriteSyncs.get(store);
|
|
213
255
|
if (!inFlightByScope) {
|
|
214
256
|
inFlightByScope = new Map();
|
|
@@ -219,6 +261,7 @@ export function syncPendingWrites({ scopeKey = "all", select = () => true, stopO
|
|
|
219
261
|
return existing;
|
|
220
262
|
}
|
|
221
263
|
const syncRun = performPendingWriteSync({
|
|
264
|
+
now,
|
|
222
265
|
select,
|
|
223
266
|
stopOnFailure,
|
|
224
267
|
store,
|
|
@@ -265,7 +308,7 @@ export function summarizePendingWrites(records) {
|
|
|
265
308
|
}, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
|
|
266
309
|
}
|
|
267
310
|
const pendingWriteSyncs = new WeakMap();
|
|
268
|
-
async function performPendingWriteSync({ select, stopOnFailure, store, sync, }) {
|
|
311
|
+
async function performPendingWriteSync({ now, select, stopOnFailure, store, sync, }) {
|
|
269
312
|
const summary = {
|
|
270
313
|
attempted: 0,
|
|
271
314
|
failed: 0,
|
|
@@ -278,6 +321,7 @@ async function performPendingWriteSync({ select, stopOnFailure, store, sync, })
|
|
|
278
321
|
for (const record of records) {
|
|
279
322
|
const result = await syncPendingWrite({
|
|
280
323
|
id: record.id,
|
|
324
|
+
now,
|
|
281
325
|
store,
|
|
282
326
|
sync,
|
|
283
327
|
});
|
|
@@ -313,7 +357,7 @@ function isPendingWriteSyncClaimAvailable(record, now) {
|
|
|
313
357
|
* workspace. Use its `recover` method for background replay and `run` for new
|
|
314
358
|
* writes so a newer remote mutation cannot overtake an older queued mutation.
|
|
315
359
|
*/
|
|
316
|
-
export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => true, store, sync, }) {
|
|
360
|
+
export function createOrderedPendingWriteExecutor({ now, scopeKey = "all", select = () => true, store, sync, }) {
|
|
317
361
|
let execution = Promise.resolve();
|
|
318
362
|
function inOrder(operation) {
|
|
319
363
|
const result = execution.then(operation, operation);
|
|
@@ -322,6 +366,7 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
|
|
|
322
366
|
}
|
|
323
367
|
function recover() {
|
|
324
368
|
return inOrder(() => syncPendingWrites({
|
|
369
|
+
now,
|
|
325
370
|
scopeKey,
|
|
326
371
|
select,
|
|
327
372
|
stopOnFailure: true,
|
|
@@ -337,6 +382,7 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
|
|
|
337
382
|
return runPendingWrite(options);
|
|
338
383
|
}
|
|
339
384
|
const replay = await syncPendingWrites({
|
|
385
|
+
now,
|
|
340
386
|
scopeKey,
|
|
341
387
|
select,
|
|
342
388
|
stopOnFailure: true,
|
|
@@ -354,13 +400,13 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
|
|
|
354
400
|
},
|
|
355
401
|
};
|
|
356
402
|
}
|
|
357
|
-
function
|
|
403
|
+
function createUnavailablePendingWriteDriver() {
|
|
404
|
+
const unavailable = () => new OfflineStorageError("IndexedDB is unavailable; the pending write was not stored.");
|
|
358
405
|
return {
|
|
359
406
|
async clear() { },
|
|
360
407
|
async clearIf() {
|
|
361
408
|
return false;
|
|
362
409
|
},
|
|
363
|
-
async clearAll() { },
|
|
364
410
|
async close() { },
|
|
365
411
|
async get() {
|
|
366
412
|
return null;
|
|
@@ -368,9 +414,11 @@ function createNoopPendingWriteDriver() {
|
|
|
368
414
|
async list() {
|
|
369
415
|
return [];
|
|
370
416
|
},
|
|
371
|
-
async set() {
|
|
417
|
+
async set() {
|
|
418
|
+
throw unavailable();
|
|
419
|
+
},
|
|
372
420
|
async setIf() {
|
|
373
|
-
|
|
421
|
+
throw unavailable();
|
|
374
422
|
},
|
|
375
423
|
};
|
|
376
424
|
}
|
|
@@ -382,6 +430,7 @@ function toPendingWriteDriver(driver) {
|
|
|
382
430
|
close: driver.close,
|
|
383
431
|
get: driver.get,
|
|
384
432
|
list: driver.list,
|
|
433
|
+
listEntries: driver.listEntries,
|
|
385
434
|
set: driver.set,
|
|
386
435
|
setIf: driver.setIf,
|
|
387
436
|
};
|
package/dist/readCache.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type BrowserKeyValueDriverOptions } from "./idbDriver";
|
|
1
|
+
import { type BrowserKeyValueDriverOptions } from "./idbDriver.js";
|
|
2
2
|
export declare const readCacheSchemaVersion = 1;
|
|
3
3
|
export declare const readCacheFreshnessMaxAgeMs = 30000;
|
|
4
4
|
export declare const durableReadCacheFreshnessMaxAgeMs: number;
|
|
5
5
|
export declare const identityReadCacheFreshnessMaxAgeMs: number;
|
|
6
|
-
export declare const browserReadCacheOperationTimeoutMs =
|
|
6
|
+
export declare const browserReadCacheOperationTimeoutMs = 1000;
|
|
7
7
|
export type ReadCacheHit<T> = {
|
|
8
8
|
fromCache: true;
|
|
9
9
|
savedAt: string;
|
|
@@ -29,21 +29,36 @@ export type ReadCache = {
|
|
|
29
29
|
clear: () => Promise<void>;
|
|
30
30
|
close: () => Promise<void>;
|
|
31
31
|
delete: (key: string) => Promise<void>;
|
|
32
|
+
/** Advances a key's generation without deleting its retained value. */
|
|
33
|
+
invalidate?: (key: string) => void;
|
|
32
34
|
get: <T>(key: string) => Promise<ReadCacheHit<T> | null>;
|
|
33
35
|
getVersion: (key: string) => string;
|
|
34
36
|
set: <T>(key: string, value: T) => Promise<void>;
|
|
35
37
|
setIfVersion: <T>(key: string, value: T, version: string) => Promise<boolean>;
|
|
36
38
|
};
|
|
39
|
+
export type ReadCachePolicy = {
|
|
40
|
+
/** Time that a cached value can suppress a remote read. */
|
|
41
|
+
freshForMs: number;
|
|
42
|
+
/** Maximum age at which cached data remains eligible as an offline fallback. */
|
|
43
|
+
retainForMs: number;
|
|
44
|
+
/** Behavior when revalidation fails while retained data is available. */
|
|
45
|
+
refreshFailure: "use-retained" | "throw";
|
|
46
|
+
};
|
|
47
|
+
export type ReadCacheStatus = "fresh" | "stale" | "miss";
|
|
48
|
+
/** Payload-free cache lifecycle events suitable for host-owned telemetry. */
|
|
49
|
+
export type ReadCacheOutcome = "fresh-hit" | "stale-hit" | "cache-miss" | "forced-refresh" | "loader-success" | "loader-failure";
|
|
37
50
|
export type ReadThroughCacheResult<T> = {
|
|
38
51
|
data: T;
|
|
39
52
|
error: Error | null;
|
|
40
53
|
fromCache: boolean;
|
|
41
54
|
stale: boolean;
|
|
55
|
+
status: ReadCacheStatus;
|
|
42
56
|
};
|
|
43
57
|
export type ReadCacheResourceUpdate<T> = {
|
|
44
58
|
data: T;
|
|
45
59
|
error?: Error;
|
|
46
60
|
fromCache: boolean;
|
|
61
|
+
status: ReadCacheStatus;
|
|
47
62
|
};
|
|
48
63
|
export type BrowserReadCacheOptions = Partial<Pick<BrowserKeyValueDriverOptions, "databaseVersion" | "indexedDB">> & {
|
|
49
64
|
databaseName: string;
|
|
@@ -69,22 +84,28 @@ export declare function pruneReadCache({ cache, maxAgeMs, now, }: {
|
|
|
69
84
|
now?: () => number;
|
|
70
85
|
}): Promise<number>;
|
|
71
86
|
export declare function createReadCacheKey({ namespace, params, prefix, scope, }: ReadCacheKeyOptions): string;
|
|
72
|
-
export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, }: {
|
|
87
|
+
export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, }: {
|
|
73
88
|
cache: ReadCache;
|
|
74
89
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
90
|
+
* Revalidates after advancing the key generation, retaining the prior value
|
|
91
|
+
* as a possible fallback if refresh fails.
|
|
77
92
|
*/
|
|
78
93
|
forceFresh?: boolean;
|
|
79
94
|
key: string;
|
|
80
95
|
loadFresh: () => Promise<T>;
|
|
81
96
|
maxAgeMs?: number;
|
|
97
|
+
onOutcome?: (outcome: ReadCacheOutcome) => void;
|
|
98
|
+
policy?: ReadCachePolicy;
|
|
82
99
|
}): Promise<ReadThroughCacheResult<T>>;
|
|
83
|
-
export declare function loadReadCacheResource<T>({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache, }: {
|
|
100
|
+
export declare function loadReadCacheResource<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, }: {
|
|
84
101
|
cache: ReadCache;
|
|
102
|
+
/** Revalidate while retaining eligible data as fallback on loader failure. */
|
|
103
|
+
forceFresh?: boolean;
|
|
85
104
|
key: string;
|
|
86
105
|
loadFresh: () => Promise<T>;
|
|
87
106
|
maxAgeMs?: number;
|
|
107
|
+
onOutcome?: (outcome: ReadCacheOutcome) => void;
|
|
108
|
+
policy?: ReadCachePolicy;
|
|
88
109
|
onCacheWriteError?: (error: Error) => void;
|
|
89
110
|
onUpdate: (update: ReadCacheResourceUpdate<T>) => void;
|
|
90
111
|
skipCache?: boolean;
|