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