@bayonai/offline 0.11.0 → 0.13.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,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.groupPendingChangeItems = groupPendingChangeItems;
4
+ exports.createPendingChangeSnapshot = createPendingChangeSnapshot;
5
+ exports.createPendingChangeTracker = createPendingChangeTracker;
6
+ const recoveryPolicy_1 = require("./recoveryPolicy");
7
+ /** Groups host-scoped rows without imposing account or domain semantics. */
8
+ function groupPendingChangeItems(items) {
9
+ const groups = new Map();
10
+ for (const item of items) {
11
+ let group = groups.get(item.groupKey);
12
+ if (!group) {
13
+ group = { key: item.groupKey, title: item.groupTitle, items: [] };
14
+ groups.set(item.groupKey, group);
15
+ }
16
+ group.items.push(item);
17
+ }
18
+ return [...groups.values()];
19
+ }
20
+ /** Same-ID captured edits replace durable rows; insertion order remains stable. */
21
+ function createPendingChangeSnapshot({ accountKey, items, isOffline = false, loading = false, readError = null, }) {
22
+ const unique = new Map();
23
+ for (const item of items) {
24
+ if (accountKey !== null && item.accountKey === accountKey)
25
+ unique.set(item.id, { ...item });
26
+ }
27
+ const rows = [...unique.values()];
28
+ const groups = groupPendingChangeItems(rows);
29
+ const draftCount = rows.filter((item) => item.status === "draft").length;
30
+ const readyCount = rows.filter((item) => item.status === "waiting" || item.status === "sending").length;
31
+ const attentionCount = rows.filter((item) => item.needsAttention || item.status === "attention").length;
32
+ const storageErrorCount = rows.filter((item) => item.storageError || item.status === "storage-error").length;
33
+ return {
34
+ accountKey,
35
+ items: rows,
36
+ groups,
37
+ count: rows.length,
38
+ draftCount,
39
+ readyCount,
40
+ attentionCount,
41
+ storageErrorCount,
42
+ sendingCount: rows.filter((item) => item.sending || item.status === "sending").length,
43
+ guidance: readError !== null
44
+ ? "Pending changes could not be read. Your last known changes are preserved."
45
+ : (0, recoveryPolicy_1.getPendingChangesGuidance)({
46
+ hasReadyChanges: readyCount > 0,
47
+ hasDrafts: draftCount > 0,
48
+ hasErrors: attentionCount > 0 || storageErrorCount > 0 || readError !== null,
49
+ isOffline,
50
+ }),
51
+ loading,
52
+ readError,
53
+ };
54
+ }
55
+ /**
56
+ * Observes existing host queues, never persists or replays changes. Reads fail
57
+ * closed across accounts but retain the current account's last known rows.
58
+ * Hosts must notify for draft, command-store and connectivity source changes.
59
+ */
60
+ function createPendingChangeTracker(options) {
61
+ let accountKey = options.accountKey ?? null;
62
+ let isOffline = options.isOffline ?? false;
63
+ let durable = [];
64
+ let captured = [];
65
+ let durableReadError = null;
66
+ let captureReadError = null;
67
+ let loading = false;
68
+ let generation = 0;
69
+ let closed = false;
70
+ const listeners = new Set();
71
+ let snapshot = createPendingChangeSnapshot({
72
+ accountKey,
73
+ items: [],
74
+ isOffline,
75
+ });
76
+ const report = (error) => {
77
+ try {
78
+ if (options.onError)
79
+ options.onError(error);
80
+ else
81
+ console.error("Pending-change tracker could not read host changes; retaining the last known account snapshot.", error);
82
+ }
83
+ catch (callbackError) {
84
+ console.error("Pending-change tracker error reporter failed while reporting a host source failure.", callbackError, error);
85
+ }
86
+ };
87
+ const publish = () => {
88
+ const publishingGeneration = generation;
89
+ snapshot = createPendingChangeSnapshot({
90
+ accountKey,
91
+ items: [...durable, ...captured],
92
+ isOffline,
93
+ loading,
94
+ readError: durableReadError ?? captureReadError,
95
+ });
96
+ for (const listener of [...listeners]) {
97
+ if (closed || publishingGeneration !== generation)
98
+ return;
99
+ try {
100
+ listener();
101
+ }
102
+ catch (error) {
103
+ console.error("Pending-change tracker subscriber failed; other subscribers will still receive updates.", error);
104
+ }
105
+ }
106
+ };
107
+ const refresh = async () => {
108
+ if (closed)
109
+ return;
110
+ const request = ++generation;
111
+ const requestedAccount = accountKey;
112
+ const current = () => !closed && request === generation && requestedAccount === accountKey;
113
+ if (requestedAccount === null)
114
+ return;
115
+ // Capture callbacks may notify sources or switch accounts synchronously.
116
+ const capture = () => {
117
+ try {
118
+ const rows = options.capture?.(requestedAccount) ?? [];
119
+ if (!current())
120
+ return;
121
+ captured = rows;
122
+ captureReadError = null;
123
+ }
124
+ catch (error) {
125
+ if (!current())
126
+ return;
127
+ captureReadError = error;
128
+ report(error);
129
+ }
130
+ };
131
+ capture();
132
+ if (!current())
133
+ return;
134
+ loading = true;
135
+ publish();
136
+ if (!current())
137
+ return;
138
+ try {
139
+ const rows = await options.load(requestedAccount);
140
+ if (!current())
141
+ return;
142
+ // A successful capture cannot prove durable storage recovered.
143
+ durableReadError = null;
144
+ durable = rows;
145
+ // A durable read may retire host acceptance receipts without emitting an
146
+ // event. Recapture after proof so drained receipts cannot become ghosts.
147
+ capture();
148
+ }
149
+ catch (error) {
150
+ if (!current())
151
+ return;
152
+ durableReadError = error;
153
+ report(error);
154
+ }
155
+ if (!current())
156
+ return;
157
+ loading = false;
158
+ publish();
159
+ };
160
+ const unsubscribe = options.subscribe?.(() => {
161
+ void refresh();
162
+ });
163
+ const tracker = {
164
+ getSnapshot: () => snapshot,
165
+ subscribe: (listener) => {
166
+ if (closed)
167
+ return () => { };
168
+ listeners.add(listener);
169
+ return () => {
170
+ listeners.delete(listener);
171
+ };
172
+ },
173
+ refresh,
174
+ setAccountKey: (next) => {
175
+ if (closed || next === accountKey)
176
+ return;
177
+ ++generation;
178
+ accountKey = next;
179
+ durable = [];
180
+ captured = [];
181
+ durableReadError = null;
182
+ captureReadError = null;
183
+ loading = false;
184
+ publish();
185
+ void refresh();
186
+ },
187
+ setOffline: (next) => {
188
+ if (closed || next === isOffline)
189
+ return;
190
+ isOffline = next;
191
+ publish();
192
+ },
193
+ close: () => {
194
+ if (closed)
195
+ return;
196
+ closed = true;
197
+ ++generation;
198
+ unsubscribe?.();
199
+ listeners.clear();
200
+ },
201
+ };
202
+ void refresh();
203
+ return tracker;
204
+ }
@@ -38,7 +38,8 @@ function projectPendingWriteFinalState(records, getKey, { isBarrier = () => true
38
38
  const previous = latest.get(key);
39
39
  if (previous) {
40
40
  superseded.add(previous);
41
- for (const effect of dependents.get(key) ?? [])
41
+ // Every latest entry creates its matching dependency list below.
42
+ for (const effect of dependents.get(key))
42
43
  superseded.add(effect);
43
44
  }
44
45
  latest.set(key, record);
@@ -13,12 +13,14 @@ export type ReadCacheDriver = {
13
13
  clear: () => Promise<void>;
14
14
  close?: () => Promise<void>;
15
15
  delete: (key: string) => Promise<void>;
16
+ deleteIf?: (key: string, predicate: (value: unknown | null) => boolean) => Promise<boolean>;
16
17
  get: (key: string) => Promise<unknown | null>;
17
18
  listEntries?: () => Promise<Array<{
18
19
  key: string;
19
20
  value: unknown;
20
21
  }>>;
21
22
  set: (key: string, value: unknown) => Promise<void>;
23
+ setIf?: (key: string, value: unknown, predicate: (value: unknown | null) => boolean) => Promise<boolean>;
22
24
  };
23
25
  export type ReadCache = {
24
26
  /**
@@ -29,12 +31,15 @@ export type ReadCache = {
29
31
  clear: () => Promise<void>;
30
32
  close: () => Promise<void>;
31
33
  delete: (key: string) => Promise<void>;
34
+ /** Retire only a snapshot still owned by the captured generation. */
35
+ deleteIfVersion?: (key: string, version: string, snapshot: ReadCacheHit<unknown>) => Promise<boolean>;
32
36
  /** Advances a key's generation without deleting its retained value. */
33
37
  invalidate?: (key: string) => void;
34
38
  get: <T>(key: string) => Promise<ReadCacheHit<T> | null>;
35
39
  getVersion: (key: string) => string;
36
40
  set: <T>(key: string, value: T) => Promise<void>;
37
- setIfVersion: <T>(key: string, value: T, version: string) => Promise<boolean>;
41
+ setIfVersion: <T>(key: string, value: T, version: string, snapshot?: ReadCacheHit<unknown> | null, confirmedAt?: string) => Promise<boolean>;
42
+ setIfVersionAt?: <T>(key: string, value: T, version: string, snapshot: ReadCacheHit<unknown> | null, confirmedAt: string) => Promise<boolean>;
38
43
  };
39
44
  export type ReadCachePolicy = {
40
45
  /** Time that a cached value can suppress a remote read. */
@@ -42,9 +47,10 @@ export type ReadCachePolicy = {
42
47
  /** Maximum age at which cached data remains eligible as an offline fallback. */
43
48
  retainForMs: number;
44
49
  /** Behavior when revalidation fails while retained data is available. */
45
- refreshFailure: "use-retained" | "throw";
50
+ refreshFailure: "use-retained" | "throw" | ((error: unknown) => "use-retained" | "throw" | "retire");
46
51
  };
47
52
  export type ReadCacheStatus = "fresh" | "stale" | "miss";
53
+ export type ReadCacheProvenance = "cache-hit" | "source-confirmed" | "retained-fallback";
48
54
  /** Payload-free cache lifecycle events suitable for host-owned telemetry. */
49
55
  export type ReadCacheOutcome = "fresh-hit" | "stale-hit" | "cache-miss" | "forced-refresh" | "loader-success" | "loader-failure";
50
56
  export type ReadThroughCacheResult<T> = {
@@ -53,12 +59,27 @@ export type ReadThroughCacheResult<T> = {
53
59
  fromCache: boolean;
54
60
  stale: boolean;
55
61
  status: ReadCacheStatus;
62
+ provenance?: ReadCacheProvenance;
63
+ confirmedAt?: string;
64
+ savedAt?: string;
65
+ originalError?: unknown;
56
66
  };
57
67
  export type ReadCacheResourceUpdate<T> = {
58
68
  data: T;
59
69
  error?: Error;
60
70
  fromCache: boolean;
61
71
  status: ReadCacheStatus;
72
+ provenance?: ReadCacheProvenance;
73
+ confirmedAt?: string;
74
+ savedAt?: string;
75
+ originalError?: unknown;
76
+ };
77
+ export type ReadCacheLoader<T> = {
78
+ loadFresh: () => Promise<T>;
79
+ loadFreshResult?: never;
80
+ } | {
81
+ loadFresh?: never;
82
+ loadFreshResult: () => Promise<ReadThroughCacheResult<T>>;
62
83
  };
63
84
  export type BrowserReadCacheOptions = Partial<Pick<BrowserKeyValueDriverOptions, "databaseVersion" | "indexedDB">> & {
64
85
  databaseName: string;
@@ -84,29 +105,31 @@ export declare function pruneReadCache({ cache, maxAgeMs, now, }: {
84
105
  now?: () => number;
85
106
  }): Promise<number>;
86
107
  export declare function createReadCacheKey({ namespace, params, prefix, scope, }: ReadCacheKeyOptions): string;
87
- export declare function readThroughCache<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, }: {
108
+ export declare function readThroughCache<T>({ cache, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, requireSource, }: {
88
109
  cache: ReadCache;
89
110
  /**
90
111
  * Revalidates after advancing the key generation, retaining the prior value
91
112
  * as a possible fallback if refresh fails.
92
113
  */
93
114
  forceFresh?: boolean;
115
+ includeProvenance?: boolean;
94
116
  key: string;
95
- loadFresh: () => Promise<T>;
96
117
  maxAgeMs?: number;
97
118
  onOutcome?: (outcome: ReadCacheOutcome) => void;
98
119
  policy?: ReadCachePolicy;
99
- }): Promise<ReadThroughCacheResult<T>>;
100
- export declare function loadReadCacheResource<T>({ cache, forceFresh, key, loadFresh, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, }: {
120
+ requireSource?: boolean;
121
+ } & ReadCacheLoader<T>): Promise<ReadThroughCacheResult<T>>;
122
+ export declare function loadReadCacheResource<T>({ cache, forceFresh, includeProvenance, key, loadFresh, loadFreshResult, maxAgeMs, onOutcome, policy, onCacheWriteError, onUpdate, skipCache, requireSource, }: {
101
123
  cache: ReadCache;
102
124
  /** Revalidate while retaining eligible data as fallback on loader failure. */
103
125
  forceFresh?: boolean;
126
+ includeProvenance?: boolean;
104
127
  key: string;
105
- loadFresh: () => Promise<T>;
106
128
  maxAgeMs?: number;
107
129
  onOutcome?: (outcome: ReadCacheOutcome) => void;
108
130
  policy?: ReadCachePolicy;
109
131
  onCacheWriteError?: (error: Error) => void;
110
132
  onUpdate: (update: ReadCacheResourceUpdate<T>) => void;
111
133
  skipCache?: boolean;
112
- }): Promise<T>;
134
+ requireSource?: boolean;
135
+ } & ReadCacheLoader<T>): Promise<T>;