@bayonai/offline 0.8.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 +17 -1
- package/README.md +46 -5
- package/dist/browserFallbackRecovery.js +2 -0
- package/dist/browserRecovery.d.ts +2 -2
- package/dist/durableCommandStore.d.ts +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/pendingWrites.d.ts +6 -2
- package/dist/pendingWrites.js +42 -9
- package/dist/readCache.d.ts +26 -5
- package/dist/readCache.js +113 -19
- package/dist-cjs/browserCoordination.d.ts +23 -0
- package/dist-cjs/browserFallbackRecovery.d.ts +32 -0
- package/dist-cjs/browserFallbackRecovery.js +2 -0
- package/dist-cjs/browserRecovery.d.ts +42 -0
- package/dist-cjs/durableCommandStore.d.ts +49 -0
- package/dist-cjs/idbDriver.d.ts +26 -0
- package/dist-cjs/index.d.ts +7 -0
- package/dist-cjs/pendingWrites.d.ts +179 -0
- package/dist-cjs/pendingWrites.js +42 -9
- package/dist-cjs/readCache.d.ts +112 -0
- package/dist-cjs/readCache.js +113 -19
- package/package.json +13 -5
package/dist-cjs/readCache.js
CHANGED
|
@@ -26,6 +26,10 @@ function createReadCache(driver) {
|
|
|
26
26
|
function getVersion(key) {
|
|
27
27
|
return `${globalVersion}:${keyVersions.get(key) ?? 0}`;
|
|
28
28
|
}
|
|
29
|
+
function invalidate(key) {
|
|
30
|
+
pendingLoads.delete(key);
|
|
31
|
+
keyVersions.set(key, (keyVersions.get(key) ?? 0) + 1);
|
|
32
|
+
}
|
|
29
33
|
function enqueue(operation) {
|
|
30
34
|
const next = operationQueue.then(operation, operation);
|
|
31
35
|
operationQueue = next.then(() => undefined, () => undefined);
|
|
@@ -42,10 +46,10 @@ function createReadCache(driver) {
|
|
|
42
46
|
await enqueue(async () => driver.close?.());
|
|
43
47
|
},
|
|
44
48
|
async delete(key) {
|
|
45
|
-
|
|
46
|
-
keyVersions.set(key, (keyVersions.get(key) ?? 0) + 1);
|
|
49
|
+
invalidate(key);
|
|
47
50
|
await enqueue(() => driver.delete(key));
|
|
48
51
|
},
|
|
52
|
+
invalidate,
|
|
49
53
|
async get(key) {
|
|
50
54
|
const envelope = toReadCacheEnvelope(await driver.get(key));
|
|
51
55
|
return envelope
|
|
@@ -162,13 +166,21 @@ function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
|
|
|
162
166
|
.filter(Boolean)
|
|
163
167
|
.join(":");
|
|
164
168
|
}
|
|
165
|
-
async function readThroughCache({ cache, forceFresh = false, key, loadFresh, maxAgeMs, }) {
|
|
169
|
+
async function readThroughCache({ cache, forceFresh = false, key, loadFresh, maxAgeMs, onOutcome, policy, }) {
|
|
166
170
|
const resourceCache = cache.capture?.() ?? cache;
|
|
171
|
+
const resolvedPolicy = resolvePolicy(policy, maxAgeMs);
|
|
172
|
+
const forcedFallback = forceFresh ? await resourceCache.get(key) : null;
|
|
167
173
|
if (forceFresh) {
|
|
168
|
-
|
|
169
|
-
//
|
|
174
|
+
reportReadCacheOutcome(onOutcome, "forced-refresh");
|
|
175
|
+
// Built-in caches advance generation without deleting fallback data.
|
|
176
|
+
// Legacy custom adapters retain their old delete behavior.
|
|
170
177
|
try {
|
|
171
|
-
|
|
178
|
+
if (resourceCache.invalidate) {
|
|
179
|
+
resourceCache.invalidate(key);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
await resourceCache.delete(key);
|
|
183
|
+
}
|
|
172
184
|
}
|
|
173
185
|
catch (error) {
|
|
174
186
|
console.error("[Bayon offline] Unable to invalidate the read cache.", {
|
|
@@ -178,17 +190,22 @@ async function readThroughCache({ cache, forceFresh = false, key, loadFresh, max
|
|
|
178
190
|
}
|
|
179
191
|
}
|
|
180
192
|
const version = resourceCache.getVersion(key);
|
|
181
|
-
const cached = await resourceCache.get(key);
|
|
182
|
-
|
|
193
|
+
const cached = forcedFallback ?? (await resourceCache.get(key));
|
|
194
|
+
const retained = cached && isCacheRetained(cached.savedAt, resolvedPolicy);
|
|
195
|
+
if (!forceFresh && retained && isCacheFresh(cached.savedAt, resolvedPolicy)) {
|
|
196
|
+
reportReadCacheOutcome(onOutcome, "fresh-hit");
|
|
183
197
|
return {
|
|
184
198
|
data: cached.value,
|
|
185
199
|
error: null,
|
|
186
200
|
fromCache: true,
|
|
187
201
|
stale: false,
|
|
202
|
+
status: "fresh",
|
|
188
203
|
};
|
|
189
204
|
}
|
|
205
|
+
reportReadCacheOutcome(onOutcome, retained ? "stale-hit" : "cache-miss");
|
|
190
206
|
try {
|
|
191
207
|
const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
|
|
208
|
+
reportReadCacheOutcome(onOutcome, "loader-success");
|
|
192
209
|
try {
|
|
193
210
|
await resourceCache.setIfVersion(key, fresh, version);
|
|
194
211
|
}
|
|
@@ -203,33 +220,74 @@ async function readThroughCache({ cache, forceFresh = false, key, loadFresh, max
|
|
|
203
220
|
error: null,
|
|
204
221
|
fromCache: false,
|
|
205
222
|
stale: false,
|
|
223
|
+
status: retained ? "fresh" : "miss",
|
|
206
224
|
};
|
|
207
225
|
}
|
|
208
226
|
catch (error) {
|
|
209
|
-
|
|
227
|
+
reportReadCacheOutcome(onOutcome, "loader-failure");
|
|
228
|
+
if (retained && resolvedPolicy.refreshFailure === "use-retained") {
|
|
210
229
|
return {
|
|
211
230
|
data: cached.value,
|
|
212
231
|
error: normalizeError(error),
|
|
213
232
|
fromCache: true,
|
|
214
233
|
stale: true,
|
|
234
|
+
status: "stale",
|
|
215
235
|
};
|
|
216
236
|
}
|
|
217
237
|
throw error;
|
|
218
238
|
}
|
|
219
239
|
}
|
|
220
|
-
async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onCacheWriteError, onUpdate, skipCache = false, }) {
|
|
240
|
+
async function loadReadCacheResource({ cache, forceFresh = false, key, loadFresh, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache = false, }) {
|
|
221
241
|
const resourceCache = cache.capture?.() ?? cache;
|
|
242
|
+
const resolvedPolicy = resolvePolicy(policy, maxAgeMs);
|
|
243
|
+
const forcedFallback = forceFresh ? await resourceCache.get(key) : null;
|
|
244
|
+
if (forceFresh) {
|
|
245
|
+
reportReadCacheOutcome(onOutcome, "forced-refresh");
|
|
246
|
+
try {
|
|
247
|
+
if (resourceCache.invalidate) {
|
|
248
|
+
resourceCache.invalidate(key);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
await resourceCache.delete(key);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
console.error("[Bayon offline] Unable to invalidate the read cache.", {
|
|
256
|
+
error: normalizeError(error),
|
|
257
|
+
key,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
222
261
|
const version = resourceCache.getVersion(key);
|
|
223
|
-
const cached = skipCache
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
262
|
+
const cached = skipCache
|
|
263
|
+
? null
|
|
264
|
+
: (forcedFallback ?? (await resourceCache.get(key)));
|
|
265
|
+
const retained = cached && isCacheRetained(cached.savedAt, resolvedPolicy);
|
|
266
|
+
if (retained) {
|
|
267
|
+
const fresh = isCacheFresh(cached.savedAt, resolvedPolicy);
|
|
268
|
+
reportReadCacheOutcome(onOutcome, fresh && !forceFresh ? "fresh-hit" : "stale-hit");
|
|
269
|
+
onUpdate({
|
|
270
|
+
data: cached.value,
|
|
271
|
+
fromCache: true,
|
|
272
|
+
status: fresh && !forceFresh ? "fresh" : "stale",
|
|
273
|
+
});
|
|
274
|
+
if (fresh && !forceFresh) {
|
|
227
275
|
return cached.value;
|
|
228
276
|
}
|
|
229
277
|
}
|
|
278
|
+
else {
|
|
279
|
+
reportReadCacheOutcome(onOutcome, "cache-miss");
|
|
280
|
+
}
|
|
230
281
|
try {
|
|
231
282
|
const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
|
|
232
|
-
|
|
283
|
+
reportReadCacheOutcome(onOutcome, "loader-success");
|
|
284
|
+
if (resourceCache.getVersion(key) === version) {
|
|
285
|
+
onUpdate({
|
|
286
|
+
data: fresh,
|
|
287
|
+
fromCache: false,
|
|
288
|
+
status: retained ? "fresh" : "miss",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
233
291
|
void resourceCache.setIfVersion(key, fresh, version).catch((error) => {
|
|
234
292
|
const normalizedError = normalizeError(error);
|
|
235
293
|
console.error("[Bayon offline] Unable to refresh the read cache.", {
|
|
@@ -241,17 +299,32 @@ async function loadReadCacheResource({ cache, key, loadFresh, maxAgeMs, onCacheW
|
|
|
241
299
|
return fresh;
|
|
242
300
|
}
|
|
243
301
|
catch (error) {
|
|
244
|
-
|
|
302
|
+
reportReadCacheOutcome(onOutcome, "loader-failure");
|
|
303
|
+
if (!retained || resolvedPolicy.refreshFailure === "throw") {
|
|
245
304
|
throw error;
|
|
246
305
|
}
|
|
247
306
|
onUpdate({
|
|
248
307
|
data: cached.value,
|
|
249
308
|
error: normalizeError(error),
|
|
250
309
|
fromCache: true,
|
|
310
|
+
status: "stale",
|
|
251
311
|
});
|
|
252
312
|
return cached.value;
|
|
253
313
|
}
|
|
254
314
|
}
|
|
315
|
+
function reportReadCacheOutcome(onOutcome, outcome) {
|
|
316
|
+
if (!onOutcome)
|
|
317
|
+
return;
|
|
318
|
+
try {
|
|
319
|
+
onOutcome(outcome);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.error("[Bayon offline] Read-cache outcome callback failed.", {
|
|
323
|
+
error: normalizeError(error),
|
|
324
|
+
outcome,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
255
328
|
function createNoopReadCacheDriver() {
|
|
256
329
|
return {
|
|
257
330
|
async clear() { },
|
|
@@ -266,12 +339,33 @@ function createNoopReadCacheDriver() {
|
|
|
266
339
|
async set() { },
|
|
267
340
|
};
|
|
268
341
|
}
|
|
269
|
-
function isCacheFresh(savedAt,
|
|
270
|
-
if (
|
|
342
|
+
function isCacheFresh(savedAt, policy) {
|
|
343
|
+
if (policy.freshForMs <= 0) {
|
|
271
344
|
return false;
|
|
272
345
|
}
|
|
273
346
|
const savedTime = Date.parse(savedAt);
|
|
274
|
-
return Number.isFinite(savedTime) && Date.now() - savedTime <=
|
|
347
|
+
return (Number.isFinite(savedTime) && Date.now() - savedTime <= policy.freshForMs);
|
|
348
|
+
}
|
|
349
|
+
function resolvePolicy(policy, maxAgeMs) {
|
|
350
|
+
if (policy) {
|
|
351
|
+
if (!Number.isFinite(policy.freshForMs) ||
|
|
352
|
+
policy.freshForMs < 0 ||
|
|
353
|
+
!Number.isFinite(policy.retainForMs) ||
|
|
354
|
+
policy.retainForMs < policy.freshForMs ||
|
|
355
|
+
!["use-retained", "throw"].includes(policy.refreshFailure)) {
|
|
356
|
+
throw new RangeError("[Bayon offline] Invalid read-cache policy: require finite non-negative durations, retainForMs >= freshForMs, and a supported refreshFailure mode.");
|
|
357
|
+
}
|
|
358
|
+
return policy;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
freshForMs: maxAgeMs ?? 0,
|
|
362
|
+
retainForMs: Number.POSITIVE_INFINITY,
|
|
363
|
+
refreshFailure: "use-retained",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function isCacheRetained(savedAt, policy) {
|
|
367
|
+
const savedTime = Date.parse(savedAt);
|
|
368
|
+
return (Number.isFinite(savedTime) && Date.now() - savedTime <= policy.retainForMs);
|
|
275
369
|
}
|
|
276
370
|
async function loadSharedFreshResource(cache, key, loadFresh) {
|
|
277
371
|
const pendingLoads = getPendingFreshLoads(cache);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bayonai/offline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Reusable offline read-cache, pending-write, and silent recovery primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bayon AI",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"idb",
|
|
22
22
|
"typescript"
|
|
23
23
|
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
24
27
|
"sideEffects": false,
|
|
25
28
|
"type": "module",
|
|
26
29
|
"main": "./dist-cjs/index.js",
|
|
@@ -28,9 +31,14 @@
|
|
|
28
31
|
"types": "./dist/index.d.ts",
|
|
29
32
|
"exports": {
|
|
30
33
|
".": {
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist-cjs/index.d.ts",
|
|
40
|
+
"default": "./dist-cjs/index.js"
|
|
41
|
+
}
|
|
34
42
|
}
|
|
35
43
|
},
|
|
36
44
|
"files": [
|
|
@@ -44,7 +52,7 @@
|
|
|
44
52
|
"access": "public"
|
|
45
53
|
},
|
|
46
54
|
"scripts": {
|
|
47
|
-
"build": "tsc -p tsconfig.json && node scripts/write-esm-extensions.mjs && tsc -p tsconfig.cjs.json && node scripts/write-cjs.mjs",
|
|
55
|
+
"build": "tsc -p tsconfig.json && node scripts/write-esm-extensions.mjs && tsc -p tsconfig.cjs.json && node scripts/write-cjs.mjs && node scripts/verify-dist.mjs",
|
|
48
56
|
"lint": "tsc --noEmit -p tsconfig.json",
|
|
49
57
|
"pack:smoke": "corepack pnpm run build && npm pack --dry-run --access public",
|
|
50
58
|
"prepack": "corepack pnpm run build",
|