@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.
@@ -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
- pendingLoads.delete(key);
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
- // Deletion advances the cache version, so a request started before this
169
- // post-write refresh cannot restore stale data after it finishes.
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
- await resourceCache.delete(key);
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
- if (cached && isCacheFresh(cached.savedAt, maxAgeMs)) {
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
- if (cached) {
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 ? null : await resourceCache.get(key);
224
- if (cached) {
225
- onUpdate({ data: cached.value, fromCache: true });
226
- if (isCacheFresh(cached.savedAt, maxAgeMs)) {
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
- onUpdate({ data: fresh, fromCache: false });
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
- if (!cached) {
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, maxAgeMs) {
270
- if (!maxAgeMs || maxAgeMs <= 0) {
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 <= maxAgeMs;
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.8.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
- "types": "./dist/index.d.ts",
32
- "import": "./dist/index.js",
33
- "require": "./dist-cjs/index.js"
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",