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

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