@tanstack/db-sqlite-persistence-core 0.2.23 → 0.4.1

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 +1782 -394
  12. package/dist/cjs/persisted.cjs.map +1 -1
  13. package/dist/cjs/persisted.d.cts +176 -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 +176 -11
  36. package/dist/esm/persisted.js +1784 -396
  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 +2617 -442
  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`:
@@ -200,40 +409,52 @@ function toPersistedMutationEnvelope(mutation) {
200
409
  };
201
410
  }
202
411
  class PersistedCollectionRuntime {
203
- constructor(mode, collectionId, persistence, syncMode, dbName) {
412
+ constructor(mode, collectionId, persistence, syncMode, dbName, persistedReadiness) {
204
413
  this.mode = mode;
205
414
  this.collectionId = collectionId;
206
415
  this.persistence = persistence;
207
416
  this.syncMode = syncMode;
208
417
  this.dbName = dbName;
418
+ this.persistedReadiness = persistedReadiness;
209
419
  this.applyMutex = new ApplyMutex();
210
420
  this.activeSubsets = /* @__PURE__ */ new Map();
421
+ this.hydrationSequence = 0;
422
+ this.resetSequence = 0;
211
423
  this.pendingRemoteSubsetEnsures = /* @__PURE__ */ new Map();
212
424
  this.queuedHydrationTransactions = [];
213
425
  this.queuedTxCommitted = [];
214
426
  this.requestIds = /* @__PURE__ */ new WeakMap();
427
+ this.hydratedDemands = /* @__PURE__ */ new Set();
215
428
  this.collection = null;
216
429
  this.syncControls = {
217
430
  begin: null,
218
431
  write: null,
219
432
  commit: null,
220
433
  truncate: null,
221
- metadata: null
434
+ metadata: null,
435
+ markError: null
222
436
  };
223
- this.started = false;
224
437
  this.startupMetadataPromise = null;
225
438
  this.startPromise = null;
439
+ this.hasAttemptedStartup = false;
226
440
  this.resumeBaselinePromise = null;
441
+ this.resumeCertificationPromise = null;
442
+ this.resumeGenerationOwner = /* @__PURE__ */ Symbol(`persisted resume generation owner`);
227
443
  this.lifecycleGeneration = 0;
228
444
  this.internalApplyDepth = 0;
445
+ this.sourcePublicationWaitDepth = 0;
229
446
  this.appliedReceiptSequence = 0;
447
+ this.syncErrorReported = false;
230
448
  this.pendingAppliedReceipts = /* @__PURE__ */ new Map();
231
449
  this.hydratingGeneration = null;
232
450
  this.coordinatorUnsubscribe = null;
451
+ this.remoteSubsetOwnerUnsubscribe = null;
233
452
  this.indexAddedUnsubscribe = null;
234
453
  this.indexRemovedUnsubscribe = null;
235
454
  this.remoteEnsureRetryTimer = null;
236
455
  this.nextRequestId = 0;
456
+ this.startupSettled = false;
457
+ this.sourceTruncateGeneration = 0;
237
458
  this.latestTerm = 0;
238
459
  this.latestSeq = 0;
239
460
  this.latestRowVersion = 0;
@@ -243,12 +464,33 @@ class PersistedCollectionRuntime {
243
464
  }
244
465
  setSyncControls(syncControls) {
245
466
  this.advanceLifecycle();
467
+ this.syncErrorReported = false;
468
+ this.reportedSyncError = void 0;
246
469
  const commit = syncControls.commit;
247
470
  this.syncControls = {
248
471
  ...syncControls,
249
472
  commit: commit ? (signal) => this.trackAppliedReceipt(commit(signal)) : null
250
473
  };
251
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
+ }
252
494
  trackAppliedReceipt(receipt) {
253
495
  const sequence = ++this.appliedReceiptSequence;
254
496
  if (receipt === true) {
@@ -273,12 +515,55 @@ class PersistedCollectionRuntime {
273
515
  write: null,
274
516
  commit: null,
275
517
  truncate: null,
276
- metadata: null
518
+ metadata: null,
519
+ markError: null
277
520
  };
278
521
  }
279
522
  isHydratingNow() {
280
523
  return this.hydratingGeneration === this.lifecycleGeneration;
281
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;
566
+ }
282
567
  isApplyingInternally() {
283
568
  return this.internalApplyDepth > 0;
284
569
  }
@@ -295,15 +580,109 @@ class PersistedCollectionRuntime {
295
580
  isLeader: this.persistence.coordinator.isLeader(this.collectionId)
296
581
  };
297
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
+ }
298
589
  async ensureStarted() {
590
+ this.throwIfTerminal();
299
591
  if (this.startPromise) {
300
592
  return this.startPromise;
301
593
  }
302
594
  const lifecycleGeneration = this.lifecycleGeneration;
303
- this.startPromise = this.startInternal(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);
304
682
  return this.startPromise;
305
683
  }
306
684
  ensureResumeBaselineHydrated() {
685
+ this.throwIfTerminal();
307
686
  if (this.resumeBaselinePromise) {
308
687
  return this.resumeBaselinePromise;
309
688
  }
@@ -312,79 +691,122 @@ class PersistedCollectionRuntime {
312
691
  await this.ensureStarted();
313
692
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
314
693
  if (this.syncMode !== `on-demand`) return;
315
- await this.hydrateBaseline(lifecycleGeneration);
316
- })();
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
+ });
317
709
  return this.resumeBaselinePromise;
318
710
  }
319
- async hydrateBaseline(lifecycleGeneration) {
320
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
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;
321
739
  const baseline = {};
322
740
  this.activeSubsets.set(this.getSubsetKey(baseline), baseline);
323
741
  const appliedCursor = this.appliedReceiptSequence;
324
- await this.applyMutex.run(async () => {
325
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
326
- await this.hydrateSubsetUnsafe(baseline, {
742
+ await this.hydrateSubsetUnsafe(
743
+ baseline,
744
+ {
327
745
  requestRemoteEnsure: false,
328
- lifecycleGeneration
329
- });
330
- });
331
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
332
- await this.waitForAppliedReceiptsAfter(appliedCursor);
746
+ lifecycleGeneration,
747
+ bindKeySetEvidence: true
748
+ },
749
+ adapter
750
+ );
751
+ return lifecycleGeneration === this.lifecycleGeneration ? appliedCursor : void 0;
333
752
  }
334
753
  async ensureStartupMetadataLoaded() {
754
+ this.throwIfTerminal();
335
755
  if (this.startupMetadataPromise) {
336
756
  return this.startupMetadataPromise;
337
757
  }
338
- const lifecycleGeneration = this.lifecycleGeneration;
339
- this.startupMetadataPromise = this.loadStartupMetadataInternal(lifecycleGeneration);
758
+ void this.ensureStarted();
340
759
  return this.startupMetadataPromise;
341
760
  }
342
- async startInternal(lifecycleGeneration) {
343
- if (this.started) {
344
- return;
345
- }
346
- this.started = true;
347
- await this.ensureStartupMetadataLoaded();
348
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
761
+ async startInternal(lifecycleGeneration, adapter, onStartupMetadataLoaded) {
762
+ if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
349
763
  const indexBootstrapSnapshot = this.collection?.getIndexMetadata() ?? [];
350
764
  this.attachIndexLifecycleListeners();
351
- await this.bootstrapPersistedIndexes(indexBootstrapSnapshot);
352
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
353
- if (this.syncMode !== `on-demand`) {
354
- await this.hydrateBaseline(lifecycleGeneration);
355
- }
356
- }
357
- async loadStartupMetadataInternal(lifecycleGeneration) {
358
- if (this.persistence.adapter.getStreamPosition) {
359
- const position = await this.persistence.adapter.getStreamPosition(
360
- this.collectionId
361
- );
362
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
363
- this.observeStreamPosition(
364
- position.latestTerm,
365
- position.latestSeq,
366
- position.latestRowVersion
367
- );
368
- }
369
- const collectionMetadata = await this.loadCollectionMetadataSnapshot();
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
+ });
370
782
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
371
- this.replaceCollectionMetadataSnapshot(collectionMetadata);
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;
372
794
  }
373
- async loadCollectionMetadataSnapshot() {
374
- if (!this.persistence.adapter.loadCollectionMetadata) {
795
+ async loadCollectionMetadataSnapshot(adapter) {
796
+ if (!adapter.loadCollectionMetadata) {
375
797
  return [];
376
798
  }
377
- return this.persistence.adapter.loadCollectionMetadata(this.collectionId);
799
+ return adapter.loadCollectionMetadata(this.collectionId);
378
800
  }
379
801
  replaceCollectionMetadataSnapshot(collectionMetadata) {
380
802
  if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
381
- return;
803
+ return true;
382
804
  }
383
805
  const nextMetadata = new Map(
384
806
  collectionMetadata.map(({ key, value }) => [key, value])
385
807
  );
386
808
  const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
387
- this.withInternalApply(() => {
809
+ return this.withInternalApply(() => {
388
810
  this.syncControls.begin?.({ immediate: true });
389
811
  currentKeys.forEach((key) => {
390
812
  if (!nextMetadata.has(key)) {
@@ -394,56 +816,182 @@ class PersistedCollectionRuntime {
394
816
  nextMetadata.forEach((value, key) => {
395
817
  this.syncControls.metadata?.collection.set(key, value);
396
818
  });
397
- this.syncControls.commit?.();
819
+ return this.syncControls.commit?.() ?? true;
398
820
  });
399
821
  }
400
822
  async loadSubset(options, upstreamLoadSubset) {
823
+ this.throwIfTerminal();
401
824
  const lifecycleGeneration = this.lifecycleGeneration;
402
- this.activeSubsets.set(this.getSubsetKey(options), options);
825
+ const subsetKey = this.getSubsetKey(options);
826
+ const truncateGeneration = this.sourceTruncateGeneration;
827
+ const routeRemoteDemandDuringHydration = this.canRouteRemoteDemandThroughCoordinator();
828
+ this.activeSubsets.set(subsetKey, options);
403
829
  const appliedCursor = this.appliedReceiptSequence;
404
- await this.applyMutex.run(
405
- () => this.hydrateSubsetUnsafe(options, {
406
- requestRemoteEnsure: this.mode === `sync-present`,
407
- lifecycleGeneration
408
- })
409
- );
410
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
411
- await this.waitForAppliedReceiptsAfter(appliedCursor);
412
- 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()) {
413
863
  try {
414
- await upstreamLoadSubset(options);
864
+ await this.persistence.coordinator.requestEnsureRemoteSubset(
865
+ this.collectionId,
866
+ options
867
+ );
415
868
  } catch (error) {
416
869
  if (options.signal?.aborted || typeof error === `object` && error !== null && `name` in error && error.name === `AbortError`) {
417
870
  this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options));
418
871
  throw error;
419
872
  }
420
- console.warn(`Failed to trigger remote subset load:`, error);
421
873
  this.queueRemoteSubsetEnsure(options);
422
874
  throw error;
423
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;
887
+ }
424
888
  }
425
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
+ }
426
929
  unloadSubset(options, upstreamUnloadSubset) {
427
- this.activeSubsets.delete(this.getSubsetKey(options));
428
- this.pendingRemoteSubsetEnsures.delete(this.getSubsetKey(options));
429
- 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
+ }
430
953
  }
431
954
  async forceReloadSubset(options) {
955
+ this.throwIfTerminal();
432
956
  const lifecycleGeneration = this.lifecycleGeneration;
433
- await this.applyMutex.run(
434
- () => this.hydrateSubsetUnsafe(options, {
435
- requestRemoteEnsure: false,
436
- lifecycleGeneration
437
- })
438
- );
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
+ });
439
973
  }
440
974
  queueHydrationBufferedTransaction(transaction) {
975
+ const failure = this.getCurrentTerminalFailure();
976
+ if (failure) {
977
+ transaction.rejectApplied?.(failure.error);
978
+ return;
979
+ }
441
980
  this.queuedHydrationTransactions.push(transaction);
442
981
  }
443
- async persistAndBroadcastExternalSyncTransaction(transaction) {
444
- await this.applyMutex.run(
445
- () => this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction)
446
- );
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
+ });
447
995
  }
448
996
  normalizeSyncWriteMessage(message) {
449
997
  if (!this.collection) {
@@ -455,10 +1003,6 @@ class PersistedCollectionRuntime {
455
1003
  const key2 = message.key;
456
1004
  const previousValue = this.collection.get(key2) ?? {};
457
1005
  return {
458
- forwardMessage: {
459
- type: `delete`,
460
- key: key2
461
- },
462
1006
  operation: {
463
1007
  type: `delete`,
464
1008
  key: key2,
@@ -470,10 +1014,6 @@ class PersistedCollectionRuntime {
470
1014
  const key2 = this.collection.getKeyFromItem(message.value);
471
1015
  const previousValue = this.collection.get(key2) ?? message.value;
472
1016
  return {
473
- forwardMessage: {
474
- type: `delete`,
475
- key: key2
476
- },
477
1017
  operation: {
478
1018
  type: `delete`,
479
1019
  key: key2,
@@ -483,11 +1023,6 @@ class PersistedCollectionRuntime {
483
1023
  }
484
1024
  const key = this.collection.getKeyFromItem(message.value);
485
1025
  return {
486
- forwardMessage: {
487
- type: `update`,
488
- value: message.value,
489
- metadata: message.metadata
490
- },
491
1026
  operation: {
492
1027
  type: `update`,
493
1028
  key,
@@ -496,25 +1031,45 @@ class PersistedCollectionRuntime {
496
1031
  }
497
1032
  };
498
1033
  }
499
- async persistAndConfirmCollectionMutations(mutations) {
1034
+ async persistAndConfirmCollectionMutations(mutations, lifecycleGeneration = this.lifecycleGeneration) {
1035
+ this.throwIfTerminal();
1036
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
500
1037
  if (mutations.length === 0) {
501
1038
  return;
502
1039
  }
503
- await this.applyMutex.run(async () => {
504
- const acceptedMutationIds = await this.persistCollectionMutationsUnsafe(mutations);
505
- const acceptedMutationIdSet = new Set(acceptedMutationIds);
506
- const acceptedMutations = mutations.filter(
507
- (mutation) => acceptedMutationIdSet.has(mutation.mutationId)
508
- );
509
- if (acceptedMutations.length !== mutations.length) {
510
- throw new Error(
511
- `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)
512
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);
513
1067
  }
514
- this.confirmMutationsSyncUnsafe(acceptedMutations);
515
- });
1068
+ throw error;
1069
+ }
516
1070
  }
517
1071
  async acceptTransactionMutations(transaction) {
1072
+ this.throwIfTerminal();
518
1073
  const collectionMutations = this.filterMutationsForCollection(
519
1074
  transaction.mutations
520
1075
  );
@@ -525,11 +1080,33 @@ class PersistedCollectionRuntime {
525
1080
  }
526
1081
  cleanup() {
527
1082
  this.advanceLifecycle();
528
- this.coordinatorUnsubscribe?.();
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?.());
529
1104
  this.coordinatorUnsubscribe = null;
530
- this.indexAddedUnsubscribe?.();
1105
+ attempt(() => this.remoteSubsetOwnerUnsubscribe?.());
1106
+ this.remoteSubsetOwnerUnsubscribe = null;
1107
+ attempt(() => this.indexAddedUnsubscribe?.());
531
1108
  this.indexAddedUnsubscribe = null;
532
- this.indexRemovedUnsubscribe?.();
1109
+ attempt(() => this.indexRemovedUnsubscribe?.());
533
1110
  this.indexRemovedUnsubscribe = null;
534
1111
  if (this.remoteEnsureRetryTimer !== null) {
535
1112
  clearTimeout(this.remoteEnsureRetryTimer);
@@ -537,6 +1114,7 @@ class PersistedCollectionRuntime {
537
1114
  }
538
1115
  this.pendingRemoteSubsetEnsures.clear();
539
1116
  this.activeSubsets.clear();
1117
+ this.hydratedDemands.clear();
540
1118
  for (const transaction of this.queuedHydrationTransactions) {
541
1119
  transaction.rejectApplied?.(new db.SyncTransactionAbortedError());
542
1120
  }
@@ -544,13 +1122,28 @@ class PersistedCollectionRuntime {
544
1122
  this.queuedTxCommitted.length = 0;
545
1123
  this.clearSyncControls();
546
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
+ });
547
1133
  }
548
1134
  advanceLifecycle() {
549
1135
  this.lifecycleGeneration++;
550
- this.started = false;
1136
+ this.persistedReadiness?.set({ status: `loading` });
1137
+ this.startupSettled = false;
1138
+ this.hydratedDemands.clear();
551
1139
  this.startupMetadataPromise = null;
552
1140
  this.startPromise = null;
553
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`);
554
1147
  }
555
1148
  withInternalApply(task) {
556
1149
  this.internalApplyDepth++;
@@ -566,8 +1159,8 @@ class PersistedCollectionRuntime {
566
1159
  }
567
1160
  return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
568
1161
  }
569
- loadSubsetRowsUnsafe(options) {
570
- return this.persistence.adapter.loadSubset(this.collectionId, options, {
1162
+ loadSubsetRowsUnsafe(options, adapter) {
1163
+ return adapter.loadSubset(this.collectionId, options, {
571
1164
  requiredIndexSignatures: this.getRequiredIndexSignatures()
572
1165
  });
573
1166
  }
@@ -581,30 +1174,184 @@ class PersistedCollectionRuntime {
581
1174
  );
582
1175
  }
583
1176
  async scanPersistedRows(options) {
1177
+ this.throwIfTerminal();
584
1178
  return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options));
585
1179
  }
586
- async hydrateSubsetUnsafe(options, config) {
587
- this.hydratingGeneration = config.lifecycleGeneration;
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;
588
1187
  try {
589
- const rows = await this.loadSubsetRowsUnsafe(options);
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);
590
1221
  if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
591
- this.applyRowsToCollection(rows);
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);
592
1247
  } finally {
593
- if (this.hydratingGeneration === config.lifecycleGeneration) {
594
- this.hydratingGeneration = null;
1248
+ if (this.activeHydrationContext === hydrationContext) {
1249
+ this.activeHydrationContext = void 0;
595
1250
  }
596
1251
  }
597
- await this.flushQueuedHydrationTransactionsUnsafe();
598
- await this.flushQueuedTxCommittedUnsafe();
599
- if (config.requestRemoteEnsure) {
600
- this.queueRemoteSubsetEnsure(options);
1252
+ if (replayFailure && config.rejectBufferedReplayFailure) {
1253
+ throw replayFailure.reason;
601
1254
  }
602
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);
1347
+ }
1348
+ this.queuedHydrationTransactions.length = 0;
1349
+ }
603
1350
  applyRowsToCollection(rows) {
604
1351
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
605
- return;
1352
+ return true;
606
1353
  }
607
- this.withInternalApply(() => {
1354
+ return this.withInternalApply(() => {
608
1355
  this.syncControls.begin?.({ immediate: true });
609
1356
  for (const row of rows) {
610
1357
  if (this.collection?._hasHydratedKey(row.key)) {
@@ -616,18 +1363,41 @@ class PersistedCollectionRuntime {
616
1363
  metadata: row.metadata
617
1364
  });
618
1365
  }
619
- this.syncControls.commit?.();
1366
+ return this.syncControls.commit?.() ?? true;
620
1367
  });
621
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
+ }
622
1392
  replaceCollectionSnapshot(rows, collectionMetadata) {
623
1393
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
624
- return;
1394
+ return true;
625
1395
  }
626
1396
  const nextMetadata = new Map(
627
1397
  collectionMetadata.map(({ key, value }) => [key, value])
628
1398
  );
629
1399
  const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
630
- this.withInternalApply(() => {
1400
+ return this.withInternalApply(() => {
631
1401
  this.syncControls.begin?.({ immediate: true });
632
1402
  this.syncControls.truncate?.();
633
1403
  for (const row of rows) {
@@ -645,17 +1415,22 @@ class PersistedCollectionRuntime {
645
1415
  nextMetadata.forEach((value, key) => {
646
1416
  this.syncControls.metadata?.collection.set(key, value);
647
1417
  });
648
- this.syncControls.commit?.();
1418
+ return this.syncControls.commit?.() ?? true;
649
1419
  });
650
1420
  }
651
- async flushQueuedHydrationTransactionsUnsafe() {
1421
+ async flushQueuedHydrationTransactionsUnsafe(adapter) {
1422
+ let operationFailure;
652
1423
  while (this.queuedHydrationTransactions.length > 0) {
653
1424
  const transaction = this.queuedHydrationTransactions.shift();
654
1425
  if (!transaction) {
655
1426
  continue;
656
1427
  }
657
1428
  try {
658
- await this.applyBufferedSyncTransactionUnsafe(transaction);
1429
+ const outcome = await this.applyBufferedSyncTransactionUnsafe(
1430
+ transaction,
1431
+ adapter
1432
+ );
1433
+ operationFailure ??= outcome.hydrationFailure;
659
1434
  } catch (error) {
660
1435
  transaction.rejectApplied?.(error);
661
1436
  for (const abandoned of this.queuedHydrationTransactions) {
@@ -665,103 +1440,126 @@ class PersistedCollectionRuntime {
665
1440
  throw error;
666
1441
  }
667
1442
  }
1443
+ return operationFailure;
668
1444
  }
669
- async applyBufferedSyncTransactionUnsafe(transaction) {
670
- if (transaction.signal?.aborted) {
671
- transaction.rejectApplied?.(new db.SyncTransactionAbortedError());
672
- return;
673
- }
674
- const { begin, write, commit, truncate, metadata } = this.syncControls;
675
- if (!begin || !write || !commit) {
676
- transaction.rejectApplied?.(new db.SyncTransactionAbortedError());
677
- return;
678
- }
679
- const applyToCollection = () => {
680
- begin();
681
- if (transaction.truncate) {
682
- truncate?.();
683
- }
684
- for (const operation of transaction.operations) {
685
- if (operation.type === `delete`) {
686
- write({
687
- type: `delete`,
688
- key: operation.key
689
- });
690
- } else {
691
- write({
692
- type: `update`,
693
- value: operation.value,
694
- metadata: operation.metadata
695
- });
696
- }
697
- }
698
- for (const [key, metadataWrite] of transaction.rowMetadataWrites) {
699
- if (metadataWrite.type === `delete`) {
700
- metadata?.row.delete(key);
701
- } else {
702
- metadata?.row.set(key, metadataWrite.value);
703
- }
704
- }
705
- for (const [key, metadataWrite] of transaction.collectionMetadataWrites) {
706
- if (metadataWrite.type === `delete`) {
707
- metadata?.collection.delete(key);
708
- } else {
709
- metadata?.collection.set(key, metadataWrite.value);
710
- }
711
- }
712
- return commit(transaction.signal);
713
- };
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;
714
1451
  try {
715
- 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;
716
1455
  if (applied !== true) {
717
- await applied;
1456
+ this.sourcePublicationWaitDepth++;
1457
+ try {
1458
+ await applied;
1459
+ } finally {
1460
+ this.sourcePublicationWaitDepth--;
1461
+ }
718
1462
  }
1463
+ this.throwIfLifecycleReplaced(transaction.lifecycleGeneration);
719
1464
  if (!transaction.internal) {
720
- await this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction);
1465
+ await this.persistAndBroadcastExternalSyncTransactionUnsafe(
1466
+ transaction,
1467
+ scopedAdapter
1468
+ );
721
1469
  }
722
1470
  transaction.resolveApplied?.();
1471
+ return { applied: true };
723
1472
  } catch (error) {
724
- transaction.rejectApplied?.(error);
725
- 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;
726
1499
  }
1500
+ return this.markTerminalFailure(error, transaction.lifecycleGeneration);
727
1501
  }
728
- async persistAndBroadcastExternalSyncTransactionUnsafe(transaction) {
1502
+ async persistAndBroadcastExternalSyncTransactionUnsafe(transaction, scopedAdapter) {
729
1503
  if (transaction.internal) {
730
1504
  return;
731
1505
  }
732
- const streamPosition = this.nextLocalStreamPosition();
733
- if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
734
- this.publishTxCommittedEvent(
735
- this.createTxCommittedPayload({
736
- term: streamPosition.term,
737
- seq: streamPosition.seq,
738
- txId: db.safeRandomUUID(),
739
- latestRowVersion: streamPosition.rowVersion,
740
- changedRows: [],
741
- deletedKeys: [],
742
- requiresFullReload: true
743
- })
1506
+ this.throwIfLifecycleReplaced(transaction.lifecycleGeneration);
1507
+ if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
1508
+ return;
1509
+ }
1510
+ const streamPosition = this.nextLocalStreamPosition();
1511
+ const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
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
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
744
1560
  );
745
- return;
746
1561
  }
747
- const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
748
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
749
- this.publishTxCommittedEvent(
750
- this.createTxCommittedPayload({
751
- term: tx.term,
752
- seq: tx.seq,
753
- txId: tx.txId,
754
- latestRowVersion: tx.rowVersion,
755
- requiresFullReload: transaction.truncate,
756
- changedRows: transaction.operations.filter((operation) => operation.type === `update`).map((operation) => ({
757
- key: operation.key,
758
- value: operation.value
759
- })),
760
- deletedKeys: transaction.operations.filter((operation) => operation.type === `delete`).map((operation) => operation.key),
761
- rowMetadataMutations: tx.rowMetadataMutations,
762
- collectionMetadataMutations: tx.collectionMetadataMutations
763
- })
764
- );
1562
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
765
1563
  }
766
1564
  createPersistedTxFromOperations(transaction, streamPosition) {
767
1565
  return {
@@ -826,11 +1624,11 @@ class PersistedCollectionRuntime {
826
1624
  })
827
1625
  };
828
1626
  }
829
- confirmMutationsSyncUnsafe(mutations) {
1627
+ async confirmMutationsSyncUnsafe(mutations) {
830
1628
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
831
1629
  return;
832
1630
  }
833
- this.withInternalApply(() => {
1631
+ const applied = this.withInternalApply(() => {
834
1632
  this.syncControls.begin?.({ immediate: true });
835
1633
  for (const mutation of mutations) {
836
1634
  if (mutation.type === `delete`) {
@@ -845,8 +1643,11 @@ class PersistedCollectionRuntime {
845
1643
  });
846
1644
  }
847
1645
  }
848
- this.syncControls.commit?.();
1646
+ return this.syncControls.commit?.() ?? true;
849
1647
  });
1648
+ if (applied !== true) {
1649
+ await applied;
1650
+ }
850
1651
  }
851
1652
  filterMutationsForCollection(mutations) {
852
1653
  const collection = this.collection;
@@ -857,7 +1658,8 @@ class PersistedCollectionRuntime {
857
1658
  return mutation.collection.id === this.collectionId;
858
1659
  });
859
1660
  }
860
- async persistCollectionMutationsUnsafe(mutations) {
1661
+ async persistCollectionMutationsUnsafe(mutations, lifecycleGeneration) {
1662
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
861
1663
  if (this.persistence.coordinator.requestApplyLocalMutations) {
862
1664
  const envelopeMutations = mutations.map(
863
1665
  (mutation) => toPersistedMutationEnvelope(
@@ -868,7 +1670,19 @@ class PersistedCollectionRuntime {
868
1670
  this.collectionId,
869
1671
  envelopeMutations
870
1672
  );
1673
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
871
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
+ }
872
1686
  throw new Error(
873
1687
  `failed to apply local mutations through coordinator: ${response.error}`
874
1688
  );
@@ -896,7 +1710,8 @@ class PersistedCollectionRuntime {
896
1710
  }
897
1711
  const streamPosition = this.nextLocalStreamPosition();
898
1712
  const tx = this.createPersistedTxFromMutations(mutations, streamPosition);
899
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
1713
+ await this.applyCommittedTx(tx, lifecycleGeneration);
1714
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
900
1715
  this.publishTxCommittedEvent(
901
1716
  this.createTxCommittedPayload({
902
1717
  term: tx.term,
@@ -993,15 +1808,23 @@ class PersistedCollectionRuntime {
993
1808
  }
994
1809
  return id;
995
1810
  }
1811
+ canRouteRemoteDemandThroughCoordinator() {
1812
+ if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1813
+ return false;
1814
+ }
1815
+ return !this.persistence.coordinator.isLeader(this.collectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
1816
+ }
996
1817
  queueRemoteSubsetEnsure(options) {
997
- 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) {
998
1819
  return;
999
1820
  }
1000
- 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);
1001
1824
  void this.flushPendingRemoteSubsetEnsures();
1002
1825
  }
1003
1826
  scheduleRemoteEnsureRetry() {
1004
- if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
1827
+ if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1005
1828
  return;
1006
1829
  }
1007
1830
  if (this.pendingRemoteSubsetEnsures.size === 0 || this.remoteEnsureRetryTimer !== null) {
@@ -1013,7 +1836,7 @@ class PersistedCollectionRuntime {
1013
1836
  }, REMOTE_ENSURE_RETRY_DELAY_MS);
1014
1837
  }
1015
1838
  async flushPendingRemoteSubsetEnsures() {
1016
- if (this.mode !== `sync-present` || !this.persistence.coordinator.requestEnsureRemoteSubset) {
1839
+ if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1017
1840
  return;
1018
1841
  }
1019
1842
  if (this.remoteEnsureRetryTimer !== null) {
@@ -1021,6 +1844,10 @@ class PersistedCollectionRuntime {
1021
1844
  this.remoteEnsureRetryTimer = null;
1022
1845
  }
1023
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
+ }
1024
1851
  try {
1025
1852
  await this.persistence.coordinator.requestEnsureRemoteSubset(
1026
1853
  this.collectionId,
@@ -1028,7 +1855,11 @@ class PersistedCollectionRuntime {
1028
1855
  );
1029
1856
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1030
1857
  } catch (error) {
1031
- 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
+ }
1032
1863
  }
1033
1864
  }
1034
1865
  this.scheduleRemoteEnsureRetry();
@@ -1037,6 +1868,10 @@ class PersistedCollectionRuntime {
1037
1868
  if (this.coordinatorUnsubscribe) {
1038
1869
  return;
1039
1870
  }
1871
+ this.persistence.coordinator.setAdapterForCollection?.(
1872
+ this.collectionId,
1873
+ this.persistence.adapter
1874
+ );
1040
1875
  this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
1041
1876
  this.collectionId,
1042
1877
  (message) => {
@@ -1048,15 +1883,22 @@ class PersistedCollectionRuntime {
1048
1883
  if (message.collectionId !== this.collectionId) {
1049
1884
  return;
1050
1885
  }
1886
+ if (this.getCurrentTerminalFailure()) {
1887
+ return;
1888
+ }
1051
1889
  const { payload } = message;
1890
+ const lifecycleGeneration = this.lifecycleGeneration;
1052
1891
  const isSelf = message.senderId === this.persistence.coordinator.getNodeId();
1053
1892
  if (isTxCommittedPayload(payload)) {
1054
1893
  if (this.isHydratingNow()) {
1055
- this.queuedTxCommitted.push(payload);
1894
+ this.queuedTxCommitted.push({
1895
+ txCommitted: payload,
1896
+ lifecycleGeneration
1897
+ });
1056
1898
  return;
1057
1899
  }
1058
- void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload)).catch((error) => {
1059
- 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);
1060
1902
  });
1061
1903
  return;
1062
1904
  }
@@ -1064,8 +1906,15 @@ class PersistedCollectionRuntime {
1064
1906
  return;
1065
1907
  }
1066
1908
  if (isCollectionResetPayload(payload)) {
1067
- void this.applyMutex.run(() => this.truncateAndReloadUnsafe()).catch((error) => {
1068
- 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);
1069
1918
  });
1070
1919
  }
1071
1920
  }
@@ -1075,10 +1924,14 @@ class PersistedCollectionRuntime {
1075
1924
  if (!queued) {
1076
1925
  continue;
1077
1926
  }
1078
- await this.processCommittedTxUnsafe(queued);
1927
+ await this.processCommittedTxUnsafe(
1928
+ queued.txCommitted,
1929
+ queued.lifecycleGeneration
1930
+ );
1079
1931
  }
1080
1932
  }
1081
- async processCommittedTxUnsafe(txCommitted) {
1933
+ async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
1934
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1082
1935
  if (txCommitted.term < this.latestTerm) {
1083
1936
  return;
1084
1937
  }
@@ -1089,7 +1942,8 @@ class PersistedCollectionRuntime {
1089
1942
  const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
1090
1943
  const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
1091
1944
  if (hasGap) {
1092
- await this.recoverFromSeqGapUnsafe();
1945
+ await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
1946
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1093
1947
  if (txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
1094
1948
  return;
1095
1949
  }
@@ -1099,70 +1953,109 @@ class PersistedCollectionRuntime {
1099
1953
  txCommitted.seq,
1100
1954
  txCommitted.latestRowVersion
1101
1955
  );
1102
- 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
+ }
1103
1963
  }
1104
- async recoverFromSeqGapUnsafe() {
1964
+ async recoverFromSeqGapUnsafe(lifecycleGeneration) {
1965
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1105
1966
  if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
1967
+ let pullResponse;
1106
1968
  try {
1107
- const pullResponse = await this.persistence.coordinator.pullSince(
1969
+ pullResponse = await this.persistence.coordinator.pullSince(
1108
1970
  this.collectionId,
1109
1971
  this.latestRowVersion
1110
1972
  );
1973
+ } catch (error) {
1974
+ console.warn(`Failed pullSince recovery attempt:`, error);
1975
+ }
1976
+ if (pullResponse) {
1977
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
1111
1978
  if (pullResponse.ok) {
1112
1979
  this.observeStreamPosition(
1113
1980
  pullResponse.latestTerm,
1114
1981
  pullResponse.latestSeq,
1115
1982
  pullResponse.latestRowVersion
1116
1983
  );
1117
- if (pullResponse.requiresFullReload || !pullResponse.deltas) {
1118
- await this.reloadActiveSubsetsUnsafe();
1984
+ if (pullResponse.requiresFullReload) {
1985
+ await this.runInHydrationScope(
1986
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1987
+ );
1119
1988
  return;
1120
1989
  }
1121
- for (const delta of pullResponse.deltas) {
1122
- await this.invalidateFromCommittedTxUnsafe({
1123
- type: `tx:committed`,
1124
- term: pullResponse.latestTerm,
1125
- seq: pullResponse.latestSeq,
1126
- txId: delta.txId,
1127
- latestRowVersion: delta.latestRowVersion,
1128
- requiresFullReload: false,
1129
- changedRows: delta.changedRows,
1130
- deletedKeys: delta.deletedKeys,
1131
- rowMetadataMutations: delta.rowMetadataMutations,
1132
- collectionMetadataMutations: delta.collectionMetadataMutations
1133
- });
1990
+ const deltas = pullResponse.deltas;
1991
+ if (!deltas) {
1992
+ await this.runInHydrationScope(
1993
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1994
+ );
1995
+ return;
1134
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
+ });
1135
2018
  return;
1136
2019
  }
1137
- } catch (error) {
1138
- console.warn(`Failed pullSince recovery attempt:`, error);
1139
2020
  }
1140
2021
  }
1141
- await this.truncateAndReloadUnsafe();
2022
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2023
+ await this.runInHydrationScope(
2024
+ (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
2025
+ );
1142
2026
  if (this.mode === `sync-present`) {
1143
2027
  for (const options of this.activeSubsets.values()) {
1144
2028
  this.queueRemoteSubsetEnsure(options);
1145
2029
  }
1146
2030
  }
1147
2031
  }
1148
- async truncateAndReloadUnsafe() {
2032
+ async truncateAndReloadUnsafe(adapter, lifecycleGeneration = this.lifecycleGeneration) {
2033
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2034
+ this.hydratedDemands.clear();
2035
+ this.resetSequence++;
1149
2036
  if (this.syncControls.begin && this.syncControls.commit) {
1150
- this.withInternalApply(() => {
2037
+ const applied = this.withInternalApply(() => {
1151
2038
  this.syncControls.begin?.({ immediate: true });
1152
2039
  this.syncControls.truncate?.();
1153
- this.syncControls.commit?.();
2040
+ return this.syncControls.commit?.() ?? true;
1154
2041
  });
2042
+ if (applied !== true) await applied;
1155
2043
  }
1156
- await this.reloadActiveSubsetsUnsafe();
2044
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2045
+ await this.reloadActiveSubsetsUnsafe(adapter);
1157
2046
  }
1158
- async invalidateFromCommittedTxUnsafe(txCommitted) {
2047
+ async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
2048
+ const reloadActiveSubsets = () => this.runInHydrationScope(
2049
+ (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
2050
+ adapter
2051
+ );
1159
2052
  if (txCommitted.requiresFullReload) {
1160
- await this.reloadActiveSubsetsUnsafe();
2053
+ await reloadActiveSubsets();
1161
2054
  return;
1162
2055
  }
1163
2056
  const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
1164
2057
  if (changedKeyCount > TARGETED_INVALIDATION_KEY_LIMIT) {
1165
- await this.reloadActiveSubsetsUnsafe();
2058
+ await reloadActiveSubsets();
1166
2059
  return;
1167
2060
  }
1168
2061
  const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
@@ -1172,13 +2065,13 @@ class PersistedCollectionRuntime {
1172
2065
  await this.applyTargetedInvalidationUnsafe(txCommitted);
1173
2066
  return;
1174
2067
  }
1175
- await this.reloadActiveSubsetsUnsafe();
2068
+ await reloadActiveSubsets();
1176
2069
  }
1177
2070
  async applyTargetedInvalidationUnsafe(txCommitted) {
1178
2071
  const subsetEvaluators = Array.from(this.activeSubsets.values()).map(
1179
2072
  (opt) => opt.where ? db.compileSingleRowExpression(opt.where) : null
1180
2073
  );
1181
- this.withInternalApply(() => {
2074
+ const applied = this.withInternalApply(() => {
1182
2075
  this.syncControls.begin?.({ immediate: true });
1183
2076
  for (const {
1184
2077
  key: changedKey,
@@ -1217,19 +2110,25 @@ class PersistedCollectionRuntime {
1217
2110
  );
1218
2111
  }
1219
2112
  });
1220
- this.syncControls.commit?.();
2113
+ return this.syncControls.commit?.() ?? true;
1221
2114
  });
2115
+ if (applied !== true) await applied;
1222
2116
  }
1223
- async reloadActiveSubsetsUnsafe() {
2117
+ async reloadActiveSubsetsUnsafe(adapter) {
1224
2118
  const lifecycleGeneration = this.lifecycleGeneration;
2119
+ const truncateGeneration = this.sourceTruncateGeneration;
1225
2120
  const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
2121
+ this.hydratedDemands.clear();
2122
+ this.hydrationSequence++;
2123
+ const hydrationContext = { suppliedRowKeys: /* @__PURE__ */ new Set() };
2124
+ this.activeHydrationContext = hydrationContext;
1226
2125
  this.hydratingGeneration = lifecycleGeneration;
1227
2126
  try {
1228
2127
  const mergedRows = /* @__PURE__ */ new Map();
1229
- const collectionMetadata = await this.loadCollectionMetadataSnapshot();
2128
+ const collectionMetadata = await this.loadCollectionMetadataSnapshot(adapter);
1230
2129
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1231
2130
  for (const options of activeSubsetOptions) {
1232
- const subsetRows = await this.loadSubsetRowsUnsafe(options);
2131
+ const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter);
1233
2132
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1234
2133
  for (const row of subsetRows) {
1235
2134
  mergedRows.set(row.key, {
@@ -1238,7 +2137,10 @@ class PersistedCollectionRuntime {
1238
2137
  });
1239
2138
  }
1240
2139
  }
1241
- this.replaceCollectionSnapshot(
2140
+ for (const key of mergedRows.keys()) {
2141
+ hydrationContext.suppliedRowKeys.add(key);
2142
+ }
2143
+ const applied = this.replaceCollectionSnapshot(
1242
2144
  Array.from(mergedRows.entries()).map(([key, row]) => ({
1243
2145
  key,
1244
2146
  value: row.value,
@@ -1246,13 +2148,25 @@ class PersistedCollectionRuntime {
1246
2148
  })),
1247
2149
  collectionMetadata
1248
2150
  );
2151
+ if (applied !== true) await applied;
1249
2152
  } finally {
1250
- if (this.hydratingGeneration === lifecycleGeneration) {
2153
+ if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === lifecycleGeneration) {
1251
2154
  this.hydratingGeneration = null;
1252
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
+ }
1253
2169
  }
1254
- await this.flushQueuedHydrationTransactionsUnsafe();
1255
- await this.flushQueuedTxCommittedUnsafe();
1256
2170
  }
1257
2171
  attachIndexLifecycleListeners() {
1258
2172
  if (!this.collection || this.indexAddedUnsubscribe || this.indexRemovedUnsubscribe) {
@@ -1268,14 +2182,26 @@ class PersistedCollectionRuntime {
1268
2182
  }
1269
2183
  );
1270
2184
  }
1271
- async bootstrapPersistedIndexes(indexMetadataSnapshot) {
2185
+ async bootstrapPersistedIndexes(indexMetadataSnapshot, adapter = this.persistence.adapter) {
1272
2186
  const collection = this.collection;
1273
2187
  if (!collection && !indexMetadataSnapshot) {
1274
- return;
2188
+ return /* @__PURE__ */ new Set();
1275
2189
  }
1276
2190
  const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
2191
+ const completedLocalIndexSignatures = /* @__PURE__ */ new Set();
2192
+ for (const metadata of indexMetadata) {
2193
+ if (await this.ensureLocalPersistedIndex(metadata, adapter)) {
2194
+ completedLocalIndexSignatures.add(metadata.signature);
2195
+ }
2196
+ }
2197
+ return completedLocalIndexSignatures;
2198
+ }
2199
+ async requestCoordinatorPersistedIndexes(indexMetadata, completedLocalIndexSignatures) {
1277
2200
  for (const metadata of indexMetadata) {
1278
- await this.ensurePersistedIndex(metadata);
2201
+ await this.requestCoordinatorPersistedIndex(
2202
+ metadata,
2203
+ completedLocalIndexSignatures.has(metadata.signature)
2204
+ );
1279
2205
  }
1280
2206
  }
1281
2207
  buildPersistedIndexSpec(index) {
@@ -1288,22 +2214,36 @@ class PersistedCollectionRuntime {
1288
2214
  }
1289
2215
  };
1290
2216
  }
1291
- 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) {
1292
2225
  const spec = this.buildPersistedIndexSpec(indexMetadata);
1293
2226
  try {
1294
- await this.persistence.adapter.ensureIndex(
2227
+ await adapter.ensureIndex(
1295
2228
  this.collectionId,
1296
2229
  indexMetadata.signature,
1297
2230
  spec
1298
2231
  );
2232
+ return true;
1299
2233
  } catch (error) {
1300
2234
  console.warn(`Failed to ensure persisted index in adapter:`, error);
2235
+ return false;
1301
2236
  }
2237
+ }
2238
+ async requestCoordinatorPersistedIndex(indexMetadata, completedLocally) {
2239
+ const spec = this.buildPersistedIndexSpec(indexMetadata);
1302
2240
  try {
1303
2241
  await this.persistence.coordinator.requestEnsurePersistedIndex(
1304
2242
  this.collectionId,
1305
2243
  indexMetadata.signature,
1306
- spec
2244
+ spec,
2245
+ completedLocally ? this.persistence.adapter : void 0,
2246
+ completedLocally
1307
2247
  );
1308
2248
  } catch (error) {
1309
2249
  console.warn(
@@ -1326,71 +2266,327 @@ class PersistedCollectionRuntime {
1326
2266
  }
1327
2267
  }
1328
2268
  }
1329
- function createWrappedSyncConfig(sourceSyncConfig, runtime) {
2269
+ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness) {
1330
2270
  return {
1331
2271
  ...sourceSyncConfig,
1332
2272
  sync: (params) => {
1333
2273
  const transactionStack = [];
2274
+ const pendingPublicationTransactions = [];
1334
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
+ };
1335
2354
  let fullStartPromise = null;
2355
+ let sourceResultPromise = null;
2356
+ let resolveSourceResultAssigned;
2357
+ const sourceResultAssigned = new Promise((resolve) => {
2358
+ resolveSourceResultAssigned = resolve;
2359
+ });
1336
2360
  const startupState = { cleanedUp: false };
2361
+ const isCleanedUp = () => startupState.cleanedUp;
1337
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
+ };
1338
2466
  runtime.setSyncControls({
1339
2467
  begin: params.begin,
1340
2468
  write: params.write,
1341
2469
  commit: params.commit,
1342
2470
  truncate: params.truncate,
1343
- metadata: params.metadata ?? null
2471
+ metadata: params.metadata ?? null,
2472
+ markError: params.markError
1344
2473
  });
1345
2474
  runtime.setCollection(
1346
2475
  params.collection
1347
2476
  );
1348
- const wrappedParams = {
1349
- ...params,
1350
- markReady: () => {
2477
+ const persistenceCapability = {
2478
+ protocol: db.SYNC_PERSISTENCE_PROTOCOL,
2479
+ version: db.SYNC_PERSISTENCE_VERSION,
2480
+ hydrateBaseline: async () => {
1351
2481
  if (startupState.cleanedUp) return;
1352
- void (fullStartPromise ?? runtime.ensureStarted()).then(() => {
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 () => {
1353
2491
  if (startupState.cleanedUp) return;
1354
- params.markReady();
1355
- }).catch((error) => {
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: () => {
1356
2500
  if (startupState.cleanedUp) return;
1357
- console.warn(
1358
- `Failed persisted sync startup before markReady:`,
1359
- error
1360
- );
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
+ };
2511
+ const wrappedParams = {
2512
+ ...params,
2513
+ markError: reportUpstreamError,
2514
+ markReady: () => {
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;
1361
2526
  params.markReady();
1362
- });
2527
+ }).catch(() => void 0);
1363
2528
  },
1364
2529
  begin: (options) => {
1365
- if (startupState.cleanedUp) return;
2530
+ if (startupState.cleanedUp) return void 0;
2531
+ const terminalFailure = getTerminalFailure();
2532
+ const internal = runtime.isApplyingInternally();
1366
2533
  const transaction = {
1367
2534
  operations: [],
2535
+ partialUpdateOperationIndexes: /* @__PURE__ */ new Set(),
1368
2536
  rowMetadataWrites: /* @__PURE__ */ new Map(),
1369
2537
  collectionMetadataWrites: /* @__PURE__ */ new Map(),
2538
+ deferredHydrationMetadataDeleteKeys: /* @__PURE__ */ new Set(),
1370
2539
  truncate: false,
1371
- internal: runtime.isApplyingInternally(),
1372
- 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 }
1373
2549
  };
1374
2550
  transactionStack.push(transaction);
1375
- if (!transaction.queuedBecauseHydrating) {
1376
- params.begin(options);
1377
- }
1378
2551
  },
1379
2552
  write: (message) => {
1380
2553
  if (startupState.cleanedUp) return;
1381
- const normalization = runtime.normalizeSyncWriteMessage(message);
1382
2554
  const openTransaction = getOpenTransaction();
1383
- if (!openTransaction) {
1384
- params.write(normalization.forwardMessage);
2555
+ const terminalFailure = openTransaction?.terminalFailure ?? getTerminalFailure();
2556
+ if (terminalFailure) {
2557
+ if (openTransaction)
2558
+ openTransaction.terminalFailure = terminalFailure;
1385
2559
  return;
1386
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
+ }
1387
2578
  openTransaction.operations.push(normalization.operation);
2579
+ openTransaction.operationKeys.add(normalization.operation.key);
1388
2580
  if (normalization.operation.type === `delete`) {
1389
2581
  openTransaction.rowMetadataWrites.set(normalization.operation.key, {
1390
2582
  type: `delete`
1391
2583
  });
1392
2584
  } else if (message.type === `insert` && normalization.operation.metadata === void 0) {
1393
- if (!openTransaction.rowMetadataWrites.has(
2585
+ if (openTransaction.queuedBecauseHydrating) {
2586
+ openTransaction.deferredHydrationMetadataDeleteKeys.add(
2587
+ normalization.operation.key
2588
+ );
2589
+ } else if (!openTransaction.rowMetadataWrites.has(
1394
2590
  normalization.operation.key
1395
2591
  )) {
1396
2592
  openTransaction.rowMetadataWrites.set(
@@ -1406,13 +2602,10 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1406
2602
  value: normalization.operation.metadata
1407
2603
  });
1408
2604
  }
1409
- if (!openTransaction.queuedBecauseHydrating) {
1410
- params.write(normalization.forwardMessage);
1411
- }
1412
2605
  },
1413
2606
  metadata: params.metadata ? {
2607
+ persistence: persistenceCapability,
1414
2608
  row: {
1415
- whenHydrated: () => startupState.cleanedUp ? Promise.resolve() : runtime.ensureResumeBaselineHydrated(),
1416
2609
  get: (key) => {
1417
2610
  if (startupState.cleanedUp) return void 0;
1418
2611
  const openTransaction = getOpenTransaction();
@@ -1423,12 +2616,17 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1423
2616
  if (openTransaction?.truncate) {
1424
2617
  return void 0;
1425
2618
  }
2619
+ const pending = getPendingRowMetadataWrite(key);
2620
+ if (pending.found) {
2621
+ return pending.write.type === `delete` ? void 0 : pending.write.value;
2622
+ }
1426
2623
  return params.metadata.row.get(key);
1427
2624
  },
1428
- scanPersisted: (options) => startupState.cleanedUp ? Promise.resolve([]) : runtime.scanPersistedRows(options),
1429
2625
  set: (key, value) => {
1430
2626
  if (startupState.cleanedUp) return;
1431
2627
  const openTransaction = getOpenTransaction();
2628
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2629
+ return;
1432
2630
  if (!openTransaction) {
1433
2631
  throw new errors.InvalidPersistedCollectionConfigError(
1434
2632
  `metadata.row.set must be called within an open sync transaction`
@@ -1438,13 +2636,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1438
2636
  type: `set`,
1439
2637
  value
1440
2638
  });
1441
- if (!openTransaction.queuedBecauseHydrating) {
1442
- params.metadata.row.set(key, value);
1443
- }
1444
2639
  },
1445
2640
  delete: (key) => {
1446
2641
  if (startupState.cleanedUp) return;
1447
2642
  const openTransaction = getOpenTransaction();
2643
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2644
+ return;
1448
2645
  if (!openTransaction) {
1449
2646
  throw new errors.InvalidPersistedCollectionConfigError(
1450
2647
  `metadata.row.delete must be called within an open sync transaction`
@@ -1453,9 +2650,6 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1453
2650
  openTransaction.rowMetadataWrites.set(key, {
1454
2651
  type: `delete`
1455
2652
  });
1456
- if (!openTransaction.queuedBecauseHydrating) {
1457
- params.metadata.row.delete(key);
1458
- }
1459
2653
  }
1460
2654
  },
1461
2655
  collection: {
@@ -1466,11 +2660,17 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1466
2660
  if (pendingWrite) {
1467
2661
  return pendingWrite.type === `delete` ? void 0 : pendingWrite.value;
1468
2662
  }
2663
+ const pending = getPendingCollectionMetadataWrite(key);
2664
+ if (pending.found) {
2665
+ return pending.write.type === `delete` ? void 0 : pending.write.value;
2666
+ }
1469
2667
  return params.metadata.collection.get(key);
1470
2668
  },
1471
2669
  set: (key, value) => {
1472
2670
  if (startupState.cleanedUp) return;
1473
2671
  const openTransaction = getOpenTransaction();
2672
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2673
+ return;
1474
2674
  if (!openTransaction) {
1475
2675
  throw new errors.InvalidPersistedCollectionConfigError(
1476
2676
  `metadata.collection.set must be called within an open sync transaction`
@@ -1480,13 +2680,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1480
2680
  type: `set`,
1481
2681
  value
1482
2682
  });
1483
- if (!openTransaction.queuedBecauseHydrating) {
1484
- params.metadata.collection.set(key, value);
1485
- }
1486
2683
  },
1487
2684
  delete: (key) => {
1488
2685
  if (startupState.cleanedUp) return;
1489
2686
  const openTransaction = getOpenTransaction();
2687
+ if (openTransaction?.terminalFailure ?? getTerminalFailure())
2688
+ return;
1490
2689
  if (!openTransaction) {
1491
2690
  throw new errors.InvalidPersistedCollectionConfigError(
1492
2691
  `metadata.collection.delete must be called within an open sync transaction`
@@ -1495,15 +2694,31 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1495
2694
  openTransaction.collectionMetadataWrites.set(key, {
1496
2695
  type: `delete`
1497
2696
  });
1498
- if (!openTransaction.queuedBecauseHydrating) {
1499
- params.metadata.collection.delete(key);
1500
- }
1501
2697
  },
1502
2698
  list: (prefix) => {
1503
2699
  if (startupState.cleanedUp) return [];
1504
2700
  const merged = new Map(
1505
2701
  params.metadata.collection.list().map(({ key, value }) => [key, value])
1506
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
+ }
1507
2722
  const openTransaction = getOpenTransaction();
1508
2723
  if (openTransaction) {
1509
2724
  for (const [
@@ -1526,30 +2741,91 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1526
2741
  } : void 0,
1527
2742
  truncate: () => {
1528
2743
  if (startupState.cleanedUp) return;
2744
+ runtime.noteSourceTruncate();
1529
2745
  const openTransaction = getOpenTransaction();
2746
+ if (openTransaction?.terminalFailure ?? getTerminalFailure()) return;
1530
2747
  if (!openTransaction) {
1531
- params.truncate();
1532
- return;
2748
+ throw new db.NoPendingSyncTransactionWriteError();
1533
2749
  }
1534
2750
  openTransaction.operations = [];
2751
+ openTransaction.operationKeys.clear();
2752
+ openTransaction.partialUpdateOperationIndexes.clear();
1535
2753
  openTransaction.rowMetadataWrites.clear();
2754
+ openTransaction.deferredHydrationMetadataDeleteKeys.clear();
1536
2755
  openTransaction.truncate = true;
1537
- if (!openTransaction.queuedBecauseHydrating) {
1538
- params.truncate();
1539
- }
1540
2756
  },
1541
2757
  commit: (signal) => {
1542
2758
  if (startupState.cleanedUp) return true;
1543
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
+ }
1544
2771
  if (!openTransaction) {
1545
- return params.commit(signal);
2772
+ throw new db.NoPendingSyncTransactionCommitError();
1546
2773
  }
1547
- if (openTransaction.queuedBecauseHydrating) {
1548
- if (signal?.aborted) {
1549
- const aborted = Promise.reject(new db.SyncTransactionAbortedError());
1550
- void aborted.catch(() => void 0);
1551
- return aborted;
1552
- }
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()) {
1553
2829
  let resolveApplied;
1554
2830
  let rejectApplied;
1555
2831
  const applied2 = new Promise((resolve, reject) => {
@@ -1558,65 +2834,118 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1558
2834
  });
1559
2835
  void applied2.catch(() => void 0);
1560
2836
  runtime.queueHydrationBufferedTransaction({
1561
- operations: openTransaction.operations,
1562
- rowMetadataWrites: openTransaction.rowMetadataWrites,
1563
- collectionMetadataWrites: openTransaction.collectionMetadataWrites,
1564
- truncate: openTransaction.truncate,
1565
- internal: openTransaction.internal,
1566
- signal,
2837
+ ...transaction,
1567
2838
  resolveApplied,
1568
2839
  rejectApplied
1569
2840
  });
2841
+ settlePublicationAdmissionWaiters(openTransaction);
2842
+ settleRuntimeTransaction(openTransaction, applied2);
1570
2843
  return applied2;
1571
2844
  }
1572
- const applied = params.commit(signal);
1573
- if (!openTransaction.internal) {
1574
- const persistAfterApplication = async () => {
1575
- if (applied !== true) await applied;
1576
- await runtime.persistAndBroadcastExternalSyncTransaction({
1577
- operations: openTransaction.operations,
1578
- rowMetadataWrites: openTransaction.rowMetadataWrites,
1579
- collectionMetadataWrites: openTransaction.collectionMetadataWrites,
1580
- truncate: openTransaction.truncate,
1581
- internal: false
1582
- });
1583
- };
1584
- const persisted = persistAfterApplication();
1585
- void persisted.catch(() => void 0);
1586
- return persisted;
2845
+ let applied;
2846
+ try {
2847
+ applied = runtime.applyHydrationBufferedTransaction(transaction);
2848
+ } catch (error) {
2849
+ settlePublicationAdmissionWaiters(openTransaction, error);
2850
+ throw error;
1587
2851
  }
2852
+ settlePublicationAdmissionWaiters(openTransaction);
2853
+ settleRuntimeTransaction(openTransaction, applied);
1588
2854
  return applied;
1589
2855
  }
1590
2856
  };
1591
2857
  let sourceResult = {};
2858
+ let sourceResultSettled = false;
2859
+ let activeSourceSyncEntry;
1592
2860
  fullStartPromise = runtime.ensureStarted();
1593
- const sourceResultPromise = (async () => {
2861
+ void fullStartPromise.catch(() => void 0);
2862
+ sourceResultPromise = (async () => {
1594
2863
  await runtime.ensureStartupMetadataLoaded();
1595
2864
  if (startupState.cleanedUp) {
2865
+ sourceResultSettled = true;
1596
2866
  return sourceResult;
1597
2867
  }
1598
- sourceResult = normalizeSyncFnResult(
1599
- sourceSyncConfig.sync(wrappedParams)
1600
- );
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;
1601
2899
  return sourceResult;
1602
2900
  })();
2901
+ resolveSourceResultAssigned();
2902
+ void sourceResultPromise.catch((error) => {
2903
+ reportUpstreamError(error);
2904
+ });
1603
2905
  return {
1604
- cleanup: () => {
2906
+ cleanup: async () => {
1605
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
+ }
1606
2913
  acquisitions.clear();
1607
- sourceResult.cleanup?.();
1608
- runtime.cleanup();
1609
- 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;
1610
2944
  },
1611
- loadSubset: async (options) => {
2945
+ loadSubset: (options) => {
1612
2946
  const acquisition = { forwarded: false };
1613
2947
  acquisitions.set(options, acquisition);
1614
- await fullStartPromise;
1615
- const resolvedSourceResult = await sourceResultPromise;
1616
- if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
1617
- return;
1618
- }
1619
- return runtime.loadSubset(options, (loadOptions) => {
2948
+ const forwardUpstream = (resolvedSourceResult, loadOptions) => {
1620
2949
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
1621
2950
  return true;
1622
2951
  }
@@ -1628,10 +2957,42 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime) {
1628
2957
  acquisition.forwarded = false;
1629
2958
  throw error;
1630
2959
  }
1631
- });
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
+ })();
1632
2992
  },
1633
2993
  unloadSubset: (options) => {
1634
2994
  const acquisition = acquisitions.get(options);
2995
+ acquisition?.cancelAdmissionWait?.();
1635
2996
  acquisitions.delete(options);
1636
2997
  runtime.unloadSubset(
1637
2998
  options,
@@ -1650,23 +3011,19 @@ function createLoopbackSyncConfig(runtime) {
1650
3011
  write: params.write,
1651
3012
  commit: params.commit,
1652
3013
  truncate: params.truncate,
1653
- metadata: params.metadata ?? null
3014
+ metadata: params.metadata ?? null,
3015
+ markError: params.markError
1654
3016
  });
1655
3017
  runtime.setCollection(
1656
3018
  params.collection
1657
3019
  );
1658
3020
  void runtime.ensureStarted().then(() => {
3021
+ if (runtime.getCurrentTerminalFailure()) return;
1659
3022
  params.markReady();
1660
- }).catch((error) => {
1661
- console.warn(`Failed persisted loopback startup:`, error);
1662
- params.markReady();
1663
- });
3023
+ }).catch(() => void 0);
1664
3024
  return {
1665
- cleanup: () => {
1666
- runtime.cleanup();
1667
- runtime.clearSyncControls();
1668
- },
1669
- loadSubset: (options) => runtime.loadSubset(options),
3025
+ loadSubset: (options) => runtime.loadHydratedSubset(options) ?? runtime.loadSubset(options),
3026
+ cleanup: () => runtime.cleanup(),
1670
3027
  unloadSubset: (options) => runtime.unloadSubset(options)
1671
3028
  };
1672
3029
  },
@@ -1681,13 +3038,24 @@ function persistedCollectionOptions(options) {
1681
3038
  `persistedCollectionOptions requires a persistence adapter`
1682
3039
  );
1683
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;
1684
3048
  if (hasOwnSyncKey(options)) {
1685
3049
  if (!isValidSyncConfig(options.sync)) {
1686
3050
  throw new errors.InvalidSyncConfigError(
1687
3051
  `when the "sync" key is present it must provide a callable sync function`
1688
3052
  );
1689
3053
  }
1690
- const { schemaVersion: schemaVersion2, ...syncOptions } = options;
3054
+ const {
3055
+ schemaVersion: schemaVersion2,
3056
+ initialRender: _initialRender2,
3057
+ ...syncOptions
3058
+ } = options;
1691
3059
  const collectionId2 = syncOptions.id ?? `persisted-collection:${db.safeRandomUUID()}`;
1692
3060
  const persistence2 = resolvePersistenceForCollection(
1693
3061
  syncOptions.persistence,
@@ -1702,13 +3070,19 @@ function persistedCollectionOptions(options) {
1702
3070
  collectionId2,
1703
3071
  persistence2,
1704
3072
  syncOptions.syncMode ?? `eager`,
1705
- collectionId2
3073
+ collectionId2,
3074
+ persistedReadinessTracker
1706
3075
  );
1707
3076
  const result2 = {
1708
3077
  ...syncOptions,
1709
3078
  id: collectionId2,
1710
- sync: createWrappedSyncConfig(syncOptions.sync, runtime2),
1711
- persistence: persistence2
3079
+ sync: createWrappedSyncConfig(
3080
+ syncOptions.sync,
3081
+ runtime2,
3082
+ persistedReadinessTracker
3083
+ ),
3084
+ persistence: persistence2,
3085
+ ...persistedReadinessTracker ? { [db.PERSISTED_READINESS]: persistedReadinessTracker } : {}
1712
3086
  };
1713
3087
  return db.withCollectionConfigFactory(
1714
3088
  result2,
@@ -1718,7 +3092,11 @@ function persistedCollectionOptions(options) {
1718
3092
  })
1719
3093
  );
1720
3094
  }
1721
- const { schemaVersion, ...localOnlyOptions } = options;
3095
+ const {
3096
+ schemaVersion,
3097
+ initialRender: _initialRender,
3098
+ ...localOnlyOptions
3099
+ } = options;
1722
3100
  const collectionId = localOnlyOptions.id ?? `persisted-collection:${db.safeRandomUUID()}`;
1723
3101
  const persistence = resolvePersistenceForCollection(
1724
3102
  localOnlyOptions.persistence,
@@ -1733,32 +3111,39 @@ function persistedCollectionOptions(options) {
1733
3111
  collectionId,
1734
3112
  persistence,
1735
3113
  localOnlyOptions.syncMode ?? `eager`,
1736
- localOnlyOptions.id ?? DEFAULT_DB_NAME
3114
+ localOnlyOptions.id ?? DEFAULT_DB_NAME,
3115
+ persistedReadinessTracker
1737
3116
  );
1738
3117
  const wrappedOnInsert = async (params) => {
3118
+ const lifecycleGeneration = runtime.getLifecycleGeneration();
1739
3119
  const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
1740
3120
  params
1741
3121
  ) : void 0;
1742
3122
  await runtime.persistAndConfirmCollectionMutations(
1743
- params.transaction.mutations
3123
+ params.transaction.mutations,
3124
+ lifecycleGeneration
1744
3125
  );
1745
3126
  return handlerResult ?? {};
1746
3127
  };
1747
3128
  const wrappedOnUpdate = async (params) => {
3129
+ const lifecycleGeneration = runtime.getLifecycleGeneration();
1748
3130
  const handlerResult = localOnlyOptions.onUpdate ? await localOnlyOptions.onUpdate(
1749
3131
  params
1750
3132
  ) : void 0;
1751
3133
  await runtime.persistAndConfirmCollectionMutations(
1752
- params.transaction.mutations
3134
+ params.transaction.mutations,
3135
+ lifecycleGeneration
1753
3136
  );
1754
3137
  return handlerResult ?? {};
1755
3138
  };
1756
3139
  const wrappedOnDelete = async (params) => {
3140
+ const lifecycleGeneration = runtime.getLifecycleGeneration();
1757
3141
  const handlerResult = localOnlyOptions.onDelete ? await localOnlyOptions.onDelete(
1758
3142
  params
1759
3143
  ) : void 0;
1760
3144
  await runtime.persistAndConfirmCollectionMutations(
1761
- params.transaction.mutations
3145
+ params.transaction.mutations,
3146
+ lifecycleGeneration
1762
3147
  );
1763
3148
  return handlerResult ?? {};
1764
3149
  };
@@ -1778,6 +3163,7 @@ function persistedCollectionOptions(options) {
1778
3163
  ...localOnlyOptions,
1779
3164
  id: collectionId,
1780
3165
  persistence,
3166
+ ...persistedReadinessTracker ? { [db.PERSISTED_READINESS]: persistedReadinessTracker } : {},
1781
3167
  sync: createLoopbackSyncConfig(runtime),
1782
3168
  onInsert: wrappedOnInsert,
1783
3169
  onUpdate: wrappedOnUpdate,
@@ -1849,10 +3235,12 @@ function createPersistedTableName(collectionId, prefix = `c`) {
1849
3235
  const lengthPart = collectionId.length.toString(36);
1850
3236
  return `${prefix}_${hashPart}_${lengthPart}`;
1851
3237
  }
3238
+ exports.SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY = SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY;
1852
3239
  exports.SingleProcessCoordinator = SingleProcessCoordinator;
1853
3240
  exports.createPersistedTableName = createPersistedTableName;
1854
3241
  exports.decodePersistedStorageKey = decodePersistedStorageKey;
1855
3242
  exports.encodePersistedStorageKey = encodePersistedStorageKey;
3243
+ exports.forwardSQLiteDriverSharedLogicalScheduling = forwardSQLiteDriverSharedLogicalScheduling;
1856
3244
  exports.persistedCollectionOptions = persistedCollectionOptions;
1857
3245
  exports.validatePersistedCollectionCoordinator = validatePersistedCollectionCoordinator;
1858
3246
  //# sourceMappingURL=persisted.cjs.map