@bayonai/offline 0.5.1 → 0.8.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.
@@ -6,6 +6,8 @@ export type PendingWriteStatus = "pending" | "syncing" | "failed";
6
6
  export type PendingWriteRecord<Payload = unknown> = {
7
7
  error?: string;
8
8
  id: string;
9
+ /** Do not execute this accepted command before this ISO timestamp. */
10
+ notBefore?: string;
9
11
  payload: Payload;
10
12
  savedAt: string;
11
13
  snapshotId: string;
@@ -80,6 +82,7 @@ export type SyncPendingWriteResult = {
80
82
  status: "superseded";
81
83
  } | {
82
84
  error: string;
85
+ failure: unknown;
83
86
  status: "failed";
84
87
  };
85
88
  export type SyncPendingWritesResult = {
@@ -91,7 +94,11 @@ export type SyncPendingWritesResult = {
91
94
  superseded: number;
92
95
  synced: number;
93
96
  };
94
- export type PendingWriteSync<Payload> = (payload: Payload) => Promise<unknown>;
97
+ export type PendingWriteSyncContext = {
98
+ /** Stable durable-record identity; safe to use as a remote idempotency key. */
99
+ recordId: string;
100
+ };
101
+ export type PendingWriteSync<Payload, Result = unknown> = (payload: Payload, context: PendingWriteSyncContext) => Promise<Result>;
95
102
  export type RunPendingWriteResult<TResult, TQueued> = {
96
103
  result: TResult;
97
104
  status: "synced";
@@ -127,7 +134,7 @@ export declare function createMemoryPendingWriteDriver(): PendingWriteDriver;
127
134
  export declare function createBrowserPendingWriteDriver({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, storeName, }: Omit<BrowserPendingWriteStoreOptions, "keyPrefix" | "normalizePayload" | "schemaVersion">): PendingWriteDriver | null;
128
135
  export declare function createBrowserPendingWriteStore<Payload = unknown>({ databaseName, databaseVersion, indexedDB, keyPrefix, normalizeLegacyRecord, normalizePayload, operationTimeoutMs, schemaVersion, storeName, }: BrowserPendingWriteStoreOptions<Payload>): PendingWriteStore<Payload>;
129
136
  export declare function createPendingWriteKey({ id, prefix }: PendingWriteKeyOptions): string;
130
- export declare function createPendingWriteSnapshotId(): `${string}-${string}-${string}-${string}-${string}`;
137
+ export declare function createPendingWriteSnapshotId(): string;
131
138
  export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
132
139
  id: string;
133
140
  now?: () => string;
@@ -139,7 +146,8 @@ export declare function syncPendingWrite<Payload>({ id, now, store, sync, }: {
139
146
  * same store/scope. Callers own the selection predicate and idempotent remote
140
147
  * mutation semantics.
141
148
  */
142
- export declare function syncPendingWrites<Payload>({ scopeKey, select, stopOnFailure, store, sync, }: {
149
+ export declare function syncPendingWrites<Payload>({ now, scopeKey, select, stopOnFailure, store, sync, }: {
150
+ now?: () => string;
143
151
  scopeKey?: string;
144
152
  select?: (record: PendingWriteRecord<Payload>) => boolean;
145
153
  /** Preserve caller ordering by leaving later records for a future retry. */
@@ -158,7 +166,8 @@ export declare function summarizePendingWrites<Payload>(records: Array<PendingWr
158
166
  * workspace. Use its `recover` method for background replay and `run` for new
159
167
  * writes so a newer remote mutation cannot overtake an older queued mutation.
160
168
  */
161
- export declare function createOrderedPendingWriteExecutor<Payload>({ scopeKey, select, store, sync, }: {
169
+ export declare function createOrderedPendingWriteExecutor<Payload>({ now, scopeKey, select, store, sync, }: {
170
+ now?: () => string;
162
171
  scopeKey?: string;
163
172
  select?: (record: PendingWriteRecord<Payload>) => boolean;
164
173
  store: PendingWriteStore<Payload>;
@@ -1,4 +1,4 @@
1
- import { createIdbKeyValueDriver, } from "./idbDriver.js";
1
+ import { createIdbKeyValueDriver, OfflineStorageError, } from "./idbDriver.js";
2
2
  export const pendingWriteSchemaVersion = 1;
3
3
  export const pendingWriteOperationTimeoutMs = 2_000;
4
4
  export const pendingWriteSyncLeaseTimeoutMs = 60_000;
@@ -148,19 +148,34 @@ export function createBrowserPendingWriteStore({ databaseName, databaseVersion,
148
148
  operationTimeoutMs,
149
149
  storeName,
150
150
  });
151
- return createPendingWriteStore(driver ?? createNoopPendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
151
+ return createPendingWriteStore(driver ?? createUnavailablePendingWriteDriver(), { keyPrefix, normalizeLegacyRecord, normalizePayload, schemaVersion });
152
152
  }
153
153
  export function createPendingWriteKey({ id, prefix }) {
154
154
  return `${prefix}:${encodeURIComponent(id)}`;
155
155
  }
156
156
  export function createPendingWriteSnapshotId() {
157
- return globalThis.crypto.randomUUID();
157
+ const randomId = globalThis.crypto?.randomUUID?.();
158
+ if (randomId)
159
+ return randomId;
160
+ pendingWriteSnapshotSequence += 1;
161
+ return [
162
+ "snapshot",
163
+ Date.now().toString(36),
164
+ pendingWriteSnapshotSequence.toString(36),
165
+ Math.random().toString(36).slice(2),
166
+ ].join("-");
158
167
  }
168
+ let pendingWriteSnapshotSequence = 0;
159
169
  export async function syncPendingWrite({ id, now = () => new Date().toISOString(), store, sync, }) {
160
170
  const pending = await store.get(id);
161
171
  if (!pending) {
162
172
  return { status: "idle" };
163
173
  }
174
+ // A delayed command remains in its ordered lane until its grace period has
175
+ // elapsed. This makes cancellation survive UI unmounts and app restarts.
176
+ if (pending.notBefore && Date.parse(pending.notBefore) > Date.parse(now())) {
177
+ return { status: "idle" };
178
+ }
164
179
  const claimedSnapshotId = createPendingWriteSnapshotId();
165
180
  const claimed = await store.claimIfSnapshotId(id, pending.snapshotId, {
166
181
  ...pending,
@@ -173,7 +188,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
173
188
  return { status: "superseded" };
174
189
  }
175
190
  try {
176
- await sync(pending.payload);
191
+ await sync(pending.payload, { recordId: pending.id });
177
192
  }
178
193
  catch (error) {
179
194
  const message = error instanceof Error ? error.message : "Unable to sync pending write.";
@@ -190,7 +205,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
190
205
  store,
191
206
  });
192
207
  return markedFailed
193
- ? { error: message, status: "failed" }
208
+ ? { error: message, failure: error, status: "failed" }
194
209
  : { status: "superseded" };
195
210
  }
196
211
  return (await store.clearIfSnapshotId(id, claimedSnapshotId))
@@ -202,7 +217,7 @@ export async function syncPendingWrite({ id, now = () => new Date().toISOString(
202
217
  * same store/scope. Callers own the selection predicate and idempotent remote
203
218
  * mutation semantics.
204
219
  */
205
- export function syncPendingWrites({ scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
220
+ export function syncPendingWrites({ now, scopeKey = "all", select = () => true, stopOnFailure = false, store, sync, }) {
206
221
  let inFlightByScope = pendingWriteSyncs.get(store);
207
222
  if (!inFlightByScope) {
208
223
  inFlightByScope = new Map();
@@ -213,6 +228,7 @@ export function syncPendingWrites({ scopeKey = "all", select = () => true, stopO
213
228
  return existing;
214
229
  }
215
230
  const syncRun = performPendingWriteSync({
231
+ now,
216
232
  select,
217
233
  stopOnFailure,
218
234
  store,
@@ -259,7 +275,7 @@ export function summarizePendingWrites(records) {
259
275
  }, { failedCount: 0, pendingCount: 0, syncingCount: 0, totalCount: 0 });
260
276
  }
261
277
  const pendingWriteSyncs = new WeakMap();
262
- async function performPendingWriteSync({ select, stopOnFailure, store, sync, }) {
278
+ async function performPendingWriteSync({ now, select, stopOnFailure, store, sync, }) {
263
279
  const summary = {
264
280
  attempted: 0,
265
281
  failed: 0,
@@ -272,6 +288,7 @@ async function performPendingWriteSync({ select, stopOnFailure, store, sync, })
272
288
  for (const record of records) {
273
289
  const result = await syncPendingWrite({
274
290
  id: record.id,
291
+ now,
275
292
  store,
276
293
  sync,
277
294
  });
@@ -280,7 +297,9 @@ async function performPendingWriteSync({ select, stopOnFailure, store, sync, })
280
297
  // Product decision: a superseded result can represent an older live claim.
281
298
  // Preserve strict ordering by leaving every later record for a future run.
282
299
  if (stopOnFailure &&
283
- (result.status === "failed" || result.status === "superseded")) {
300
+ (result.status === "failed" ||
301
+ result.status === "idle" ||
302
+ result.status === "superseded")) {
284
303
  break;
285
304
  }
286
305
  }
@@ -305,7 +324,7 @@ function isPendingWriteSyncClaimAvailable(record, now) {
305
324
  * workspace. Use its `recover` method for background replay and `run` for new
306
325
  * writes so a newer remote mutation cannot overtake an older queued mutation.
307
326
  */
308
- export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = () => true, store, sync, }) {
327
+ export function createOrderedPendingWriteExecutor({ now, scopeKey = "all", select = () => true, store, sync, }) {
309
328
  let execution = Promise.resolve();
310
329
  function inOrder(operation) {
311
330
  const result = execution.then(operation, operation);
@@ -314,6 +333,7 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
314
333
  }
315
334
  function recover() {
316
335
  return inOrder(() => syncPendingWrites({
336
+ now,
317
337
  scopeKey,
318
338
  select,
319
339
  stopOnFailure: true,
@@ -329,6 +349,7 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
329
349
  return runPendingWrite(options);
330
350
  }
331
351
  const replay = await syncPendingWrites({
352
+ now,
332
353
  scopeKey,
333
354
  select,
334
355
  stopOnFailure: true,
@@ -346,7 +367,8 @@ export function createOrderedPendingWriteExecutor({ scopeKey = "all", select = (
346
367
  },
347
368
  };
348
369
  }
349
- function createNoopPendingWriteDriver() {
370
+ function createUnavailablePendingWriteDriver() {
371
+ const unavailable = () => new OfflineStorageError("IndexedDB is unavailable; the pending write was not stored.");
350
372
  return {
351
373
  async clear() { },
352
374
  async clearIf() {
@@ -360,9 +382,11 @@ function createNoopPendingWriteDriver() {
360
382
  async list() {
361
383
  return [];
362
384
  },
363
- async set() { },
385
+ async set() {
386
+ throw unavailable();
387
+ },
364
388
  async setIf() {
365
- return false;
389
+ throw unavailable();
366
390
  },
367
391
  };
368
392
  }
@@ -402,6 +426,9 @@ function normalizePendingWriteRecord(record, normalizePayload) {
402
426
  return {
403
427
  ...(typeof record.error === "string" ? { error: record.error } : {}),
404
428
  id: record.id,
429
+ ...(typeof record.notBefore === "string"
430
+ ? { notBefore: record.notBefore }
431
+ : {}),
405
432
  payload,
406
433
  savedAt: record.savedAt,
407
434
  snapshotId: record.snapshotId ?? createLegacyPendingWriteSnapshotId(record),
@@ -430,6 +457,7 @@ function isPendingWriteRecord(value) {
430
457
  typeof value === "object" &&
431
458
  "id" in value &&
432
459
  typeof value.id === "string" &&
460
+ (!("notBefore" in value) || typeof value.notBefore === "string") &&
433
461
  "savedAt" in value &&
434
462
  typeof value.savedAt === "string" &&
435
463
  (!("snapshotId" in value) || typeof value.snapshotId === "string") &&
@@ -3,7 +3,7 @@ export declare const readCacheSchemaVersion = 1;
3
3
  export declare const readCacheFreshnessMaxAgeMs = 30000;
4
4
  export declare const durableReadCacheFreshnessMaxAgeMs: number;
5
5
  export declare const identityReadCacheFreshnessMaxAgeMs: number;
6
- export declare const browserReadCacheOperationTimeoutMs = 2000;
6
+ export declare const browserReadCacheOperationTimeoutMs = 1000;
7
7
  export type ReadCacheHit<T> = {
8
8
  fromCache: true;
9
9
  savedAt: string;
package/dist/readCache.js CHANGED
@@ -3,7 +3,9 @@ export const readCacheSchemaVersion = 1;
3
3
  export const readCacheFreshnessMaxAgeMs = 30_000;
4
4
  export const durableReadCacheFreshnessMaxAgeMs = 60 * 60_000;
5
5
  export const identityReadCacheFreshnessMaxAgeMs = 24 * 60 * 60_000;
6
- export const browserReadCacheOperationTimeoutMs = 2_000;
6
+ // Cache reads are optional and should yield to the fresh loader quickly when
7
+ // IndexedDB is blocked by a browser/WebView lifecycle or another tab.
8
+ export const browserReadCacheOperationTimeoutMs = 1_000;
7
9
  const pendingFreshLoads = new WeakMap();
8
10
  const cachePruners = new WeakMap();
9
11
  export function createReadCache(driver) {
@@ -133,8 +135,7 @@ export function createBrowserReadCache({ databaseName, databaseVersion, indexedD
133
135
  */
134
136
  export function pruneReadCache({ cache, maxAgeMs, now = () => Date.now(), }) {
135
137
  const resourceCache = cache.capture?.() ?? cache;
136
- return cachePruners.get(resourceCache)?.({ maxAgeMs, now }) ??
137
- Promise.resolve(0);
138
+ return (cachePruners.get(resourceCache)?.({ maxAgeMs, now }) ?? Promise.resolve(0));
138
139
  }
139
140
  export function createReadCacheKey({ namespace, params = {}, prefix, scope, }) {
140
141
  const serializedParams = Object.entries(params)
@@ -156,7 +157,15 @@ export async function readThroughCache({ cache, forceFresh = false, key, loadFre
156
157
  if (forceFresh) {
157
158
  // Deletion advances the cache version, so a request started before this
158
159
  // post-write refresh cannot restore stale data after it finishes.
159
- await resourceCache.delete(key);
160
+ try {
161
+ await resourceCache.delete(key);
162
+ }
163
+ catch (error) {
164
+ console.error("[Bayon offline] Unable to invalidate the read cache.", {
165
+ error: normalizeError(error),
166
+ key,
167
+ });
168
+ }
160
169
  }
161
170
  const version = resourceCache.getVersion(key);
162
171
  const cached = await resourceCache.get(key);
@@ -170,7 +179,15 @@ export async function readThroughCache({ cache, forceFresh = false, key, loadFre
170
179
  }
171
180
  try {
172
181
  const fresh = await loadSharedFreshResource(resourceCache, key, loadFresh);
173
- await resourceCache.setIfVersion(key, fresh, version);
182
+ try {
183
+ await resourceCache.setIfVersion(key, fresh, version);
184
+ }
185
+ catch (error) {
186
+ console.error("[Bayon offline] Unable to refresh the read cache.", {
187
+ error: normalizeError(error),
188
+ key,
189
+ });
190
+ }
174
191
  return {
175
192
  data: fresh,
176
193
  error: null,
@@ -12,6 +12,7 @@ function createBrowserOfflineFallbackRecovery({ control, environment = createDef
12
12
  let active = false;
13
13
  let retrying = false;
14
14
  let pullStartY = null;
15
+ const touchThreshold = pullThreshold === null ? Number.POSITIVE_INFINITY : pullThreshold;
15
16
  function retry() {
16
17
  if (retrying)
17
18
  return;
@@ -23,7 +24,9 @@ function createBrowserOfflineFallbackRecovery({ control, environment = createDef
23
24
  }
24
25
  catch (error) {
25
26
  // A failed navigation must leave the recovery action visible and usable.
26
- console.error("[Bayon offline] Offline fallback reload failed.", { error });
27
+ console.error("[Bayon offline] Offline fallback reload failed.", {
28
+ error,
29
+ });
27
30
  retrying = false;
28
31
  control.disabled = false;
29
32
  }
@@ -36,7 +39,7 @@ function createBrowserOfflineFallbackRecovery({ control, environment = createDef
36
39
  const touchY = getSingleTouchY(event, "changedTouches");
37
40
  if (pullStartY !== null &&
38
41
  touchY !== null &&
39
- touchY - pullStartY >= (pullThreshold ?? Number.POSITIVE_INFINITY)) {
42
+ touchY - pullStartY >= touchThreshold) {
40
43
  retry();
41
44
  }
42
45
  pullStartY = null;
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDurableCommandStore = createDurableCommandStore;
4
+ const pendingWrites_1 = require("./pendingWrites");
5
+ /**
6
+ * Hosts provide domain projection and transport; this package only owns the
7
+ * durable command lifecycle. A command becomes visible immediately, but is
8
+ * never sent remotely until IndexedDB has accepted it.
9
+ */
10
+ function createDurableCommandStore({ canCancel = () => true, classifyError = () => "unexpected", createId = createDurableCommandId, execute, initialState, load, now = () => new Date().toISOString(), project, reconcile, shouldRetry = () => true, store, }) {
11
+ let baseState = initialState;
12
+ let baseStateGeneration = 0;
13
+ let savedAtMillis = 0;
14
+ let publishGeneration = 0;
15
+ let storedRecords = [];
16
+ let deferredRecoveryTimer = null;
17
+ const provisionalRecords = new Map();
18
+ const localStorageFailures = new Map();
19
+ let snapshot = {
20
+ operations: [],
21
+ state: initialState,
22
+ };
23
+ const listeners = new Set();
24
+ const executor = (0, pendingWrites_1.createOrderedPendingWriteExecutor)({
25
+ now,
26
+ scopeKey: "durable-command-store",
27
+ select: isActiveCommand,
28
+ store,
29
+ sync: async (command, { recordId }) => {
30
+ const result = await execute(command, { operationId: recordId });
31
+ // A completed remote write is newer than any read already in flight.
32
+ // Reconciliation is local-only: a projector bug must never retry a
33
+ // command whose remote effect may already have committed.
34
+ baseStateGeneration += 1;
35
+ if (reconcile) {
36
+ try {
37
+ baseState = reconcile(baseState, command, result);
38
+ }
39
+ catch (error) {
40
+ console.error("[Bayon offline] Unable to reconcile a confirmed durable command.", { error, operationId: recordId });
41
+ }
42
+ }
43
+ },
44
+ });
45
+ function notify() {
46
+ for (const listener of listeners) {
47
+ try {
48
+ listener();
49
+ }
50
+ catch (error) {
51
+ console.error("[Bayon offline] Durable-command listener failed.", {
52
+ error,
53
+ });
54
+ }
55
+ }
56
+ }
57
+ function classifyStoredError(error) {
58
+ return classifyError(new Error(error ?? "Unable to sync durable command."));
59
+ }
60
+ function isActiveCommand(record) {
61
+ return (record.syncStatus !== "failed" ||
62
+ shouldRetry(classifyStoredError(record.error), record.payload));
63
+ }
64
+ function scheduleDeferredRecovery(records) {
65
+ if (deferredRecoveryTimer) {
66
+ clearTimeout(deferredRecoveryTimer);
67
+ deferredRecoveryTimer = null;
68
+ }
69
+ const currentTime = Date.parse(now());
70
+ const nextDeadline = records.reduce((earliest, record) => {
71
+ const deadline = record.notBefore ? Date.parse(record.notBefore) : NaN;
72
+ if (!Number.isFinite(deadline) || deadline <= currentTime)
73
+ return earliest;
74
+ return earliest === null ? deadline : Math.min(earliest, deadline);
75
+ }, null);
76
+ if (nextDeadline === null)
77
+ return;
78
+ deferredRecoveryTimer = setTimeout(recoverInBackground, Math.max(0, nextDeadline - currentTime));
79
+ }
80
+ function currentRecords() {
81
+ const records = new Map(storedRecords.map((record) => [record.id, record]));
82
+ for (const record of provisionalRecords.values()) {
83
+ records.set(record.id, record);
84
+ }
85
+ return [...records.values()].sort((left, right) => left.savedAt.localeCompare(right.savedAt) ||
86
+ left.id.localeCompare(right.id));
87
+ }
88
+ function applySnapshot(records) {
89
+ snapshot = {
90
+ operations: [
91
+ ...records.map((record) => ({
92
+ command: record.payload,
93
+ ...(record.syncStatus === "failed"
94
+ ? { error: classifyStoredError(record.error) }
95
+ : {}),
96
+ id: record.id,
97
+ status: record.syncStatus === "failed"
98
+ ? "failed"
99
+ : record.syncStatus === "syncing"
100
+ ? "syncing"
101
+ : "accepted",
102
+ })),
103
+ ...[...localStorageFailures].map(([id, command]) => ({
104
+ command,
105
+ error: "local-storage",
106
+ id,
107
+ status: "failed",
108
+ })),
109
+ ],
110
+ state: project(baseState, records.filter(isActiveCommand)),
111
+ };
112
+ scheduleDeferredRecovery(records);
113
+ }
114
+ async function publish() {
115
+ const generation = ++publishGeneration;
116
+ const records = await store.list();
117
+ if (generation !== publishGeneration)
118
+ return;
119
+ storedRecords = records;
120
+ applySnapshot(currentRecords());
121
+ notify();
122
+ }
123
+ async function recover() {
124
+ await executor.recover();
125
+ await publish();
126
+ }
127
+ function recoverInBackground() {
128
+ void recover().catch((error) => {
129
+ console.error("[Bayon offline] Unable to recover durable commands.", {
130
+ error,
131
+ });
132
+ });
133
+ }
134
+ const unsubscribe = store.subscribe(() => {
135
+ void publish().catch((error) => {
136
+ console.error("[Bayon offline] Unable to publish durable commands.", {
137
+ error,
138
+ });
139
+ });
140
+ });
141
+ return {
142
+ async cancel(operationId) {
143
+ if (localStorageFailures.delete(operationId)) {
144
+ applySnapshot(currentRecords());
145
+ notify();
146
+ return true;
147
+ }
148
+ const record = await store.get(operationId);
149
+ if (!record ||
150
+ record.syncStatus === "syncing" ||
151
+ !canCancel(record.payload, now())) {
152
+ return false;
153
+ }
154
+ const cancelled = await store.clearIfSnapshotId(record.id, record.snapshotId);
155
+ if (cancelled)
156
+ await publish();
157
+ return cancelled;
158
+ },
159
+ close() {
160
+ if (deferredRecoveryTimer)
161
+ clearTimeout(deferredRecoveryTimer);
162
+ publishGeneration += 1;
163
+ unsubscribe();
164
+ },
165
+ dispatch(command, options = {}) {
166
+ const operationId = createId();
167
+ const timestamp = Math.max(Date.parse(now()), savedAtMillis + 1);
168
+ savedAtMillis = timestamp;
169
+ const record = {
170
+ id: operationId,
171
+ ...(options.notBefore ? { notBefore: options.notBefore } : {}),
172
+ payload: command,
173
+ savedAt: new Date(timestamp).toISOString(),
174
+ snapshotId: operationId,
175
+ syncStatus: "pending",
176
+ };
177
+ // Product decision: provisional records remain part of the same ordered
178
+ // projection as persisted retryable failures while storage settles.
179
+ provisionalRecords.set(operationId, record);
180
+ localStorageFailures.delete(operationId);
181
+ applySnapshot(currentRecords());
182
+ notify();
183
+ const accepted = store.set(record).then(async () => {
184
+ provisionalRecords.delete(operationId);
185
+ await publish();
186
+ recoverInBackground();
187
+ }, async (error) => {
188
+ provisionalRecords.delete(operationId);
189
+ localStorageFailures.set(operationId, command);
190
+ applySnapshot(currentRecords());
191
+ notify();
192
+ throw error;
193
+ });
194
+ return { accepted, operationId };
195
+ },
196
+ getSnapshot: () => snapshot,
197
+ async recover() {
198
+ await recover();
199
+ },
200
+ async refresh(scope) {
201
+ const generation = ++baseStateGeneration;
202
+ const loaded = await load(scope);
203
+ if (generation !== baseStateGeneration)
204
+ return;
205
+ baseState = loaded;
206
+ await publish();
207
+ },
208
+ subscribe(listener) {
209
+ listeners.add(listener);
210
+ return () => listeners.delete(listener);
211
+ },
212
+ };
213
+ }
214
+ function createDurableCommandId() {
215
+ return globalThis.crypto?.randomUUID?.() ?? createFallbackDurableCommandId();
216
+ }
217
+ let fallbackDurableCommandSequence = 0;
218
+ function createFallbackDurableCommandId() {
219
+ fallbackDurableCommandSequence += 1;
220
+ return [
221
+ "command",
222
+ Date.now().toString(36),
223
+ fallbackDurableCommandSequence.toString(36),
224
+ Math.random().toString(36).slice(2),
225
+ ].join("-");
226
+ }