@bayonai/offline 0.1.0 → 0.1.1
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 +4 -1
- package/README.md +7 -0
- package/dist/idbDriver.d.ts +3 -0
- package/dist/idbDriver.js +57 -3
- package/dist/pendingWrites.d.ts +14 -4
- package/dist/pendingWrites.js +80 -35
- package/dist/readCache.d.ts +16 -2
- package/dist/readCache.js +91 -18
- package/dist-cjs/idbDriver.js +57 -3
- package/dist-cjs/pendingWrites.js +81 -35
- package/dist-cjs/readCache.js +92 -19
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.1.1 - 2026-08-10
|
|
4
4
|
|
|
5
|
+
- Added unique pending-write snapshot identities and atomic conditional
|
|
6
|
+
replacement/deletion, preventing overlapping sync and autosave operations
|
|
7
|
+
from deleting newer local data. Existing records remain compatible.
|
|
5
8
|
- Added the initial workspace package with generic read-cache and pending-write
|
|
6
9
|
primitives backed by `idb`.
|
|
7
10
|
- Replaced app-specific test fixtures with neutral package examples.
|
package/README.md
CHANGED
|
@@ -81,6 +81,13 @@ await syncPendingWrite({
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
Each call to `set` assigns a unique `snapshotId`. Sync status updates and
|
|
85
|
+
successful cleanup compare that identity inside a single storage transaction,
|
|
86
|
+
so a newer local write cannot be overwritten or deleted by an older sync.
|
|
87
|
+
Records created before snapshot identities were introduced remain readable;
|
|
88
|
+
they receive a stable compatibility identity and are upgraded when next
|
|
89
|
+
updated.
|
|
90
|
+
|
|
84
91
|
Host apps that change their pending-write payload shape can normalize current
|
|
85
92
|
records while keeping domain logic outside the package:
|
|
86
93
|
|
package/dist/idbDriver.d.ts
CHANGED
|
@@ -2,10 +2,13 @@ export type BrowserKeyValueDriver = {
|
|
|
2
2
|
clear: () => Promise<void>;
|
|
3
3
|
close: () => Promise<void>;
|
|
4
4
|
delete: (key: string) => Promise<void>;
|
|
5
|
+
deleteIf: (key: string, predicate: KeyValuePredicate) => Promise<boolean>;
|
|
5
6
|
get: (key: string) => Promise<unknown | null>;
|
|
6
7
|
list: () => Promise<unknown[]>;
|
|
7
8
|
set: (key: string, value: unknown) => Promise<void>;
|
|
9
|
+
setIf: (key: string, value: unknown, predicate: KeyValuePredicate) => Promise<boolean>;
|
|
8
10
|
};
|
|
11
|
+
export type KeyValuePredicate = (value: unknown | null) => boolean;
|
|
9
12
|
export type BrowserKeyValueDriverOptions = {
|
|
10
13
|
databaseName: string;
|
|
11
14
|
databaseVersion?: number;
|
package/dist/idbDriver.js
CHANGED
|
@@ -13,6 +13,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
13
13
|
async clear() {
|
|
14
14
|
await runBrowserStorageOperation({
|
|
15
15
|
fallback: undefined,
|
|
16
|
+
operationName: "clear object store",
|
|
16
17
|
operation: async () => {
|
|
17
18
|
const database = await databasePromise;
|
|
18
19
|
await database.clear(storeName);
|
|
@@ -23,6 +24,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
23
24
|
async close() {
|
|
24
25
|
await runBrowserStorageOperation({
|
|
25
26
|
fallback: undefined,
|
|
27
|
+
operationName: "close database",
|
|
26
28
|
operation: async () => {
|
|
27
29
|
const database = await databasePromise;
|
|
28
30
|
database.close();
|
|
@@ -33,6 +35,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
33
35
|
async delete(key) {
|
|
34
36
|
await runBrowserStorageOperation({
|
|
35
37
|
fallback: undefined,
|
|
38
|
+
operationName: "delete key",
|
|
36
39
|
operation: async () => {
|
|
37
40
|
const database = await databasePromise;
|
|
38
41
|
await database.delete(storeName, key);
|
|
@@ -40,9 +43,32 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
40
43
|
timeoutMs: operationTimeoutMs,
|
|
41
44
|
});
|
|
42
45
|
},
|
|
46
|
+
async deleteIf(key, predicate) {
|
|
47
|
+
return runBrowserStorageOperation({
|
|
48
|
+
fallback: false,
|
|
49
|
+
operationName: "conditionally delete key",
|
|
50
|
+
operation: async () => {
|
|
51
|
+
const database = await databasePromise;
|
|
52
|
+
// The ownership check and deletion must share a transaction; splitting
|
|
53
|
+
// them can delete a newer offline snapshot written by another trigger.
|
|
54
|
+
const transaction = database.transaction(storeName, "readwrite");
|
|
55
|
+
const objectStore = transaction.objectStore(storeName);
|
|
56
|
+
const current = (await objectStore.get(key)) ?? null;
|
|
57
|
+
if (!predicate(current)) {
|
|
58
|
+
await transaction.done;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
await objectStore.delete(key);
|
|
62
|
+
await transaction.done;
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
timeoutMs: operationTimeoutMs,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
43
68
|
async get(key) {
|
|
44
69
|
return runBrowserStorageOperation({
|
|
45
70
|
fallback: null,
|
|
71
|
+
operationName: "read key",
|
|
46
72
|
operation: async () => {
|
|
47
73
|
const database = await databasePromise;
|
|
48
74
|
return (await database.get(storeName, key)) ?? null;
|
|
@@ -53,6 +79,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
53
79
|
async list() {
|
|
54
80
|
return runBrowserStorageOperation({
|
|
55
81
|
fallback: [],
|
|
82
|
+
operationName: "list object store",
|
|
56
83
|
operation: async () => {
|
|
57
84
|
const database = await databasePromise;
|
|
58
85
|
return database.getAll(storeName);
|
|
@@ -63,6 +90,7 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
63
90
|
async set(key, value) {
|
|
64
91
|
await runBrowserStorageOperation({
|
|
65
92
|
fallback: undefined,
|
|
93
|
+
operationName: "write key",
|
|
66
94
|
operation: async () => {
|
|
67
95
|
const database = await databasePromise;
|
|
68
96
|
await database.put(storeName, value, key);
|
|
@@ -70,6 +98,26 @@ export function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, ind
|
|
|
70
98
|
timeoutMs: operationTimeoutMs,
|
|
71
99
|
});
|
|
72
100
|
},
|
|
101
|
+
async setIf(key, value, predicate) {
|
|
102
|
+
return runBrowserStorageOperation({
|
|
103
|
+
fallback: false,
|
|
104
|
+
operationName: "conditionally replace key",
|
|
105
|
+
operation: async () => {
|
|
106
|
+
const database = await databasePromise;
|
|
107
|
+
const transaction = database.transaction(storeName, "readwrite");
|
|
108
|
+
const objectStore = transaction.objectStore(storeName);
|
|
109
|
+
const current = (await objectStore.get(key)) ?? null;
|
|
110
|
+
if (!predicate(current)) {
|
|
111
|
+
await transaction.done;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
await objectStore.put(value, key);
|
|
115
|
+
await transaction.done;
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
timeoutMs: operationTimeoutMs,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
73
121
|
};
|
|
74
122
|
}
|
|
75
123
|
async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB, storeName, }) {
|
|
@@ -86,17 +134,23 @@ async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB,
|
|
|
86
134
|
}
|
|
87
135
|
return database;
|
|
88
136
|
}
|
|
89
|
-
async function runBrowserStorageOperation({ fallback, operation, timeoutMs, }) {
|
|
137
|
+
async function runBrowserStorageOperation({ fallback, operation, operationName, timeoutMs, }) {
|
|
90
138
|
let timeoutId = null;
|
|
91
139
|
try {
|
|
92
140
|
return await Promise.race([
|
|
93
141
|
operation(),
|
|
94
142
|
new Promise((resolve) => {
|
|
95
|
-
timeoutId = setTimeout(() =>
|
|
143
|
+
timeoutId = setTimeout(() => {
|
|
144
|
+
console.error(`IndexedDB timed out while trying to ${operationName}.`, {
|
|
145
|
+
timeoutMs,
|
|
146
|
+
});
|
|
147
|
+
resolve(fallback);
|
|
148
|
+
}, timeoutMs);
|
|
96
149
|
}),
|
|
97
150
|
]);
|
|
98
151
|
}
|
|
99
|
-
catch {
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(`IndexedDB failed to ${operationName}.`, { error });
|
|
100
154
|
return fallback;
|
|
101
155
|
}
|
|
102
156
|
finally {
|
package/dist/pendingWrites.d.ts
CHANGED
|
@@ -7,28 +7,37 @@ export type PendingWriteRecord<Payload = unknown> = {
|
|
|
7
7
|
id: string;
|
|
8
8
|
payload: Payload;
|
|
9
9
|
savedAt: string;
|
|
10
|
+
snapshotId: string;
|
|
10
11
|
syncStatus: PendingWriteStatus;
|
|
11
12
|
};
|
|
13
|
+
export type PendingWriteRecordInput<Payload = unknown> = Omit<PendingWriteRecord<Payload>, "snapshotId"> & {
|
|
14
|
+
snapshotId?: string;
|
|
15
|
+
};
|
|
12
16
|
export type PendingWritePayloadNormalizer<Payload = unknown> = (payload: unknown) => Payload | null;
|
|
13
17
|
export type PendingWriteDriver = {
|
|
14
18
|
clear: (key: string) => Promise<void>;
|
|
19
|
+
clearIf: (key: string, predicate: PendingWritePredicate) => Promise<boolean>;
|
|
15
20
|
clearAll: () => Promise<void>;
|
|
16
21
|
close?: () => Promise<void>;
|
|
17
22
|
get: (key: string) => Promise<unknown | null>;
|
|
18
23
|
list: () => Promise<unknown[]>;
|
|
19
24
|
set: (key: string, value: unknown) => Promise<void>;
|
|
25
|
+
setIf: (key: string, value: unknown, predicate: PendingWritePredicate) => Promise<boolean>;
|
|
20
26
|
};
|
|
27
|
+
export type PendingWritePredicate = (value: unknown | null) => boolean;
|
|
21
28
|
export type PendingWriteStore<Payload = unknown> = {
|
|
22
29
|
clear: (id: string) => Promise<void>;
|
|
23
30
|
clearAll: () => Promise<void>;
|
|
24
|
-
|
|
31
|
+
clearIfSnapshotId: (id: string, snapshotId: string) => Promise<boolean>;
|
|
25
32
|
close: () => Promise<void>;
|
|
26
33
|
get: (id: string) => Promise<PendingWriteRecord<Payload> | null>;
|
|
27
34
|
list: () => Promise<Array<PendingWriteRecord<Payload>>>;
|
|
28
|
-
|
|
35
|
+
replaceIfSnapshotId: (id: string, snapshotId: string, record: PendingWriteRecord<Payload>) => Promise<boolean>;
|
|
36
|
+
set: (record: PendingWriteRecordInput<Payload>) => Promise<PendingWriteRecord<Payload>>;
|
|
29
37
|
};
|
|
30
38
|
export type PendingWriteStoreOptions<Payload = unknown> = {
|
|
31
39
|
keyPrefix: string;
|
|
40
|
+
normalizeLegacyRecord?: PendingWritePayloadNormalizer<PendingWriteRecordInput<Payload>>;
|
|
32
41
|
normalizePayload?: PendingWritePayloadNormalizer<Payload>;
|
|
33
42
|
schemaVersion?: number;
|
|
34
43
|
};
|
|
@@ -51,11 +60,12 @@ export type SyncPendingWriteResult = {
|
|
|
51
60
|
error: string;
|
|
52
61
|
status: "failed";
|
|
53
62
|
};
|
|
54
|
-
export declare function createPendingWriteStore<Payload = unknown>(driver: PendingWriteDriver, { keyPrefix, normalizePayload, schemaVersion, }: PendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
|
|
63
|
+
export declare function createPendingWriteStore<Payload = unknown>(driver: PendingWriteDriver, { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion, }: PendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
|
|
55
64
|
export declare function createMemoryPendingWriteDriver(): PendingWriteDriver;
|
|
56
65
|
export declare function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, storeName, }: Omit<BrowserPendingWriteStoreOptions, "keyPrefix" | "normalizePayload" | "schemaVersion">): PendingWriteDriver | null;
|
|
57
|
-
export declare function createBrowserPendingWriteStore<Payload = unknown>({ databaseName, databaseVersion, indexedDB, keyPrefix, normalizePayload, operationTimeoutMs, schemaVersion, storeName, }: BrowserPendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
|
|
66
|
+
export declare function createBrowserPendingWriteStore<Payload = unknown>({ databaseName, databaseVersion, indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs, schemaVersion, storeName, }: BrowserPendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
|
|
58
67
|
export declare function createPendingWriteKey({ id, prefix }: PendingWriteKeyOptions): string;
|
|
68
|
+
export declare function createPendingWriteSnapshotId(): `${string}-${string}-${string}-${string}-${string}`;
|
|
59
69
|
export declare function syncPendingWrite<Payload>({ id, store, sync, }: {
|
|
60
70
|
id: string;
|
|
61
71
|
store: PendingWriteStore<Payload>;
|
package/dist/pendingWrites.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createIdbKeyValueDriver, } from "./idbDriver.js";
|
|
2
2
|
export const pendingWriteSchemaVersion = 1;
|
|
3
3
|
export const pendingWriteOperationTimeoutMs = 2_000;
|
|
4
|
-
export function createPendingWriteStore(driver, { keyPrefix, normalizePayload, schemaVersion = pendingWriteSchemaVersion, }) {
|
|
4
|
+
export function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion = pendingWriteSchemaVersion, }) {
|
|
5
5
|
return {
|
|
6
6
|
async clear(id) {
|
|
7
7
|
await driver.clear(createPendingWriteKey({ id, prefix: keyPrefix }));
|
|
@@ -9,33 +9,43 @@ export function createPendingWriteStore(driver, { keyPrefix, normalizePayload, s
|
|
|
9
9
|
async clearAll() {
|
|
10
10
|
await driver.clearAll();
|
|
11
11
|
},
|
|
12
|
-
async
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
await this.clear(id);
|
|
18
|
-
return true;
|
|
12
|
+
async clearIfSnapshotId(id, snapshotId) {
|
|
13
|
+
return driver.clearIf(createPendingWriteKey({ id, prefix: keyPrefix }), (value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
14
|
+
normalizeLegacyRecord,
|
|
15
|
+
normalizePayload,
|
|
16
|
+
})?.record.snapshotId === snapshotId);
|
|
19
17
|
},
|
|
20
18
|
async close() {
|
|
21
19
|
await driver.close?.();
|
|
22
20
|
},
|
|
23
21
|
async get(id) {
|
|
24
|
-
return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizePayload })?.record ?? null);
|
|
22
|
+
return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizeLegacyRecord, normalizePayload })?.record ?? null);
|
|
25
23
|
},
|
|
26
24
|
async list() {
|
|
27
25
|
return (await driver.list())
|
|
28
26
|
.map((value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
27
|
+
normalizeLegacyRecord,
|
|
29
28
|
normalizePayload,
|
|
30
29
|
})?.record)
|
|
31
30
|
.filter((record) => Boolean(record))
|
|
32
31
|
.sort((left, right) => left.savedAt.localeCompare(right.savedAt));
|
|
33
32
|
},
|
|
33
|
+
async replaceIfSnapshotId(id, snapshotId, record) {
|
|
34
|
+
return driver.setIf(createPendingWriteKey({ id, prefix: keyPrefix }), { record, schemaVersion }, (value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
35
|
+
normalizeLegacyRecord,
|
|
36
|
+
normalizePayload,
|
|
37
|
+
})?.record.snapshotId === snapshotId);
|
|
38
|
+
},
|
|
34
39
|
async set(record) {
|
|
40
|
+
const storedRecord = {
|
|
41
|
+
...record,
|
|
42
|
+
snapshotId: record.snapshotId ?? createPendingWriteSnapshotId(),
|
|
43
|
+
};
|
|
35
44
|
await driver.set(createPendingWriteKey({ id: record.id, prefix: keyPrefix }), {
|
|
36
|
-
record,
|
|
45
|
+
record: storedRecord,
|
|
37
46
|
schemaVersion,
|
|
38
47
|
});
|
|
48
|
+
return storedRecord;
|
|
39
49
|
},
|
|
40
50
|
};
|
|
41
51
|
}
|
|
@@ -45,6 +55,14 @@ export function createMemoryPendingWriteDriver() {
|
|
|
45
55
|
async clear(key) {
|
|
46
56
|
records.delete(key);
|
|
47
57
|
},
|
|
58
|
+
async clearIf(key, predicate) {
|
|
59
|
+
const current = records.get(key) ?? null;
|
|
60
|
+
if (!predicate(current)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
records.delete(key);
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
48
66
|
async clearAll() {
|
|
49
67
|
records.clear();
|
|
50
68
|
},
|
|
@@ -58,6 +76,13 @@ export function createMemoryPendingWriteDriver() {
|
|
|
58
76
|
async set(key, value) {
|
|
59
77
|
records.set(key, value);
|
|
60
78
|
},
|
|
79
|
+
async setIf(key, value, predicate) {
|
|
80
|
+
if (!predicate(records.get(key) ?? null)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
records.set(key, value);
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
61
86
|
};
|
|
62
87
|
}
|
|
63
88
|
export function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, operationTimeoutMs = pendingWriteOperationTimeoutMs, storeName, }) {
|
|
@@ -70,7 +95,7 @@ export function createBrowserPendingWriteDriver({ databaseName, databaseVersion,
|
|
|
70
95
|
});
|
|
71
96
|
return driver ? toPendingWriteDriver(driver) : null;
|
|
72
97
|
}
|
|
73
|
-
export function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, keyPrefix, normalizePayload, operationTimeoutMs = pendingWriteOperationTimeoutMs, schemaVersion = pendingWriteSchemaVersion, storeName, }) {
|
|
98
|
+
export function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs = pendingWriteOperationTimeoutMs, schemaVersion = pendingWriteSchemaVersion, storeName, }) {
|
|
74
99
|
const driver = createBrowserPendingWriteDriver({
|
|
75
100
|
databaseName,
|
|
76
101
|
databaseVersion,
|
|
@@ -78,11 +103,14 @@ export function createBrowserPendingWriteStore({ databaseName, databaseVersion,
|
|
|
78
103
|
operationTimeoutMs,
|
|
79
104
|
storeName,
|
|
80
105
|
});
|
|
81
|
-
return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizePayload, schemaVersion });
|
|
106
|
+
return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
|
|
82
107
|
}
|
|
83
108
|
export function createPendingWriteKey({ id, prefix }) {
|
|
84
109
|
return `${prefix}:${encodeURIComponent(id)}`;
|
|
85
110
|
}
|
|
111
|
+
export function createPendingWriteSnapshotId() {
|
|
112
|
+
return globalThis.crypto.randomUUID();
|
|
113
|
+
}
|
|
86
114
|
export async function syncPendingWrite({ id, store, sync, }) {
|
|
87
115
|
const pending = await store.get(id);
|
|
88
116
|
if (!pending) {
|
|
@@ -91,7 +119,7 @@ export async function syncPendingWrite({ id, store, sync, }) {
|
|
|
91
119
|
await markPendingSnapshot({
|
|
92
120
|
id,
|
|
93
121
|
nextRecord: { ...pending, error: undefined, syncStatus: "syncing" },
|
|
94
|
-
|
|
122
|
+
snapshotId: pending.snapshotId,
|
|
95
123
|
store,
|
|
96
124
|
});
|
|
97
125
|
try {
|
|
@@ -102,18 +130,21 @@ export async function syncPendingWrite({ id, store, sync, }) {
|
|
|
102
130
|
await markPendingSnapshot({
|
|
103
131
|
id,
|
|
104
132
|
nextRecord: { ...pending, error: message, syncStatus: "failed" },
|
|
105
|
-
|
|
133
|
+
snapshotId: pending.snapshotId,
|
|
106
134
|
store,
|
|
107
135
|
});
|
|
108
136
|
return { error: message, status: "failed" };
|
|
109
137
|
}
|
|
110
|
-
return (await store.
|
|
138
|
+
return (await store.clearIfSnapshotId(id, pending.snapshotId))
|
|
111
139
|
? { status: "synced" }
|
|
112
140
|
: { status: "superseded" };
|
|
113
141
|
}
|
|
114
142
|
function createNoopPendingWriteDriver() {
|
|
115
143
|
return {
|
|
116
144
|
async clear() { },
|
|
145
|
+
async clearIf() {
|
|
146
|
+
return false;
|
|
147
|
+
},
|
|
117
148
|
async clearAll() { },
|
|
118
149
|
async close() { },
|
|
119
150
|
async get() {
|
|
@@ -123,43 +154,41 @@ function createNoopPendingWriteDriver() {
|
|
|
123
154
|
return [];
|
|
124
155
|
},
|
|
125
156
|
async set() { },
|
|
157
|
+
async setIf() {
|
|
158
|
+
return false;
|
|
159
|
+
},
|
|
126
160
|
};
|
|
127
161
|
}
|
|
128
162
|
function toPendingWriteDriver(driver) {
|
|
129
163
|
return {
|
|
130
164
|
clear: driver.delete,
|
|
165
|
+
clearIf: driver.deleteIf,
|
|
131
166
|
clearAll: driver.clear,
|
|
132
167
|
close: driver.close,
|
|
133
168
|
get: driver.get,
|
|
134
169
|
list: driver.list,
|
|
135
170
|
set: driver.set,
|
|
171
|
+
setIf: driver.setIf,
|
|
136
172
|
};
|
|
137
173
|
}
|
|
138
|
-
async function markPendingSnapshot({ id, nextRecord,
|
|
139
|
-
|
|
140
|
-
if (current?.savedAt !== savedAt) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
await store.set(nextRecord);
|
|
144
|
-
return true;
|
|
174
|
+
async function markPendingSnapshot({ id, nextRecord, snapshotId, store, }) {
|
|
175
|
+
return store.replaceIfSnapshotId(id, snapshotId, nextRecord);
|
|
145
176
|
}
|
|
146
|
-
function toPendingWriteEnvelope(value, schemaVersion, { normalizePayload }) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
value
|
|
151
|
-
|
|
152
|
-
!isPendingWriteRecord(value.record)) {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
const record = normalizePendingWriteRecord(value.record, normalizePayload);
|
|
177
|
+
function toPendingWriteEnvelope(value, schemaVersion, { normalizeLegacyRecord, normalizePayload }) {
|
|
178
|
+
const envelopeRecord = readPendingWriteEnvelopeRecord(value, schemaVersion);
|
|
179
|
+
const legacyRecord = envelopeRecord
|
|
180
|
+
? null
|
|
181
|
+
: (normalizeLegacyRecord?.(value) ?? null);
|
|
182
|
+
const record = normalizePendingWriteRecord(envelopeRecord ?? legacyRecord, normalizePayload);
|
|
156
183
|
return record ? { record, schemaVersion } : null;
|
|
157
184
|
}
|
|
158
185
|
function normalizePendingWriteRecord(record, normalizePayload) {
|
|
159
|
-
if (!
|
|
160
|
-
return
|
|
186
|
+
if (!record) {
|
|
187
|
+
return null;
|
|
161
188
|
}
|
|
162
|
-
const payload = normalizePayload
|
|
189
|
+
const payload = normalizePayload
|
|
190
|
+
? normalizePayload(record.payload)
|
|
191
|
+
: record.payload;
|
|
163
192
|
if (payload === null) {
|
|
164
193
|
return null;
|
|
165
194
|
}
|
|
@@ -168,9 +197,24 @@ function normalizePendingWriteRecord(record, normalizePayload) {
|
|
|
168
197
|
id: record.id,
|
|
169
198
|
payload,
|
|
170
199
|
savedAt: record.savedAt,
|
|
200
|
+
snapshotId: record.snapshotId ?? createLegacyPendingWriteSnapshotId(record),
|
|
171
201
|
syncStatus: record.syncStatus,
|
|
172
202
|
};
|
|
173
203
|
}
|
|
204
|
+
function readPendingWriteEnvelopeRecord(value, schemaVersion) {
|
|
205
|
+
if (!value ||
|
|
206
|
+
typeof value !== "object" ||
|
|
207
|
+
!("schemaVersion" in value) ||
|
|
208
|
+
value.schemaVersion !== schemaVersion ||
|
|
209
|
+
!("record" in value) ||
|
|
210
|
+
!isPendingWriteRecord(value.record)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return value.record;
|
|
214
|
+
}
|
|
215
|
+
function createLegacyPendingWriteSnapshotId(record) {
|
|
216
|
+
return `legacy:${encodeURIComponent(record.id)}:${encodeURIComponent(record.savedAt)}`;
|
|
217
|
+
}
|
|
174
218
|
function isPendingWriteRecord(value) {
|
|
175
219
|
return (value !== null &&
|
|
176
220
|
typeof value === "object" &&
|
|
@@ -178,6 +222,7 @@ function isPendingWriteRecord(value) {
|
|
|
178
222
|
typeof value.id === "string" &&
|
|
179
223
|
"savedAt" in value &&
|
|
180
224
|
typeof value.savedAt === "string" &&
|
|
225
|
+
(!("snapshotId" in value) || typeof value.snapshotId === "string") &&
|
|
181
226
|
"syncStatus" in value &&
|
|
182
227
|
(value.syncStatus === "pending" ||
|
|
183
228
|
value.syncStatus === "syncing" ||
|
package/dist/readCache.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type BrowserKeyValueDriverOptions } from "./idbDriver";
|
|
2
2
|
export declare const readCacheSchemaVersion = 1;
|
|
3
3
|
export declare const readCacheFreshnessMaxAgeMs = 30000;
|
|
4
|
+
export declare const durableReadCacheFreshnessMaxAgeMs: number;
|
|
5
|
+
export declare const identityReadCacheFreshnessMaxAgeMs: number;
|
|
4
6
|
export declare const browserReadCacheOperationTimeoutMs = 2000;
|
|
5
7
|
export type ReadCacheHit<T> = {
|
|
6
8
|
fromCache: true;
|
|
@@ -10,14 +12,23 @@ export type ReadCacheHit<T> = {
|
|
|
10
12
|
export type ReadCacheDriver = {
|
|
11
13
|
clear: () => Promise<void>;
|
|
12
14
|
close?: () => Promise<void>;
|
|
15
|
+
delete: (key: string) => Promise<void>;
|
|
13
16
|
get: (key: string) => Promise<unknown | null>;
|
|
14
17
|
set: (key: string, value: unknown) => Promise<void>;
|
|
15
18
|
};
|
|
16
19
|
export type ReadCache = {
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a stable cache for the duration of a resource load. Dynamic
|
|
22
|
+
* adapters use this to pin the active account before asynchronous work.
|
|
23
|
+
*/
|
|
24
|
+
capture?: () => ReadCache;
|
|
17
25
|
clear: () => Promise<void>;
|
|
18
26
|
close: () => Promise<void>;
|
|
27
|
+
delete: (key: string) => Promise<void>;
|
|
19
28
|
get: <T>(key: string) => Promise<ReadCacheHit<T> | null>;
|
|
29
|
+
getVersion: (key: string) => string;
|
|
20
30
|
set: <T>(key: string, value: T) => Promise<void>;
|
|
31
|
+
setIfVersion: <T>(key: string, value: T, version: string) => Promise<boolean>;
|
|
21
32
|
};
|
|
22
33
|
export type ReadThroughCacheResult<T> = {
|
|
23
34
|
data: T;
|
|
@@ -45,15 +56,18 @@ export declare function createReadCache(driver: ReadCacheDriver): ReadCache;
|
|
|
45
56
|
export declare function createMemoryReadCacheDriver(): ReadCacheDriver;
|
|
46
57
|
export declare function createBrowserReadCache({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, storeName, }: BrowserReadCacheOptions): ReadCache;
|
|
47
58
|
export declare function createReadCacheKey({ namespace, params, prefix, scope, }: ReadCacheKeyOptions): string;
|
|
48
|
-
export declare function readThroughCache<T>({ cache, key, loadFresh, }: {
|
|
59
|
+
export declare function readThroughCache<T>({ cache, key, loadFresh, maxAgeMs, }: {
|
|
49
60
|
cache: ReadCache;
|
|
50
61
|
key: string;
|
|
51
62
|
loadFresh: () => Promise<T>;
|
|
63
|
+
maxAgeMs?: number;
|
|
52
64
|
}): Promise<ReadThroughCacheResult<T>>;
|
|
53
|
-
export declare function loadReadCacheResource<T>({ cache, key, loadFresh, maxAgeMs, onUpdate, }: {
|
|
65
|
+
export declare function loadReadCacheResource<T>({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache, }: {
|
|
54
66
|
cache: ReadCache;
|
|
55
67
|
key: string;
|
|
56
68
|
loadFresh: () => Promise<T>;
|
|
57
69
|
maxAgeMs?: number;
|
|
70
|
+
onCacheWriteError?: (error: Error) => void;
|
|
58
71
|
onUpdate: (update: ReadCacheResourceUpdate<T>) => void;
|
|
72
|
+
skipCache?: boolean;
|
|
59
73
|
}): Promise<T>;
|
package/dist/readCache.js
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
import { createIdbKeyValueDriver, } from "./idbDriver.js";
|
|
2
2
|
export const readCacheSchemaVersion = 1;
|
|
3
3
|
export const readCacheFreshnessMaxAgeMs = 30_000;
|
|
4
|
+
export const durableReadCacheFreshnessMaxAgeMs = 60 * 60_000;
|
|
5
|
+
export const identityReadCacheFreshnessMaxAgeMs = 24 * 60 * 60_000;
|
|
4
6
|
export const browserReadCacheOperationTimeoutMs = 2_000;
|
|
5
|
-
const pendingFreshLoads = new
|
|
7
|
+
const pendingFreshLoads = new WeakMap();
|
|
6
8
|
export function createReadCache(driver) {
|
|
7
|
-
|
|
9
|
+
let globalVersion = 0;
|
|
10
|
+
const keyVersions = new Map();
|
|
11
|
+
let operationQueue = Promise.resolve();
|
|
12
|
+
const pendingLoads = new Map();
|
|
13
|
+
function getVersion(key) {
|
|
14
|
+
return `${globalVersion}:${keyVersions.get(key) ?? 0}`;
|
|
15
|
+
}
|
|
16
|
+
function enqueue(operation) {
|
|
17
|
+
const next = operationQueue.then(operation, operation);
|
|
18
|
+
operationQueue = next.then(() => undefined, () => undefined);
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
const cache = {
|
|
8
22
|
async clear() {
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
pendingLoads.clear();
|
|
24
|
+
globalVersion += 1;
|
|
25
|
+
keyVersions.clear();
|
|
26
|
+
await enqueue(() => driver.clear());
|
|
11
27
|
},
|
|
12
28
|
async close() {
|
|
13
|
-
await driver.close?.();
|
|
29
|
+
await enqueue(async () => driver.close?.());
|
|
30
|
+
},
|
|
31
|
+
async delete(key) {
|
|
32
|
+
pendingLoads.delete(key);
|
|
33
|
+
keyVersions.set(key, (keyVersions.get(key) ?? 0) + 1);
|
|
34
|
+
await enqueue(() => driver.delete(key));
|
|
14
35
|
},
|
|
15
36
|
async get(key) {
|
|
16
37
|
const envelope = toReadCacheEnvelope(await driver.get(key));
|
|
@@ -22,14 +43,33 @@ export function createReadCache(driver) {
|
|
|
22
43
|
}
|
|
23
44
|
: null;
|
|
24
45
|
},
|
|
46
|
+
getVersion,
|
|
25
47
|
async set(key, value) {
|
|
26
|
-
await driver.set(key, {
|
|
48
|
+
await enqueue(() => driver.set(key, {
|
|
27
49
|
savedAt: new Date().toISOString(),
|
|
28
50
|
schemaVersion: readCacheSchemaVersion,
|
|
29
51
|
value,
|
|
52
|
+
}));
|
|
53
|
+
},
|
|
54
|
+
async setIfVersion(key, value, version) {
|
|
55
|
+
if (getVersion(key) !== version) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return enqueue(async () => {
|
|
59
|
+
if (getVersion(key) !== version) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
await driver.set(key, {
|
|
63
|
+
savedAt: new Date().toISOString(),
|
|
64
|
+
schemaVersion: readCacheSchemaVersion,
|
|
65
|
+
value,
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
30
68
|
});
|
|
31
69
|
},
|
|
32
70
|
};
|
|
71
|
+
pendingFreshLoads.set(cache, pendingLoads);
|
|
72
|
+
return cache;
|
|
33
73
|
}
|
|
34
74
|
export function createMemoryReadCacheDriver() {
|
|
35
75
|
const records = new Map();
|
|
@@ -38,6 +78,9 @@ export function createMemoryReadCacheDriver() {
|
|
|
38
78
|
records.clear();
|
|
39
79
|
},
|
|
40
80
|
async close() { },
|
|
81
|
+
async delete(key) {
|
|
82
|
+
records.delete(key);
|
|
83
|
+
},
|
|
41
84
|
async get(key) {
|
|
42
85
|
return records.get(key) ?? null;
|
|
43
86
|
},
|
|
@@ -71,11 +114,21 @@ export function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
|
|
|
71
114
|
.filter(Boolean)
|
|
72
115
|
.join(":");
|
|
73
116
|
}
|
|
74
|
-
export async function readThroughCache({ cache, key, loadFresh, }) {
|
|
75
|
-
const
|
|
117
|
+
export async function readThroughCache({ cache, key, loadFresh, maxAgeMs, }) {
|
|
118
|
+
const resourceCache = cache.capture?.() ?? cache;
|
|
119
|
+
const version = resourceCache.getVersion(key);
|
|
120
|
+
const cached = await resourceCache.get(key);
|
|
121
|
+
if (cached && isCacheFresh(cached.savedAt, maxAgeMs)) {
|
|
122
|
+
return {
|
|
123
|
+
data: cached.value,
|
|
124
|
+
error: null,
|
|
125
|
+
fromCache: true,
|
|
126
|
+
stale: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
76
129
|
try {
|
|
77
130
|
const fresh = await loadFresh();
|
|
78
|
-
await
|
|
131
|
+
await resourceCache.setIfVersion(key, fresh, version);
|
|
79
132
|
return {
|
|
80
133
|
data: fresh,
|
|
81
134
|
error: null,
|
|
@@ -95,8 +148,10 @@ export async function readThroughCache({ cache, key, loadFresh, }) {
|
|
|
95
148
|
throw error;
|
|
96
149
|
}
|
|
97
150
|
}
|
|
98
|
-
export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onUpdate, }) {
|
|
99
|
-
const
|
|
151
|
+
export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache = false, }) {
|
|
152
|
+
const resourceCache = cache.capture?.() ?? cache;
|
|
153
|
+
const version = resourceCache.getVersion(key);
|
|
154
|
+
const cached = skipCache ? null : await resourceCache.get(key);
|
|
100
155
|
if (cached) {
|
|
101
156
|
onUpdate({ data: cached.value, fromCache: true });
|
|
102
157
|
if (isCacheFresh(cached.savedAt, maxAgeMs)) {
|
|
@@ -104,9 +159,16 @@ export async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, o
|
|
|
104
159
|
}
|
|
105
160
|
}
|
|
106
161
|
try {
|
|
107
|
-
const fresh = await loadSharedFreshResource(key, loadFresh);
|
|
162
|
+
const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
|
|
108
163
|
onUpdate({ data: fresh, fromCache: false });
|
|
109
|
-
void
|
|
164
|
+
void resourceCache.setIfVersion(key, fresh, version).catch((error) => {
|
|
165
|
+
const normalizedError = normalizeError(error);
|
|
166
|
+
console.error("[Bayon offline] Unable to refresh the read cache.", {
|
|
167
|
+
error: normalizedError,
|
|
168
|
+
key,
|
|
169
|
+
});
|
|
170
|
+
onCacheWriteError?.(normalizedError);
|
|
171
|
+
});
|
|
110
172
|
return fresh;
|
|
111
173
|
}
|
|
112
174
|
catch (error) {
|
|
@@ -125,6 +187,7 @@ function createNoopReadCacheDriver() {
|
|
|
125
187
|
return {
|
|
126
188
|
async clear() { },
|
|
127
189
|
async close() { },
|
|
190
|
+
async delete() { },
|
|
128
191
|
async get() {
|
|
129
192
|
return null;
|
|
130
193
|
},
|
|
@@ -138,22 +201,32 @@ function isCacheFresh(savedAt, maxAgeMs) {
|
|
|
138
201
|
const savedTime = Date.parse(savedAt);
|
|
139
202
|
return Number.isFinite(savedTime) && Date.now() - savedTime <= maxAgeMs;
|
|
140
203
|
}
|
|
141
|
-
async function loadSharedFreshResource(key, loadFresh) {
|
|
142
|
-
const
|
|
204
|
+
async function loadSharedFreshResource(cache, key, loadFresh) {
|
|
205
|
+
const pendingLoads = getPendingFreshLoads(cache);
|
|
206
|
+
const pendingLoad = pendingLoads.get(key);
|
|
143
207
|
if (pendingLoad) {
|
|
144
208
|
return (await pendingLoad);
|
|
145
209
|
}
|
|
146
210
|
const nextLoad = loadFresh();
|
|
147
|
-
|
|
211
|
+
pendingLoads.set(key, nextLoad);
|
|
148
212
|
try {
|
|
149
213
|
return await nextLoad;
|
|
150
214
|
}
|
|
151
215
|
finally {
|
|
152
|
-
if (
|
|
153
|
-
|
|
216
|
+
if (pendingLoads.get(key) === nextLoad) {
|
|
217
|
+
pendingLoads.delete(key);
|
|
154
218
|
}
|
|
155
219
|
}
|
|
156
220
|
}
|
|
221
|
+
function getPendingFreshLoads(cache) {
|
|
222
|
+
const existing = pendingFreshLoads.get(cache);
|
|
223
|
+
if (existing) {
|
|
224
|
+
return existing;
|
|
225
|
+
}
|
|
226
|
+
const next = new Map();
|
|
227
|
+
pendingFreshLoads.set(cache, next);
|
|
228
|
+
return next;
|
|
229
|
+
}
|
|
157
230
|
function toReadCacheEnvelope(value) {
|
|
158
231
|
if (!value || typeof value !== "object") {
|
|
159
232
|
return null;
|
package/dist-cjs/idbDriver.js
CHANGED
|
@@ -16,6 +16,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
16
16
|
async clear() {
|
|
17
17
|
await runBrowserStorageOperation({
|
|
18
18
|
fallback: undefined,
|
|
19
|
+
operationName: "clear object store",
|
|
19
20
|
operation: async () => {
|
|
20
21
|
const database = await databasePromise;
|
|
21
22
|
await database.clear(storeName);
|
|
@@ -26,6 +27,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
26
27
|
async close() {
|
|
27
28
|
await runBrowserStorageOperation({
|
|
28
29
|
fallback: undefined,
|
|
30
|
+
operationName: "close database",
|
|
29
31
|
operation: async () => {
|
|
30
32
|
const database = await databasePromise;
|
|
31
33
|
database.close();
|
|
@@ -36,6 +38,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
36
38
|
async delete(key) {
|
|
37
39
|
await runBrowserStorageOperation({
|
|
38
40
|
fallback: undefined,
|
|
41
|
+
operationName: "delete key",
|
|
39
42
|
operation: async () => {
|
|
40
43
|
const database = await databasePromise;
|
|
41
44
|
await database.delete(storeName, key);
|
|
@@ -43,9 +46,32 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
43
46
|
timeoutMs: operationTimeoutMs,
|
|
44
47
|
});
|
|
45
48
|
},
|
|
49
|
+
async deleteIf(key, predicate) {
|
|
50
|
+
return runBrowserStorageOperation({
|
|
51
|
+
fallback: false,
|
|
52
|
+
operationName: "conditionally delete key",
|
|
53
|
+
operation: async () => {
|
|
54
|
+
const database = await databasePromise;
|
|
55
|
+
// The ownership check and deletion must share a transaction; splitting
|
|
56
|
+
// them can delete a newer offline snapshot written by another trigger.
|
|
57
|
+
const transaction = database.transaction(storeName, "readwrite");
|
|
58
|
+
const objectStore = transaction.objectStore(storeName);
|
|
59
|
+
const current = (await objectStore.get(key)) ?? null;
|
|
60
|
+
if (!predicate(current)) {
|
|
61
|
+
await transaction.done;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
await objectStore.delete(key);
|
|
65
|
+
await transaction.done;
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
timeoutMs: operationTimeoutMs,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
46
71
|
async get(key) {
|
|
47
72
|
return runBrowserStorageOperation({
|
|
48
73
|
fallback: null,
|
|
74
|
+
operationName: "read key",
|
|
49
75
|
operation: async () => {
|
|
50
76
|
const database = await databasePromise;
|
|
51
77
|
return (await database.get(storeName, key)) ?? null;
|
|
@@ -56,6 +82,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
56
82
|
async list() {
|
|
57
83
|
return runBrowserStorageOperation({
|
|
58
84
|
fallback: [],
|
|
85
|
+
operationName: "list object store",
|
|
59
86
|
operation: async () => {
|
|
60
87
|
const database = await databasePromise;
|
|
61
88
|
return database.getAll(storeName);
|
|
@@ -66,6 +93,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
66
93
|
async set(key, value) {
|
|
67
94
|
await runBrowserStorageOperation({
|
|
68
95
|
fallback: undefined,
|
|
96
|
+
operationName: "write key",
|
|
69
97
|
operation: async () => {
|
|
70
98
|
const database = await databasePromise;
|
|
71
99
|
await database.put(storeName, value, key);
|
|
@@ -73,6 +101,26 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
|
|
|
73
101
|
timeoutMs: operationTimeoutMs,
|
|
74
102
|
});
|
|
75
103
|
},
|
|
104
|
+
async setIf(key, value, predicate) {
|
|
105
|
+
return runBrowserStorageOperation({
|
|
106
|
+
fallback: false,
|
|
107
|
+
operationName: "conditionally replace key",
|
|
108
|
+
operation: async () => {
|
|
109
|
+
const database = await databasePromise;
|
|
110
|
+
const transaction = database.transaction(storeName, "readwrite");
|
|
111
|
+
const objectStore = transaction.objectStore(storeName);
|
|
112
|
+
const current = (await objectStore.get(key)) ?? null;
|
|
113
|
+
if (!predicate(current)) {
|
|
114
|
+
await transaction.done;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
await objectStore.put(value, key);
|
|
118
|
+
await transaction.done;
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
timeoutMs: operationTimeoutMs,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
76
124
|
};
|
|
77
125
|
}
|
|
78
126
|
async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB, storeName, }) {
|
|
@@ -89,17 +137,23 @@ async function openKeyValueDatabase({ databaseName, databaseVersion, indexedDB,
|
|
|
89
137
|
}
|
|
90
138
|
return database;
|
|
91
139
|
}
|
|
92
|
-
async function runBrowserStorageOperation({ fallback, operation, timeoutMs, }) {
|
|
140
|
+
async function runBrowserStorageOperation({ fallback, operation, operationName, timeoutMs, }) {
|
|
93
141
|
let timeoutId = null;
|
|
94
142
|
try {
|
|
95
143
|
return await Promise.race([
|
|
96
144
|
operation(),
|
|
97
145
|
new Promise((resolve) => {
|
|
98
|
-
timeoutId = setTimeout(() =>
|
|
146
|
+
timeoutId = setTimeout(() => {
|
|
147
|
+
console.error(`IndexedDB timed out while trying to ${operationName}.`, {
|
|
148
|
+
timeoutMs,
|
|
149
|
+
});
|
|
150
|
+
resolve(fallback);
|
|
151
|
+
}, timeoutMs);
|
|
99
152
|
}),
|
|
100
153
|
]);
|
|
101
154
|
}
|
|
102
|
-
catch {
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error(`IndexedDB failed to ${operationName}.`, { error });
|
|
103
157
|
return fallback;
|
|
104
158
|
}
|
|
105
159
|
finally {
|
|
@@ -6,11 +6,12 @@ exports.createMemoryPendingWriteDriver = createMemoryPendingWriteDriver;
|
|
|
6
6
|
exports.createBrowserPendingWriteDriver = createBrowserPendingWriteDriver;
|
|
7
7
|
exports.createBrowserPendingWriteStore = createBrowserPendingWriteStore;
|
|
8
8
|
exports.createPendingWriteKey = createPendingWriteKey;
|
|
9
|
+
exports.createPendingWriteSnapshotId = createPendingWriteSnapshotId;
|
|
9
10
|
exports.syncPendingWrite = syncPendingWrite;
|
|
10
11
|
const idbDriver_1 = require("./idbDriver");
|
|
11
12
|
exports.pendingWriteSchemaVersion = 1;
|
|
12
13
|
exports.pendingWriteOperationTimeoutMs = 2_000;
|
|
13
|
-
function createPendingWriteStore(driver, { keyPrefix, normalizePayload, schemaVersion = exports.pendingWriteSchemaVersion, }) {
|
|
14
|
+
function createPendingWriteStore(driver, { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion = exports.pendingWriteSchemaVersion, }) {
|
|
14
15
|
return {
|
|
15
16
|
async clear(id) {
|
|
16
17
|
await driver.clear(createPendingWriteKey({ id, prefix: keyPrefix }));
|
|
@@ -18,33 +19,43 @@ function createPendingWriteStore(driver, { keyPrefix, normalizePayload, schemaVe
|
|
|
18
19
|
async clearAll() {
|
|
19
20
|
await driver.clearAll();
|
|
20
21
|
},
|
|
21
|
-
async
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
await this.clear(id);
|
|
27
|
-
return true;
|
|
22
|
+
async clearIfSnapshotId(id, snapshotId) {
|
|
23
|
+
return driver.clearIf(createPendingWriteKey({ id, prefix: keyPrefix }), (value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
24
|
+
normalizeLegacyRecord,
|
|
25
|
+
normalizePayload,
|
|
26
|
+
})?.record.snapshotId === snapshotId);
|
|
28
27
|
},
|
|
29
28
|
async close() {
|
|
30
29
|
await driver.close?.();
|
|
31
30
|
},
|
|
32
31
|
async get(id) {
|
|
33
|
-
return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizePayload })?.record ?? null);
|
|
32
|
+
return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizeLegacyRecord, normalizePayload })?.record ?? null);
|
|
34
33
|
},
|
|
35
34
|
async list() {
|
|
36
35
|
return (await driver.list())
|
|
37
36
|
.map((value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
37
|
+
normalizeLegacyRecord,
|
|
38
38
|
normalizePayload,
|
|
39
39
|
})?.record)
|
|
40
40
|
.filter((record) => Boolean(record))
|
|
41
41
|
.sort((left, right) => left.savedAt.localeCompare(right.savedAt));
|
|
42
42
|
},
|
|
43
|
+
async replaceIfSnapshotId(id, snapshotId, record) {
|
|
44
|
+
return driver.setIf(createPendingWriteKey({ id, prefix: keyPrefix }), { record, schemaVersion }, (value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
45
|
+
normalizeLegacyRecord,
|
|
46
|
+
normalizePayload,
|
|
47
|
+
})?.record.snapshotId === snapshotId);
|
|
48
|
+
},
|
|
43
49
|
async set(record) {
|
|
50
|
+
const storedRecord = {
|
|
51
|
+
...record,
|
|
52
|
+
snapshotId: record.snapshotId ?? createPendingWriteSnapshotId(),
|
|
53
|
+
};
|
|
44
54
|
await driver.set(createPendingWriteKey({ id: record.id, prefix: keyPrefix }), {
|
|
45
|
-
record,
|
|
55
|
+
record: storedRecord,
|
|
46
56
|
schemaVersion,
|
|
47
57
|
});
|
|
58
|
+
return storedRecord;
|
|
48
59
|
},
|
|
49
60
|
};
|
|
50
61
|
}
|
|
@@ -54,6 +65,14 @@ function createMemoryPendingWriteDriver() {
|
|
|
54
65
|
async clear(key) {
|
|
55
66
|
records.delete(key);
|
|
56
67
|
},
|
|
68
|
+
async clearIf(key, predicate) {
|
|
69
|
+
const current = records.get(key) ?? null;
|
|
70
|
+
if (!predicate(current)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
records.delete(key);
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
57
76
|
async clearAll() {
|
|
58
77
|
records.clear();
|
|
59
78
|
},
|
|
@@ -67,6 +86,13 @@ function createMemoryPendingWriteDriver() {
|
|
|
67
86
|
async set(key, value) {
|
|
68
87
|
records.set(key, value);
|
|
69
88
|
},
|
|
89
|
+
async setIf(key, value, predicate) {
|
|
90
|
+
if (!predicate(records.get(key) ?? null)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
records.set(key, value);
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
70
96
|
};
|
|
71
97
|
}
|
|
72
98
|
function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, operationTimeoutMs = exports.pendingWriteOperationTimeoutMs, storeName, }) {
|
|
@@ -79,7 +105,7 @@ function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexe
|
|
|
79
105
|
});
|
|
80
106
|
return driver ? toPendingWriteDriver(driver) : null;
|
|
81
107
|
}
|
|
82
|
-
function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, keyPrefix, normalizePayload, operationTimeoutMs = exports.pendingWriteOperationTimeoutMs, schemaVersion = exports.pendingWriteSchemaVersion, storeName, }) {
|
|
108
|
+
function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs = exports.pendingWriteOperationTimeoutMs, schemaVersion = exports.pendingWriteSchemaVersion, storeName, }) {
|
|
83
109
|
const driver = createBrowserPendingWriteDriver({
|
|
84
110
|
databaseName,
|
|
85
111
|
databaseVersion,
|
|
@@ -87,11 +113,14 @@ function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexed
|
|
|
87
113
|
operationTimeoutMs,
|
|
88
114
|
storeName,
|
|
89
115
|
});
|
|
90
|
-
return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizePayload, schemaVersion });
|
|
116
|
+
return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
|
|
91
117
|
}
|
|
92
118
|
function createPendingWriteKey({ id, prefix }) {
|
|
93
119
|
return `${prefix}:${encodeURIComponent(id)}`;
|
|
94
120
|
}
|
|
121
|
+
function createPendingWriteSnapshotId() {
|
|
122
|
+
return globalThis.crypto.randomUUID();
|
|
123
|
+
}
|
|
95
124
|
async function syncPendingWrite({ id, store, sync, }) {
|
|
96
125
|
const pending = await store.get(id);
|
|
97
126
|
if (!pending) {
|
|
@@ -100,7 +129,7 @@ async function syncPendingWrite({ id, store, sync, }) {
|
|
|
100
129
|
await markPendingSnapshot({
|
|
101
130
|
id,
|
|
102
131
|
nextRecord: { ...pending, error: undefined, syncStatus: "syncing" },
|
|
103
|
-
|
|
132
|
+
snapshotId: pending.snapshotId,
|
|
104
133
|
store,
|
|
105
134
|
});
|
|
106
135
|
try {
|
|
@@ -111,18 +140,21 @@ async function syncPendingWrite({ id, store, sync, }) {
|
|
|
111
140
|
await markPendingSnapshot({
|
|
112
141
|
id,
|
|
113
142
|
nextRecord: { ...pending, error: message, syncStatus: "failed" },
|
|
114
|
-
|
|
143
|
+
snapshotId: pending.snapshotId,
|
|
115
144
|
store,
|
|
116
145
|
});
|
|
117
146
|
return { error: message, status: "failed" };
|
|
118
147
|
}
|
|
119
|
-
return (await store.
|
|
148
|
+
return (await store.clearIfSnapshotId(id, pending.snapshotId))
|
|
120
149
|
? { status: "synced" }
|
|
121
150
|
: { status: "superseded" };
|
|
122
151
|
}
|
|
123
152
|
function createNoopPendingWriteDriver() {
|
|
124
153
|
return {
|
|
125
154
|
async clear() { },
|
|
155
|
+
async clearIf() {
|
|
156
|
+
return false;
|
|
157
|
+
},
|
|
126
158
|
async clearAll() { },
|
|
127
159
|
async close() { },
|
|
128
160
|
async get() {
|
|
@@ -132,43 +164,41 @@ function createNoopPendingWriteDriver() {
|
|
|
132
164
|
return [];
|
|
133
165
|
},
|
|
134
166
|
async set() { },
|
|
167
|
+
async setIf() {
|
|
168
|
+
return false;
|
|
169
|
+
},
|
|
135
170
|
};
|
|
136
171
|
}
|
|
137
172
|
function toPendingWriteDriver(driver) {
|
|
138
173
|
return {
|
|
139
174
|
clear: driver.delete,
|
|
175
|
+
clearIf: driver.deleteIf,
|
|
140
176
|
clearAll: driver.clear,
|
|
141
177
|
close: driver.close,
|
|
142
178
|
get: driver.get,
|
|
143
179
|
list: driver.list,
|
|
144
180
|
set: driver.set,
|
|
181
|
+
setIf: driver.setIf,
|
|
145
182
|
};
|
|
146
183
|
}
|
|
147
|
-
async function markPendingSnapshot({ id, nextRecord,
|
|
148
|
-
|
|
149
|
-
if (current?.savedAt !== savedAt) {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
await store.set(nextRecord);
|
|
153
|
-
return true;
|
|
184
|
+
async function markPendingSnapshot({ id, nextRecord, snapshotId, store, }) {
|
|
185
|
+
return store.replaceIfSnapshotId(id, snapshotId, nextRecord);
|
|
154
186
|
}
|
|
155
|
-
function toPendingWriteEnvelope(value, schemaVersion, { normalizePayload }) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
value
|
|
160
|
-
|
|
161
|
-
!isPendingWriteRecord(value.record)) {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
const record = normalizePendingWriteRecord(value.record, normalizePayload);
|
|
187
|
+
function toPendingWriteEnvelope(value, schemaVersion, { normalizeLegacyRecord, normalizePayload }) {
|
|
188
|
+
const envelopeRecord = readPendingWriteEnvelopeRecord(value, schemaVersion);
|
|
189
|
+
const legacyRecord = envelopeRecord
|
|
190
|
+
? null
|
|
191
|
+
: (normalizeLegacyRecord?.(value) ?? null);
|
|
192
|
+
const record = normalizePendingWriteRecord(envelopeRecord ?? legacyRecord, normalizePayload);
|
|
165
193
|
return record ? { record, schemaVersion } : null;
|
|
166
194
|
}
|
|
167
195
|
function normalizePendingWriteRecord(record, normalizePayload) {
|
|
168
|
-
if (!
|
|
169
|
-
return
|
|
196
|
+
if (!record) {
|
|
197
|
+
return null;
|
|
170
198
|
}
|
|
171
|
-
const payload = normalizePayload
|
|
199
|
+
const payload = normalizePayload
|
|
200
|
+
? normalizePayload(record.payload)
|
|
201
|
+
: record.payload;
|
|
172
202
|
if (payload === null) {
|
|
173
203
|
return null;
|
|
174
204
|
}
|
|
@@ -177,9 +207,24 @@ function normalizePendingWriteRecord(record, normalizePayload) {
|
|
|
177
207
|
id: record.id,
|
|
178
208
|
payload,
|
|
179
209
|
savedAt: record.savedAt,
|
|
210
|
+
snapshotId: record.snapshotId ?? createLegacyPendingWriteSnapshotId(record),
|
|
180
211
|
syncStatus: record.syncStatus,
|
|
181
212
|
};
|
|
182
213
|
}
|
|
214
|
+
function readPendingWriteEnvelopeRecord(value, schemaVersion) {
|
|
215
|
+
if (!value ||
|
|
216
|
+
typeof value !== "object" ||
|
|
217
|
+
!("schemaVersion" in value) ||
|
|
218
|
+
value.schemaVersion !== schemaVersion ||
|
|
219
|
+
!("record" in value) ||
|
|
220
|
+
!isPendingWriteRecord(value.record)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return value.record;
|
|
224
|
+
}
|
|
225
|
+
function createLegacyPendingWriteSnapshotId(record) {
|
|
226
|
+
return `legacy:${encodeURIComponent(record.id)}:${encodeURIComponent(record.savedAt)}`;
|
|
227
|
+
}
|
|
183
228
|
function isPendingWriteRecord(value) {
|
|
184
229
|
return (value !== null &&
|
|
185
230
|
typeof value === "object" &&
|
|
@@ -187,6 +232,7 @@ function isPendingWriteRecord(value) {
|
|
|
187
232
|
typeof value.id === "string" &&
|
|
188
233
|
"savedAt" in value &&
|
|
189
234
|
typeof value.savedAt === "string" &&
|
|
235
|
+
(!("snapshotId" in value) || typeof value.snapshotId === "string") &&
|
|
190
236
|
"syncStatus" in value &&
|
|
191
237
|
(value.syncStatus === "pending" ||
|
|
192
238
|
value.syncStatus === "syncing" ||
|
package/dist-cjs/readCache.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.browserReadCacheOperationTimeoutMs = exports.readCacheFreshnessMaxAgeMs = exports.readCacheSchemaVersion = void 0;
|
|
3
|
+
exports.browserReadCacheOperationTimeoutMs = exports.identityReadCacheFreshnessMaxAgeMs = exports.durableReadCacheFreshnessMaxAgeMs = exports.readCacheFreshnessMaxAgeMs = exports.readCacheSchemaVersion = void 0;
|
|
4
4
|
exports.createReadCache = createReadCache;
|
|
5
5
|
exports.createMemoryReadCacheDriver = createMemoryReadCacheDriver;
|
|
6
6
|
exports.createBrowserReadCache = createBrowserReadCache;
|
|
@@ -10,16 +10,37 @@ exports.loadReadCacheResource = loadReadCacheResource;
|
|
|
10
10
|
const idbDriver_1 = require("./idbDriver");
|
|
11
11
|
exports.readCacheSchemaVersion = 1;
|
|
12
12
|
exports.readCacheFreshnessMaxAgeMs = 30_000;
|
|
13
|
+
exports.durableReadCacheFreshnessMaxAgeMs = 60 * 60_000;
|
|
14
|
+
exports.identityReadCacheFreshnessMaxAgeMs = 24 * 60 * 60_000;
|
|
13
15
|
exports.browserReadCacheOperationTimeoutMs = 2_000;
|
|
14
|
-
const pendingFreshLoads = new
|
|
16
|
+
const pendingFreshLoads = new WeakMap();
|
|
15
17
|
function createReadCache(driver) {
|
|
16
|
-
|
|
18
|
+
let globalVersion = 0;
|
|
19
|
+
const keyVersions = new Map();
|
|
20
|
+
let operationQueue = Promise.resolve();
|
|
21
|
+
const pendingLoads = new Map();
|
|
22
|
+
function getVersion(key) {
|
|
23
|
+
return `${globalVersion}:${keyVersions.get(key) ?? 0}`;
|
|
24
|
+
}
|
|
25
|
+
function enqueue(operation) {
|
|
26
|
+
const next = operationQueue.then(operation, operation);
|
|
27
|
+
operationQueue = next.then(() => undefined, () => undefined);
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
const cache = {
|
|
17
31
|
async clear() {
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
pendingLoads.clear();
|
|
33
|
+
globalVersion += 1;
|
|
34
|
+
keyVersions.clear();
|
|
35
|
+
await enqueue(() => driver.clear());
|
|
20
36
|
},
|
|
21
37
|
async close() {
|
|
22
|
-
await driver.close?.();
|
|
38
|
+
await enqueue(async () => driver.close?.());
|
|
39
|
+
},
|
|
40
|
+
async delete(key) {
|
|
41
|
+
pendingLoads.delete(key);
|
|
42
|
+
keyVersions.set(key, (keyVersions.get(key) ?? 0) + 1);
|
|
43
|
+
await enqueue(() => driver.delete(key));
|
|
23
44
|
},
|
|
24
45
|
async get(key) {
|
|
25
46
|
const envelope = toReadCacheEnvelope(await driver.get(key));
|
|
@@ -31,14 +52,33 @@ function createReadCache(driver) {
|
|
|
31
52
|
}
|
|
32
53
|
: null;
|
|
33
54
|
},
|
|
55
|
+
getVersion,
|
|
34
56
|
async set(key, value) {
|
|
35
|
-
await driver.set(key, {
|
|
57
|
+
await enqueue(() => driver.set(key, {
|
|
36
58
|
savedAt: new Date().toISOString(),
|
|
37
59
|
schemaVersion: exports.readCacheSchemaVersion,
|
|
38
60
|
value,
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
async setIfVersion(key, value, version) {
|
|
64
|
+
if (getVersion(key) !== version) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return enqueue(async () => {
|
|
68
|
+
if (getVersion(key) !== version) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
await driver.set(key, {
|
|
72
|
+
savedAt: new Date().toISOString(),
|
|
73
|
+
schemaVersion: exports.readCacheSchemaVersion,
|
|
74
|
+
value,
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
39
77
|
});
|
|
40
78
|
},
|
|
41
79
|
};
|
|
80
|
+
pendingFreshLoads.set(cache, pendingLoads);
|
|
81
|
+
return cache;
|
|
42
82
|
}
|
|
43
83
|
function createMemoryReadCacheDriver() {
|
|
44
84
|
const records = new Map();
|
|
@@ -47,6 +87,9 @@ function createMemoryReadCacheDriver() {
|
|
|
47
87
|
records.clear();
|
|
48
88
|
},
|
|
49
89
|
async close() { },
|
|
90
|
+
async delete(key) {
|
|
91
|
+
records.delete(key);
|
|
92
|
+
},
|
|
50
93
|
async get(key) {
|
|
51
94
|
return records.get(key) ?? null;
|
|
52
95
|
},
|
|
@@ -80,11 +123,21 @@ function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
|
|
|
80
123
|
.filter(Boolean)
|
|
81
124
|
.join(":");
|
|
82
125
|
}
|
|
83
|
-
async function readThroughCache({ cache, key, loadFresh, }) {
|
|
84
|
-
const
|
|
126
|
+
async function readThroughCache({ cache, key, loadFresh, maxAgeMs, }) {
|
|
127
|
+
const resourceCache = cache.capture?.() ?? cache;
|
|
128
|
+
const version = resourceCache.getVersion(key);
|
|
129
|
+
const cached = await resourceCache.get(key);
|
|
130
|
+
if (cached && isCacheFresh(cached.savedAt, maxAgeMs)) {
|
|
131
|
+
return {
|
|
132
|
+
data: cached.value,
|
|
133
|
+
error: null,
|
|
134
|
+
fromCache: true,
|
|
135
|
+
stale: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
85
138
|
try {
|
|
86
139
|
const fresh = await loadFresh();
|
|
87
|
-
await
|
|
140
|
+
await resourceCache.setIfVersion(key, fresh, version);
|
|
88
141
|
return {
|
|
89
142
|
data: fresh,
|
|
90
143
|
error: null,
|
|
@@ -104,8 +157,10 @@ async function readThroughCache({ cache, key, loadFresh, }) {
|
|
|
104
157
|
throw error;
|
|
105
158
|
}
|
|
106
159
|
}
|
|
107
|
-
async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onUpdate, }) {
|
|
108
|
-
const
|
|
160
|
+
async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache = false, }) {
|
|
161
|
+
const resourceCache = cache.capture?.() ?? cache;
|
|
162
|
+
const version = resourceCache.getVersion(key);
|
|
163
|
+
const cached = skipCache ? null : await resourceCache.get(key);
|
|
109
164
|
if (cached) {
|
|
110
165
|
onUpdate({ data: cached.value, fromCache: true });
|
|
111
166
|
if (isCacheFresh(cached.savedAt, maxAgeMs)) {
|
|
@@ -113,9 +168,16 @@ async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onUpdate
|
|
|
113
168
|
}
|
|
114
169
|
}
|
|
115
170
|
try {
|
|
116
|
-
const fresh = await loadSharedFreshResource(key, loadFresh);
|
|
171
|
+
const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
|
|
117
172
|
onUpdate({ data: fresh, fromCache: false });
|
|
118
|
-
void
|
|
173
|
+
void resourceCache.setIfVersion(key, fresh, version).catch((error) => {
|
|
174
|
+
const normalizedError = normalizeError(error);
|
|
175
|
+
console.error("[Bayon offline] Unable to refresh the read cache.", {
|
|
176
|
+
error: normalizedError,
|
|
177
|
+
key,
|
|
178
|
+
});
|
|
179
|
+
onCacheWriteError?.(normalizedError);
|
|
180
|
+
});
|
|
119
181
|
return fresh;
|
|
120
182
|
}
|
|
121
183
|
catch (error) {
|
|
@@ -134,6 +196,7 @@ function createNoopReadCacheDriver() {
|
|
|
134
196
|
return {
|
|
135
197
|
async clear() { },
|
|
136
198
|
async close() { },
|
|
199
|
+
async delete() { },
|
|
137
200
|
async get() {
|
|
138
201
|
return null;
|
|
139
202
|
},
|
|
@@ -147,22 +210,32 @@ function isCacheFresh(savedAt, maxAgeMs) {
|
|
|
147
210
|
const savedTime = Date.parse(savedAt);
|
|
148
211
|
return Number.isFinite(savedTime) && Date.now() - savedTime <= maxAgeMs;
|
|
149
212
|
}
|
|
150
|
-
async function loadSharedFreshResource(key, loadFresh) {
|
|
151
|
-
const
|
|
213
|
+
async function loadSharedFreshResource(cache, key, loadFresh) {
|
|
214
|
+
const pendingLoads = getPendingFreshLoads(cache);
|
|
215
|
+
const pendingLoad = pendingLoads.get(key);
|
|
152
216
|
if (pendingLoad) {
|
|
153
217
|
return (await pendingLoad);
|
|
154
218
|
}
|
|
155
219
|
const nextLoad = loadFresh();
|
|
156
|
-
|
|
220
|
+
pendingLoads.set(key, nextLoad);
|
|
157
221
|
try {
|
|
158
222
|
return await nextLoad;
|
|
159
223
|
}
|
|
160
224
|
finally {
|
|
161
|
-
if (
|
|
162
|
-
|
|
225
|
+
if (pendingLoads.get(key) === nextLoad) {
|
|
226
|
+
pendingLoads.delete(key);
|
|
163
227
|
}
|
|
164
228
|
}
|
|
165
229
|
}
|
|
230
|
+
function getPendingFreshLoads(cache) {
|
|
231
|
+
const existing = pendingFreshLoads.get(cache);
|
|
232
|
+
if (existing) {
|
|
233
|
+
return existing;
|
|
234
|
+
}
|
|
235
|
+
const next = new Map();
|
|
236
|
+
pendingFreshLoads.set(cache, next);
|
|
237
|
+
return next;
|
|
238
|
+
}
|
|
166
239
|
function toReadCacheEnvelope(value) {
|
|
167
240
|
if (!value || typeof value !== "object") {
|
|
168
241
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bayonai/offline",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Reusable offline read-cache and pending-write primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bayon AI",
|
|
@@ -46,8 +46,10 @@
|
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsc -p tsconfig.json && node scripts/write-esm-extensions.mjs && tsc -p tsconfig.cjs.json && node scripts/write-cjs.mjs",
|
|
48
48
|
"lint": "tsc --noEmit -p tsconfig.json",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
49
|
+
"pack:smoke": "corepack pnpm run build && npm pack --dry-run --access public",
|
|
50
|
+
"prepack": "corepack pnpm run build",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:coverage": "vitest run --coverage"
|
|
51
53
|
},
|
|
52
54
|
"dependencies": {
|
|
53
55
|
"idb": "^8.0.3"
|