@tanstack/db-sqlite-persistence-core 0.2.21 → 0.4.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.
Files changed (58) hide show
  1. package/README.md +258 -7
  2. package/dist/cjs/broadcast-coordinator.cjs +1689 -0
  3. package/dist/cjs/broadcast-coordinator.cjs.map +1 -0
  4. package/dist/cjs/broadcast-coordinator.d.cts +116 -0
  5. package/dist/cjs/errors.cjs +73 -0
  6. package/dist/cjs/errors.cjs.map +1 -1
  7. package/dist/cjs/errors.d.cts +44 -0
  8. package/dist/cjs/index.cjs +19 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/index.d.cts +2 -0
  11. package/dist/cjs/persisted.cjs +1840 -399
  12. package/dist/cjs/persisted.cjs.map +1 -1
  13. package/dist/cjs/persisted.d.cts +177 -11
  14. package/dist/cjs/remote-subset-owner.cjs +20 -0
  15. package/dist/cjs/remote-subset-owner.cjs.map +1 -0
  16. package/dist/cjs/remote-subset-owner.d.cts +7 -0
  17. package/dist/cjs/remote-subset-wire.cjs +681 -0
  18. package/dist/cjs/remote-subset-wire.cjs.map +1 -0
  19. package/dist/cjs/remote-subset-wire.d.cts +59 -0
  20. package/dist/cjs/sqlite-core-adapter.cjs +969 -364
  21. package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
  22. package/dist/cjs/sqlite-core-adapter.d.cts +45 -1
  23. package/dist/cjs/sqlite-value.cjs +31 -0
  24. package/dist/cjs/sqlite-value.cjs.map +1 -0
  25. package/dist/cjs/sqlite-value.d.cts +10 -0
  26. package/dist/esm/broadcast-coordinator.d.ts +116 -0
  27. package/dist/esm/broadcast-coordinator.js +1689 -0
  28. package/dist/esm/broadcast-coordinator.js.map +1 -0
  29. package/dist/esm/errors.d.ts +44 -0
  30. package/dist/esm/errors.js +74 -1
  31. package/dist/esm/errors.js.map +1 -1
  32. package/dist/esm/index.d.ts +2 -0
  33. package/dist/esm/index.js +21 -2
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/persisted.d.ts +177 -11
  36. package/dist/esm/persisted.js +1842 -401
  37. package/dist/esm/persisted.js.map +1 -1
  38. package/dist/esm/remote-subset-owner.d.ts +7 -0
  39. package/dist/esm/remote-subset-owner.js +20 -0
  40. package/dist/esm/remote-subset-owner.js.map +1 -0
  41. package/dist/esm/remote-subset-wire.d.ts +59 -0
  42. package/dist/esm/remote-subset-wire.js +681 -0
  43. package/dist/esm/remote-subset-wire.js.map +1 -0
  44. package/dist/esm/sqlite-core-adapter.d.ts +45 -1
  45. package/dist/esm/sqlite-core-adapter.js +955 -350
  46. package/dist/esm/sqlite-core-adapter.js.map +1 -1
  47. package/dist/esm/sqlite-value.d.ts +10 -0
  48. package/dist/esm/sqlite-value.js +31 -0
  49. package/dist/esm/sqlite-value.js.map +1 -0
  50. package/package.json +12 -2
  51. package/src/broadcast-coordinator.ts +2456 -0
  52. package/src/errors.ts +120 -0
  53. package/src/index.ts +2 -0
  54. package/src/persisted.ts +2690 -444
  55. package/src/remote-subset-owner.ts +36 -0
  56. package/src/remote-subset-wire.ts +946 -0
  57. package/src/sqlite-core-adapter.ts +1297 -410
  58. package/src/sqlite-value.ts +30 -0
@@ -1,19 +1,87 @@
1
- import { safeRandomUUID, withCollectionConfigFactory, SyncTransactionAbortedError, compileSingleRowExpression, toBooleanPredicate } from "@tanstack/db";
2
- import { InvalidPersistedCollectionCoordinatorError, InvalidPersistedCollectionConfigError, InvalidSyncConfigError, InvalidPersistedStorageKeyError, InvalidPersistedStorageKeyEncodingError, InvalidPersistenceAdapterError } from "./errors.js";
1
+ import { safeRandomUUID, withCollectionConfigFactory, SyncTransactionAbortedError, getLoadSubsetDemandKey, compileSingleRowExpression, toBooleanPredicate, SYNC_PERSISTENCE_VERSION, SYNC_PERSISTENCE_PROTOCOL, PERSISTED_READINESS, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError } from "@tanstack/db";
2
+ import { InvalidPersistedCollectionConfigError, DuplicateRemoteSubsetOwnerError, toPersistedCollectionDurabilityError, InvalidPersistedCollectionCoordinatorError, InvalidSyncConfigError, InvalidPersistedStorageKeyError, InvalidPersistedStorageKeyEncodingError, PersistedCollectionDurabilityError, InvalidPersistenceAdapterError } from "./errors.js";
3
+ import { serializeSQLiteBigInt } from "./sqlite-value.js";
4
+ import { toTransportedLoadSubsetOptions, toProcessLocalLoadSubsetOptions } from "./remote-subset-wire.js";
5
+ import { reportRemoteSubsetOwnerError, unloadRemoteSubsetOwner } from "./remote-subset-owner.js";
6
+ const SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY = /* @__PURE__ */ Symbol.for(
7
+ `tanstack-db.sqlite-driver-supports-shared-logical-scheduling`
8
+ );
9
+ function forwardSQLiteDriverSharedLogicalScheduling(source, wrapper) {
10
+ const key = source[SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY];
11
+ if (key) {
12
+ Object.defineProperty(
13
+ wrapper,
14
+ SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
15
+ { value: key }
16
+ );
17
+ }
18
+ return wrapper;
19
+ }
20
+ class PersistedReadinessTracker {
21
+ constructor(networkTimeoutMs) {
22
+ this.networkTimeoutMs = networkTimeoutMs;
23
+ this.snapshot = { status: `loading` };
24
+ this.listeners = /* @__PURE__ */ new Set();
25
+ this.getSnapshot = () => this.snapshot;
26
+ this.getOrStartNetworkDeadline = () => {
27
+ return this.networkDeadlineAt ??= Date.now() + this.networkTimeoutMs;
28
+ };
29
+ this.subscribe = (listener) => {
30
+ this.listeners.add(listener);
31
+ return () => this.listeners.delete(listener);
32
+ };
33
+ }
34
+ set(snapshot) {
35
+ const deadlineRestarted = snapshot.status === `loading` && this.networkDeadlineAt !== void 0;
36
+ if (deadlineRestarted) {
37
+ this.networkDeadlineAt = Date.now() + this.networkTimeoutMs;
38
+ }
39
+ if (this.snapshot.status === snapshot.status && this.snapshot.error === snapshot.error && !deadlineRestarted) {
40
+ return;
41
+ }
42
+ this.snapshot = snapshot;
43
+ for (const listener of this.listeners) listener();
44
+ }
45
+ }
46
+ const DEFAULT_NETWORK_TIMEOUT_MS = 3e3;
47
+ const MAX_TIMER_TIMEOUT_MS = 2147483647;
48
+ function getNetworkTimeoutMs(initialRender) {
49
+ if (initialRender === void 0) return void 0;
50
+ const strategy = initialRender.strategy;
51
+ if (strategy !== `network-first`) {
52
+ throw new InvalidPersistedCollectionConfigError(
53
+ `initialRender.strategy must be "network-first"`
54
+ );
55
+ }
56
+ const timeoutMs = initialRender.networkTimeoutMs ?? DEFAULT_NETWORK_TIMEOUT_MS;
57
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > MAX_TIMER_TIMEOUT_MS) {
58
+ throw new InvalidPersistedCollectionConfigError(
59
+ `initialRender.networkTimeoutMs must be between 0 and ${MAX_TIMER_TIMEOUT_MS}`
60
+ );
61
+ }
62
+ return timeoutMs;
63
+ }
3
64
  const REQUIRED_COORDINATOR_METHODS = [
4
65
  `getNodeId`,
5
66
  `subscribe`,
6
67
  `publish`,
7
68
  `isLeader`,
8
69
  `ensureLeadership`,
9
- `requestEnsurePersistedIndex`
70
+ `requestEnsureRemoteSubset`,
71
+ `requestReleaseRemoteSubset`,
72
+ `registerRemoteSubsetOwner`,
73
+ `requestEnsurePersistedIndex`,
74
+ `requestApplyCommittedTx`
10
75
  ];
11
- const REQUIRED_ADAPTER_METHODS = [`loadSubset`, `applyCommittedTx`, `ensureIndex`];
76
+ const REQUIRED_ADAPTER_METHODS = [`loadSubset`, `loadResumeSnapshot`, `applyCommittedTx`, `ensureIndex`];
12
77
  const TARGETED_INVALIDATION_KEY_LIMIT = 128;
13
78
  const DEFAULT_DB_NAME = `tanstack-db`;
14
79
  const REMOTE_ENSURE_RETRY_DELAY_MS = 50;
15
80
  class SingleProcessCoordinator {
16
81
  constructor(nodeId = safeRandomUUID()) {
82
+ this.collectionAdapters = /* @__PURE__ */ new Map();
83
+ this.remoteSubsetOwners = /* @__PURE__ */ new Map();
84
+ this.remoteSubsetAcquisitions = /* @__PURE__ */ new Map();
17
85
  this.nodeId = nodeId;
18
86
  }
19
87
  getNodeId() {
@@ -30,10 +98,105 @@ class SingleProcessCoordinator {
30
98
  }
31
99
  async ensureLeadership() {
32
100
  }
33
- async requestEnsureRemoteSubset() {
101
+ async requestEnsureRemoteSubset(collectionId, options) {
102
+ const transported = toTransportedLoadSubsetOptions(options);
103
+ const localOptions = toProcessLocalLoadSubsetOptions(options, transported);
104
+ const owner = this.remoteSubsetOwners.get(collectionId);
105
+ if (!owner) {
106
+ throw new InvalidPersistedCollectionConfigError(
107
+ `SingleProcessCoordinator has no remote subset owner configured for collection "${collectionId}"`
108
+ );
109
+ }
110
+ let acquisitions = this.remoteSubsetAcquisitions.get(collectionId);
111
+ if (!acquisitions) {
112
+ acquisitions = /* @__PURE__ */ new Map();
113
+ this.remoteSubsetAcquisitions.set(collectionId, acquisitions);
114
+ }
115
+ const existing = acquisitions.get(options);
116
+ if (existing) {
117
+ await existing.load;
118
+ return;
119
+ }
120
+ let resolveLoad;
121
+ let rejectLoad;
122
+ const load = new Promise((resolve, reject) => {
123
+ resolveLoad = resolve;
124
+ rejectLoad = reject;
125
+ });
126
+ acquisitions.set(options, { owner, options: localOptions, load });
127
+ try {
128
+ const ownerLoad = owner(localOptions);
129
+ void Promise.resolve(ownerLoad).then(resolveLoad, (error) => {
130
+ reportRemoteSubsetOwnerError(owner, error);
131
+ rejectLoad(error);
132
+ });
133
+ } catch (error) {
134
+ reportRemoteSubsetOwnerError(owner, error);
135
+ rejectLoad(error);
136
+ }
137
+ await load;
138
+ }
139
+ async requestReleaseRemoteSubset(collectionId, options) {
140
+ const acquisitions = this.remoteSubsetAcquisitions.get(collectionId);
141
+ const acquisition = acquisitions?.get(options);
142
+ if (!acquisition) return;
143
+ acquisitions.delete(options);
144
+ if (acquisitions.size === 0) {
145
+ this.remoteSubsetAcquisitions.delete(collectionId);
146
+ }
147
+ try {
148
+ await acquisition.load;
149
+ } catch {
150
+ }
151
+ await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options);
152
+ }
153
+ registerRemoteSubsetOwner(collectionId, owner) {
154
+ if (this.remoteSubsetOwners.has(collectionId)) {
155
+ throw new DuplicateRemoteSubsetOwnerError(collectionId);
156
+ }
157
+ this.remoteSubsetOwners.set(collectionId, owner);
158
+ return () => {
159
+ if (this.remoteSubsetOwners.get(collectionId) !== owner) return;
160
+ const acquisitions = this.remoteSubsetAcquisitions.get(collectionId);
161
+ this.remoteSubsetAcquisitions.delete(collectionId);
162
+ this.remoteSubsetOwners.delete(collectionId);
163
+ for (const acquisition of acquisitions?.values() ?? []) {
164
+ void (async () => {
165
+ try {
166
+ await acquisition.load;
167
+ } catch {
168
+ }
169
+ await unloadRemoteSubsetOwner(owner, acquisition.options);
170
+ })().catch(() => void 0);
171
+ }
172
+ };
34
173
  }
35
174
  async requestEnsurePersistedIndex() {
36
175
  }
176
+ setAdapterForCollection(collectionId, adapter) {
177
+ this.collectionAdapters.set(collectionId, adapter);
178
+ }
179
+ async requestApplyCommittedTx(collectionId, tx) {
180
+ const adapter = this.collectionAdapters.get(collectionId);
181
+ if (!adapter) {
182
+ throw new InvalidPersistedCollectionConfigError(
183
+ `SingleProcessCoordinator has no persistence adapter configured for collection "${collectionId}"`
184
+ );
185
+ }
186
+ try {
187
+ await adapter.applyCommittedTx(collectionId, tx);
188
+ } catch (error) {
189
+ throw toPersistedCollectionDurabilityError(collectionId, error);
190
+ }
191
+ return {
192
+ type: `rpc:applyCommittedTx:res`,
193
+ rpcId: safeRandomUUID(),
194
+ ok: true,
195
+ term: tx.term,
196
+ seq: tx.seq,
197
+ latestRowVersion: tx.rowVersion
198
+ };
199
+ }
37
200
  pullSince() {
38
201
  return Promise.resolve({
39
202
  type: `rpc:pullSince:res`,
@@ -63,25 +226,38 @@ function validatePersistenceAdapter(adapter) {
63
226
  }
64
227
  }
65
228
  }
66
- function resolvePersistence(persistence) {
229
+ function resolvePersistence(persistence, collectionId) {
67
230
  validatePersistenceAdapter(persistence.adapter);
68
231
  const coordinator = persistence.coordinator ?? new SingleProcessCoordinator();
232
+ if (coordinator instanceof SingleProcessCoordinator) {
233
+ coordinator.setAdapterForCollection(collectionId, persistence.adapter);
234
+ }
69
235
  validatePersistedCollectionCoordinator(coordinator);
70
236
  return {
71
237
  ...persistence,
72
238
  coordinator
73
239
  };
74
240
  }
75
- function resolvePersistenceForMode(persistence, mode) {
241
+ function resolvePersistenceForMode(persistence, mode, collectionId) {
76
242
  const modeSpecificPersistence = persistence.resolvePersistenceForMode?.(mode);
77
- return resolvePersistence(modeSpecificPersistence ?? persistence);
243
+ return resolvePersistence(
244
+ modeSpecificPersistence ?? persistence,
245
+ collectionId
246
+ );
78
247
  }
79
248
  function resolvePersistenceForCollection(persistence, options) {
80
249
  const collectionSpecificPersistence = persistence.resolvePersistenceForCollection?.(options);
81
250
  if (collectionSpecificPersistence) {
82
- return resolvePersistence(collectionSpecificPersistence);
251
+ return resolvePersistence(
252
+ collectionSpecificPersistence,
253
+ options.collectionId
254
+ );
83
255
  }
84
- return resolvePersistenceForMode(persistence, options.mode);
256
+ return resolvePersistenceForMode(
257
+ persistence,
258
+ options.mode,
259
+ options.collectionId
260
+ );
85
261
  }
86
262
  function hasOwnSyncKey(options) {
87
263
  return Object.prototype.hasOwnProperty.call(options, `sync`);
@@ -98,14 +274,47 @@ function isValidSyncConfig(value) {
98
274
  class ApplyMutex {
99
275
  constructor() {
100
276
  this.queue = Promise.resolve();
277
+ this.pending = 0;
278
+ }
279
+ reserve() {
280
+ const runImmediately = this.pending === 0;
281
+ const predecessor = this.queue;
282
+ this.pending++;
283
+ let releaseReservation;
284
+ const reservation = new Promise((resolve) => {
285
+ releaseReservation = resolve;
286
+ });
287
+ this.queue = runImmediately ? reservation : predecessor.then(() => reservation);
288
+ let used = false;
289
+ return {
290
+ run: (task) => {
291
+ if (used) {
292
+ return Promise.reject(
293
+ new Error(`an apply-mutex reservation can only run once`)
294
+ );
295
+ }
296
+ used = true;
297
+ let taskPromise;
298
+ if (runImmediately) {
299
+ try {
300
+ taskPromise = Promise.resolve(task());
301
+ } catch (error) {
302
+ taskPromise = Promise.reject(error);
303
+ }
304
+ } else {
305
+ taskPromise = predecessor.then(() => task());
306
+ }
307
+ const markComplete = () => {
308
+ this.pending--;
309
+ releaseReservation();
310
+ };
311
+ void taskPromise.then(markComplete, markComplete);
312
+ return taskPromise;
313
+ }
314
+ };
101
315
  }
102
- async run(task) {
103
- const taskPromise = this.queue.then(() => task());
104
- this.queue = taskPromise.then(
105
- () => void 0,
106
- () => void 0
107
- );
108
- return taskPromise;
316
+ run(task) {
317
+ return this.reserve().run(task);
109
318
  }
110
319
  }
111
320
  function toStableSerializable(value) {
@@ -118,7 +327,7 @@ function toStableSerializable(value) {
118
327
  case `boolean`:
119
328
  return value;
120
329
  case `bigint`:
121
- return value.toString();
330
+ return serializeSQLiteBigInt(value);
122
331
  case `function`:
123
332
  case `symbol`:
124
333
  case `undefined`:
@@ -163,15 +372,6 @@ function toStableSerializable(value) {
163
372
  function stableSerialize(value) {
164
373
  return JSON.stringify(toStableSerializable(value) ?? null);
165
374
  }
166
- function normalizeSubsetOptionsForKey(options) {
167
- return {
168
- where: toStableSerializable(options.where),
169
- orderBy: toStableSerializable(options.orderBy),
170
- limit: options.limit,
171
- cursor: toStableSerializable(options.cursor),
172
- offset: options.offset
173
- };
174
- }
175
375
  function normalizeSyncFnResult(result) {
176
376
  if (typeof result === `function`) {
177
377
  return { cleanup: result };
@@ -207,38 +407,52 @@ function toPersistedMutationEnvelope(mutation) {
207
407
  };
208
408
  }
209
409
  class PersistedCollectionRuntime {
210
- constructor(mode, collectionId, persistence, syncMode, dbName) {
410
+ constructor(mode, collectionId, persistence, syncMode, dbName, persistedReadiness) {
211
411
  this.mode = mode;
212
412
  this.collectionId = collectionId;
213
413
  this.persistence = persistence;
214
414
  this.syncMode = syncMode;
215
415
  this.dbName = dbName;
416
+ this.persistedReadiness = persistedReadiness;
216
417
  this.applyMutex = new ApplyMutex();
217
418
  this.activeSubsets = /* @__PURE__ */ new Map();
419
+ this.hydrationSequence = 0;
420
+ this.resetSequence = 0;
218
421
  this.pendingRemoteSubsetEnsures = /* @__PURE__ */ new Map();
219
422
  this.queuedHydrationTransactions = [];
220
423
  this.queuedTxCommitted = [];
221
- this.subscriptionIds = /* @__PURE__ */ new WeakMap();
424
+ this.requestIds = /* @__PURE__ */ new WeakMap();
425
+ this.hydratedDemands = /* @__PURE__ */ new Set();
222
426
  this.collection = null;
223
427
  this.syncControls = {
224
428
  begin: null,
225
429
  write: null,
226
430
  commit: null,
227
431
  truncate: null,
228
- metadata: null
432
+ metadata: null,
433
+ markError: null
229
434
  };
230
- this.started = false;
231
435
  this.startupMetadataPromise = null;
232
436
  this.startPromise = null;
437
+ this.hasAttemptedStartup = false;
438
+ this.resumeBaselinePromise = null;
439
+ this.resumeCertificationPromise = null;
440
+ this.resumeGenerationOwner = /* @__PURE__ */ Symbol(`persisted resume generation owner`);
441
+ this.lifecycleGeneration = 0;
233
442
  this.internalApplyDepth = 0;
443
+ this.sourcePublicationWaitDepth = 0;
234
444
  this.appliedReceiptSequence = 0;
445
+ this.syncErrorReported = false;
235
446
  this.pendingAppliedReceipts = /* @__PURE__ */ new Map();
236
- this.isHydrating = false;
447
+ this.hydratingGeneration = null;
237
448
  this.coordinatorUnsubscribe = null;
449
+ this.remoteSubsetOwnerUnsubscribe = null;
238
450
  this.indexAddedUnsubscribe = null;
239
451
  this.indexRemovedUnsubscribe = null;
240
452
  this.remoteEnsureRetryTimer = null;
241
- this.nextSubscriptionId = 0;
453
+ this.nextRequestId = 0;
454
+ this.startupSettled = false;
455
+ this.sourceTruncateGeneration = 0;
242
456
  this.latestTerm = 0;
243
457
  this.latestSeq = 0;
244
458
  this.latestRowVersion = 0;
@@ -247,12 +461,34 @@ class PersistedCollectionRuntime {
247
461
  this.localRowVersion = 0;
248
462
  }
249
463
  setSyncControls(syncControls) {
464
+ this.advanceLifecycle();
465
+ this.syncErrorReported = false;
466
+ this.reportedSyncError = void 0;
250
467
  const commit = syncControls.commit;
251
468
  this.syncControls = {
252
469
  ...syncControls,
253
470
  commit: commit ? (signal) => this.trackAppliedReceipt(commit(signal)) : null
254
471
  };
255
472
  }
473
+ reportSyncError(error) {
474
+ const markError = this.syncControls.markError;
475
+ if (this.syncErrorReported) return this.reportedSyncError;
476
+ if (!markError) return error;
477
+ this.syncErrorReported = true;
478
+ this.reportedSyncError = error;
479
+ try {
480
+ markError(error);
481
+ } catch {
482
+ }
483
+ return error;
484
+ }
485
+ registerRemoteSubsetOwner(owner) {
486
+ this.remoteSubsetOwnerUnsubscribe?.();
487
+ this.remoteSubsetOwnerUnsubscribe = this.persistence.coordinator.registerRemoteSubsetOwner(
488
+ this.collectionId,
489
+ owner
490
+ );
491
+ }
256
492
  trackAppliedReceipt(receipt) {
257
493
  const sequence = ++this.appliedReceiptSequence;
258
494
  if (receipt === true) {
@@ -277,11 +513,54 @@ class PersistedCollectionRuntime {
277
513
  write: null,
278
514
  commit: null,
279
515
  truncate: null,
280
- metadata: null
516
+ metadata: null,
517
+ markError: null
281
518
  };
282
519
  }
283
520
  isHydratingNow() {
284
- return this.isHydrating;
521
+ return this.hydratingGeneration === this.lifecycleGeneration;
522
+ }
523
+ getActiveHydrationContext() {
524
+ return this.activeHydrationContext;
525
+ }
526
+ getHydrationSequence() {
527
+ return this.hydrationSequence;
528
+ }
529
+ getCurrentTerminalFailure() {
530
+ const failure = this.terminalFailure;
531
+ return failure?.lifecycleGeneration === this.lifecycleGeneration ? failure : void 0;
532
+ }
533
+ getLifecycleGeneration() {
534
+ return this.lifecycleGeneration;
535
+ }
536
+ throwIfLifecycleReplaced(lifecycleGeneration) {
537
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
538
+ throw new SyncTransactionAbortedError();
539
+ }
540
+ }
541
+ throwIfTerminal() {
542
+ const failure = this.getCurrentTerminalFailure();
543
+ if (failure) throw failure.error;
544
+ }
545
+ markTerminalFailure(error, lifecycleGeneration = this.lifecycleGeneration) {
546
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
547
+ return error;
548
+ }
549
+ const existing = this.getCurrentTerminalFailure();
550
+ if (existing) {
551
+ return existing.error;
552
+ }
553
+ this.terminalFailure = { lifecycleGeneration, error };
554
+ this.persistedReadiness?.set({ status: `error`, error });
555
+ this.pendingRemoteSubsetEnsures.clear();
556
+ this.queuedTxCommitted.length = 0;
557
+ if (this.remoteEnsureRetryTimer !== null) {
558
+ clearTimeout(this.remoteEnsureRetryTimer);
559
+ this.remoteEnsureRetryTimer = null;
560
+ }
561
+ this.rejectQueuedHydrationTransactions(error);
562
+ this.reportSyncError(error);
563
+ return error;
285
564
  }
286
565
  isApplyingInternally() {
287
566
  return this.internalApplyDepth > 0;
@@ -299,70 +578,233 @@ class PersistedCollectionRuntime {
299
578
  isLeader: this.persistence.coordinator.isLeader(this.collectionId)
300
579
  };
301
580
  }
581
+ runInHydrationScope(task, adapter = this.persistence.adapter) {
582
+ if (adapter.runInHydrationScope) {
583
+ return adapter.runInHydrationScope(task);
584
+ }
585
+ return Promise.resolve().then(() => task(adapter));
586
+ }
302
587
  async ensureStarted() {
588
+ this.throwIfTerminal();
303
589
  if (this.startPromise) {
304
590
  return this.startPromise;
305
591
  }
306
- this.startPromise = this.startInternal();
592
+ const lifecycleGeneration = this.lifecycleGeneration;
593
+ const isRestart = this.hasAttemptedStartup;
594
+ this.hasAttemptedStartup = true;
595
+ let resolveStartupMetadata;
596
+ let rejectStartupMetadata;
597
+ this.startupMetadataPromise = new Promise((resolve, reject) => {
598
+ resolveStartupMetadata = resolve;
599
+ rejectStartupMetadata = reject;
600
+ });
601
+ void this.startupMetadataPromise.catch(() => void 0);
602
+ this.startPromise = (async () => {
603
+ const loadStartupMetadata = async (adapter) => {
604
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
605
+ resolveStartupMetadata();
606
+ return false;
607
+ }
608
+ try {
609
+ await this.loadStartupMetadataInternal(lifecycleGeneration, adapter);
610
+ return lifecycleGeneration === this.lifecycleGeneration;
611
+ } catch (error) {
612
+ rejectStartupMetadata(error);
613
+ throw error;
614
+ }
615
+ };
616
+ let startup;
617
+ const scheduleStartupAsOneHydrate = this.persistence.adapter.runInHydrationScope !== void 0 && (!isRestart || (this.persistence.adapter.isHydrationScopeScheduled?.() ?? true));
618
+ if (scheduleStartupAsOneHydrate) {
619
+ startup = await this.applyMutex.run(async () => {
620
+ const result = await this.runInHydrationScope(async (adapter) => {
621
+ if (!await loadStartupMetadata(adapter)) return void 0;
622
+ return this.startInternal(
623
+ lifecycleGeneration,
624
+ adapter,
625
+ resolveStartupMetadata
626
+ );
627
+ });
628
+ if (lifecycleGeneration === this.lifecycleGeneration) {
629
+ await this.flushQueuedTxCommittedUnsafe();
630
+ }
631
+ return result;
632
+ });
633
+ } else {
634
+ if (await loadStartupMetadata(this.persistence.adapter)) {
635
+ if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
636
+ startup = await this.applyMutex.run(async () => {
637
+ const result = await this.runInHydrationScope(
638
+ (adapter) => this.startInternal(
639
+ lifecycleGeneration,
640
+ adapter,
641
+ resolveStartupMetadata
642
+ )
643
+ );
644
+ if (lifecycleGeneration === this.lifecycleGeneration) {
645
+ await this.flushQueuedTxCommittedUnsafe();
646
+ }
647
+ return result;
648
+ });
649
+ } else {
650
+ startup = await this.startInternal(
651
+ lifecycleGeneration,
652
+ this.persistence.adapter,
653
+ resolveStartupMetadata
654
+ );
655
+ if (lifecycleGeneration === this.lifecycleGeneration) {
656
+ await this.flushQueuedTxCommittedUnsafe();
657
+ }
658
+ }
659
+ }
660
+ }
661
+ if (startup !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
662
+ await this.requestCoordinatorPersistedIndexes(
663
+ startup.indexBootstrapSnapshot,
664
+ startup.completedLocalIndexSignatures
665
+ );
666
+ if (startup.appliedCursor !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
667
+ await this.waitForAppliedReceiptsAfter(startup.appliedCursor);
668
+ }
669
+ }
670
+ if (lifecycleGeneration === this.lifecycleGeneration) {
671
+ this.startupSettled = true;
672
+ this.persistedReadiness?.set({ status: `ready` });
673
+ }
674
+ resolveStartupMetadata();
675
+ })().catch((error) => {
676
+ rejectStartupMetadata(error);
677
+ throw this.markTerminalFailure(error, lifecycleGeneration);
678
+ });
679
+ void this.startPromise.catch(() => void 0);
307
680
  return this.startPromise;
308
681
  }
682
+ ensureResumeBaselineHydrated() {
683
+ this.throwIfTerminal();
684
+ if (this.resumeBaselinePromise) {
685
+ return this.resumeBaselinePromise;
686
+ }
687
+ const lifecycleGeneration = this.lifecycleGeneration;
688
+ this.resumeBaselinePromise = (async () => {
689
+ await this.ensureStarted();
690
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
691
+ if (this.syncMode !== `on-demand`) return;
692
+ const appliedCursor = await this.applyMutex.run(async () => {
693
+ const result = await this.runInHydrationScope(
694
+ (adapter) => this.hydrateBaseline(lifecycleGeneration, adapter)
695
+ );
696
+ if (lifecycleGeneration === this.lifecycleGeneration) {
697
+ await this.flushQueuedTxCommittedUnsafe();
698
+ }
699
+ return result;
700
+ });
701
+ if (appliedCursor !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
702
+ await this.waitForAppliedReceiptsAfter(appliedCursor);
703
+ }
704
+ })().catch((error) => {
705
+ throw this.markTerminalFailure(error, lifecycleGeneration);
706
+ });
707
+ return this.resumeBaselinePromise;
708
+ }
709
+ ensureResumeBaselineCertified() {
710
+ if (this.resumeCertificationPromise) {
711
+ return this.resumeCertificationPromise;
712
+ }
713
+ const lifecycleGeneration = this.lifecycleGeneration;
714
+ this.resumeCertificationPromise = (async () => {
715
+ await this.ensureStarted();
716
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
717
+ const snapshot = await this.persistence.adapter.loadResumeSnapshot(
718
+ this.collectionId,
719
+ {
720
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
721
+ includeRows: false
722
+ }
723
+ );
724
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
725
+ this.bindResumeSnapshotEvidence(snapshot);
726
+ })();
727
+ return this.resumeCertificationPromise;
728
+ }
729
+ getKeySetEvidence() {
730
+ return this.persistedKeySetEvidence;
731
+ }
732
+ getResumeGenerationOwner() {
733
+ return this.resumeGenerationOwner;
734
+ }
735
+ async hydrateBaseline(lifecycleGeneration, adapter) {
736
+ if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
737
+ const baseline = {};
738
+ this.activeSubsets.set(this.getSubsetKey(baseline), baseline);
739
+ const appliedCursor = this.appliedReceiptSequence;
740
+ await this.hydrateSubsetUnsafe(
741
+ baseline,
742
+ {
743
+ requestRemoteEnsure: false,
744
+ lifecycleGeneration,
745
+ bindKeySetEvidence: true
746
+ },
747
+ adapter
748
+ );
749
+ return lifecycleGeneration === this.lifecycleGeneration ? appliedCursor : void 0;
750
+ }
309
751
  async ensureStartupMetadataLoaded() {
752
+ this.throwIfTerminal();
310
753
  if (this.startupMetadataPromise) {
311
754
  return this.startupMetadataPromise;
312
755
  }
313
- this.startupMetadataPromise = this.loadStartupMetadataInternal();
756
+ void this.ensureStarted();
314
757
  return this.startupMetadataPromise;
315
758
  }
316
- async startInternal() {
317
- if (this.started) {
318
- return;
319
- }
320
- this.started = true;
321
- await this.ensureStartupMetadataLoaded();
759
+ async startInternal(lifecycleGeneration, adapter, onStartupMetadataLoaded) {
760
+ if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
322
761
  const indexBootstrapSnapshot = this.collection?.getIndexMetadata() ?? [];
323
762
  this.attachIndexLifecycleListeners();
324
- await this.bootstrapPersistedIndexes(indexBootstrapSnapshot);
325
- if (this.syncMode !== `on-demand`) {
326
- this.activeSubsets.set(this.getSubsetKey({}), {});
327
- const appliedCursor = this.appliedReceiptSequence;
328
- await this.applyMutex.run(
329
- () => this.hydrateSubsetUnsafe({}, { requestRemoteEnsure: false })
330
- );
331
- await this.waitForAppliedReceiptsAfter(appliedCursor);
332
- }
333
- }
334
- async loadStartupMetadataInternal() {
335
- if (this.persistence.adapter.getStreamPosition) {
336
- const position = await this.persistence.adapter.getStreamPosition(
337
- this.collectionId
338
- );
339
- this.observeStreamPosition(
340
- position.latestTerm,
341
- position.latestSeq,
342
- position.latestRowVersion
343
- );
344
- }
345
- await this.loadCollectionMetadataIntoCollection();
346
- }
347
- async loadCollectionMetadataIntoCollection() {
348
- const collectionMetadata = await this.loadCollectionMetadataSnapshot();
349
- this.replaceCollectionMetadataSnapshot(collectionMetadata);
763
+ const completedLocalIndexSignatures = await this.bootstrapPersistedIndexes(
764
+ indexBootstrapSnapshot,
765
+ adapter
766
+ );
767
+ if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
768
+ onStartupMetadataLoaded();
769
+ const appliedCursor = this.syncMode !== `on-demand` ? await this.hydrateBaseline(lifecycleGeneration, adapter) : void 0;
770
+ return lifecycleGeneration === this.lifecycleGeneration ? {
771
+ appliedCursor,
772
+ indexBootstrapSnapshot,
773
+ completedLocalIndexSignatures
774
+ } : void 0;
775
+ }
776
+ async loadStartupMetadataInternal(lifecycleGeneration, adapter) {
777
+ const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
778
+ includeRows: false
779
+ });
780
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
781
+ this.persistedResumeGeneration = this.getResumeSnapshotGeneration(snapshot);
782
+ this.persistedKeySetEvidence = snapshot.keySet;
783
+ this.observeStreamPosition(
784
+ snapshot.latestTerm,
785
+ snapshot.latestSeq,
786
+ snapshot.latestRowVersion
787
+ );
788
+ const applied = this.replaceCollectionMetadataSnapshot(
789
+ snapshot.collectionMetadata
790
+ );
791
+ if (applied !== true) await applied;
350
792
  }
351
- async loadCollectionMetadataSnapshot() {
352
- if (!this.persistence.adapter.loadCollectionMetadata) {
793
+ async loadCollectionMetadataSnapshot(adapter) {
794
+ if (!adapter.loadCollectionMetadata) {
353
795
  return [];
354
796
  }
355
- return this.persistence.adapter.loadCollectionMetadata(this.collectionId);
797
+ return adapter.loadCollectionMetadata(this.collectionId);
356
798
  }
357
799
  replaceCollectionMetadataSnapshot(collectionMetadata) {
358
800
  if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
359
- return;
801
+ return true;
360
802
  }
361
803
  const nextMetadata = new Map(
362
804
  collectionMetadata.map(({ key, value }) => [key, value])
363
805
  );
364
806
  const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
365
- this.withInternalApply(() => {
807
+ return this.withInternalApply(() => {
366
808
  this.syncControls.begin?.({ immediate: true });
367
809
  currentKeys.forEach((key) => {
368
810
  if (!nextMetadata.has(key)) {
@@ -372,49 +814,182 @@ class PersistedCollectionRuntime {
372
814
  nextMetadata.forEach((value, key) => {
373
815
  this.syncControls.metadata?.collection.set(key, value);
374
816
  });
375
- this.syncControls.commit?.();
817
+ return this.syncControls.commit?.() ?? true;
376
818
  });
377
819
  }
378
820
  async loadSubset(options, upstreamLoadSubset) {
379
- this.activeSubsets.set(this.getSubsetKey(options), options);
821
+ this.throwIfTerminal();
822
+ const lifecycleGeneration = this.lifecycleGeneration;
823
+ const subsetKey = this.getSubsetKey(options);
824
+ const truncateGeneration = this.sourceTruncateGeneration;
825
+ const routeRemoteDemandDuringHydration = this.canRouteRemoteDemandThroughCoordinator();
826
+ this.activeSubsets.set(subsetKey, options);
380
827
  const appliedCursor = this.appliedReceiptSequence;
381
- await this.applyMutex.run(
382
- () => this.hydrateSubsetUnsafe(options, {
383
- requestRemoteEnsure: this.mode === `sync-present`
384
- })
385
- );
386
- await this.waitForAppliedReceiptsAfter(appliedCursor);
387
- if (upstreamLoadSubset) {
828
+ try {
829
+ await this.applyMutex.run(async () => {
830
+ try {
831
+ await this.runInHydrationScope(
832
+ (adapter) => this.hydrateSubsetUnsafe(
833
+ options,
834
+ {
835
+ requestRemoteEnsure: this.mode === `sync-present` && !routeRemoteDemandDuringHydration,
836
+ lifecycleGeneration,
837
+ requestLocalLoadFailure: true,
838
+ rejectBufferedReplayFailure: true
839
+ },
840
+ adapter
841
+ )
842
+ );
843
+ } finally {
844
+ if (lifecycleGeneration === this.lifecycleGeneration && !this.getCurrentTerminalFailure()) {
845
+ await this.flushQueuedTxCommittedUnsafe();
846
+ }
847
+ }
848
+ });
849
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
850
+ await this.waitForAppliedReceiptsAfter(appliedCursor);
851
+ } catch (error) {
852
+ if (this.activeSubsets.get(subsetKey) === options) {
853
+ this.activeSubsets.delete(subsetKey);
854
+ }
855
+ throw error;
856
+ }
857
+ if (options.signal?.aborted || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
858
+ return;
859
+ }
860
+ if (this.canRouteRemoteDemandThroughCoordinator()) {
388
861
  try {
389
- await upstreamLoadSubset(options);
862
+ await this.persistence.coordinator.requestEnsureRemoteSubset(
863
+ this.collectionId,
864
+ options
865
+ );
390
866
  } catch (error) {
391
867
  if (options.signal?.aborted || typeof error === `object` && error !== null && `name` in error && error.name === `AbortError`) {
392
868
  this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options));
393
869
  throw error;
394
870
  }
395
- console.warn(`Failed to trigger remote subset load:`, error);
396
871
  this.queueRemoteSubsetEnsure(options);
872
+ throw error;
873
+ }
874
+ return;
875
+ }
876
+ if (truncateGeneration === this.sourceTruncateGeneration && this.activeSubsets.get(subsetKey) === options) {
877
+ this.hydratedDemands.add(getLoadSubsetDemandKey(options));
878
+ }
879
+ if (upstreamLoadSubset) {
880
+ try {
881
+ await upstreamLoadSubset(options);
882
+ } catch (error) {
883
+ this.noteUpstreamLoadFailure(options, error);
884
+ throw error;
397
885
  }
398
886
  }
399
887
  }
888
+ loadHydratedSubset(options, upstreamLoadSubset) {
889
+ const failure = this.getCurrentTerminalFailure();
890
+ if (failure) return Promise.reject(failure.error);
891
+ if (!this.startupSettled || this.isHydratingNow() || this.canRouteRemoteDemandThroughCoordinator() || options.refetch === true || !this.hydratedDemands.has(getLoadSubsetDemandKey(options))) {
892
+ return;
893
+ }
894
+ this.activeSubsets.set(this.getSubsetKey(options), options);
895
+ let result;
896
+ try {
897
+ result = upstreamLoadSubset?.(options) ?? true;
898
+ } catch (error) {
899
+ this.noteUpstreamLoadFailure(options, error);
900
+ return Promise.reject(error);
901
+ }
902
+ if (result === true) {
903
+ return true;
904
+ }
905
+ return result.then(
906
+ () => {
907
+ this.queueRemoteSubsetEnsure(options);
908
+ },
909
+ (error) => {
910
+ this.noteUpstreamLoadFailure(options, error);
911
+ throw error;
912
+ }
913
+ );
914
+ }
915
+ noteUpstreamLoadFailure(options, error) {
916
+ if (options.signal?.aborted || typeof error === `object` && error !== null && `name` in error && error.name === `AbortError`) {
917
+ this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options));
918
+ return;
919
+ }
920
+ console.warn(`Failed to trigger remote subset load:`, error);
921
+ this.queueRemoteSubsetEnsure(options);
922
+ }
923
+ noteSourceTruncate() {
924
+ this.sourceTruncateGeneration++;
925
+ this.hydratedDemands.clear();
926
+ }
400
927
  unloadSubset(options, upstreamUnloadSubset) {
401
- this.activeSubsets.delete(this.getSubsetKey(options));
402
- this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options));
403
- upstreamUnloadSubset?.(options);
928
+ const subsetKey = this.getSubsetKey(options);
929
+ this.activeSubsets.delete(subsetKey);
930
+ this.pendingRemoteSubsetEnsures.delete(subsetKey);
931
+ const demandKey = getLoadSubsetDemandKey(options);
932
+ const stillActive = Array.from(this.activeSubsets.values()).some(
933
+ (active) => getLoadSubsetDemandKey(active) === demandKey
934
+ );
935
+ if (!stillActive) this.hydratedDemands.delete(demandKey);
936
+ if (this.mode === `sync-present`) {
937
+ void this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
938
+ this.reportSyncError(error);
939
+ });
940
+ }
941
+ if (upstreamUnloadSubset) {
942
+ try {
943
+ const result = upstreamUnloadSubset(options);
944
+ void Promise.resolve(result).catch((error) => {
945
+ this.reportSyncError(error);
946
+ });
947
+ } catch (error) {
948
+ this.reportSyncError(error);
949
+ }
950
+ }
404
951
  }
405
952
  async forceReloadSubset(options) {
406
- this.activeSubsets.set(this.getSubsetKey(options), options);
407
- await this.applyMutex.run(
408
- () => this.hydrateSubsetUnsafe(options, { requestRemoteEnsure: false })
409
- );
953
+ this.throwIfTerminal();
954
+ const lifecycleGeneration = this.lifecycleGeneration;
955
+ await this.applyMutex.run(async () => {
956
+ await this.runInHydrationScope(
957
+ (adapter) => this.hydrateSubsetUnsafe(
958
+ options,
959
+ {
960
+ requestRemoteEnsure: false,
961
+ lifecycleGeneration,
962
+ rejectBufferedReplayFailure: true
963
+ },
964
+ adapter
965
+ )
966
+ );
967
+ if (lifecycleGeneration === this.lifecycleGeneration) {
968
+ await this.flushQueuedTxCommittedUnsafe();
969
+ }
970
+ });
410
971
  }
411
972
  queueHydrationBufferedTransaction(transaction) {
973
+ const failure = this.getCurrentTerminalFailure();
974
+ if (failure) {
975
+ transaction.rejectApplied?.(failure.error);
976
+ return;
977
+ }
412
978
  this.queuedHydrationTransactions.push(transaction);
413
979
  }
414
- async persistAndBroadcastExternalSyncTransaction(transaction) {
415
- await this.applyMutex.run(
416
- () => this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction)
417
- );
980
+ applyHydrationBufferedTransaction(transaction) {
981
+ const failure = this.getCurrentTerminalFailure();
982
+ if (failure) return Promise.reject(failure.error);
983
+ if (transaction.beginOptions?.immediate && this.sourcePublicationWaitDepth > 0) {
984
+ return Promise.reject(
985
+ new InvalidPersistedCollectionConfigError(
986
+ `immediate persisted source replay cannot enter while an earlier source publication is waiting`
987
+ )
988
+ );
989
+ }
990
+ return this.applyMutex.run(async () => {
991
+ await this.applyBufferedSyncTransactionUnsafe(transaction);
992
+ });
418
993
  }
419
994
  normalizeSyncWriteMessage(message) {
420
995
  if (!this.collection) {
@@ -426,10 +1001,6 @@ class PersistedCollectionRuntime {
426
1001
  const key2 = message.key;
427
1002
  const previousValue = this.collection.get(key2) ?? {};
428
1003
  return {
429
- forwardMessage: {
430
- type: `delete`,
431
- key: key2
432
- },
433
1004
  operation: {
434
1005
  type: `delete`,
435
1006
  key: key2,
@@ -441,10 +1012,6 @@ class PersistedCollectionRuntime {
441
1012
  const key2 = this.collection.getKeyFromItem(message.value);
442
1013
  const previousValue = this.collection.get(key2) ?? message.value;
443
1014
  return {
444
- forwardMessage: {
445
- type: `delete`,
446
- key: key2
447
- },
448
1015
  operation: {
449
1016
  type: `delete`,
450
1017
  key: key2,
@@ -454,11 +1021,6 @@ class PersistedCollectionRuntime {
454
1021
  }
455
1022
  const key = this.collection.getKeyFromItem(message.value);
456
1023
  return {
457
- forwardMessage: {
458
- type: `update`,
459
- value: message.value,
460
- metadata: message.metadata
461
- },
462
1024
  operation: {
463
1025
  type: `update`,
464
1026
  key,
@@ -467,25 +1029,45 @@ class PersistedCollectionRuntime {
467
1029
  }
468
1030
  };
469
1031
  }
470
- async persistAndConfirmCollectionMutations(mutations) {
1032
+ async persistAndConfirmCollectionMutations(mutations, lifecycleGeneration = this.lifecycleGeneration) {
1033
+ this.throwIfTerminal();
1034
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
471
1035
  if (mutations.length === 0) {
472
1036
  return;
473
1037
  }
474
- await this.applyMutex.run(async () => {
475
- const acceptedMutationIds = await this.persistCollectionMutationsUnsafe(mutations);
476
- const acceptedMutationIdSet = new Set(acceptedMutationIds);
477
- const acceptedMutations = mutations.filter(
478
- (mutation) => acceptedMutationIdSet.has(mutation.mutationId)
479
- );
480
- if (acceptedMutations.length !== mutations.length) {
481
- throw new Error(
482
- `persistence coordinator accepted ${acceptedMutations.length} of ${mutations.length} mutations; partial acceptance is not supported`
1038
+ try {
1039
+ await this.applyMutex.run(async () => {
1040
+ await this.ensureStartupMetadataLoaded();
1041
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1042
+ const acceptedMutationIds = await this.persistCollectionMutationsUnsafe(
1043
+ mutations,
1044
+ lifecycleGeneration
1045
+ );
1046
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1047
+ const acceptedMutationIdSet = new Set(acceptedMutationIds);
1048
+ const acceptedMutations = mutations.filter(
1049
+ (mutation) => acceptedMutationIdSet.has(mutation.mutationId)
483
1050
  );
1051
+ if (acceptedMutations.length !== mutations.length) {
1052
+ throw new Error(
1053
+ `persistence coordinator accepted ${acceptedMutations.length} of ${mutations.length} mutations; partial acceptance is not supported`
1054
+ );
1055
+ }
1056
+ try {
1057
+ await this.confirmMutationsSyncUnsafe(acceptedMutations);
1058
+ } catch (error) {
1059
+ throw this.markTerminalFailure(error, lifecycleGeneration);
1060
+ }
1061
+ });
1062
+ } catch (error) {
1063
+ if (error instanceof PersistedCollectionDurabilityError) {
1064
+ throw this.markTerminalFailure(error, lifecycleGeneration);
484
1065
  }
485
- this.confirmMutationsSyncUnsafe(acceptedMutations);
486
- });
1066
+ throw error;
1067
+ }
487
1068
  }
488
1069
  async acceptTransactionMutations(transaction) {
1070
+ this.throwIfTerminal();
489
1071
  const collectionMutations = this.filterMutationsForCollection(
490
1072
  transaction.mutations
491
1073
  );
@@ -495,11 +1077,34 @@ class PersistedCollectionRuntime {
495
1077
  await this.persistAndConfirmCollectionMutations(collectionMutations);
496
1078
  }
497
1079
  cleanup() {
498
- this.coordinatorUnsubscribe?.();
1080
+ this.advanceLifecycle();
1081
+ const remoteReleases = [];
1082
+ const failures = [];
1083
+ const attempt = (callback) => {
1084
+ try {
1085
+ callback();
1086
+ } catch (error) {
1087
+ failures.push(error);
1088
+ }
1089
+ };
1090
+ if (this.mode === `sync-present`) {
1091
+ for (const options of this.activeSubsets.values()) {
1092
+ attempt(() => {
1093
+ remoteReleases.push(
1094
+ this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
1095
+ this.reportSyncError(error);
1096
+ })
1097
+ );
1098
+ });
1099
+ }
1100
+ }
1101
+ attempt(() => this.coordinatorUnsubscribe?.());
499
1102
  this.coordinatorUnsubscribe = null;
500
- this.indexAddedUnsubscribe?.();
1103
+ attempt(() => this.remoteSubsetOwnerUnsubscribe?.());
1104
+ this.remoteSubsetOwnerUnsubscribe = null;
1105
+ attempt(() => this.indexAddedUnsubscribe?.());
501
1106
  this.indexAddedUnsubscribe = null;
502
- this.indexRemovedUnsubscribe?.();
1107
+ attempt(() => this.indexRemovedUnsubscribe?.());
503
1108
  this.indexRemovedUnsubscribe = null;
504
1109
  if (this.remoteEnsureRetryTimer !== null) {
505
1110
  clearTimeout(this.remoteEnsureRetryTimer);
@@ -507,12 +1112,36 @@ class PersistedCollectionRuntime {
507
1112
  }
508
1113
  this.pendingRemoteSubsetEnsures.clear();
509
1114
  this.activeSubsets.clear();
1115
+ this.hydratedDemands.clear();
510
1116
  for (const transaction of this.queuedHydrationTransactions) {
511
1117
  transaction.rejectApplied?.(new SyncTransactionAbortedError());
512
1118
  }
513
1119
  this.queuedHydrationTransactions.length = 0;
514
1120
  this.queuedTxCommitted.length = 0;
515
1121
  this.clearSyncControls();
1122
+ this.collection = null;
1123
+ return Promise.all(remoteReleases).then(() => {
1124
+ if (failures.length === 1) throw failures[0];
1125
+ if (failures.length > 1) {
1126
+ throw new AggregateError(failures, `Persistence cleanup failed`, {
1127
+ cause: failures[0]
1128
+ });
1129
+ }
1130
+ });
1131
+ }
1132
+ advanceLifecycle() {
1133
+ this.lifecycleGeneration++;
1134
+ this.persistedReadiness?.set({ status: `loading` });
1135
+ this.startupSettled = false;
1136
+ this.hydratedDemands.clear();
1137
+ this.startupMetadataPromise = null;
1138
+ this.startPromise = null;
1139
+ this.resumeBaselinePromise = null;
1140
+ this.terminalFailure = void 0;
1141
+ this.resumeCertificationPromise = null;
1142
+ this.persistedKeySetEvidence = void 0;
1143
+ this.persistedResumeGeneration = void 0;
1144
+ this.resumeGenerationOwner = /* @__PURE__ */ Symbol(`persisted resume generation owner`);
516
1145
  }
517
1146
  withInternalApply(task) {
518
1147
  this.internalApplyDepth++;
@@ -528,8 +1157,8 @@ class PersistedCollectionRuntime {
528
1157
  }
529
1158
  return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
530
1159
  }
531
- loadSubsetRowsUnsafe(options) {
532
- return this.persistence.adapter.loadSubset(this.collectionId, options, {
1160
+ loadSubsetRowsUnsafe(options, adapter) {
1161
+ return adapter.loadSubset(this.collectionId, options, {
533
1162
  requiredIndexSignatures: this.getRequiredIndexSignatures()
534
1163
  });
535
1164
  }
@@ -543,27 +1172,184 @@ class PersistedCollectionRuntime {
543
1172
  );
544
1173
  }
545
1174
  async scanPersistedRows(options) {
1175
+ this.throwIfTerminal();
546
1176
  return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options));
547
1177
  }
548
- async hydrateSubsetUnsafe(options, config) {
549
- this.isHydrating = true;
1178
+ async hydrateSubsetUnsafe(options, config, adapter) {
1179
+ let rowsLoaded = false;
1180
+ let replayFailure;
1181
+ const resetSequence = this.resetSequence;
1182
+ this.hydrationSequence++;
1183
+ const hydrationContext = { suppliedRowKeys: /* @__PURE__ */ new Set() };
1184
+ this.activeHydrationContext = hydrationContext;
550
1185
  try {
551
- const rows = await this.loadSubsetRowsUnsafe(options);
552
- this.applyRowsToCollection(rows);
1186
+ this.throwIfTerminal();
1187
+ this.hydratingGeneration = config.lifecycleGeneration;
1188
+ try {
1189
+ let rows;
1190
+ if (config.bindKeySetEvidence) {
1191
+ const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
1192
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1193
+ includeRows: true
1194
+ });
1195
+ rows = snapshot.rows;
1196
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1197
+ if (resetSequence !== this.resetSequence) return;
1198
+ this.bindResumeSnapshotEvidence(snapshot);
1199
+ } else {
1200
+ rows = await this.loadSubsetRowsUnsafe(options, adapter);
1201
+ }
1202
+ rowsLoaded = true;
1203
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1204
+ if (resetSequence !== this.resetSequence) return;
1205
+ if (!config.bindKeySetEvidence || this.persistedKeySetEvidence?.status !== `incompatible`) {
1206
+ for (const row of rows) {
1207
+ hydrationContext.suppliedRowKeys.add(row.key);
1208
+ }
1209
+ const applied = this.applyRowsToCollection(rows);
1210
+ if (applied !== true) await applied;
1211
+ }
1212
+ } finally {
1213
+ if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === config.lifecycleGeneration) {
1214
+ this.hydratingGeneration = null;
1215
+ }
1216
+ }
1217
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1218
+ replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1219
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1220
+ if (config.requestRemoteEnsure && !replayFailure) {
1221
+ this.queueRemoteSubsetEnsure(options);
1222
+ }
1223
+ } catch (error) {
1224
+ if (config.requestLocalLoadFailure && !rowsLoaded) {
1225
+ this.hydratingGeneration = config.lifecycleGeneration;
1226
+ try {
1227
+ await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
1228
+ config.lifecycleGeneration,
1229
+ adapter
1230
+ );
1231
+ } catch (baselineError) {
1232
+ const terminalError = this.markTerminalFailure(
1233
+ baselineError,
1234
+ config.lifecycleGeneration
1235
+ );
1236
+ throw terminalError;
1237
+ } finally {
1238
+ if (this.hydratingGeneration === config.lifecycleGeneration) {
1239
+ this.hydratingGeneration = null;
1240
+ }
1241
+ }
1242
+ throw error;
1243
+ }
1244
+ throw this.markTerminalFailure(error, config.lifecycleGeneration);
553
1245
  } finally {
554
- this.isHydrating = false;
1246
+ if (this.activeHydrationContext === hydrationContext) {
1247
+ this.activeHydrationContext = void 0;
1248
+ }
1249
+ }
1250
+ if (replayFailure && config.rejectBufferedReplayFailure) {
1251
+ throw replayFailure.reason;
555
1252
  }
556
- await this.flushQueuedHydrationTransactionsUnsafe();
557
- await this.flushQueuedTxCommittedUnsafe();
558
- if (config.requestRemoteEnsure) {
559
- this.queueRemoteSubsetEnsure(options);
1253
+ }
1254
+ async recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(lifecycleGeneration, adapter) {
1255
+ let snapshotRows;
1256
+ const recoveredPresence = /* @__PURE__ */ new Map();
1257
+ let snapshotInvalidatedByTruncate = false;
1258
+ while (this.queuedHydrationTransactions.length > 0) {
1259
+ const transaction = this.queuedHydrationTransactions.shift();
1260
+ if (!transaction) continue;
1261
+ try {
1262
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1263
+ const baselineRows = /* @__PURE__ */ new Map();
1264
+ const transactionPresence = /* @__PURE__ */ new Map();
1265
+ const transactionStartsWithTruncate = transaction.truncate;
1266
+ const getPresence = (key) => {
1267
+ const known = transactionPresence.get(key);
1268
+ if (known !== void 0) return known;
1269
+ if (transactionStartsWithTruncate) {
1270
+ transactionPresence.set(key, `absent`);
1271
+ return `absent`;
1272
+ }
1273
+ const recovered = recoveredPresence.get(key);
1274
+ if (recovered !== void 0) {
1275
+ transactionPresence.set(key, recovered);
1276
+ return recovered;
1277
+ }
1278
+ const present = this.collection?._hasHydratedKey(key) === true ? `present` : snapshotInvalidatedByTruncate ? `absent` : `unknown`;
1279
+ transactionPresence.set(key, present);
1280
+ return present;
1281
+ };
1282
+ if (!transaction.signal?.aborted) {
1283
+ for (const [index, operation] of transaction.operations.entries()) {
1284
+ const key = operation.key;
1285
+ if (operation.type === `update` && transaction.partialUpdateOperationIndexes.has(index) && getPresence(key) === `unknown`) {
1286
+ if (snapshotRows === void 0) {
1287
+ const snapshot = await adapter.loadResumeSnapshot(
1288
+ this.collectionId,
1289
+ {
1290
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1291
+ includeRows: true
1292
+ }
1293
+ );
1294
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1295
+ if (transaction.signal?.aborted) break;
1296
+ snapshotRows = new Map(
1297
+ snapshot.rows.map((row) => [row.key, row])
1298
+ );
1299
+ }
1300
+ const baseline = snapshotRows.get(key);
1301
+ if (baseline === void 0) {
1302
+ transactionPresence.set(key, `absent`);
1303
+ } else {
1304
+ baselineRows.set(key, baseline);
1305
+ transactionPresence.set(key, `present`);
1306
+ }
1307
+ }
1308
+ transactionPresence.set(
1309
+ key,
1310
+ operation.type === `delete` ? `absent` : `present`
1311
+ );
1312
+ }
1313
+ if (baselineRows.size > 0) {
1314
+ if (!transaction.signal?.aborted) {
1315
+ transaction.prependHydrationRows([...baselineRows.values()]);
1316
+ }
1317
+ }
1318
+ }
1319
+ const transactionOutcome = await this.applyBufferedSyncTransactionUnsafe(transaction, adapter);
1320
+ if (transactionOutcome.applied) {
1321
+ if (transaction.truncate) {
1322
+ snapshotInvalidatedByTruncate = true;
1323
+ recoveredPresence.clear();
1324
+ }
1325
+ for (const operation of transaction.operations) {
1326
+ recoveredPresence.set(
1327
+ operation.key,
1328
+ operation.type === `delete` ? `absent` : `present`
1329
+ );
1330
+ }
1331
+ }
1332
+ } catch (error) {
1333
+ transaction.rejectApplied?.(error);
1334
+ for (const abandoned of this.queuedHydrationTransactions) {
1335
+ abandoned.rejectApplied?.(error);
1336
+ }
1337
+ this.queuedHydrationTransactions.length = 0;
1338
+ throw error;
1339
+ }
1340
+ }
1341
+ }
1342
+ rejectQueuedHydrationTransactions(error) {
1343
+ for (const transaction of this.queuedHydrationTransactions) {
1344
+ transaction.rejectApplied?.(error);
560
1345
  }
1346
+ this.queuedHydrationTransactions.length = 0;
561
1347
  }
562
1348
  applyRowsToCollection(rows) {
563
1349
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
564
- return;
1350
+ return true;
565
1351
  }
566
- this.withInternalApply(() => {
1352
+ return this.withInternalApply(() => {
567
1353
  this.syncControls.begin?.({ immediate: true });
568
1354
  for (const row of rows) {
569
1355
  if (this.collection?._hasHydratedKey(row.key)) {
@@ -575,18 +1361,41 @@ class PersistedCollectionRuntime {
575
1361
  metadata: row.metadata
576
1362
  });
577
1363
  }
578
- this.syncControls.commit?.();
1364
+ return this.syncControls.commit?.() ?? true;
579
1365
  });
580
1366
  }
1367
+ getResumeSnapshotGeneration(snapshot) {
1368
+ return {
1369
+ latestTerm: snapshot.latestTerm,
1370
+ latestSeq: snapshot.latestSeq,
1371
+ latestRowVersion: snapshot.latestRowVersion,
1372
+ resetEpoch: snapshot.resetEpoch
1373
+ };
1374
+ }
1375
+ isExpectedResumeGeneration(generation) {
1376
+ const expected = this.persistedResumeGeneration;
1377
+ return expected !== void 0 && expected.latestTerm === generation.latestTerm && expected.latestSeq === generation.latestSeq && expected.latestRowVersion === generation.latestRowVersion && expected.resetEpoch === generation.resetEpoch;
1378
+ }
1379
+ bindResumeSnapshotEvidence(snapshot) {
1380
+ const generation = this.getResumeSnapshotGeneration(snapshot);
1381
+ const previousEvidenceStatus = this.persistedKeySetEvidence?.status;
1382
+ const mayAcceptUnownedGeneration = this.mode === `sync-present` && previousEvidenceStatus !== `consistent` && previousEvidenceStatus !== `incompatible` || this.mode === `sync-absent` && previousEvidenceStatus === `consistent` && snapshot.keySet?.status === `consistent`;
1383
+ this.observeStreamPosition(
1384
+ snapshot.latestTerm,
1385
+ snapshot.latestSeq,
1386
+ snapshot.latestRowVersion
1387
+ );
1388
+ this.persistedKeySetEvidence = this.isExpectedResumeGeneration(generation) || mayAcceptUnownedGeneration ? snapshot.keySet : { status: `incompatible` };
1389
+ }
581
1390
  replaceCollectionSnapshot(rows, collectionMetadata) {
582
1391
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
583
- return;
1392
+ return true;
584
1393
  }
585
1394
  const nextMetadata = new Map(
586
1395
  collectionMetadata.map(({ key, value }) => [key, value])
587
1396
  );
588
1397
  const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
589
- this.withInternalApply(() => {
1398
+ return this.withInternalApply(() => {
590
1399
  this.syncControls.begin?.({ immediate: true });
591
1400
  this.syncControls.truncate?.();
592
1401
  for (const row of rows) {
@@ -604,17 +1413,22 @@ class PersistedCollectionRuntime {
604
1413
  nextMetadata.forEach((value, key) => {
605
1414
  this.syncControls.metadata?.collection.set(key, value);
606
1415
  });
607
- this.syncControls.commit?.();
1416
+ return this.syncControls.commit?.() ?? true;
608
1417
  });
609
1418
  }
610
- async flushQueuedHydrationTransactionsUnsafe() {
1419
+ async flushQueuedHydrationTransactionsUnsafe(adapter) {
1420
+ let operationFailure;
611
1421
  while (this.queuedHydrationTransactions.length > 0) {
612
1422
  const transaction = this.queuedHydrationTransactions.shift();
613
1423
  if (!transaction) {
614
1424
  continue;
615
1425
  }
616
1426
  try {
617
- await this.applyBufferedSyncTransactionUnsafe(transaction);
1427
+ const outcome = await this.applyBufferedSyncTransactionUnsafe(
1428
+ transaction,
1429
+ adapter
1430
+ );
1431
+ operationFailure ??= outcome.hydrationFailure;
618
1432
  } catch (error) {
619
1433
  transaction.rejectApplied?.(error);
620
1434
  for (const abandoned of this.queuedHydrationTransactions) {
@@ -624,103 +1438,126 @@ class PersistedCollectionRuntime {
624
1438
  throw error;
625
1439
  }
626
1440
  }
1441
+ return operationFailure;
627
1442
  }
628
- async applyBufferedSyncTransactionUnsafe(transaction) {
629
- if (transaction.signal?.aborted) {
630
- transaction.rejectApplied?.(new SyncTransactionAbortedError());
631
- return;
632
- }
633
- const { begin, write, commit, truncate, metadata } = this.syncControls;
634
- if (!begin || !write || !commit) {
635
- transaction.rejectApplied?.(new SyncTransactionAbortedError());
636
- return;
637
- }
638
- const applyToCollection = () => {
639
- begin();
640
- if (transaction.truncate) {
641
- truncate?.();
642
- }
643
- for (const operation of transaction.operations) {
644
- if (operation.type === `delete`) {
645
- write({
646
- type: `delete`,
647
- key: operation.key
648
- });
649
- } else {
650
- write({
651
- type: `update`,
652
- value: operation.value,
653
- metadata: operation.metadata
654
- });
655
- }
656
- }
657
- for (const [key, metadataWrite] of transaction.rowMetadataWrites) {
658
- if (metadataWrite.type === `delete`) {
659
- metadata?.row.delete(key);
660
- } else {
661
- metadata?.row.set(key, metadataWrite.value);
662
- }
663
- }
664
- for (const [key, metadataWrite] of transaction.collectionMetadataWrites) {
665
- if (metadataWrite.type === `delete`) {
666
- metadata?.collection.delete(key);
667
- } else {
668
- metadata?.collection.set(key, metadataWrite.value);
669
- }
670
- }
671
- return commit(transaction.signal);
672
- };
1443
+ async applyBufferedSyncTransactionUnsafe(transaction, scopedAdapter) {
1444
+ this.throwIfTerminal();
1445
+ this.throwIfLifecycleReplaced(transaction.lifecycleGeneration);
1446
+ const abortedBeforeApplication = transaction.signal?.aborted === true;
1447
+ let abortedDuringApplication = false;
1448
+ let applicationReturned = false;
673
1449
  try {
674
- const applied = transaction.internal ? this.withInternalApply(applyToCollection) : applyToCollection();
1450
+ const applied = transaction.internal ? this.withInternalApply(transaction.applyToCollection) : transaction.applyToCollection();
1451
+ applicationReturned = true;
1452
+ abortedDuringApplication = transaction.signal?.aborted === true;
675
1453
  if (applied !== true) {
676
- await applied;
1454
+ this.sourcePublicationWaitDepth++;
1455
+ try {
1456
+ await applied;
1457
+ } finally {
1458
+ this.sourcePublicationWaitDepth--;
1459
+ }
677
1460
  }
1461
+ this.throwIfLifecycleReplaced(transaction.lifecycleGeneration);
678
1462
  if (!transaction.internal) {
679
- await this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction);
1463
+ await this.persistAndBroadcastExternalSyncTransactionUnsafe(
1464
+ transaction,
1465
+ scopedAdapter
1466
+ );
680
1467
  }
681
1468
  transaction.resolveApplied?.();
1469
+ return { applied: true };
682
1470
  } catch (error) {
683
- transaction.rejectApplied?.(error);
684
- throw error;
1471
+ if (!applicationReturned) {
1472
+ abortedDuringApplication = transaction.signal?.aborted === true;
1473
+ }
1474
+ const aborted = transaction.signal?.aborted || error instanceof SyncTransactionAbortedError;
1475
+ const shouldFailStop = aborted && transaction.shouldFailStopOnAbort?.() === true;
1476
+ const terminalError = this.classifyBufferedTransactionFailure(
1477
+ transaction,
1478
+ error
1479
+ );
1480
+ transaction.rejectApplied?.(terminalError);
1481
+ if (aborted && !shouldFailStop && transaction.rejectApplied) {
1482
+ return {
1483
+ applied: false,
1484
+ hydrationFailure: !abortedBeforeApplication && abortedDuringApplication ? { reason: terminalError } : void 0
1485
+ };
1486
+ }
1487
+ throw terminalError;
1488
+ }
1489
+ }
1490
+ classifyBufferedTransactionFailure(transaction, error) {
1491
+ if (error instanceof PersistedCollectionDurabilityError) {
1492
+ return this.markTerminalFailure(error, transaction.lifecycleGeneration);
1493
+ }
1494
+ const aborted = transaction.signal?.aborted || error instanceof SyncTransactionAbortedError;
1495
+ if (aborted && transaction.shouldFailStopOnAbort?.() !== true) {
1496
+ return error;
685
1497
  }
1498
+ return this.markTerminalFailure(error, transaction.lifecycleGeneration);
686
1499
  }
687
- async persistAndBroadcastExternalSyncTransactionUnsafe(transaction) {
1500
+ async persistAndBroadcastExternalSyncTransactionUnsafe(transaction, scopedAdapter) {
688
1501
  if (transaction.internal) {
689
1502
  return;
690
1503
  }
691
- const streamPosition = this.nextLocalStreamPosition();
1504
+ this.throwIfLifecycleReplaced(transaction.lifecycleGeneration);
692
1505
  if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
693
- this.publishTxCommittedEvent(
694
- this.createTxCommittedPayload({
695
- term: streamPosition.term,
696
- seq: streamPosition.seq,
697
- txId: safeRandomUUID(),
698
- latestRowVersion: streamPosition.rowVersion,
699
- changedRows: [],
700
- deletedKeys: [],
701
- requiresFullReload: true
702
- })
703
- );
704
1506
  return;
705
1507
  }
1508
+ const streamPosition = this.nextLocalStreamPosition();
706
1509
  const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
707
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
708
- this.publishTxCommittedEvent(
709
- this.createTxCommittedPayload({
710
- term: tx.term,
711
- seq: tx.seq,
712
- txId: tx.txId,
713
- latestRowVersion: tx.rowVersion,
714
- requiresFullReload: transaction.truncate,
715
- changedRows: transaction.operations.filter((operation) => operation.type === `update`).map((operation) => ({
716
- key: operation.key,
717
- value: operation.value
718
- })),
719
- deletedKeys: transaction.operations.filter((operation) => operation.type === `delete`).map((operation) => operation.key),
720
- rowMetadataMutations: tx.rowMetadataMutations,
721
- collectionMetadataMutations: tx.collectionMetadataMutations
722
- })
1510
+ const response = await this.persistence.coordinator.requestApplyCommittedTx(
1511
+ this.collectionId,
1512
+ tx,
1513
+ // A hydration-scoped adapter is already inside the shared scheduler.
1514
+ // Ordinary source commits pass no scoped adapter so the coordinator
1515
+ // enters that scheduler before taking the database-wide writer lock.
1516
+ scopedAdapter
1517
+ );
1518
+ if (!response.ok) {
1519
+ if (response.code === `PERSISTENCE_ERROR`) {
1520
+ const error = new PersistedCollectionDurabilityError(
1521
+ `Failed to durably persist collection "${this.collectionId}": ${response.error}`,
1522
+ {
1523
+ cause: response,
1524
+ code: response.sourceCode ?? response.code,
1525
+ path: response.path
1526
+ }
1527
+ );
1528
+ throw error;
1529
+ }
1530
+ throw new Error(
1531
+ `failed to apply external sync transaction through coordinator: ${response.error}`
1532
+ );
1533
+ }
1534
+ if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return;
1535
+ this.observeStreamPosition(
1536
+ response.term,
1537
+ response.seq,
1538
+ response.latestRowVersion
723
1539
  );
1540
+ if (transaction.expectedResumeGenerationOwner === this.resumeGenerationOwner && this.persistedResumeGeneration !== void 0) {
1541
+ this.persistedResumeGeneration = {
1542
+ ...this.persistedResumeGeneration,
1543
+ latestTerm: response.term,
1544
+ latestSeq: response.seq,
1545
+ latestRowVersion: response.latestRowVersion
1546
+ };
1547
+ }
1548
+ }
1549
+ async applyCommittedTx(tx, lifecycleGeneration) {
1550
+ this.throwIfTerminal();
1551
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1552
+ try {
1553
+ await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
1554
+ } catch (error) {
1555
+ throw this.markTerminalFailure(
1556
+ toPersistedCollectionDurabilityError(this.collectionId, error),
1557
+ lifecycleGeneration
1558
+ );
1559
+ }
1560
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
724
1561
  }
725
1562
  createPersistedTxFromOperations(transaction, streamPosition) {
726
1563
  return {
@@ -785,11 +1622,11 @@ class PersistedCollectionRuntime {
785
1622
  })
786
1623
  };
787
1624
  }
788
- confirmMutationsSyncUnsafe(mutations) {
1625
+ async confirmMutationsSyncUnsafe(mutations) {
789
1626
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
790
1627
  return;
791
1628
  }
792
- this.withInternalApply(() => {
1629
+ const applied = this.withInternalApply(() => {
793
1630
  this.syncControls.begin?.({ immediate: true });
794
1631
  for (const mutation of mutations) {
795
1632
  if (mutation.type === `delete`) {
@@ -804,8 +1641,11 @@ class PersistedCollectionRuntime {
804
1641
  });
805
1642
  }
806
1643
  }
807
- this.syncControls.commit?.();
1644
+ return this.syncControls.commit?.() ?? true;
808
1645
  });
1646
+ if (applied !== true) {
1647
+ await applied;
1648
+ }
809
1649
  }
810
1650
  filterMutationsForCollection(mutations) {
811
1651
  const collection = this.collection;
@@ -816,7 +1656,8 @@ class PersistedCollectionRuntime {
816
1656
  return mutation.collection.id === this.collectionId;
817
1657
  });
818
1658
  }
819
- async persistCollectionMutationsUnsafe(mutations) {
1659
+ async persistCollectionMutationsUnsafe(mutations, lifecycleGeneration) {
1660
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
820
1661
  if (this.persistence.coordinator.requestApplyLocalMutations) {
821
1662
  const envelopeMutations = mutations.map(
822
1663
  (mutation) => toPersistedMutationEnvelope(
@@ -827,7 +1668,19 @@ class PersistedCollectionRuntime {
827
1668
  this.collectionId,
828
1669
  envelopeMutations
829
1670
  );
1671
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
830
1672
  if (!response.ok) {
1673
+ if (response.code === `PERSISTENCE_ERROR`) {
1674
+ const error = new PersistedCollectionDurabilityError(
1675
+ `Failed to durably persist collection "${this.collectionId}": ${response.error}`,
1676
+ {
1677
+ cause: response,
1678
+ code: response.sourceCode ?? response.code,
1679
+ path: response.path
1680
+ }
1681
+ );
1682
+ throw error;
1683
+ }
831
1684
  throw new Error(
832
1685
  `failed to apply local mutations through coordinator: ${response.error}`
833
1686
  );
@@ -855,7 +1708,8 @@ class PersistedCollectionRuntime {
855
1708
  }
856
1709
  const streamPosition = this.nextLocalStreamPosition();
857
1710
  const tx = this.createPersistedTxFromMutations(mutations, streamPosition);
858
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
1711
+ await this.applyCommittedTx(tx, lifecycleGeneration);
1712
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
859
1713
  this.publishTxCommittedEvent(
860
1714
  this.createTxCommittedPayload({
861
1715
  term: tx.term,
@@ -945,28 +1799,30 @@ class PersistedCollectionRuntime {
945
1799
  };
946
1800
  }
947
1801
  getSubsetKey(options) {
948
- const subscription = options.subscription;
949
- if (subscription && typeof subscription === `object`) {
950
- const existingId = this.subscriptionIds.get(subscription);
951
- if (existingId) {
952
- return existingId;
953
- }
954
- this.nextSubscriptionId++;
955
- const id = `sub:${this.nextSubscriptionId}`;
956
- this.subscriptionIds.set(subscription, id);
957
- return id;
1802
+ let id = this.requestIds.get(options);
1803
+ if (id === void 0) {
1804
+ id = `request:${++this.nextRequestId}`;
1805
+ this.requestIds.set(options, id);
1806
+ }
1807
+ return id;
1808
+ }
1809
+ canRouteRemoteDemandThroughCoordinator() {
1810
+ if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1811
+ return false;
958
1812
  }
959
- return `opts:${stableSerialize(normalizeSubsetOptionsForKey(options))}`;
1813
+ return !this.persistence.coordinator.isLeader(this.collectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
960
1814
  }
961
1815
  queueRemoteSubsetEnsure(options) {
962
- if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
1816
+ if (options.signal?.aborted || !this.canRouteRemoteDemandThroughCoordinator() || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
963
1817
  return;
964
1818
  }
965
- this.pendingRemoteSubsetEnsures.set(this.getSubsetKey(options), options);
1819
+ const subsetKey = this.getSubsetKey(options);
1820
+ if (this.activeSubsets.get(subsetKey) !== options) return;
1821
+ this.pendingRemoteSubsetEnsures.set(subsetKey, options);
966
1822
  void this.flushPendingRemoteSubsetEnsures();
967
1823
  }
968
1824
  scheduleRemoteEnsureRetry() {
969
- if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
1825
+ if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
970
1826
  return;
971
1827
  }
972
1828
  if (this.pendingRemoteSubsetEnsures.size === 0 || this.remoteEnsureRetryTimer !== null) {
@@ -978,7 +1834,7 @@ class PersistedCollectionRuntime {
978
1834
  }, REMOTE_ENSURE_RETRY_DELAY_MS);
979
1835
  }
980
1836
  async flushPendingRemoteSubsetEnsures() {
981
- if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
1837
+ if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
982
1838
  return;
983
1839
  }
984
1840
  if (this.remoteEnsureRetryTimer !== null) {
@@ -986,6 +1842,10 @@ class PersistedCollectionRuntime {
986
1842
  this.remoteEnsureRetryTimer = null;
987
1843
  }
988
1844
  for (const [subsetKey, options] of this.pendingRemoteSubsetEnsures) {
1845
+ if (options.signal?.aborted || this.activeSubsets.get(subsetKey) !== options) {
1846
+ this.pendingRemoteSubsetEnsures.delete(subsetKey);
1847
+ continue;
1848
+ }
989
1849
  try {
990
1850
  await this.persistence.coordinator.requestEnsureRemoteSubset(
991
1851
  this.collectionId,
@@ -993,7 +1853,11 @@ class PersistedCollectionRuntime {
993
1853
  );
994
1854
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
995
1855
  } catch (error) {
996
- console.warn(`Failed to ensure remote subset:`, error);
1856
+ if (options.signal?.aborted || this.activeSubsets.get(subsetKey) !== options) {
1857
+ this.pendingRemoteSubsetEnsures.delete(subsetKey);
1858
+ } else {
1859
+ console.warn(`Failed to ensure remote subset:`, error);
1860
+ }
997
1861
  }
998
1862
  }
999
1863
  this.scheduleRemoteEnsureRetry();
@@ -1002,6 +1866,10 @@ class PersistedCollectionRuntime {
1002
1866
  if (this.coordinatorUnsubscribe) {
1003
1867
  return;
1004
1868
  }
1869
+ this.persistence.coordinator.setAdapterForCollection?.(
1870
+ this.collectionId,
1871
+ this.persistence.adapter
1872
+ );
1005
1873
  this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
1006
1874
  this.collectionId,
1007
1875
  (message) => {
@@ -1013,15 +1881,22 @@ class PersistedCollectionRuntime {
1013
1881
  if (message.collectionId !== this.collectionId) {
1014
1882
  return;
1015
1883
  }
1884
+ if (this.getCurrentTerminalFailure()) {
1885
+ return;
1886
+ }
1016
1887
  const { payload } = message;
1888
+ const lifecycleGeneration = this.lifecycleGeneration;
1017
1889
  const isSelf = message.senderId === this.persistence.coordinator.getNodeId();
1018
1890
  if (isTxCommittedPayload(payload)) {
1019
- if (this.isHydrating) {
1020
- this.queuedTxCommitted.push(payload);
1891
+ if (this.isHydratingNow()) {
1892
+ this.queuedTxCommitted.push({
1893
+ txCommitted: payload,
1894
+ lifecycleGeneration
1895
+ });
1021
1896
  return;
1022
1897
  }
1023
- void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload)).catch((error) => {
1024
- console.warn(`Failed to process tx:committed message:`, error);
1898
+ void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload, lifecycleGeneration)).catch((error) => {
1899
+ this.markTerminalFailure(error, lifecycleGeneration);
1025
1900
  });
1026
1901
  return;
1027
1902
  }
@@ -1029,8 +1904,15 @@ class PersistedCollectionRuntime {
1029
1904
  return;
1030
1905
  }
1031
1906
  if (isCollectionResetPayload(payload)) {
1032
- void this.applyMutex.run(() => this.truncateAndReloadUnsafe()).catch((error) => {
1033
- console.warn(`Failed to process collection reset message:`, error);
1907
+ void this.applyMutex.run(async () => {
1908
+ await this.runInHydrationScope(
1909
+ (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
1910
+ );
1911
+ if (lifecycleGeneration === this.lifecycleGeneration) {
1912
+ await this.flushQueuedTxCommittedUnsafe();
1913
+ }
1914
+ }).catch((error) => {
1915
+ this.markTerminalFailure(error, lifecycleGeneration);
1034
1916
  });
1035
1917
  }
1036
1918
  }
@@ -1040,10 +1922,14 @@ class PersistedCollectionRuntime {
1040
1922
  if (!queued) {
1041
1923
  continue;
1042
1924
  }
1043
- await this.processCommittedTxUnsafe(queued);
1925
+ await this.processCommittedTxUnsafe(
1926
+ queued.txCommitted,
1927
+ queued.lifecycleGeneration
1928
+ );
1044
1929
  }
1045
1930
  }
1046
- async processCommittedTxUnsafe(txCommitted) {
1931
+ async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
1932
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1047
1933
  if (txCommitted.term < this.latestTerm) {
1048
1934
  return;
1049
1935
  }
@@ -1054,7 +1940,8 @@ class PersistedCollectionRuntime {
1054
1940
  const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
1055
1941
  const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
1056
1942
  if (hasGap) {
1057
- await this.recoverFromSeqGapUnsafe();
1943
+ await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
1944
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1058
1945
  if (txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
1059
1946
  return;
1060
1947
  }
@@ -1064,70 +1951,109 @@ class PersistedCollectionRuntime {
1064
1951
  txCommitted.seq,
1065
1952
  txCommitted.latestRowVersion
1066
1953
  );
1067
- await this.invalidateFromCommittedTxUnsafe(txCommitted);
1954
+ await this.invalidateFromCommittedTxUnsafe(
1955
+ txCommitted,
1956
+ this.persistence.adapter
1957
+ );
1958
+ if (lifecycleGeneration === this.lifecycleGeneration) {
1959
+ await this.flushQueuedTxCommittedUnsafe();
1960
+ }
1068
1961
  }
1069
- async recoverFromSeqGapUnsafe() {
1962
+ async recoverFromSeqGapUnsafe(lifecycleGeneration) {
1963
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1070
1964
  if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
1965
+ let pullResponse;
1071
1966
  try {
1072
- const pullResponse = await this.persistence.coordinator.pullSince(
1967
+ pullResponse = await this.persistence.coordinator.pullSince(
1073
1968
  this.collectionId,
1074
1969
  this.latestRowVersion
1075
1970
  );
1971
+ } catch (error) {
1972
+ console.warn(`Failed pullSince recovery attempt:`, error);
1973
+ }
1974
+ if (pullResponse) {
1975
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1076
1976
  if (pullResponse.ok) {
1077
1977
  this.observeStreamPosition(
1078
1978
  pullResponse.latestTerm,
1079
1979
  pullResponse.latestSeq,
1080
1980
  pullResponse.latestRowVersion
1081
1981
  );
1082
- if (pullResponse.requiresFullReload || !pullResponse.deltas) {
1083
- await this.reloadActiveSubsetsUnsafe();
1982
+ if (pullResponse.requiresFullReload) {
1983
+ await this.runInHydrationScope(
1984
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1985
+ );
1084
1986
  return;
1085
1987
  }
1086
- for (const delta of pullResponse.deltas) {
1087
- await this.invalidateFromCommittedTxUnsafe({
1088
- type: `tx:committed`,
1089
- term: pullResponse.latestTerm,
1090
- seq: pullResponse.latestSeq,
1091
- txId: delta.txId,
1092
- latestRowVersion: delta.latestRowVersion,
1093
- requiresFullReload: false,
1094
- changedRows: delta.changedRows,
1095
- deletedKeys: delta.deletedKeys,
1096
- rowMetadataMutations: delta.rowMetadataMutations,
1097
- collectionMetadataMutations: delta.collectionMetadataMutations
1098
- });
1988
+ const deltas = pullResponse.deltas;
1989
+ if (!deltas) {
1990
+ await this.runInHydrationScope(
1991
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1992
+ );
1993
+ return;
1099
1994
  }
1995
+ await this.runInHydrationScope(async (adapter) => {
1996
+ for (const delta of deltas) {
1997
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1998
+ await this.invalidateFromCommittedTxUnsafe(
1999
+ {
2000
+ type: `tx:committed`,
2001
+ term: pullResponse.latestTerm,
2002
+ seq: pullResponse.latestSeq,
2003
+ txId: delta.txId,
2004
+ latestRowVersion: delta.latestRowVersion,
2005
+ requiresFullReload: false,
2006
+ changedRows: delta.changedRows,
2007
+ deletedKeys: delta.deletedKeys,
2008
+ rowMetadataMutations: delta.rowMetadataMutations,
2009
+ collectionMetadataMutations: delta.collectionMetadataMutations
2010
+ },
2011
+ adapter
2012
+ );
2013
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2014
+ }
2015
+ });
1100
2016
  return;
1101
2017
  }
1102
- } catch (error) {
1103
- console.warn(`Failed pullSince recovery attempt:`, error);
1104
2018
  }
1105
2019
  }
1106
- await this.truncateAndReloadUnsafe();
2020
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2021
+ await this.runInHydrationScope(
2022
+ (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
2023
+ );
1107
2024
  if (this.mode === `sync-present`) {
1108
2025
  for (const options of this.activeSubsets.values()) {
1109
2026
  this.queueRemoteSubsetEnsure(options);
1110
2027
  }
1111
2028
  }
1112
2029
  }
1113
- async truncateAndReloadUnsafe() {
2030
+ async truncateAndReloadUnsafe(adapter, lifecycleGeneration = this.lifecycleGeneration) {
2031
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2032
+ this.hydratedDemands.clear();
2033
+ this.resetSequence++;
1114
2034
  if (this.syncControls.begin && this.syncControls.commit) {
1115
- this.withInternalApply(() => {
2035
+ const applied = this.withInternalApply(() => {
1116
2036
  this.syncControls.begin?.({ immediate: true });
1117
2037
  this.syncControls.truncate?.();
1118
- this.syncControls.commit?.();
2038
+ return this.syncControls.commit?.() ?? true;
1119
2039
  });
2040
+ if (applied !== true) await applied;
1120
2041
  }
1121
- await this.reloadActiveSubsetsUnsafe();
2042
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2043
+ await this.reloadActiveSubsetsUnsafe(adapter);
1122
2044
  }
1123
- async invalidateFromCommittedTxUnsafe(txCommitted) {
2045
+ async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
2046
+ const reloadActiveSubsets = () => this.runInHydrationScope(
2047
+ (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
2048
+ adapter
2049
+ );
1124
2050
  if (txCommitted.requiresFullReload) {
1125
- await this.reloadActiveSubsetsUnsafe();
2051
+ await reloadActiveSubsets();
1126
2052
  return;
1127
2053
  }
1128
2054
  const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
1129
2055
  if (changedKeyCount > TARGETED_INVALIDATION_KEY_LIMIT) {
1130
- await this.reloadActiveSubsetsUnsafe();
2056
+ await reloadActiveSubsets();
1131
2057
  return;
1132
2058
  }
1133
2059
  const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
@@ -1137,13 +2063,13 @@ class PersistedCollectionRuntime {
1137
2063
  await this.applyTargetedInvalidationUnsafe(txCommitted);
1138
2064
  return;
1139
2065
  }
1140
- await this.reloadActiveSubsetsUnsafe();
2066
+ await reloadActiveSubsets();
1141
2067
  }
1142
2068
  async applyTargetedInvalidationUnsafe(txCommitted) {
1143
2069
  const subsetEvaluators = Array.from(this.activeSubsets.values()).map(
1144
2070
  (opt) => opt.where ? compileSingleRowExpression(opt.where) : null
1145
2071
  );
1146
- this.withInternalApply(() => {
2072
+ const applied = this.withInternalApply(() => {
1147
2073
  this.syncControls.begin?.({ immediate: true });
1148
2074
  for (const {
1149
2075
  key: changedKey,
@@ -1182,17 +2108,26 @@ class PersistedCollectionRuntime {
1182
2108
  );
1183
2109
  }
1184
2110
  });
1185
- this.syncControls.commit?.();
2111
+ return this.syncControls.commit?.() ?? true;
1186
2112
  });
2113
+ if (applied !== true) await applied;
1187
2114
  }
1188
- async reloadActiveSubsetsUnsafe() {
2115
+ async reloadActiveSubsetsUnsafe(adapter) {
2116
+ const lifecycleGeneration = this.lifecycleGeneration;
2117
+ const truncateGeneration = this.sourceTruncateGeneration;
1189
2118
  const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
1190
- this.isHydrating = true;
2119
+ this.hydratedDemands.clear();
2120
+ this.hydrationSequence++;
2121
+ const hydrationContext = { suppliedRowKeys: /* @__PURE__ */ new Set() };
2122
+ this.activeHydrationContext = hydrationContext;
2123
+ this.hydratingGeneration = lifecycleGeneration;
1191
2124
  try {
1192
2125
  const mergedRows = /* @__PURE__ */ new Map();
1193
- const collectionMetadata = await this.loadCollectionMetadataSnapshot();
2126
+ const collectionMetadata = await this.loadCollectionMetadataSnapshot(adapter);
2127
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1194
2128
  for (const options of activeSubsetOptions) {
1195
- const subsetRows = await this.loadSubsetRowsUnsafe(options);
2129
+ const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter);
2130
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1196
2131
  for (const row of subsetRows) {
1197
2132
  mergedRows.set(row.key, {
1198
2133
  value: row.value,
@@ -1200,7 +2135,10 @@ class PersistedCollectionRuntime {
1200
2135
  });
1201
2136
  }
1202
2137
  }
1203
- this.replaceCollectionSnapshot(
2138
+ for (const key of mergedRows.keys()) {
2139
+ hydrationContext.suppliedRowKeys.add(key);
2140
+ }
2141
+ const applied = this.replaceCollectionSnapshot(
1204
2142
  Array.from(mergedRows.entries()).map(([key, row]) => ({
1205
2143
  key,
1206
2144
  value: row.value,
@@ -1208,11 +2146,25 @@ class PersistedCollectionRuntime {
1208
2146
  })),
1209
2147
  collectionMetadata
1210
2148
  );
2149
+ if (applied !== true) await applied;
1211
2150
  } finally {
1212
- this.isHydrating = false;
2151
+ if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === lifecycleGeneration) {
2152
+ this.hydratingGeneration = null;
2153
+ }
2154
+ if (this.activeHydrationContext === hydrationContext) {
2155
+ this.activeHydrationContext = void 0;
2156
+ }
2157
+ }
2158
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2159
+ await this.flushQueuedHydrationTransactionsUnsafe(adapter);
2160
+ if (truncateGeneration === this.sourceTruncateGeneration && this.queuedTxCommitted.length === 0) {
2161
+ for (const options of activeSubsetOptions) {
2162
+ if (this.activeSubsets.get(this.getSubsetKey(options)) === options) {
2163
+ this.hydratedDemands.add(getLoadSubsetDemandKey(options));
2164
+ }
2165
+ }
2166
+ }
1213
2167
  }
1214
- await this.flushQueuedHydrationTransactionsUnsafe();
1215
- await this.flushQueuedTxCommittedUnsafe();
1216
2168
  }
1217
2169
  attachIndexLifecycleListeners() {
1218
2170
  if (!this.collection || this.indexAddedUnsubscribe || this.indexRemovedUnsubscribe) {
@@ -1228,14 +2180,26 @@ class PersistedCollectionRuntime {
1228
2180
  }
1229
2181
  );
1230
2182
  }
1231
- async bootstrapPersistedIndexes(indexMetadataSnapshot) {
2183
+ async bootstrapPersistedIndexes(indexMetadataSnapshot, adapter = this.persistence.adapter) {
1232
2184
  const collection = this.collection;
1233
2185
  if (!collection && !indexMetadataSnapshot) {
1234
- return;
2186
+ return /* @__PURE__ */ new Set();
1235
2187
  }
1236
2188
  const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
2189
+ const completedLocalIndexSignatures = /* @__PURE__ */ new Set();
1237
2190
  for (const metadata of indexMetadata) {
1238
- await this.ensurePersistedIndex(metadata);
2191
+ if (await this.ensureLocalPersistedIndex(metadata, adapter)) {
2192
+ completedLocalIndexSignatures.add(metadata.signature);
2193
+ }
2194
+ }
2195
+ return completedLocalIndexSignatures;
2196
+ }
2197
+ async requestCoordinatorPersistedIndexes(indexMetadata, completedLocalIndexSignatures) {
2198
+ for (const metadata of indexMetadata) {
2199
+ await this.requestCoordinatorPersistedIndex(
2200
+ metadata,
2201
+ completedLocalIndexSignatures.has(metadata.signature)
2202
+ );
1239
2203
  }
1240
2204
  }
1241
2205
  buildPersistedIndexSpec(index) {
@@ -1248,22 +2212,36 @@ class PersistedCollectionRuntime {
1248
2212
  }
1249
2213
  };
1250
2214
  }
1251
- async ensurePersistedIndex(indexMetadata) {
2215
+ async ensurePersistedIndex(indexMetadata, adapter = this.persistence.adapter) {
2216
+ const completedLocally = await this.ensureLocalPersistedIndex(
2217
+ indexMetadata,
2218
+ adapter
2219
+ );
2220
+ await this.requestCoordinatorPersistedIndex(indexMetadata, completedLocally);
2221
+ }
2222
+ async ensureLocalPersistedIndex(indexMetadata, adapter) {
1252
2223
  const spec = this.buildPersistedIndexSpec(indexMetadata);
1253
2224
  try {
1254
- await this.persistence.adapter.ensureIndex(
2225
+ await adapter.ensureIndex(
1255
2226
  this.collectionId,
1256
2227
  indexMetadata.signature,
1257
2228
  spec
1258
2229
  );
2230
+ return true;
1259
2231
  } catch (error) {
1260
2232
  console.warn(`Failed to ensure persisted index in adapter:`, error);
2233
+ return false;
1261
2234
  }
2235
+ }
2236
+ async requestCoordinatorPersistedIndex(indexMetadata, completedLocally) {
2237
+ const spec = this.buildPersistedIndexSpec(indexMetadata);
1262
2238
  try {
1263
2239
  await this.persistence.coordinator.requestEnsurePersistedIndex(
1264
2240
  this.collectionId,
1265
2241
  indexMetadata.signature,
1266
- spec
2242
+ spec,
2243
+ completedLocally ? this.persistence.adapter : void 0,
2244
+ completedLocally
1267
2245
  );
1268
2246
  } catch (error) {
1269
2247
  console.warn(
@@ -1286,65 +2264,327 @@ class PersistedCollectionRuntime {
1286
2264
  }
1287
2265
  }
1288
2266
  }
1289
- function createWrappedSyncConfig(sourceSyncConfig, runtime) {
2267
+ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness) {
1290
2268
  return {
1291
2269
  ...sourceSyncConfig,
1292
2270
  sync: (params) => {
1293
2271
  const transactionStack = [];
2272
+ const pendingPublicationTransactions = [];
1294
2273
  const getOpenTransaction = () => transactionStack[transactionStack.length - 1];
2274
+ const settlePublicationAdmissionWaiters = (transaction, error) => {
2275
+ const waiters = transaction.publicationAdmissionWaiters;
2276
+ transaction.publicationAdmissionWaiters = void 0;
2277
+ if (!waiters) return;
2278
+ for (const waiter of waiters) {
2279
+ if (error === void 0) waiter.resolve();
2280
+ else waiter.reject(error);
2281
+ }
2282
+ };
2283
+ const bindToCurrentHydration = (transaction) => {
2284
+ if (transaction.internal || !runtime.isHydratingNow()) return;
2285
+ const hydrationSequence = runtime.getHydrationSequence();
2286
+ if (transaction.hydrationSequence !== void 0 && transaction.hydrationSequence !== hydrationSequence) {
2287
+ throw new InvalidPersistedCollectionConfigError(
2288
+ `a persisted sync transaction cannot cross a hydration cycle`
2289
+ );
2290
+ }
2291
+ transaction.queuedBecauseHydrating = true;
2292
+ transaction.hydrationSequence = hydrationSequence;
2293
+ transaction.hydrationContext = runtime.getActiveHydrationContext();
2294
+ };
2295
+ const assertHydrationSequenceCurrent = (transaction) => {
2296
+ if (transaction.hydrationSequence !== void 0 && transaction.hydrationSequence !== runtime.getHydrationSequence()) {
2297
+ throw new InvalidPersistedCollectionConfigError(
2298
+ `a persisted sync transaction cannot cross a hydration cycle`
2299
+ );
2300
+ }
2301
+ };
2302
+ const markPendingMetadataDependency = (transaction) => {
2303
+ const openTransaction = getOpenTransaction();
2304
+ if (openTransaction && !openTransaction.internal) {
2305
+ transaction.hasDependentSuccessor = true;
2306
+ }
2307
+ };
2308
+ const removePendingPublicationTransaction = (transaction) => {
2309
+ const index = pendingPublicationTransactions.indexOf(transaction);
2310
+ if (index !== -1) pendingPublicationTransactions.splice(index, 1);
2311
+ };
2312
+ const settlePendingTransaction = (transaction) => {
2313
+ removePendingPublicationTransaction(transaction);
2314
+ };
2315
+ const settleRuntimeTransaction = (transaction, applied) => {
2316
+ void applied.then(
2317
+ () => settlePendingTransaction(transaction),
2318
+ () => {
2319
+ settlePendingTransaction(transaction);
2320
+ }
2321
+ );
2322
+ };
2323
+ const getPendingRowMetadataWrite = (key) => {
2324
+ for (let index = pendingPublicationTransactions.length - 1; index >= 0; index--) {
2325
+ const transaction = pendingPublicationTransactions[index];
2326
+ const write = transaction.rowMetadataWrites.get(key);
2327
+ if (write) {
2328
+ markPendingMetadataDependency(transaction);
2329
+ return { found: true, write };
2330
+ }
2331
+ if (transaction.truncate) {
2332
+ markPendingMetadataDependency(transaction);
2333
+ return {
2334
+ found: true,
2335
+ write: { type: `delete` }
2336
+ };
2337
+ }
2338
+ }
2339
+ return { found: false };
2340
+ };
2341
+ const getPendingCollectionMetadataWrite = (key) => {
2342
+ for (let index = pendingPublicationTransactions.length - 1; index >= 0; index--) {
2343
+ const transaction = pendingPublicationTransactions[index];
2344
+ const write = transaction.collectionMetadataWrites.get(key);
2345
+ if (write) {
2346
+ markPendingMetadataDependency(transaction);
2347
+ return { found: true, write };
2348
+ }
2349
+ }
2350
+ return { found: false };
2351
+ };
1295
2352
  let fullStartPromise = null;
2353
+ let sourceResultPromise = null;
2354
+ let resolveSourceResultAssigned;
2355
+ const sourceResultAssigned = new Promise((resolve) => {
2356
+ resolveSourceResultAssigned = resolve;
2357
+ });
2358
+ const startupState = { cleanedUp: false };
2359
+ const isCleanedUp = () => startupState.cleanedUp;
1296
2360
  const acquisitions = /* @__PURE__ */ new Map();
2361
+ const waitForPublicationAdmission = (transaction, options, acquisition) => {
2362
+ if (options.signal?.aborted) {
2363
+ return Promise.reject(
2364
+ options.signal.reason ?? new SyncTransactionAbortedError()
2365
+ );
2366
+ }
2367
+ return new Promise((resolve, reject) => {
2368
+ const waiters = transaction.publicationAdmissionWaiters ?? (transaction.publicationAdmissionWaiters = /* @__PURE__ */ new Set());
2369
+ let settled = false;
2370
+ const settle = (action) => {
2371
+ if (settled) return;
2372
+ settled = true;
2373
+ waiters.delete(waiter);
2374
+ options.signal?.removeEventListener(`abort`, abort);
2375
+ acquisition.cancelAdmissionWait = void 0;
2376
+ action();
2377
+ };
2378
+ const waiter = {
2379
+ resolve: () => settle(resolve),
2380
+ reject: (error) => settle(() => reject(error))
2381
+ };
2382
+ const abort = () => waiter.reject(
2383
+ options.signal?.reason ?? new SyncTransactionAbortedError()
2384
+ );
2385
+ waiters.add(waiter);
2386
+ options.signal?.addEventListener(`abort`, abort, { once: true });
2387
+ acquisition.cancelAdmissionWait = waiter.resolve;
2388
+ });
2389
+ };
2390
+ const getTerminalFailure = () => runtime.getCurrentTerminalFailure();
2391
+ const reportUpstreamError = (error) => {
2392
+ if (persistedReadiness?.getSnapshot().status !== `loading`) {
2393
+ runtime.reportSyncError(error);
2394
+ return;
2395
+ }
2396
+ void (fullStartPromise ?? runtime.ensureStarted()).then(
2397
+ () => {
2398
+ if (!isCleanedUp() && !getTerminalFailure()) {
2399
+ runtime.reportSyncError(error);
2400
+ }
2401
+ },
2402
+ () => void 0
2403
+ );
2404
+ };
2405
+ const createHandledRejection = (error) => {
2406
+ const rejected = Promise.reject(error);
2407
+ void rejected.catch(() => void 0);
2408
+ return rejected;
2409
+ };
2410
+ const applyTransactionToCollection = (transaction, signal) => {
2411
+ if (transaction.applicationReceipt !== void 0) {
2412
+ return transaction.applicationReceipt;
2413
+ }
2414
+ try {
2415
+ assertHydrationSequenceCurrent(transaction);
2416
+ for (const key of transaction.deferredHydrationMetadataDeleteKeys) {
2417
+ if (!transaction.hydrationContext?.suppliedRowKeys.has(key) && !transaction.rowMetadataWrites.has(key)) {
2418
+ transaction.rowMetadataWrites.set(key, { type: `delete` });
2419
+ }
2420
+ }
2421
+ transaction.deferredHydrationMetadataDeleteKeys.clear();
2422
+ params.begin(
2423
+ transaction.queuedBecauseHydrating ? { immediate: true } : transaction.beginOptions
2424
+ );
2425
+ if (transaction.truncate) params.truncate();
2426
+ for (const operation of transaction.operations) {
2427
+ if (operation.type === `delete`) {
2428
+ params.write({ type: `delete`, key: operation.key });
2429
+ } else {
2430
+ params.write({
2431
+ type: `update`,
2432
+ value: operation.value,
2433
+ metadata: operation.metadata
2434
+ });
2435
+ }
2436
+ }
2437
+ if (params.metadata) {
2438
+ for (const [key, write] of transaction.rowMetadataWrites) {
2439
+ if (write.type === `delete`) params.metadata.row.delete(key);
2440
+ else params.metadata.row.set(key, write.value);
2441
+ }
2442
+ for (const [key, write] of transaction.collectionMetadataWrites) {
2443
+ if (write.type === `delete`)
2444
+ params.metadata.collection.delete(key);
2445
+ else params.metadata.collection.set(key, write.value);
2446
+ }
2447
+ }
2448
+ const applied = params.commit(signal);
2449
+ transaction.applicationReceipt = applied;
2450
+ if (applied === true) {
2451
+ removePendingPublicationTransaction(transaction);
2452
+ } else {
2453
+ void applied.then(
2454
+ () => removePendingPublicationTransaction(transaction),
2455
+ () => removePendingPublicationTransaction(transaction)
2456
+ );
2457
+ }
2458
+ return applied;
2459
+ } catch (error) {
2460
+ removePendingPublicationTransaction(transaction);
2461
+ throw error;
2462
+ }
2463
+ };
1297
2464
  runtime.setSyncControls({
1298
2465
  begin: params.begin,
1299
2466
  write: params.write,
1300
2467
  commit: params.commit,
1301
2468
  truncate: params.truncate,
1302
- metadata: params.metadata ?? null
2469
+ metadata: params.metadata ?? null,
2470
+ markError: params.markError
1303
2471
  });
1304
2472
  runtime.setCollection(
1305
2473
  params.collection
1306
2474
  );
2475
+ const persistenceCapability = {
2476
+ protocol: SYNC_PERSISTENCE_PROTOCOL,
2477
+ version: SYNC_PERSISTENCE_VERSION,
2478
+ hydrateBaseline: async () => {
2479
+ if (startupState.cleanedUp) return;
2480
+ try {
2481
+ await runtime.ensureResumeBaselineHydrated();
2482
+ } catch (error) {
2483
+ throw runtime.reportSyncError(error);
2484
+ }
2485
+ },
2486
+ scanPersistedRows: (options) => startupState.cleanedUp ? Promise.resolve([]) : runtime.scanPersistedRows(options),
2487
+ resumeSnapshot: {
2488
+ certify: async () => {
2489
+ if (startupState.cleanedUp) return;
2490
+ try {
2491
+ await runtime.ensureResumeBaselineCertified();
2492
+ } catch (error) {
2493
+ throw runtime.reportSyncError(error);
2494
+ }
2495
+ },
2496
+ getKeySetEvidence: () => startupState.cleanedUp ? void 0 : runtime.getKeySetEvidence(),
2497
+ expectCurrentCommit: () => {
2498
+ if (startupState.cleanedUp) return;
2499
+ const openTransaction = getOpenTransaction();
2500
+ if (!openTransaction) {
2501
+ throw new InvalidPersistedCollectionConfigError(
2502
+ `resumeSnapshot.expectCurrentCommit must be called within an open sync transaction`
2503
+ );
2504
+ }
2505
+ openTransaction.expectedResumeGenerationOwner = runtime.getResumeGenerationOwner();
2506
+ }
2507
+ }
2508
+ };
1307
2509
  const wrappedParams = {
1308
2510
  ...params,
2511
+ markError: reportUpstreamError,
1309
2512
  markReady: () => {
1310
- void (fullStartPromise ?? runtime.ensureStarted()).then(() => {
1311
- params.markReady();
1312
- }).catch((error) => {
1313
- console.warn(
1314
- `Failed persisted sync startup before markReady:`,
1315
- error
1316
- );
2513
+ if (startupState.cleanedUp || getTerminalFailure()) return;
2514
+ void (fullStartPromise ?? runtime.ensureStarted()).then(async () => {
2515
+ if (isCleanedUp() || getTerminalFailure()) return;
2516
+ await sourceResultAssigned;
2517
+ try {
2518
+ await sourceResultPromise;
2519
+ } catch (error) {
2520
+ reportUpstreamError(error);
2521
+ return;
2522
+ }
2523
+ if (isCleanedUp() || getTerminalFailure()) return;
1317
2524
  params.markReady();
1318
- });
2525
+ }).catch(() => void 0);
1319
2526
  },
1320
2527
  begin: (options) => {
2528
+ if (startupState.cleanedUp) return void 0;
2529
+ const terminalFailure = getTerminalFailure();
2530
+ const internal = runtime.isApplyingInternally();
1321
2531
  const transaction = {
1322
2532
  operations: [],
2533
+ partialUpdateOperationIndexes: /* @__PURE__ */ new Set(),
1323
2534
  rowMetadataWrites: /* @__PURE__ */ new Map(),
1324
2535
  collectionMetadataWrites: /* @__PURE__ */ new Map(),
2536
+ deferredHydrationMetadataDeleteKeys: /* @__PURE__ */ new Set(),
1325
2537
  truncate: false,
1326
- internal: runtime.isApplyingInternally(),
1327
- queuedBecauseHydrating: !runtime.isApplyingInternally() && runtime.isHydratingNow()
2538
+ internal,
2539
+ lifecycleGeneration: runtime.getLifecycleGeneration(),
2540
+ beginOptions: options,
2541
+ operationKeys: /* @__PURE__ */ new Set(),
2542
+ hasDependentSuccessor: false,
2543
+ queuedBecauseHydrating: terminalFailure === void 0 && !internal && runtime.isHydratingNow(),
2544
+ hydrationSequence: terminalFailure === void 0 && !internal ? runtime.getHydrationSequence() : void 0,
2545
+ hydrationContext: terminalFailure === void 0 && !internal && runtime.isHydratingNow() ? runtime.getActiveHydrationContext() : void 0,
2546
+ ...terminalFailure === void 0 ? {} : { terminalFailure }
1328
2547
  };
1329
2548
  transactionStack.push(transaction);
1330
- if (!transaction.queuedBecauseHydrating) {
1331
- params.begin(options);
1332
- }
1333
2549
  },
1334
2550
  write: (message) => {
1335
- const normalization = runtime.normalizeSyncWriteMessage(message);
2551
+ if (startupState.cleanedUp) return;
1336
2552
  const openTransaction = getOpenTransaction();
1337
- if (!openTransaction) {
1338
- params.write(normalization.forwardMessage);
2553
+ const terminalFailure = openTransaction?.terminalFailure ?? getTerminalFailure();
2554
+ if (terminalFailure) {
2555
+ if (openTransaction)
2556
+ openTransaction.terminalFailure = terminalFailure;
1339
2557
  return;
1340
2558
  }
2559
+ if (!openTransaction) {
2560
+ throw new NoPendingSyncTransactionWriteError();
2561
+ }
2562
+ bindToCurrentHydration(openTransaction);
2563
+ const normalization = runtime.normalizeSyncWriteMessage(message);
2564
+ if (message.type === `update` && sourceSyncConfig.rowUpdateMode !== `full`) {
2565
+ if (!openTransaction.internal) {
2566
+ openTransaction.partialUpdateOperationIndexes.add(
2567
+ openTransaction.operations.length
2568
+ );
2569
+ }
2570
+ for (const pending of pendingPublicationTransactions) {
2571
+ if (pending.truncate || pending.operationKeys.has(normalization.operation.key)) {
2572
+ markPendingMetadataDependency(pending);
2573
+ }
2574
+ }
2575
+ }
1341
2576
  openTransaction.operations.push(normalization.operation);
2577
+ openTransaction.operationKeys.add(normalization.operation.key);
1342
2578
  if (normalization.operation.type === `delete`) {
1343
2579
  openTransaction.rowMetadataWrites.set(normalization.operation.key, {
1344
2580
  type: `delete`
1345
2581
  });
1346
2582
  } else if (message.type === `insert` && normalization.operation.metadata === void 0) {
1347
- if (!openTransaction.rowMetadataWrites.has(
2583
+ if (openTransaction.queuedBecauseHydrating) {
2584
+ openTransaction.deferredHydrationMetadataDeleteKeys.add(
2585
+ normalization.operation.key
2586
+ );
2587
+ } else if (!openTransaction.rowMetadataWrites.has(
1348
2588
  normalization.operation.key
1349
2589
  )) {
1350
2590
  openTransaction.rowMetadataWrites.set(
@@ -1360,13 +2600,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1360
2600
  value: normalization.operation.metadata
1361
2601
  });
1362
2602
  }
1363
- if (!openTransaction.queuedBecauseHydrating) {
1364
- params.write(normalization.forwardMessage);
1365
- }
1366
2603
  },
1367
2604
  metadata: params.metadata ? {
2605
+ persistence: persistenceCapability,
1368
2606
  row: {
1369
2607
  get: (key) => {
2608
+ if (startupState.cleanedUp) return void 0;
1370
2609
  const openTransaction = getOpenTransaction();
1371
2610
  const pendingWrite = openTransaction?.rowMetadataWrites.get(key);
1372
2611
  if (pendingWrite) {
@@ -1375,11 +2614,17 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1375
2614
  if (openTransaction?.truncate) {
1376
2615
  return void 0;
1377
2616
  }
2617
+ const pending = getPendingRowMetadataWrite(key);
2618
+ if (pending.found) {
2619
+ return pending.write.type === `delete` ? void 0 : pending.write.value;
2620
+ }
1378
2621
  return params.metadata.row.get(key);
1379
2622
  },
1380
- scanPersisted: (options) => runtime.scanPersistedRows(options),
1381
2623
  set: (key, value) => {
2624
+ if (startupState.cleanedUp) return;
1382
2625
  const openTransaction = getOpenTransaction();
2626
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2627
+ return;
1383
2628
  if (!openTransaction) {
1384
2629
  throw new InvalidPersistedCollectionConfigError(
1385
2630
  `metadata.row.set must be called within an open sync transaction`
@@ -1389,12 +2634,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1389
2634
  type: `set`,
1390
2635
  value
1391
2636
  });
1392
- if (!openTransaction.queuedBecauseHydrating) {
1393
- params.metadata.row.set(key, value);
1394
- }
1395
2637
  },
1396
2638
  delete: (key) => {
2639
+ if (startupState.cleanedUp) return;
1397
2640
  const openTransaction = getOpenTransaction();
2641
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2642
+ return;
1398
2643
  if (!openTransaction) {
1399
2644
  throw new InvalidPersistedCollectionConfigError(
1400
2645
  `metadata.row.delete must be called within an open sync transaction`
@@ -1403,22 +2648,27 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1403
2648
  openTransaction.rowMetadataWrites.set(key, {
1404
2649
  type: `delete`
1405
2650
  });
1406
- if (!openTransaction.queuedBecauseHydrating) {
1407
- params.metadata.row.delete(key);
1408
- }
1409
2651
  }
1410
2652
  },
1411
2653
  collection: {
1412
2654
  get: (key) => {
2655
+ if (startupState.cleanedUp) return void 0;
1413
2656
  const openTransaction = getOpenTransaction();
1414
2657
  const pendingWrite = openTransaction?.collectionMetadataWrites.get(key);
1415
2658
  if (pendingWrite) {
1416
2659
  return pendingWrite.type === `delete` ? void 0 : pendingWrite.value;
1417
2660
  }
2661
+ const pending = getPendingCollectionMetadataWrite(key);
2662
+ if (pending.found) {
2663
+ return pending.write.type === `delete` ? void 0 : pending.write.value;
2664
+ }
1418
2665
  return params.metadata.collection.get(key);
1419
2666
  },
1420
2667
  set: (key, value) => {
2668
+ if (startupState.cleanedUp) return;
1421
2669
  const openTransaction = getOpenTransaction();
2670
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2671
+ return;
1422
2672
  if (!openTransaction) {
1423
2673
  throw new InvalidPersistedCollectionConfigError(
1424
2674
  `metadata.collection.set must be called within an open sync transaction`
@@ -1428,12 +2678,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1428
2678
  type: `set`,
1429
2679
  value
1430
2680
  });
1431
- if (!openTransaction.queuedBecauseHydrating) {
1432
- params.metadata.collection.set(key, value);
1433
- }
1434
2681
  },
1435
2682
  delete: (key) => {
2683
+ if (startupState.cleanedUp) return;
1436
2684
  const openTransaction = getOpenTransaction();
2685
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2686
+ return;
1437
2687
  if (!openTransaction) {
1438
2688
  throw new InvalidPersistedCollectionConfigError(
1439
2689
  `metadata.collection.delete must be called within an open sync transaction`
@@ -1442,14 +2692,31 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1442
2692
  openTransaction.collectionMetadataWrites.set(key, {
1443
2693
  type: `delete`
1444
2694
  });
1445
- if (!openTransaction.queuedBecauseHydrating) {
1446
- params.metadata.collection.delete(key);
1447
- }
1448
2695
  },
1449
2696
  list: (prefix) => {
2697
+ if (startupState.cleanedUp) return [];
1450
2698
  const merged = new Map(
1451
2699
  params.metadata.collection.list().map(({ key, value }) => [key, value])
1452
2700
  );
2701
+ const pendingOwners = /* @__PURE__ */ new Map();
2702
+ for (const transaction of pendingPublicationTransactions) {
2703
+ for (const [
2704
+ key,
2705
+ metadataWrite
2706
+ ] of transaction.collectionMetadataWrites) {
2707
+ if (!prefix || key.startsWith(prefix)) {
2708
+ pendingOwners.set(key, transaction);
2709
+ }
2710
+ if (metadataWrite.type === `delete`) {
2711
+ merged.delete(key);
2712
+ } else {
2713
+ merged.set(key, metadataWrite.value);
2714
+ }
2715
+ }
2716
+ }
2717
+ for (const owner of new Set(pendingOwners.values())) {
2718
+ markPendingMetadataDependency(owner);
2719
+ }
1453
2720
  const openTransaction = getOpenTransaction();
1454
2721
  if (openTransaction) {
1455
2722
  for (const [
@@ -1471,29 +2738,92 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1471
2738
  }
1472
2739
  } : void 0,
1473
2740
  truncate: () => {
2741
+ if (startupState.cleanedUp) return;
2742
+ runtime.noteSourceTruncate();
1474
2743
  const openTransaction = getOpenTransaction();
2744
+ if (openTransaction?.terminalFailure ?? getTerminalFailure()) return;
1475
2745
  if (!openTransaction) {
1476
- params.truncate();
1477
- return;
2746
+ throw new NoPendingSyncTransactionWriteError();
1478
2747
  }
1479
2748
  openTransaction.operations = [];
2749
+ openTransaction.operationKeys.clear();
2750
+ openTransaction.partialUpdateOperationIndexes.clear();
1480
2751
  openTransaction.rowMetadataWrites.clear();
2752
+ openTransaction.deferredHydrationMetadataDeleteKeys.clear();
1481
2753
  openTransaction.truncate = true;
1482
- if (!openTransaction.queuedBecauseHydrating) {
1483
- params.truncate();
1484
- }
1485
2754
  },
1486
2755
  commit: (signal) => {
2756
+ if (startupState.cleanedUp) return true;
1487
2757
  const openTransaction = transactionStack.pop();
2758
+ const terminalFailure = openTransaction?.terminalFailure ?? getTerminalFailure();
2759
+ if (terminalFailure) {
2760
+ if (openTransaction) {
2761
+ settlePendingTransaction(openTransaction);
2762
+ settlePublicationAdmissionWaiters(
2763
+ openTransaction,
2764
+ terminalFailure.error
2765
+ );
2766
+ }
2767
+ return createHandledRejection(terminalFailure.error);
2768
+ }
1488
2769
  if (!openTransaction) {
1489
- return params.commit(signal);
2770
+ throw new NoPendingSyncTransactionCommitError();
1490
2771
  }
1491
- if (openTransaction.queuedBecauseHydrating) {
1492
- if (signal?.aborted) {
1493
- const aborted = Promise.reject(new SyncTransactionAbortedError());
1494
- void aborted.catch(() => void 0);
1495
- return aborted;
1496
- }
2772
+ if (openTransaction.internal) {
2773
+ return applyTransactionToCollection(openTransaction, signal);
2774
+ }
2775
+ if (signal?.aborted) {
2776
+ settlePendingTransaction(openTransaction);
2777
+ const error = new SyncTransactionAbortedError();
2778
+ settlePublicationAdmissionWaiters(openTransaction, error);
2779
+ return createHandledRejection(error);
2780
+ }
2781
+ try {
2782
+ bindToCurrentHydration(openTransaction);
2783
+ assertHydrationSequenceCurrent(openTransaction);
2784
+ } catch (error) {
2785
+ settlePublicationAdmissionWaiters(openTransaction, error);
2786
+ throw error;
2787
+ }
2788
+ const transaction = {
2789
+ operations: openTransaction.operations,
2790
+ partialUpdateOperationIndexes: openTransaction.partialUpdateOperationIndexes,
2791
+ rowMetadataWrites: openTransaction.rowMetadataWrites,
2792
+ collectionMetadataWrites: openTransaction.collectionMetadataWrites,
2793
+ deferredHydrationMetadataDeleteKeys: openTransaction.deferredHydrationMetadataDeleteKeys,
2794
+ hydrationContext: openTransaction.hydrationContext,
2795
+ hydrationSequence: openTransaction.hydrationSequence,
2796
+ truncate: openTransaction.truncate,
2797
+ internal: false,
2798
+ lifecycleGeneration: openTransaction.lifecycleGeneration,
2799
+ beginOptions: openTransaction.beginOptions,
2800
+ expectedResumeGenerationOwner: openTransaction.expectedResumeGenerationOwner,
2801
+ signal,
2802
+ prependHydrationRows: (rows) => {
2803
+ const baselineOperations = rows.map(({ key, value, metadata }) => ({
2804
+ type: `update`,
2805
+ key,
2806
+ value,
2807
+ metadata
2808
+ }));
2809
+ openTransaction.operations = baselineOperations.concat(
2810
+ openTransaction.operations
2811
+ );
2812
+ for (const { key, metadata } of rows) {
2813
+ if (metadata !== void 0 && !openTransaction.rowMetadataWrites.has(key)) {
2814
+ openTransaction.rowMetadataWrites.set(key, {
2815
+ type: `set`,
2816
+ value: metadata
2817
+ });
2818
+ }
2819
+ }
2820
+ },
2821
+ applyToCollection: () => applyTransactionToCollection(openTransaction, signal),
2822
+ shouldFailStopOnAbort: () => openTransaction.hasDependentSuccessor
2823
+ };
2824
+ openTransaction.signal = signal;
2825
+ pendingPublicationTransactions.push(openTransaction);
2826
+ if (openTransaction.queuedBecauseHydrating && runtime.isHydratingNow()) {
1497
2827
  let resolveApplied;
1498
2828
  let rejectApplied;
1499
2829
  const applied2 = new Promise((resolve, reject) => {
@@ -1502,66 +2832,118 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1502
2832
  });
1503
2833
  void applied2.catch(() => void 0);
1504
2834
  runtime.queueHydrationBufferedTransaction({
1505
- operations: openTransaction.operations,
1506
- rowMetadataWrites: openTransaction.rowMetadataWrites,
1507
- collectionMetadataWrites: openTransaction.collectionMetadataWrites,
1508
- truncate: openTransaction.truncate,
1509
- internal: openTransaction.internal,
1510
- signal,
2835
+ ...transaction,
1511
2836
  resolveApplied,
1512
2837
  rejectApplied
1513
2838
  });
2839
+ settlePublicationAdmissionWaiters(openTransaction);
2840
+ settleRuntimeTransaction(openTransaction, applied2);
1514
2841
  return applied2;
1515
2842
  }
1516
- const applied = params.commit(signal);
1517
- if (!openTransaction.internal) {
1518
- const persistAfterApplication = async () => {
1519
- if (applied !== true) await applied;
1520
- await runtime.persistAndBroadcastExternalSyncTransaction({
1521
- operations: openTransaction.operations,
1522
- rowMetadataWrites: openTransaction.rowMetadataWrites,
1523
- collectionMetadataWrites: openTransaction.collectionMetadataWrites,
1524
- truncate: openTransaction.truncate,
1525
- internal: false
1526
- });
1527
- };
1528
- const persisted = persistAfterApplication();
1529
- void persisted.catch(() => void 0);
1530
- return persisted;
2843
+ let applied;
2844
+ try {
2845
+ applied = runtime.applyHydrationBufferedTransaction(transaction);
2846
+ } catch (error) {
2847
+ settlePublicationAdmissionWaiters(openTransaction, error);
2848
+ throw error;
1531
2849
  }
2850
+ settlePublicationAdmissionWaiters(openTransaction);
2851
+ settleRuntimeTransaction(openTransaction, applied);
1532
2852
  return applied;
1533
2853
  }
1534
2854
  };
1535
2855
  let sourceResult = {};
1536
- const startupState = { cleanedUp: false };
2856
+ let sourceResultSettled = false;
2857
+ let activeSourceSyncEntry;
1537
2858
  fullStartPromise = runtime.ensureStarted();
1538
- const sourceResultPromise = (async () => {
2859
+ void fullStartPromise.catch(() => void 0);
2860
+ sourceResultPromise = (async () => {
1539
2861
  await runtime.ensureStartupMetadataLoaded();
1540
2862
  if (startupState.cleanedUp) {
2863
+ sourceResultSettled = true;
1541
2864
  return sourceResult;
1542
2865
  }
1543
- sourceResult = normalizeSyncFnResult(
1544
- sourceSyncConfig.sync(wrappedParams)
1545
- );
2866
+ let settleSourceSyncEntry;
2867
+ activeSourceSyncEntry = new Promise((resolve) => {
2868
+ settleSourceSyncEntry = resolve;
2869
+ });
2870
+ try {
2871
+ sourceResult = normalizeSyncFnResult(
2872
+ sourceSyncConfig.sync(wrappedParams)
2873
+ );
2874
+ } finally {
2875
+ activeSourceSyncEntry = void 0;
2876
+ settleSourceSyncEntry();
2877
+ }
2878
+ if (!startupState.cleanedUp && sourceResult.loadSubset) {
2879
+ const loadSubset = async (options) => {
2880
+ if (startupState.cleanedUp) {
2881
+ throw new Error(`persisted sync source is no longer active`);
2882
+ }
2883
+ await sourceResult.loadSubset?.(
2884
+ options
2885
+ );
2886
+ };
2887
+ runtime.registerRemoteSubsetOwner(
2888
+ Object.assign(loadSubset, {
2889
+ unloadSubset: (options) => sourceResult.unloadSubset?.(
2890
+ options
2891
+ ),
2892
+ onError: reportUpstreamError
2893
+ })
2894
+ );
2895
+ }
2896
+ sourceResultSettled = true;
1546
2897
  return sourceResult;
1547
2898
  })();
2899
+ resolveSourceResultAssigned();
2900
+ void sourceResultPromise.catch((error) => {
2901
+ reportUpstreamError(error);
2902
+ });
1548
2903
  return {
1549
- cleanup: () => {
2904
+ cleanup: async () => {
1550
2905
  startupState.cleanedUp = true;
2906
+ const sourceSyncEntry = activeSourceSyncEntry;
2907
+ const cleanupError = new SyncTransactionAbortedError();
2908
+ for (const acquisition of acquisitions.values()) {
2909
+ acquisition.cancelAdmissionWait?.();
2910
+ }
1551
2911
  acquisitions.clear();
1552
- sourceResult.cleanup?.();
1553
- runtime.cleanup();
1554
- runtime.clearSyncControls();
2912
+ pendingPublicationTransactions.length = 0;
2913
+ for (const transaction of transactionStack) {
2914
+ settlePublicationAdmissionWaiters(transaction, cleanupError);
2915
+ }
2916
+ transactionStack.length = 0;
2917
+ let sourceCleanup;
2918
+ try {
2919
+ sourceCleanup = sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.());
2920
+ } catch (error) {
2921
+ sourceCleanup = Promise.reject(error);
2922
+ }
2923
+ let runtimeCleanup;
2924
+ try {
2925
+ runtimeCleanup = runtime.cleanup();
2926
+ } catch (error) {
2927
+ runtimeCleanup = Promise.reject(error);
2928
+ }
2929
+ const [sourceOutcome, runtimeOutcome] = await Promise.allSettled([
2930
+ sourceCleanup,
2931
+ runtimeCleanup
2932
+ ]);
2933
+ if (sourceOutcome.status === `rejected` && runtimeOutcome.status === `rejected`) {
2934
+ throw new AggregateError(
2935
+ [sourceOutcome.reason, runtimeOutcome.reason],
2936
+ `Source cleanup and persistence runtime teardown both failed`,
2937
+ { cause: sourceOutcome.reason }
2938
+ );
2939
+ }
2940
+ if (sourceOutcome.status === `rejected`) throw sourceOutcome.reason;
2941
+ if (runtimeOutcome.status === `rejected`) throw runtimeOutcome.reason;
1555
2942
  },
1556
- loadSubset: async (options) => {
2943
+ loadSubset: (options) => {
1557
2944
  const acquisition = { forwarded: false };
1558
2945
  acquisitions.set(options, acquisition);
1559
- await fullStartPromise;
1560
- const resolvedSourceResult = await sourceResultPromise;
1561
- if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
1562
- return;
1563
- }
1564
- return runtime.loadSubset(options, (loadOptions) => {
2946
+ const forwardUpstream = (resolvedSourceResult, loadOptions) => {
1565
2947
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
1566
2948
  return true;
1567
2949
  }
@@ -1573,10 +2955,42 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1573
2955
  acquisition.forwarded = false;
1574
2956
  throw error;
1575
2957
  }
1576
- });
2958
+ };
2959
+ const openTransaction = getOpenTransaction();
2960
+ const needsPublicationAdmission = openTransaction && !openTransaction.internal && openTransaction.beginOptions?.immediate;
2961
+ const hydrated = sourceResultSettled && !needsPublicationAdmission ? runtime.loadHydratedSubset(
2962
+ options,
2963
+ (loadOptions) => forwardUpstream(sourceResult, loadOptions)
2964
+ ) : void 0;
2965
+ if (hydrated !== void 0) {
2966
+ return hydrated;
2967
+ }
2968
+ return (async () => {
2969
+ await fullStartPromise;
2970
+ const resolvedSourceResult = await sourceResultPromise;
2971
+ if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
2972
+ return;
2973
+ }
2974
+ const currentTransaction = getOpenTransaction();
2975
+ if (currentTransaction && !currentTransaction.internal && currentTransaction.beginOptions?.immediate) {
2976
+ await waitForPublicationAdmission(
2977
+ currentTransaction,
2978
+ options,
2979
+ acquisition
2980
+ );
2981
+ }
2982
+ if (options.signal?.aborted || acquisitions.get(options) !== acquisition) {
2983
+ return;
2984
+ }
2985
+ return runtime.loadSubset(
2986
+ options,
2987
+ (loadOptions) => forwardUpstream(resolvedSourceResult, loadOptions)
2988
+ );
2989
+ })();
1577
2990
  },
1578
2991
  unloadSubset: (options) => {
1579
2992
  const acquisition = acquisitions.get(options);
2993
+ acquisition?.cancelAdmissionWait?.();
1580
2994
  acquisitions.delete(options);
1581
2995
  runtime.unloadSubset(
1582
2996
  options,
@@ -1595,23 +3009,19 @@ function createLoopbackSyncConfig(runtime) {
1595
3009
  write: params.write,
1596
3010
  commit: params.commit,
1597
3011
  truncate: params.truncate,
1598
- metadata: params.metadata ?? null
3012
+ metadata: params.metadata ?? null,
3013
+ markError: params.markError
1599
3014
  });
1600
3015
  runtime.setCollection(
1601
3016
  params.collection
1602
3017
  );
1603
3018
  void runtime.ensureStarted().then(() => {
3019
+ if (runtime.getCurrentTerminalFailure()) return;
1604
3020
  params.markReady();
1605
- }).catch((error) => {
1606
- console.warn(`Failed persisted loopback startup:`, error);
1607
- params.markReady();
1608
- });
3021
+ }).catch(() => void 0);
1609
3022
  return {
1610
- cleanup: () => {
1611
- runtime.cleanup();
1612
- runtime.clearSyncControls();
1613
- },
1614
- loadSubset: (options) => runtime.loadSubset(options),
3023
+ loadSubset: (options) => runtime.loadHydratedSubset(options) ?? runtime.loadSubset(options),
3024
+ cleanup: () => runtime.cleanup(),
1615
3025
  unloadSubset: (options) => runtime.unloadSubset(options)
1616
3026
  };
1617
3027
  },
@@ -1626,13 +3036,24 @@ function persistedCollectionOptions(options) {
1626
3036
  `persistedCollectionOptions requires a persistence adapter`
1627
3037
  );
1628
3038
  }
3039
+ const networkTimeoutMs = getNetworkTimeoutMs(options.initialRender);
3040
+ if (networkTimeoutMs !== void 0 && options.syncMode === `on-demand`) {
3041
+ throw new InvalidPersistedCollectionConfigError(
3042
+ `network-first initial rendering requires eager persistence syncMode`
3043
+ );
3044
+ }
3045
+ const persistedReadinessTracker = networkTimeoutMs !== void 0 ? new PersistedReadinessTracker(networkTimeoutMs) : void 0;
1629
3046
  if (hasOwnSyncKey(options)) {
1630
3047
  if (!isValidSyncConfig(options.sync)) {
1631
3048
  throw new InvalidSyncConfigError(
1632
3049
  `when the "sync" key is present it must provide a callable sync function`
1633
3050
  );
1634
3051
  }
1635
- const { schemaVersion: schemaVersion2, ...syncOptions } = options;
3052
+ const {
3053
+ schemaVersion: schemaVersion2,
3054
+ initialRender: _initialRender2,
3055
+ ...syncOptions
3056
+ } = options;
1636
3057
  const collectionId2 = syncOptions.id ?? `persisted-collection:${safeRandomUUID()}`;
1637
3058
  const persistence2 = resolvePersistenceForCollection(
1638
3059
  syncOptions.persistence,
@@ -1647,13 +3068,19 @@ function persistedCollectionOptions(options) {
1647
3068
  collectionId2,
1648
3069
  persistence2,
1649
3070
  syncOptions.syncMode ?? `eager`,
1650
- collectionId2
3071
+ collectionId2,
3072
+ persistedReadinessTracker
1651
3073
  );
1652
3074
  const result2 = {
1653
3075
  ...syncOptions,
1654
3076
  id: collectionId2,
1655
- sync: createWrappedSyncConfig(syncOptions.sync, runtime2),
1656
- persistence: persistence2
3077
+ sync: createWrappedSyncConfig(
3078
+ syncOptions.sync,
3079
+ runtime2,
3080
+ persistedReadinessTracker
3081
+ ),
3082
+ persistence: persistence2,
3083
+ ...persistedReadinessTracker ? { [PERSISTED_READINESS]: persistedReadinessTracker } : {}
1657
3084
  };
1658
3085
  return withCollectionConfigFactory(
1659
3086
  result2,
@@ -1663,7 +3090,11 @@ function persistedCollectionOptions(options) {
1663
3090
  })
1664
3091
  );
1665
3092
  }
1666
- const { schemaVersion, ...localOnlyOptions } = options;
3093
+ const {
3094
+ schemaVersion,
3095
+ initialRender: _initialRender,
3096
+ ...localOnlyOptions
3097
+ } = options;
1667
3098
  const collectionId = localOnlyOptions.id ?? `persisted-collection:${safeRandomUUID()}`;
1668
3099
  const persistence = resolvePersistenceForCollection(
1669
3100
  localOnlyOptions.persistence,
@@ -1678,32 +3109,39 @@ function persistedCollectionOptions(options) {
1678
3109
  collectionId,
1679
3110
  persistence,
1680
3111
  localOnlyOptions.syncMode ?? `eager`,
1681
- localOnlyOptions.id ?? DEFAULT_DB_NAME
3112
+ localOnlyOptions.id ?? DEFAULT_DB_NAME,
3113
+ persistedReadinessTracker
1682
3114
  );
1683
3115
  const wrappedOnInsert = async (params) => {
3116
+ const lifecycleGeneration = runtime.getLifecycleGeneration();
1684
3117
  const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
1685
3118
  params
1686
3119
  ) : void 0;
1687
3120
  await runtime.persistAndConfirmCollectionMutations(
1688
- params.transaction.mutations
3121
+ params.transaction.mutations,
3122
+ lifecycleGeneration
1689
3123
  );
1690
3124
  return handlerResult ?? {};
1691
3125
  };
1692
3126
  const wrappedOnUpdate = async (params) => {
3127
+ const lifecycleGeneration = runtime.getLifecycleGeneration();
1693
3128
  const handlerResult = localOnlyOptions.onUpdate ? await localOnlyOptions.onUpdate(
1694
3129
  params
1695
3130
  ) : void 0;
1696
3131
  await runtime.persistAndConfirmCollectionMutations(
1697
- params.transaction.mutations
3132
+ params.transaction.mutations,
3133
+ lifecycleGeneration
1698
3134
  );
1699
3135
  return handlerResult ?? {};
1700
3136
  };
1701
3137
  const wrappedOnDelete = async (params) => {
3138
+ const lifecycleGeneration = runtime.getLifecycleGeneration();
1702
3139
  const handlerResult = localOnlyOptions.onDelete ? await localOnlyOptions.onDelete(
1703
3140
  params
1704
3141
  ) : void 0;
1705
3142
  await runtime.persistAndConfirmCollectionMutations(
1706
- params.transaction.mutations
3143
+ params.transaction.mutations,
3144
+ lifecycleGeneration
1707
3145
  );
1708
3146
  return handlerResult ?? {};
1709
3147
  };
@@ -1723,6 +3161,7 @@ function persistedCollectionOptions(options) {
1723
3161
  ...localOnlyOptions,
1724
3162
  id: collectionId,
1725
3163
  persistence,
3164
+ ...persistedReadinessTracker ? { [PERSISTED_READINESS]: persistedReadinessTracker } : {},
1726
3165
  sync: createLoopbackSyncConfig(runtime),
1727
3166
  onInsert: wrappedOnInsert,
1728
3167
  onUpdate: wrappedOnUpdate,
@@ -1795,10 +3234,12 @@ function createPersistedTableName(collectionId, prefix = `c`) {
1795
3234
  return `${prefix}_${hashPart}_${lengthPart}`;
1796
3235
  }
1797
3236
  export {
3237
+ SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
1798
3238
  SingleProcessCoordinator,
1799
3239
  createPersistedTableName,
1800
3240
  decodePersistedStorageKey,
1801
3241
  encodePersistedStorageKey,
3242
+ forwardSQLiteDriverSharedLogicalScheduling,
1802
3243
  persistedCollectionOptions,
1803
3244
  validatePersistedCollectionCoordinator
1804
3245
  };