@bayonai/offline 0.1.0 → 0.3.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 +25 -1
- package/README.md +89 -1
- package/dist/browserCoordination.d.ts +23 -0
- package/dist/browserCoordination.js +57 -0
- package/dist/browserRecovery.d.ts +34 -0
- package/dist/browserRecovery.js +172 -0
- package/dist/idbDriver.d.ts +7 -0
- package/dist/idbDriver.js +81 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/pendingWrites.d.ts +78 -5
- package/dist/pendingWrites.js +189 -35
- package/dist/readCache.d.ts +34 -2
- package/dist/readCache.js +138 -20
- package/dist-cjs/browserCoordination.js +60 -0
- package/dist-cjs/browserRecovery.js +176 -0
- package/dist-cjs/idbDriver.js +81 -3
- package/dist-cjs/index.js +2 -0
- package/dist-cjs/pendingWrites.js +193 -35
- package/dist-cjs/readCache.js +140 -21
- package/package.json +6 -4
package/dist/pendingWrites.d.ts
CHANGED
|
@@ -7,28 +7,55 @@ 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>;
|
|
26
|
+
};
|
|
27
|
+
export type PendingWritePredicate = (value: unknown | null) => boolean;
|
|
28
|
+
export type PendingWriteStoreChange<Payload = unknown> = {
|
|
29
|
+
record: PendingWriteRecord<Payload>;
|
|
30
|
+
type: "set";
|
|
31
|
+
} | {
|
|
32
|
+
id: string;
|
|
33
|
+
type: "clear";
|
|
34
|
+
} | {
|
|
35
|
+
type: "clearAll";
|
|
36
|
+
} | {
|
|
37
|
+
record: PendingWriteRecord<Payload>;
|
|
38
|
+
type: "replace";
|
|
20
39
|
};
|
|
40
|
+
export type PendingWriteStoreChangeListener<Payload = unknown> = (change: PendingWriteStoreChange<Payload>) => void;
|
|
21
41
|
export type PendingWriteStore<Payload = unknown> = {
|
|
22
42
|
clear: (id: string) => Promise<void>;
|
|
23
43
|
clearAll: () => Promise<void>;
|
|
24
|
-
|
|
44
|
+
clearIfSnapshotId: (id: string, snapshotId: string) => Promise<boolean>;
|
|
25
45
|
close: () => Promise<void>;
|
|
26
46
|
get: (id: string) => Promise<PendingWriteRecord<Payload> | null>;
|
|
27
47
|
list: () => Promise<Array<PendingWriteRecord<Payload>>>;
|
|
28
|
-
|
|
48
|
+
replaceIfSnapshotId: (id: string, snapshotId: string, record: PendingWriteRecord<Payload>) => Promise<boolean>;
|
|
49
|
+
set: (record: PendingWriteRecordInput<Payload>) => Promise<PendingWriteRecord<Payload>>;
|
|
50
|
+
/**
|
|
51
|
+
* Observes changes made through this in-memory store instance. It deliberately
|
|
52
|
+
* does not claim cross-tab/process delivery.
|
|
53
|
+
*/
|
|
54
|
+
subscribe: (listener: PendingWriteStoreChangeListener<Payload>) => () => void;
|
|
29
55
|
};
|
|
30
56
|
export type PendingWriteStoreOptions<Payload = unknown> = {
|
|
31
57
|
keyPrefix: string;
|
|
58
|
+
normalizeLegacyRecord?: PendingWritePayloadNormalizer<PendingWriteRecordInput<Payload>>;
|
|
32
59
|
normalizePayload?: PendingWritePayloadNormalizer<Payload>;
|
|
33
60
|
schemaVersion?: number;
|
|
34
61
|
};
|
|
@@ -51,13 +78,59 @@ export type SyncPendingWriteResult = {
|
|
|
51
78
|
error: string;
|
|
52
79
|
status: "failed";
|
|
53
80
|
};
|
|
54
|
-
export
|
|
81
|
+
export type SyncPendingWritesResult = {
|
|
82
|
+
attempted: number;
|
|
83
|
+
failed: number;
|
|
84
|
+
idle: number;
|
|
85
|
+
/** Records still selected after this attempt, including newer superseding writes. */
|
|
86
|
+
remaining: number;
|
|
87
|
+
superseded: number;
|
|
88
|
+
synced: number;
|
|
89
|
+
};
|
|
90
|
+
export type PendingWriteSync<Payload> = (payload: Payload) => Promise<unknown>;
|
|
91
|
+
export type RunPendingWriteResult<TResult, TQueued> = {
|
|
92
|
+
result: TResult;
|
|
93
|
+
status: "synced";
|
|
94
|
+
} | {
|
|
95
|
+
queued: TQueued;
|
|
96
|
+
status: "queued";
|
|
97
|
+
};
|
|
98
|
+
export type PendingWriteSummary = {
|
|
99
|
+
failedCount: number;
|
|
100
|
+
pendingCount: number;
|
|
101
|
+
syncingCount: number;
|
|
102
|
+
totalCount: number;
|
|
103
|
+
};
|
|
104
|
+
export declare function createPendingWriteStore<Payload = unknown>(driver: PendingWriteDriver, { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion, }: PendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
|
|
55
105
|
export declare function createMemoryPendingWriteDriver(): PendingWriteDriver;
|
|
56
106
|
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>;
|
|
107
|
+
export declare function createBrowserPendingWriteStore<Payload = unknown>({ databaseName, databaseVersion, indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs, schemaVersion, storeName, }: BrowserPendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
|
|
58
108
|
export declare function createPendingWriteKey({ id, prefix }: PendingWriteKeyOptions): string;
|
|
109
|
+
export declare function createPendingWriteSnapshotId(): `${string}-${string}-${string}-${string}-${string}`;
|
|
59
110
|
export declare function syncPendingWrite<Payload>({ id, store, sync, }: {
|
|
60
111
|
id: string;
|
|
61
112
|
store: PendingWriteStore<Payload>;
|
|
62
|
-
sync:
|
|
113
|
+
sync: PendingWriteSync<Payload>;
|
|
63
114
|
}): Promise<SyncPendingWriteResult>;
|
|
115
|
+
/**
|
|
116
|
+
* Replays selected writes oldest first and coalesces overlapping runs for the
|
|
117
|
+
* same store/scope. Callers own the selection predicate and idempotent remote
|
|
118
|
+
* mutation semantics.
|
|
119
|
+
*/
|
|
120
|
+
export declare function syncPendingWrites<Payload>({ scopeKey, select, store, sync, }: {
|
|
121
|
+
scopeKey?: string;
|
|
122
|
+
select?: (record: PendingWriteRecord<Payload>) => boolean;
|
|
123
|
+
store: PendingWriteStore<Payload>;
|
|
124
|
+
sync: PendingWriteSync<Payload>;
|
|
125
|
+
}): Promise<SyncPendingWritesResult>;
|
|
126
|
+
/**
|
|
127
|
+
* Attempts a remote write now and persists a caller-created durable fallback
|
|
128
|
+
* only when offline or when the supplied retry classifier accepts the error.
|
|
129
|
+
*/
|
|
130
|
+
export declare function runPendingWrite<TResult, TQueued>({ enqueue, isOnline, shouldQueue, write, }: {
|
|
131
|
+
enqueue: () => Promise<TQueued>;
|
|
132
|
+
isOnline: () => boolean;
|
|
133
|
+
shouldQueue: (error: unknown) => boolean;
|
|
134
|
+
write: () => Promise<TResult>;
|
|
135
|
+
}): Promise<RunPendingWriteResult<TResult, TQueued>>;
|
|
136
|
+
export declare function summarizePendingWrites<Payload>(records: Array<PendingWriteRecord<Payload>>): PendingWriteSummary;
|
package/dist/pendingWrites.js
CHANGED
|
@@ -1,41 +1,81 @@
|
|
|
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
|
+
const listeners = new Set();
|
|
6
|
+
const notify = (change) => {
|
|
7
|
+
for (const listener of listeners) {
|
|
8
|
+
try {
|
|
9
|
+
listener(change);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error("[Bayon offline] Pending-write store listener failed.", {
|
|
13
|
+
error,
|
|
14
|
+
type: change.type,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
5
19
|
return {
|
|
6
20
|
async clear(id) {
|
|
7
21
|
await driver.clear(createPendingWriteKey({ id, prefix: keyPrefix }));
|
|
22
|
+
notify({ id, type: "clear" });
|
|
8
23
|
},
|
|
9
24
|
async clearAll() {
|
|
10
25
|
await driver.clearAll();
|
|
26
|
+
notify({ type: "clearAll" });
|
|
11
27
|
},
|
|
12
|
-
async
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
async clearIfSnapshotId(id, snapshotId) {
|
|
29
|
+
const cleared = await driver.clearIf(createPendingWriteKey({ id, prefix: keyPrefix }), (value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
30
|
+
normalizeLegacyRecord,
|
|
31
|
+
normalizePayload,
|
|
32
|
+
})?.record.snapshotId === snapshotId);
|
|
33
|
+
if (cleared) {
|
|
34
|
+
notify({ id, type: "clear" });
|
|
16
35
|
}
|
|
17
|
-
|
|
18
|
-
return true;
|
|
36
|
+
return cleared;
|
|
19
37
|
},
|
|
20
38
|
async close() {
|
|
21
39
|
await driver.close?.();
|
|
22
40
|
},
|
|
23
41
|
async get(id) {
|
|
24
|
-
return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizePayload })?.record ?? null);
|
|
42
|
+
return (toPendingWriteEnvelope(await driver.get(createPendingWriteKey({ id, prefix: keyPrefix })), schemaVersion, { normalizeLegacyRecord, normalizePayload })?.record ?? null);
|
|
25
43
|
},
|
|
26
44
|
async list() {
|
|
27
45
|
return (await driver.list())
|
|
28
46
|
.map((value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
47
|
+
normalizeLegacyRecord,
|
|
29
48
|
normalizePayload,
|
|
30
49
|
})?.record)
|
|
31
50
|
.filter((record) => Boolean(record))
|
|
32
|
-
.sort((left, right) => left.savedAt.localeCompare(right.savedAt)
|
|
51
|
+
.sort((left, right) => left.savedAt.localeCompare(right.savedAt) ||
|
|
52
|
+
left.id.localeCompare(right.id));
|
|
53
|
+
},
|
|
54
|
+
async replaceIfSnapshotId(id, snapshotId, record) {
|
|
55
|
+
const replaced = await driver.setIf(createPendingWriteKey({ id, prefix: keyPrefix }), { record, schemaVersion }, (value) => toPendingWriteEnvelope(value, schemaVersion, {
|
|
56
|
+
normalizeLegacyRecord,
|
|
57
|
+
normalizePayload,
|
|
58
|
+
})?.record.snapshotId === snapshotId);
|
|
59
|
+
if (replaced) {
|
|
60
|
+
notify({ record, type: "replace" });
|
|
61
|
+
}
|
|
62
|
+
return replaced;
|
|
33
63
|
},
|
|
34
64
|
async set(record) {
|
|
65
|
+
const storedRecord = {
|
|
66
|
+
...record,
|
|
67
|
+
snapshotId: record.snapshotId ?? createPendingWriteSnapshotId(),
|
|
68
|
+
};
|
|
35
69
|
await driver.set(createPendingWriteKey({ id: record.id, prefix: keyPrefix }), {
|
|
36
|
-
record,
|
|
70
|
+
record: storedRecord,
|
|
37
71
|
schemaVersion,
|
|
38
72
|
});
|
|
73
|
+
notify({ record: storedRecord, type: "set" });
|
|
74
|
+
return storedRecord;
|
|
75
|
+
},
|
|
76
|
+
subscribe(listener) {
|
|
77
|
+
listeners.add(listener);
|
|
78
|
+
return () => listeners.delete(listener);
|
|
39
79
|
},
|
|
40
80
|
};
|
|
41
81
|
}
|
|
@@ -45,6 +85,14 @@ export function createMemoryPendingWriteDriver() {
|
|
|
45
85
|
async clear(key) {
|
|
46
86
|
records.delete(key);
|
|
47
87
|
},
|
|
88
|
+
async clearIf(key, predicate) {
|
|
89
|
+
const current = records.get(key) ?? null;
|
|
90
|
+
if (!predicate(current)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
records.delete(key);
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
48
96
|
async clearAll() {
|
|
49
97
|
records.clear();
|
|
50
98
|
},
|
|
@@ -58,6 +106,13 @@ export function createMemoryPendingWriteDriver() {
|
|
|
58
106
|
async set(key, value) {
|
|
59
107
|
records.set(key, value);
|
|
60
108
|
},
|
|
109
|
+
async setIf(key, value, predicate) {
|
|
110
|
+
if (!predicate(records.get(key) ?? null)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
records.set(key, value);
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
61
116
|
};
|
|
62
117
|
}
|
|
63
118
|
export function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, operationTimeoutMs = pendingWriteOperationTimeoutMs, storeName, }) {
|
|
@@ -70,7 +125,7 @@ export function createBrowserPendingWriteDriver({ databaseName, databaseVersion,
|
|
|
70
125
|
});
|
|
71
126
|
return driver ? toPendingWriteDriver(driver) : null;
|
|
72
127
|
}
|
|
73
|
-
export function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, keyPrefix, normalizePayload, operationTimeoutMs = pendingWriteOperationTimeoutMs, schemaVersion = pendingWriteSchemaVersion, storeName, }) {
|
|
128
|
+
export function createBrowserPendingWriteStore({ databaseName, databaseVersion, indexedDB = globalThis.indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs = pendingWriteOperationTimeoutMs, schemaVersion = pendingWriteSchemaVersion, storeName, }) {
|
|
74
129
|
const driver = createBrowserPendingWriteDriver({
|
|
75
130
|
databaseName,
|
|
76
131
|
databaseVersion,
|
|
@@ -78,11 +133,14 @@ export function createBrowserPendingWriteStore({ databaseName, databaseVersion,
|
|
|
78
133
|
operationTimeoutMs,
|
|
79
134
|
storeName,
|
|
80
135
|
});
|
|
81
|
-
return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizePayload, schemaVersion });
|
|
136
|
+
return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
|
|
82
137
|
}
|
|
83
138
|
export function createPendingWriteKey({ id, prefix }) {
|
|
84
139
|
return `${prefix}:${encodeURIComponent(id)}`;
|
|
85
140
|
}
|
|
141
|
+
export function createPendingWriteSnapshotId() {
|
|
142
|
+
return globalThis.crypto.randomUUID();
|
|
143
|
+
}
|
|
86
144
|
export async function syncPendingWrite({ id, store, sync, }) {
|
|
87
145
|
const pending = await store.get(id);
|
|
88
146
|
if (!pending) {
|
|
@@ -91,7 +149,7 @@ export async function syncPendingWrite({ id, store, sync, }) {
|
|
|
91
149
|
await markPendingSnapshot({
|
|
92
150
|
id,
|
|
93
151
|
nextRecord: { ...pending, error: undefined, syncStatus: "syncing" },
|
|
94
|
-
|
|
152
|
+
snapshotId: pending.snapshotId,
|
|
95
153
|
store,
|
|
96
154
|
});
|
|
97
155
|
try {
|
|
@@ -102,18 +160,100 @@ export async function syncPendingWrite({ id, store, sync, }) {
|
|
|
102
160
|
await markPendingSnapshot({
|
|
103
161
|
id,
|
|
104
162
|
nextRecord: { ...pending, error: message, syncStatus: "failed" },
|
|
105
|
-
|
|
163
|
+
snapshotId: pending.snapshotId,
|
|
106
164
|
store,
|
|
107
165
|
});
|
|
108
166
|
return { error: message, status: "failed" };
|
|
109
167
|
}
|
|
110
|
-
return (await store.
|
|
168
|
+
return (await store.clearIfSnapshotId(id, pending.snapshotId))
|
|
111
169
|
? { status: "synced" }
|
|
112
170
|
: { status: "superseded" };
|
|
113
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Replays selected writes oldest first and coalesces overlapping runs for the
|
|
174
|
+
* same store/scope. Callers own the selection predicate and idempotent remote
|
|
175
|
+
* mutation semantics.
|
|
176
|
+
*/
|
|
177
|
+
export function syncPendingWrites({ scopeKey = "all", select = () => true, store, sync, }) {
|
|
178
|
+
let inFlightByScope = pendingWriteSyncs.get(store);
|
|
179
|
+
if (!inFlightByScope) {
|
|
180
|
+
inFlightByScope = new Map();
|
|
181
|
+
pendingWriteSyncs.set(store, inFlightByScope);
|
|
182
|
+
}
|
|
183
|
+
const existing = inFlightByScope.get(scopeKey);
|
|
184
|
+
if (existing) {
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
|
187
|
+
const syncRun = performPendingWriteSync({ select, store, sync });
|
|
188
|
+
inFlightByScope.set(scopeKey, syncRun);
|
|
189
|
+
const clearInFlight = () => {
|
|
190
|
+
inFlightByScope.delete(scopeKey);
|
|
191
|
+
};
|
|
192
|
+
void syncRun.then(clearInFlight, clearInFlight);
|
|
193
|
+
return syncRun;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Attempts a remote write now and persists a caller-created durable fallback
|
|
197
|
+
* only when offline or when the supplied retry classifier accepts the error.
|
|
198
|
+
*/
|
|
199
|
+
export async function runPendingWrite({ enqueue, isOnline, shouldQueue, write, }) {
|
|
200
|
+
if (!isOnline()) {
|
|
201
|
+
return { queued: await enqueue(), status: "queued" };
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
return { result: await write(), status: "synced" };
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (!shouldQueue(error)) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
return { queued: await enqueue(), status: "queued" };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export function summarizePendingWrites(records) {
|
|
214
|
+
return records.reduce((summary, record) => {
|
|
215
|
+
summary.totalCount += 1;
|
|
216
|
+
if (record.syncStatus === "failed") {
|
|
217
|
+
summary.failedCount += 1;
|
|
218
|
+
}
|
|
219
|
+
else if (record.syncStatus === "syncing") {
|
|
220
|
+
summary.syncingCount += 1;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
summary.pendingCount += 1;
|
|
224
|
+
}
|
|
225
|
+
return summary;
|
|
226
|
+
}, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
|
|
227
|
+
}
|
|
228
|
+
const pendingWriteSyncs = new WeakMap();
|
|
229
|
+
async function performPendingWriteSync({ select, store, sync, }) {
|
|
230
|
+
const summary = {
|
|
231
|
+
attempted: 0,
|
|
232
|
+
failed: 0,
|
|
233
|
+
idle: 0,
|
|
234
|
+
remaining: 0,
|
|
235
|
+
superseded: 0,
|
|
236
|
+
synced: 0,
|
|
237
|
+
};
|
|
238
|
+
const records = (await store.list()).filter(select);
|
|
239
|
+
for (const record of records) {
|
|
240
|
+
const result = await syncPendingWrite({
|
|
241
|
+
id: record.id,
|
|
242
|
+
store,
|
|
243
|
+
sync,
|
|
244
|
+
});
|
|
245
|
+
summary.attempted += 1;
|
|
246
|
+
summary[result.status] += 1;
|
|
247
|
+
}
|
|
248
|
+
summary.remaining = (await store.list()).filter(select).length;
|
|
249
|
+
return summary;
|
|
250
|
+
}
|
|
114
251
|
function createNoopPendingWriteDriver() {
|
|
115
252
|
return {
|
|
116
253
|
async clear() { },
|
|
254
|
+
async clearIf() {
|
|
255
|
+
return false;
|
|
256
|
+
},
|
|
117
257
|
async clearAll() { },
|
|
118
258
|
async close() { },
|
|
119
259
|
async get() {
|
|
@@ -123,43 +263,41 @@ function createNoopPendingWriteDriver() {
|
|
|
123
263
|
return [];
|
|
124
264
|
},
|
|
125
265
|
async set() { },
|
|
266
|
+
async setIf() {
|
|
267
|
+
return false;
|
|
268
|
+
},
|
|
126
269
|
};
|
|
127
270
|
}
|
|
128
271
|
function toPendingWriteDriver(driver) {
|
|
129
272
|
return {
|
|
130
273
|
clear: driver.delete,
|
|
274
|
+
clearIf: driver.deleteIf,
|
|
131
275
|
clearAll: driver.clear,
|
|
132
276
|
close: driver.close,
|
|
133
277
|
get: driver.get,
|
|
134
278
|
list: driver.list,
|
|
135
279
|
set: driver.set,
|
|
280
|
+
setIf: driver.setIf,
|
|
136
281
|
};
|
|
137
282
|
}
|
|
138
|
-
async function markPendingSnapshot({ id, nextRecord,
|
|
139
|
-
|
|
140
|
-
if (current?.savedAt !== savedAt) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
await store.set(nextRecord);
|
|
144
|
-
return true;
|
|
283
|
+
async function markPendingSnapshot({ id, nextRecord, snapshotId, store, }) {
|
|
284
|
+
return store.replaceIfSnapshotId(id, snapshotId, nextRecord);
|
|
145
285
|
}
|
|
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);
|
|
286
|
+
function toPendingWriteEnvelope(value, schemaVersion, { normalizeLegacyRecord, normalizePayload }) {
|
|
287
|
+
const envelopeRecord = readPendingWriteEnvelopeRecord(value, schemaVersion);
|
|
288
|
+
const legacyRecord = envelopeRecord
|
|
289
|
+
? null
|
|
290
|
+
: (normalizeLegacyRecord?.(value) ?? null);
|
|
291
|
+
const record = normalizePendingWriteRecord(envelopeRecord ?? legacyRecord, normalizePayload);
|
|
156
292
|
return record ? { record, schemaVersion } : null;
|
|
157
293
|
}
|
|
158
294
|
function normalizePendingWriteRecord(record, normalizePayload) {
|
|
159
|
-
if (!
|
|
160
|
-
return
|
|
295
|
+
if (!record) {
|
|
296
|
+
return null;
|
|
161
297
|
}
|
|
162
|
-
const payload = normalizePayload
|
|
298
|
+
const payload = normalizePayload
|
|
299
|
+
? normalizePayload(record.payload)
|
|
300
|
+
: record.payload;
|
|
163
301
|
if (payload === null) {
|
|
164
302
|
return null;
|
|
165
303
|
}
|
|
@@ -168,9 +306,24 @@ function normalizePendingWriteRecord(record, normalizePayload) {
|
|
|
168
306
|
id: record.id,
|
|
169
307
|
payload,
|
|
170
308
|
savedAt: record.savedAt,
|
|
309
|
+
snapshotId: record.snapshotId ?? createLegacyPendingWriteSnapshotId(record),
|
|
171
310
|
syncStatus: record.syncStatus,
|
|
172
311
|
};
|
|
173
312
|
}
|
|
313
|
+
function readPendingWriteEnvelopeRecord(value, schemaVersion) {
|
|
314
|
+
if (!value ||
|
|
315
|
+
typeof value !== "object" ||
|
|
316
|
+
!("schemaVersion" in value) ||
|
|
317
|
+
value.schemaVersion !== schemaVersion ||
|
|
318
|
+
!("record" in value) ||
|
|
319
|
+
!isPendingWriteRecord(value.record)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
return value.record;
|
|
323
|
+
}
|
|
324
|
+
function createLegacyPendingWriteSnapshotId(record) {
|
|
325
|
+
return `legacy:${encodeURIComponent(record.id)}:${encodeURIComponent(record.savedAt)}`;
|
|
326
|
+
}
|
|
174
327
|
function isPendingWriteRecord(value) {
|
|
175
328
|
return (value !== null &&
|
|
176
329
|
typeof value === "object" &&
|
|
@@ -178,6 +331,7 @@ function isPendingWriteRecord(value) {
|
|
|
178
331
|
typeof value.id === "string" &&
|
|
179
332
|
"savedAt" in value &&
|
|
180
333
|
typeof value.savedAt === "string" &&
|
|
334
|
+
(!("snapshotId" in value) || typeof value.snapshotId === "string") &&
|
|
181
335
|
"syncStatus" in value &&
|
|
182
336
|
(value.syncStatus === "pending" ||
|
|
183
337
|
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,27 @@ 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>;
|
|
17
|
+
listEntries?: () => Promise<Array<{
|
|
18
|
+
key: string;
|
|
19
|
+
value: unknown;
|
|
20
|
+
}>>;
|
|
14
21
|
set: (key: string, value: unknown) => Promise<void>;
|
|
15
22
|
};
|
|
16
23
|
export type ReadCache = {
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a stable cache for the duration of a resource load. Dynamic
|
|
26
|
+
* adapters use this to pin the active account before asynchronous work.
|
|
27
|
+
*/
|
|
28
|
+
capture?: () => ReadCache;
|
|
17
29
|
clear: () => Promise<void>;
|
|
18
30
|
close: () => Promise<void>;
|
|
31
|
+
delete: (key: string) => Promise<void>;
|
|
19
32
|
get: <T>(key: string) => Promise<ReadCacheHit<T> | null>;
|
|
33
|
+
getVersion: (key: string) => string;
|
|
20
34
|
set: <T>(key: string, value: T) => Promise<void>;
|
|
35
|
+
setIfVersion: <T>(key: string, value: T, version: string) => Promise<boolean>;
|
|
21
36
|
};
|
|
22
37
|
export type ReadThroughCacheResult<T> = {
|
|
23
38
|
data: T;
|
|
@@ -44,16 +59,33 @@ export type ReadCacheKeyOptions = {
|
|
|
44
59
|
export declare function createReadCache(driver: ReadCacheDriver): ReadCache;
|
|
45
60
|
export declare function createMemoryReadCacheDriver(): ReadCacheDriver;
|
|
46
61
|
export declare function createBrowserReadCache({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, storeName, }: BrowserReadCacheOptions): ReadCache;
|
|
62
|
+
/**
|
|
63
|
+
* Deletes only valid read-cache envelopes older than the supplied age. It is
|
|
64
|
+
* opt-in and returns zero for custom cache adapters that cannot enumerate keys.
|
|
65
|
+
*/
|
|
66
|
+
export declare function pruneReadCache({ cache, maxAgeMs, now, }: {
|
|
67
|
+
cache: ReadCache;
|
|
68
|
+
maxAgeMs: number;
|
|
69
|
+
now?: () => number;
|
|
70
|
+
}): Promise<number>;
|
|
47
71
|
export declare function createReadCacheKey({ namespace, params, prefix, scope, }: ReadCacheKeyOptions): string;
|
|
48
|
-
export declare function readThroughCache<T>({ cache, key, loadFresh, }: {
|
|
72
|
+
export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, }: {
|
|
49
73
|
cache: ReadCache;
|
|
74
|
+
/**
|
|
75
|
+
* Invalidates the current value before loading. Use after a successful write
|
|
76
|
+
* when a cached or earlier in-flight response must not become authoritative.
|
|
77
|
+
*/
|
|
78
|
+
forceFresh?: boolean;
|
|
50
79
|
key: string;
|
|
51
80
|
loadFresh: () => Promise<T>;
|
|
81
|
+
maxAgeMs?: number;
|
|
52
82
|
}): Promise<ReadThroughCacheResult<T>>;
|
|
53
|
-
export declare function loadReadCacheResource<T>({ cache, key, loadFresh, maxAgeMs, onUpdate, }: {
|
|
83
|
+
export declare function loadReadCacheResource<T>({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache, }: {
|
|
54
84
|
cache: ReadCache;
|
|
55
85
|
key: string;
|
|
56
86
|
loadFresh: () => Promise<T>;
|
|
57
87
|
maxAgeMs?: number;
|
|
88
|
+
onCacheWriteError?: (error: Error) => void;
|
|
58
89
|
onUpdate: (update: ReadCacheResourceUpdate<T>) => void;
|
|
90
|
+
skipCache?: boolean;
|
|
59
91
|
}): Promise<T>;
|