@bayonai/offline 0.13.0 → 0.15.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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Mounted presentation only: hosts retain cache, transport, identity and initial
3
+ * read policy. Accepting a snapshot preserves its source age; a successful
4
+ * refresh does not require persistence to have survived an unrelated write.
5
+ */
6
+ export function createReadResourceObserver({ enabled = true, current, now, freshForMs, canRetain, isConnected = () => true, canonical, captureSnapshot, read, retireSnapshot, classifyFailure, subscribeWake, onUpdate, onData, onError, }) {
7
+ let disposed = false;
8
+ let retired = false;
9
+ let confirmedAt;
10
+ let retainable = true;
11
+ let pending;
12
+ let stop = () => { };
13
+ const detach = () => { const cleanup = stop; stop = () => { }; cleanup(); };
14
+ const active = () => !disposed && !retired && current();
15
+ const canRefresh = () => enabled && active() && retainable && confirmedAt !== undefined;
16
+ const accept = (update, age = now()) => {
17
+ if (!active())
18
+ return;
19
+ if ("data" in update) {
20
+ confirmedAt = age;
21
+ retainable = canRetain(update.data);
22
+ onData?.(update.data);
23
+ }
24
+ if (active())
25
+ onUpdate(update);
26
+ };
27
+ const adoptCanonical = () => {
28
+ const snapshot = canonical();
29
+ if (!snapshot || !active() || now() - snapshot.confirmedAt >= freshForMs)
30
+ return false;
31
+ accept({ status: "fresh", data: snapshot.data }, snapshot.confirmedAt);
32
+ return true;
33
+ };
34
+ const run = async (force) => {
35
+ if (!canRefresh())
36
+ return;
37
+ if (!force && now() - confirmedAt < freshForMs)
38
+ return;
39
+ if (!isConnected()) {
40
+ accept({ status: "retry", error: new Error("Resource unavailable while disconnected") });
41
+ return;
42
+ }
43
+ if (!force && adoptCanonical())
44
+ return;
45
+ accept({ status: "refreshing" });
46
+ if (!canRefresh())
47
+ return;
48
+ const snapshot = captureSnapshot();
49
+ let result;
50
+ try {
51
+ result = await read();
52
+ }
53
+ catch (error) {
54
+ if (!active())
55
+ return;
56
+ const failure = classifyFailure(error);
57
+ if (failure === "superseded") {
58
+ adoptCanonical();
59
+ return;
60
+ }
61
+ if (failure === "retired")
62
+ return;
63
+ if (onError)
64
+ onError(error);
65
+ else
66
+ console.error("[Bayon offline] Mounted resource refresh failed.", { error });
67
+ if (failure === "transient")
68
+ accept({ status: "retry", error });
69
+ else {
70
+ try {
71
+ await retireSnapshot(snapshot);
72
+ }
73
+ catch (storageError) {
74
+ // Storage cleanup cannot turn a known denial into retained presentation.
75
+ console.error("[Bayon offline] Unable to retire a mounted read snapshot.", { error: storageError });
76
+ }
77
+ if (!active())
78
+ return;
79
+ confirmedAt = undefined;
80
+ accept({ status: "failed", error });
81
+ }
82
+ return;
83
+ }
84
+ accept({ status: "fresh", data: result.data }, result.confirmedAt);
85
+ };
86
+ const refresh = ({ force = true } = {}) => {
87
+ if (!canRefresh())
88
+ return;
89
+ // Install ownership before callbacks can synchronously request another read.
90
+ if (!pending)
91
+ pending = Promise.resolve().then(() => run(force)).finally(() => { pending = undefined; });
92
+ return pending;
93
+ };
94
+ if (enabled && subscribeWake)
95
+ stop = subscribeWake(() => refresh({ force: false }) ?? Promise.resolve());
96
+ return {
97
+ accept, canRefresh, refresh,
98
+ retire() {
99
+ if (disposed || retired)
100
+ return;
101
+ retired = true;
102
+ detach();
103
+ // Clearing protected presentation is safe even after the host changed scope.
104
+ onUpdate({ status: "invalidated" });
105
+ },
106
+ dispose() { disposed = true; detach(); },
107
+ };
108
+ }
@@ -0,0 +1,5 @@
1
+ /** Native wake binding only; hosts own visibility, eligibility and request sharing. */
2
+ export declare function bindBrowserReadLifecycle(targets: {
3
+ window: EventTarget;
4
+ document: EventTarget;
5
+ }, notify: () => Promise<void>): () => void;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bindBrowserReadLifecycle = bindBrowserReadLifecycle;
4
+ /** Native wake binding only; hosts own visibility, eligibility and request sharing. */
5
+ function bindBrowserReadLifecycle(targets, notify) {
6
+ const report = (error) => console.error("[Bayon offline] Browser read refresh failed.", { error });
7
+ const wake = () => {
8
+ try {
9
+ void notify().catch(report);
10
+ }
11
+ catch (error) {
12
+ report(error);
13
+ }
14
+ };
15
+ targets.window.addEventListener("focus", wake);
16
+ targets.window.addEventListener("online", wake);
17
+ targets.document.addEventListener("visibilitychange", wake);
18
+ let stopped = false;
19
+ return () => {
20
+ if (stopped)
21
+ return;
22
+ stopped = true;
23
+ targets.window.removeEventListener("focus", wake);
24
+ targets.window.removeEventListener("online", wake);
25
+ targets.document.removeEventListener("visibilitychange", wake);
26
+ };
27
+ }
@@ -1,4 +1,7 @@
1
1
  export * from "./readCache.js";
2
+ export * from "./readResourceObserver.js";
3
+ export * from "./readResourceBatch.js";
4
+ export * from "./browserReadLifecycle.js";
2
5
  export * from "./pendingWrites.js";
3
6
  export * from "./browserRecovery.js";
4
7
  export * from "./browserCoordination.js";
package/dist-cjs/index.js CHANGED
@@ -16,6 +16,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.projectPendingWriteFinalState = exports.OfflineStorageError = void 0;
18
18
  __exportStar(require("./readCache"), exports);
19
+ __exportStar(require("./readResourceObserver"), exports);
20
+ __exportStar(require("./readResourceBatch"), exports);
21
+ __exportStar(require("./browserReadLifecycle"), exports);
19
22
  __exportStar(require("./pendingWrites"), exports);
20
23
  __exportStar(require("./browserRecovery"), exports);
21
24
  __exportStar(require("./browserCoordination"), exports);
@@ -151,9 +151,9 @@ export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
151
151
  sync: PendingWriteSync<Payload>;
152
152
  }): Promise<SyncPendingWriteResult>;
153
153
  /**
154
- * Replays selected writes oldest first and coalesces overlapping runs for the
155
- * same store/scope. Callers own the selection predicate and idempotent remote
156
- * mutation semantics.
154
+ * Replays selected writes oldest first. Identical overlapping calls coalesce;
155
+ * calls with different options run serially for the same store and scope.
156
+ * Callers own the selection predicate and idempotent remote mutation semantics.
157
157
  */
158
158
  export declare function syncPendingWrites<Payload>({ now, scopeKey, select, stopOnFailure, store, sync, }: {
159
159
  now?: () => string;
@@ -299,30 +299,34 @@ async function syncPendingWrite({ id, now = () => new Date().toISOString(), stor
299
299
  : { status: "superseded" };
300
300
  }
301
301
  /**
302
- * Replays selected writes oldest first and coalesces overlapping runs for the
303
- * same store/scope. Callers own the selection predicate and idempotent remote
304
- * mutation semantics.
302
+ * Replays selected writes oldest first. Identical overlapping calls coalesce;
303
+ * calls with different options run serially for the same store and scope.
304
+ * Callers own the selection predicate and idempotent remote mutation semantics.
305
305
  */
306
- function syncPendingWrites({ now, scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
306
+ function syncPendingWrites({ now, scopeKey = "all", select = selectAllPendingWrites, stopOnFailure = false, store, sync, }) {
307
307
  let inFlightByScope = pendingWriteSyncs.get(store);
308
308
  if (!inFlightByScope) {
309
309
  inFlightByScope = new Map();
310
310
  pendingWriteSyncs.set(store, inFlightByScope);
311
311
  }
312
312
  const existing = inFlightByScope.get(scopeKey);
313
- if (existing) {
314
- return existing;
313
+ if (existing &&
314
+ existing.select === select &&
315
+ existing.sync === sync &&
316
+ existing.now === now &&
317
+ existing.stopOnFailure === stopOnFailure) {
318
+ return existing.promise;
315
319
  }
316
- const syncRun = performPendingWriteSync({
317
- now,
318
- select,
319
- stopOnFailure,
320
- store,
321
- sync,
322
- });
323
- inFlightByScope.set(scopeKey, syncRun);
320
+ const run = () => performPendingWriteSync({ now, select, stopOnFailure, store, sync });
321
+ // Product decision: distinct filters must never share counts or run in
322
+ // parallel; each waits for the preceding scope run and uses its own policy.
323
+ const syncRun = existing ? existing.promise.then(run, run) : run();
324
+ const entry = { now, select, stopOnFailure, sync, promise: syncRun };
325
+ inFlightByScope.set(scopeKey, entry);
324
326
  const clearInFlight = () => {
325
- inFlightByScope.delete(scopeKey);
327
+ if (inFlightByScope.get(scopeKey) === entry) {
328
+ inFlightByScope.delete(scopeKey);
329
+ }
326
330
  };
327
331
  void syncRun.then(clearInFlight, clearInFlight);
328
332
  return syncRun;
@@ -360,6 +364,7 @@ function summarizePendingWrites(records) {
360
364
  return summary;
361
365
  }, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
362
366
  }
367
+ const selectAllPendingWrites = () => true;
363
368
  const pendingWriteSyncs = new WeakMap();
364
369
  async function performPendingWriteSync({ now, select, stopOnFailure, store, sync, }) {
365
370
  const summary = {
@@ -23,6 +23,11 @@ export type ReadCacheDriver = {
23
23
  setIf?: (key: string, value: unknown, predicate: (value: unknown | null) => boolean) => Promise<boolean>;
24
24
  };
25
25
  export type ReadCache = {
26
+ /** Atomic ownership proof for opt-in canonical presentation; version is captured at commit. */
27
+ commitIfVersion?: <T>(key: string, value: T, version: string, snapshot: ReadCacheHit<unknown> | null, confirmedAt?: string) => Promise<{
28
+ written: boolean;
29
+ version: string;
30
+ }>;
26
31
  /**
27
32
  * Resolves a stable cache for the duration of a resource load. Dynamic
28
33
  * adapters use this to pin the active account before asynchronous work.
@@ -119,8 +124,10 @@ export declare function readThroughCache<T>({ cache, forceFresh, includeProvenan
119
124
  policy?: ReadCachePolicy;
120
125
  requireSource?: boolean;
121
126
  } & ReadCacheLoader<T>): Promise<ReadThroughCacheResult<T>>;
122
- export declare function loadReadCacheResource<T>({ cache, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, requireSource, }: {
127
+ export declare function loadReadCacheResource<T>({ cache, deliveryPolicy, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, requireSource, }: {
123
128
  cache: ReadCache;
129
+ /** Opt in to presenting the eligible canonical cache after conditional persistence. */
130
+ deliveryPolicy?: "immediate" | "cache-current";
124
131
  /** Revalidate while retaining eligible data as fallback on loader failure. */
125
132
  forceFresh?: boolean;
126
133
  includeProvenance?: boolean;
@@ -58,7 +58,7 @@ function createReadCache(driver) {
58
58
  operationQueue = next.then(() => undefined, () => undefined);
59
59
  return next;
60
60
  }
61
- async function writeIfVersion(key, value, version, snapshot, savedAt) {
61
+ async function writeIfVersion(key, value, version, snapshot, savedAt, receipt) {
62
62
  if (getVersion(key) !== version)
63
63
  return false;
64
64
  return enqueue(async () => {
@@ -74,15 +74,28 @@ function createReadCache(driver) {
74
74
  const written = await driver.setIf(key, next, (current) => getVersion(key) === version && ownsSnapshot(current, snapshot));
75
75
  if (!written)
76
76
  return false;
77
+ // A driver may finish after the caller has retired this generation.
78
+ // Keep that retirement visible instead of issuing a new owned receipt.
79
+ if (receipt && getVersion(key) !== version)
80
+ return false;
77
81
  }
78
82
  else {
79
83
  await driver.set(key, next);
80
84
  }
81
85
  invalidate(key);
86
+ if (receipt)
87
+ receipt.version = getVersion(key);
82
88
  return true;
83
89
  });
84
90
  }
85
91
  const cache = {
92
+ async commitIfVersion(key, value, version, snapshot, confirmedAt) {
93
+ if (!driver.setIf)
94
+ throw new Error("[Bayon offline] Canonical presentation requires atomic conditional cache writes.");
95
+ const receipt = { version };
96
+ const written = await writeIfVersion(key, value, version, snapshot, confirmedAt ?? new Date().toISOString(), receipt);
97
+ return { written, version: receipt.version };
98
+ },
86
99
  async clear() {
87
100
  pendingLoads.clear();
88
101
  globalVersion += 1;
@@ -349,7 +362,7 @@ async function readThroughCache({ cache, forceFresh = false, includeProvenance =
349
362
  throw error;
350
363
  }
351
364
  }
352
- async function loadReadCacheResource({ cache, forceFresh = false, includeProvenance = false, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache = false, requireSource = false, }) {
365
+ async function loadReadCacheResource({ cache, deliveryPolicy = "immediate", forceFresh = false, includeProvenance = false, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache = false, requireSource = false, }) {
353
366
  const resourceCache = cache.capture?.() ?? cache;
354
367
  const resolvedPolicy = resolvePolicy(policy, maxAgeMs);
355
368
  const capturedVersion = resourceCache.getVersion(key);
@@ -377,11 +390,56 @@ async function loadReadCacheResource({ cache, forceFresh = false, includeProvena
377
390
  // Capture ownership even when presentation skips the cached value.
378
391
  const writeSnapshot = forcedFallback ?? (await resourceCache.get(key));
379
392
  const cached = skipCache ? null : writeSnapshot;
380
- const retained = cached && isCacheRetained(cached.savedAt, resolvedPolicy);
393
+ const retained = cached && isCacheRetained(cached.savedAt, resolvedPolicy)
394
+ && (deliveryPolicy === "immediate" || resourceCache.getVersion(key) === version);
395
+ function publishUpdate(update) {
396
+ if (deliveryPolicy === "immediate") {
397
+ onUpdate(update);
398
+ return;
399
+ }
400
+ try {
401
+ onUpdate(update);
402
+ }
403
+ catch (error) {
404
+ // Opted-in presentation is independent of the caller's source outcome.
405
+ console.error("[Bayon offline] Read-cache presentation callback failed.", {
406
+ error: normalizeError(error), key,
407
+ });
408
+ }
409
+ }
410
+ function reportPresentationStorageError(error) {
411
+ const normalizedError = normalizeError(error);
412
+ console.error("[Bayon offline] Unable to confirm current read-cache presentation.", {
413
+ error: normalizedError, key,
414
+ });
415
+ onCacheWriteError?.(normalizedError);
416
+ }
417
+ async function canonicalUpdate(expectedVersion) {
418
+ if (resourceCache.getVersion(key) !== expectedVersion)
419
+ return;
420
+ let canonical;
421
+ try {
422
+ canonical = await resourceCache.get(key);
423
+ }
424
+ catch (error) {
425
+ reportPresentationStorageError(error);
426
+ return;
427
+ }
428
+ if (!canonical || resourceCache.getVersion(key) !== expectedVersion
429
+ || !isCacheRetained(canonical.savedAt, resolvedPolicy))
430
+ return;
431
+ return {
432
+ data: canonical.value, fromCache: true,
433
+ status: isCacheFresh(canonical.savedAt, resolvedPolicy) ? "fresh" : "stale",
434
+ ...(includeProvenance ? {
435
+ provenance: "cache-hit", savedAt: canonical.savedAt,
436
+ } : {}),
437
+ };
438
+ }
381
439
  if (retained && !requireSource) {
382
440
  const fresh = isCacheFresh(cached.savedAt, resolvedPolicy);
383
441
  reportReadCacheOutcome(onOutcome, fresh && !forceFresh ? "fresh-hit" : "stale-hit");
384
- onUpdate({
442
+ publishUpdate({
385
443
  data: cached.value,
386
444
  fromCache: true,
387
445
  status: fresh && !forceFresh ? "fresh" : "stale",
@@ -403,8 +461,14 @@ async function loadReadCacheResource({ cache, forceFresh = false, includeProvena
403
461
  throw (source.originalError ??
404
462
  source.error ??
405
463
  new Error("[Bayon offline] Source confirmation was required."));
464
+ if (deliveryPolicy === "cache-current") {
465
+ const update = await canonicalUpdate(version);
466
+ if (update)
467
+ publishUpdate(update);
468
+ return source.data;
469
+ }
406
470
  if (resourceCache.getVersion(key) === version)
407
- onUpdate({
471
+ publishUpdate({
408
472
  data: source.data,
409
473
  fromCache: source.fromCache,
410
474
  status: source.status,
@@ -421,8 +485,35 @@ async function loadReadCacheResource({ cache, forceFresh = false, includeProvena
421
485
  }
422
486
  const fresh = source.data;
423
487
  reportReadCacheOutcome(onOutcome, "loader-success");
488
+ if (deliveryPolicy === "cache-current") {
489
+ let update;
490
+ try {
491
+ if (!resourceCache.commitIfVersion)
492
+ throw new Error("[Bayon offline] Canonical presentation requires an atomic commit receipt.");
493
+ const receipt = await resourceCache.commitIfVersion(key, fresh, version, writeSnapshot, source.confirmedAt);
494
+ if (requireSource) {
495
+ // Source-required consumers may never mistake a readback for proof.
496
+ if (receipt.written && resourceCache.getVersion(key) === receipt.version)
497
+ update = {
498
+ data: fresh, fromCache: false, status: retained ? "fresh" : "miss",
499
+ ...(includeProvenance ? {
500
+ provenance: "source-confirmed", confirmedAt: source.confirmedAt,
501
+ } : {}),
502
+ };
503
+ }
504
+ else
505
+ update = await canonicalUpdate(receipt.version);
506
+ }
507
+ catch (error) {
508
+ reportPresentationStorageError(error);
509
+ }
510
+ // Keep observer exceptions outside the storage diagnostic boundary.
511
+ if (update)
512
+ publishUpdate(update);
513
+ return fresh;
514
+ }
424
515
  if (resourceCache.getVersion(key) === version) {
425
- onUpdate({
516
+ publishUpdate({
426
517
  data: fresh,
427
518
  fromCache: false,
428
519
  status: retained ? "fresh" : "miss",
@@ -452,7 +543,18 @@ async function loadReadCacheResource({ cache, forceFresh = false, includeProvena
452
543
  if (requireSource || !retained || failure !== "use-retained") {
453
544
  throw error;
454
545
  }
455
- onUpdate({
546
+ if (deliveryPolicy === "cache-current") {
547
+ const update = await canonicalUpdate(version);
548
+ if (update)
549
+ publishUpdate({
550
+ ...update, status: "stale", error: normalizeError(error),
551
+ ...(includeProvenance ? {
552
+ provenance: "retained-fallback", originalError: error,
553
+ } : {}),
554
+ });
555
+ return cached.value;
556
+ }
557
+ publishUpdate({
456
558
  data: cached.value,
457
559
  error: normalizeError(error),
458
560
  fromCache: true,
@@ -0,0 +1,13 @@
1
+ import type { ReadResourceObservation, ReadResourceUpdate } from "./readResourceObserver.js";
2
+ /** A mounted presentation barrier; child tasks retain sole network/cache ownership. */
3
+ export declare function observeReadResourceBatch<T>({ count, observe, freshForMs, now, current, canRetain, retiredError, subscribe, onUpdate }: {
4
+ count: number;
5
+ observe: (index: number, update: (value: ReadResourceUpdate<T>) => void, age: (createdAt: number) => void) => ReadResourceObservation<T>;
6
+ freshForMs: number[];
7
+ now: () => number;
8
+ current: () => boolean;
9
+ canRetain: (value: T) => boolean;
10
+ retiredError: () => Error;
11
+ subscribe: (listener: () => Promise<void>) => () => void;
12
+ onUpdate: (update: ReadResourceUpdate<T[]>) => void;
13
+ }): ReadResourceObservation<T[]>;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.observeReadResourceBatch = observeReadResourceBatch;
4
+ /** A mounted presentation barrier; child tasks retain sole network/cache ownership. */
5
+ function observeReadResourceBatch({ count, observe, freshForMs, now, current, canRetain, retiredError, subscribe, onUpdate }) {
6
+ let disposed = false;
7
+ let retired = false;
8
+ let presented = false;
9
+ let strict = false;
10
+ let initialRetry;
11
+ let pending;
12
+ let failure;
13
+ let stop = () => { };
14
+ const children = [];
15
+ const disposedChildren = new Set();
16
+ const detach = () => { const cleanup = stop; stop = () => { }; cleanup(); };
17
+ const disposeChildren = () => children.forEach(child => {
18
+ if (!disposedChildren.has(child)) {
19
+ disposedChildren.add(child);
20
+ child.dispose();
21
+ }
22
+ });
23
+ const values = new Array(count);
24
+ const received = new Set();
25
+ const ages = new Array(count).fill(-Infinity);
26
+ const active = () => !disposed && !retired && current();
27
+ const retire = () => { retired = true; detach(); disposeChildren(); };
28
+ for (let index = 0; index < count; index++) {
29
+ const child = observe(index, update => {
30
+ if (disposed || retired)
31
+ return;
32
+ if (update.status === "invalidated" || update.status === "expired") {
33
+ onUpdate({ status: "invalidated" });
34
+ retire();
35
+ return;
36
+ }
37
+ if (!current())
38
+ return;
39
+ if ("data" in update) {
40
+ received.add(index);
41
+ values[index] = update.data;
42
+ if (!canRetain(update.data))
43
+ strict = true;
44
+ }
45
+ // Denial retires the group even before every initial source has settled.
46
+ if (update.status === "failed") {
47
+ onUpdate(update);
48
+ presented = false;
49
+ retire();
50
+ return;
51
+ }
52
+ if (update.status === "retry") {
53
+ if (presented)
54
+ failure = update;
55
+ else
56
+ initialRetry = { error: update.error };
57
+ }
58
+ }, createdAt => { ages[index] = createdAt; });
59
+ children.push(child);
60
+ if (retired) {
61
+ disposeChildren();
62
+ break;
63
+ }
64
+ }
65
+ const refresh = (force = false) => {
66
+ if (!active() || !presented || strict)
67
+ return Promise.resolve();
68
+ if (pending)
69
+ return pending;
70
+ if (!force && ages.every((age, index) => now() - age < freshForMs[index]))
71
+ return Promise.resolve();
72
+ // Claim the barrier before a presentation callback can request it again.
73
+ pending = Promise.resolve().then(async () => {
74
+ if (!active() || strict)
75
+ return;
76
+ failure = undefined;
77
+ onUpdate({ status: "refreshing" });
78
+ if (!active() || strict)
79
+ return;
80
+ await Promise.all(children.map(child => child.refresh?.({ force })));
81
+ if (!active())
82
+ return;
83
+ if (failure)
84
+ onUpdate(failure);
85
+ else {
86
+ // Never expose a successful sibling beside an older failed member.
87
+ onUpdate({ status: "fresh", data: [...values] });
88
+ if (strict)
89
+ detach();
90
+ }
91
+ }).finally(() => { pending = undefined; });
92
+ return pending;
93
+ };
94
+ if (!retired)
95
+ stop = subscribe(() => refresh());
96
+ const promise = Promise.allSettled(children.map(child => child.promise)).then(results => {
97
+ if (!active())
98
+ throw retiredError();
99
+ const rejected = results.find(result => result.status === "rejected");
100
+ if (rejected?.status === "rejected")
101
+ throw rejected.reason;
102
+ const data = results.map(result => result.value);
103
+ data.forEach((value, index) => { if (!received.has(index))
104
+ values[index] = value; });
105
+ presented = true;
106
+ // Presentation follows accepted canonical updates, while the promise keeps
107
+ // each shared source caller's original result (including duplicate positions).
108
+ onUpdate({ status: initialRetry === undefined ? "fresh" : "retained", data: [...values] });
109
+ if (initialRetry !== undefined && active())
110
+ onUpdate({ status: "retry", error: initialRetry.error });
111
+ if (strict)
112
+ detach();
113
+ return data;
114
+ });
115
+ return { promise,
116
+ refresh: ({ force = true } = {}) => active() && presented && !strict ? refresh(force) : undefined,
117
+ dispose() { disposed = true; detach(); disposeChildren(); },
118
+ };
119
+ }
@@ -0,0 +1,57 @@
1
+ export type ReadResourceUpdate<T> = {
2
+ status: "retained" | "fresh";
3
+ data: T;
4
+ } | {
5
+ status: "refreshing";
6
+ } | {
7
+ status: "retry";
8
+ error: unknown;
9
+ } | {
10
+ status: "failed";
11
+ error: unknown;
12
+ } | {
13
+ status: "expired";
14
+ } | {
15
+ status: "invalidated";
16
+ };
17
+ export type ReadResourceSnapshot<T> = {
18
+ data: T;
19
+ confirmedAt: number;
20
+ };
21
+ export type ReadResourceObservation<T> = {
22
+ promise: Promise<T>;
23
+ refresh?: (options?: {
24
+ force?: boolean;
25
+ }) => Promise<void> | undefined;
26
+ dispose: () => void;
27
+ };
28
+ /**
29
+ * Mounted presentation only: hosts retain cache, transport, identity and initial
30
+ * read policy. Accepting a snapshot preserves its source age; a successful
31
+ * refresh does not require persistence to have survived an unrelated write.
32
+ */
33
+ export declare function createReadResourceObserver<T, Snapshot>({ enabled, current, now, freshForMs, canRetain, isConnected, canonical, captureSnapshot, read, retireSnapshot, classifyFailure, subscribeWake, onUpdate, onData, onError, }: {
34
+ enabled?: boolean;
35
+ current: () => boolean;
36
+ now: () => number;
37
+ freshForMs: number;
38
+ canRetain: (data: T) => boolean;
39
+ isConnected?: () => boolean;
40
+ canonical: () => ReadResourceSnapshot<T> | undefined;
41
+ captureSnapshot: () => Snapshot;
42
+ read: () => Promise<ReadResourceSnapshot<T>>;
43
+ retireSnapshot: (snapshot: Snapshot) => Promise<void>;
44
+ classifyFailure: (error: unknown) => "transient" | "terminal" | "superseded" | "retired";
45
+ subscribeWake?: (listener: () => Promise<void>) => () => void;
46
+ onUpdate: (update: ReadResourceUpdate<T>) => void;
47
+ onData?: (data: T) => void;
48
+ onError?: (error: unknown) => void;
49
+ }): {
50
+ accept: (update: ReadResourceUpdate<T>, age?: number) => void;
51
+ canRefresh: () => boolean;
52
+ refresh: ({ force }?: {
53
+ force?: boolean | undefined;
54
+ }) => Promise<void> | undefined;
55
+ retire(): void;
56
+ dispose(): void;
57
+ };