@tanstack/db-sqlite-persistence-core 0.4.7 → 0.4.9

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 (40) hide show
  1. package/README.md +32 -3
  2. package/dist/cjs/broadcast-coordinator.cjs +282 -43
  3. package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
  4. package/dist/cjs/broadcast-coordinator.d.cts +25 -8
  5. package/dist/cjs/committed-tx-anchor.cjs +9 -0
  6. package/dist/cjs/committed-tx-anchor.cjs.map +1 -0
  7. package/dist/cjs/committed-tx-anchor.d.cts +1 -0
  8. package/dist/cjs/index.cjs +4 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/persisted.cjs +1599 -313
  11. package/dist/cjs/persisted.cjs.map +1 -1
  12. package/dist/cjs/persisted.d.cts +106 -13
  13. package/dist/cjs/remote-subset-wire.cjs +11 -0
  14. package/dist/cjs/remote-subset-wire.cjs.map +1 -1
  15. package/dist/cjs/remote-subset-wire.d.cts +1 -0
  16. package/dist/cjs/sqlite-core-adapter.cjs +705 -64
  17. package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
  18. package/dist/cjs/sqlite-core-adapter.d.cts +53 -10
  19. package/dist/esm/broadcast-coordinator.d.ts +25 -8
  20. package/dist/esm/broadcast-coordinator.js +283 -44
  21. package/dist/esm/broadcast-coordinator.js.map +1 -1
  22. package/dist/esm/committed-tx-anchor.d.ts +1 -0
  23. package/dist/esm/committed-tx-anchor.js +9 -0
  24. package/dist/esm/committed-tx-anchor.js.map +1 -0
  25. package/dist/esm/index.js +6 -2
  26. package/dist/esm/persisted.d.ts +106 -13
  27. package/dist/esm/persisted.js +1601 -315
  28. package/dist/esm/persisted.js.map +1 -1
  29. package/dist/esm/remote-subset-wire.d.ts +1 -0
  30. package/dist/esm/remote-subset-wire.js +11 -0
  31. package/dist/esm/remote-subset-wire.js.map +1 -1
  32. package/dist/esm/sqlite-core-adapter.d.ts +53 -10
  33. package/dist/esm/sqlite-core-adapter.js +708 -67
  34. package/dist/esm/sqlite-core-adapter.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/broadcast-coordinator.ts +415 -62
  37. package/src/committed-tx-anchor.ts +13 -0
  38. package/src/persisted.ts +2283 -370
  39. package/src/remote-subset-wire.ts +12 -0
  40. package/src/sqlite-core-adapter.ts +891 -68
@@ -5,6 +5,58 @@ const errors = require("./errors.cjs");
5
5
  const sqliteValue = require("./sqlite-value.cjs");
6
6
  const remoteSubsetWire = require("./remote-subset-wire.cjs");
7
7
  const remoteSubsetOwner = require("./remote-subset-owner.cjs");
8
+ function eagerStorageTarget(collectionId) {
9
+ return { kind: `eager`, collectionId };
10
+ }
11
+ function managedStorageTarget(storageCollectionId, claimId) {
12
+ return { kind: `managed`, storageCollectionId, claimId };
13
+ }
14
+ function resolvePersistedStorageTarget(target, legacyClaimId) {
15
+ const input = target;
16
+ if (typeof input !== `object` || input === null) {
17
+ throw new errors.InvalidPersistedCollectionConfigError(
18
+ `SQLite persistence requires an explicit eager or managed storage target`
19
+ );
20
+ }
21
+ const fields = input;
22
+ if (fields.kind === `eager`) {
23
+ if (typeof fields.collectionId !== `string` || fields.collectionId.trim().length === 0) {
24
+ throw new errors.InvalidPersistedCollectionConfigError(
25
+ `An eager storage target requires a nonempty collectionId`
26
+ );
27
+ }
28
+ if (legacyClaimId !== void 0 || `claimId` in fields || `storageCollectionId` in fields) {
29
+ throw new errors.InvalidPersistedCollectionConfigError(
30
+ `An eager storage target cannot carry a persisted cache claim`
31
+ );
32
+ }
33
+ return { collectionId: fields.collectionId };
34
+ }
35
+ if (fields.kind !== `managed`) {
36
+ throw new errors.InvalidPersistedCollectionConfigError(
37
+ `Unknown SQLite persistence storage target kind`
38
+ );
39
+ }
40
+ if (typeof fields.storageCollectionId !== `string` || fields.storageCollectionId.trim().length === 0 || typeof fields.claimId !== `string` || fields.claimId.trim().length === 0) {
41
+ throw new errors.InvalidPersistedCollectionConfigError(
42
+ `A managed storage target requires a storageCollectionId and persisted cache claim`
43
+ );
44
+ }
45
+ if (`collectionId` in fields) {
46
+ throw new errors.InvalidPersistedCollectionConfigError(
47
+ `A managed storage target cannot carry an eager collectionId`
48
+ );
49
+ }
50
+ if (legacyClaimId !== void 0 && legacyClaimId !== fields.claimId) {
51
+ throw new errors.InvalidPersistedCollectionConfigError(
52
+ `The persisted cache claim does not match its storage target`
53
+ );
54
+ }
55
+ return {
56
+ collectionId: fields.storageCollectionId,
57
+ claimId: fields.claimId
58
+ };
59
+ }
8
60
  const SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY = /* @__PURE__ */ Symbol.for(
9
61
  `tanstack-db.sqlite-driver-supports-shared-logical-scheduling`
10
62
  );
@@ -46,6 +98,7 @@ class PersistedReadinessTracker {
46
98
  }
47
99
  }
48
100
  const DEFAULT_NETWORK_TIMEOUT_MS = 3e3;
101
+ const CACHE_CLAIM_RENEW_WINDOW_MS = 6e4;
49
102
  const MAX_TIMER_TIMEOUT_MS = 2147483647;
50
103
  function getNetworkTimeoutMs(initialRender) {
51
104
  if (initialRender === void 0) return void 0;
@@ -146,10 +199,6 @@ class SingleProcessCoordinator {
146
199
  if (acquisitions.size === 0) {
147
200
  this.remoteSubsetAcquisitions.delete(collectionId);
148
201
  }
149
- try {
150
- await acquisition.load;
151
- } catch {
152
- }
153
202
  await remoteSubsetOwner.unloadRemoteSubsetOwner(acquisition.owner, acquisition.options);
154
203
  }
155
204
  registerRemoteSubsetOwner(collectionId, owner) {
@@ -163,13 +212,9 @@ class SingleProcessCoordinator {
163
212
  this.remoteSubsetAcquisitions.delete(collectionId);
164
213
  this.remoteSubsetOwners.delete(collectionId);
165
214
  for (const acquisition of acquisitions?.values() ?? []) {
166
- void (async () => {
167
- try {
168
- await acquisition.load;
169
- } catch {
170
- }
171
- await remoteSubsetOwner.unloadRemoteSubsetOwner(owner, acquisition.options);
172
- })().catch(() => void 0);
215
+ void remoteSubsetOwner.unloadRemoteSubsetOwner(owner, acquisition.options).catch(
216
+ () => void 0
217
+ );
173
218
  }
174
219
  };
175
220
  }
@@ -186,7 +231,10 @@ class SingleProcessCoordinator {
186
231
  );
187
232
  }
188
233
  try {
189
- await adapter.applyCommittedTx(collectionId, tx);
234
+ await adapter.applyCommittedTx(
235
+ tx.cacheGenerationClaimId ? managedStorageTarget(collectionId, tx.cacheGenerationClaimId) : eagerStorageTarget(collectionId),
236
+ tx
237
+ );
190
238
  } catch (error) {
191
239
  throw errors.toPersistedCollectionDurabilityError(collectionId, error);
192
240
  }
@@ -319,6 +367,13 @@ class ApplyMutex {
319
367
  return this.reserve().run(task);
320
368
  }
321
369
  }
370
+ function matchesAnySubset(value, evaluators) {
371
+ return evaluators.some(
372
+ (evaluator) => !evaluator || db.toBooleanPredicate(
373
+ evaluator(value)
374
+ )
375
+ );
376
+ }
322
377
  function toStableSerializable(value) {
323
378
  if (value == null) {
324
379
  return value;
@@ -393,7 +448,7 @@ function isTxCommittedPayload(payload) {
393
448
  return false;
394
449
  }
395
450
  if (payload.requiresFullReload) {
396
- return true;
451
+ return payload.reconciliationReload === void 0 || payload.reconciliationReload === true;
397
452
  }
398
453
  return Array.isArray(payload.changedRows) && Array.isArray(payload.deletedKeys) && (payload.rowMetadataMutations === void 0 || Array.isArray(payload.rowMetadataMutations)) && (payload.collectionMetadataMutations === void 0 || Array.isArray(payload.collectionMetadataMutations));
399
454
  }
@@ -411,23 +466,28 @@ function toPersistedMutationEnvelope(mutation) {
411
466
  };
412
467
  }
413
468
  class PersistedCollectionRuntime {
414
- constructor(mode, collectionId, persistence, syncMode, dbName, persistedReadiness) {
469
+ constructor(mode, collectionId, persistence, syncMode, dbName, persistedReadiness, optionsClaim) {
415
470
  this.mode = mode;
416
471
  this.collectionId = collectionId;
417
472
  this.persistence = persistence;
418
473
  this.syncMode = syncMode;
419
474
  this.dbName = dbName;
420
475
  this.persistedReadiness = persistedReadiness;
476
+ this.optionsClaim = optionsClaim;
421
477
  this.applyMutex = new ApplyMutex();
422
478
  this.activeSubsets = /* @__PURE__ */ new Map();
423
479
  this.hydrationSequence = 0;
424
480
  this.resetSequence = 0;
425
481
  this.pendingRemoteSubsetEnsures = /* @__PURE__ */ new Map();
482
+ this.scopedRefreshes = /* @__PURE__ */ new Map();
483
+ this.rotatedAcquisitions = /* @__PURE__ */ new Map();
484
+ this.rotatedEnsures = /* @__PURE__ */ new WeakMap();
426
485
  this.queuedHydrationTransactions = [];
427
486
  this.queuedTxCommitted = [];
428
487
  this.requestIds = /* @__PURE__ */ new WeakMap();
429
488
  this.hydratedDemands = /* @__PURE__ */ new Set();
430
489
  this.collection = null;
490
+ this.boundCollection = null;
431
491
  this.syncControls = {
432
492
  begin: null,
433
493
  write: null,
@@ -453,15 +513,184 @@ class PersistedCollectionRuntime {
453
513
  this.indexAddedUnsubscribe = null;
454
514
  this.indexRemovedUnsubscribe = null;
455
515
  this.remoteEnsureRetryTimer = null;
516
+ this.cacheClaimRenewTimer = null;
517
+ this.cacheClaimRecovery = null;
456
518
  this.nextRequestId = 0;
457
519
  this.startupSettled = false;
458
520
  this.sourceTruncateGeneration = 0;
521
+ this.scopedRecovery = false;
522
+ this.scopedRecoveryPending = false;
523
+ this.scopedRecoverySequence = 0;
524
+ this.recoverQueuedAfterLocalLoadFailure = false;
525
+ this.startupCacheGenerationRotated = false;
526
+ this.latestTerm = 0;
527
+ this.latestSeq = 0;
528
+ this.latestRowVersion = 0;
529
+ this.publicRowVersion = 0;
530
+ this.localTerm = 1;
531
+ this.localSeq = 0;
532
+ this.localRowVersion = 0;
533
+ }
534
+ get storageCollectionId() {
535
+ return this.cacheGenerationClaim?.storageCollectionId ?? this.collectionId;
536
+ }
537
+ storageTargetFor(storageCollectionId = this.storageCollectionId, claimId = this.cacheGenerationClaim?.claimId) {
538
+ if (this.mode === `sync-present` && this.syncMode === `on-demand` && this.persistence.adapter.claimCacheGeneration) {
539
+ if (!claimId) {
540
+ throw new errors.InvalidPersistedCollectionConfigError(
541
+ `A managed on-demand cache operation requires a persisted cache claim`
542
+ );
543
+ }
544
+ return { kind: `managed`, storageCollectionId, claimId };
545
+ }
546
+ return { kind: `eager`, collectionId: storageCollectionId };
547
+ }
548
+ cacheGenerationNow() {
549
+ return this.persistence.adapter.getCacheGenerationNow?.() ?? Date.now();
550
+ }
551
+ hasExpiredCacheClaim() {
552
+ const expiresAtMs = this.cacheGenerationClaim?.expiresAtMs;
553
+ return expiresAtMs !== void 0 && this.cacheGenerationNow() >= expiresAtMs;
554
+ }
555
+ async claimCacheGeneration(lifecycleGeneration) {
556
+ if (this.mode !== `sync-present` || this.syncMode !== `on-demand`) return;
557
+ const adapter = this.persistence.adapter;
558
+ const capabilities = [
559
+ adapter.claimCacheGeneration,
560
+ adapter.rotateCacheGeneration,
561
+ adapter.renewCacheGenerationClaim,
562
+ adapter.releaseCacheGenerationClaim
563
+ ];
564
+ if (capabilities.some(Boolean) && !capabilities.every(Boolean)) {
565
+ throw new errors.InvalidPersistedCollectionConfigError(
566
+ `An on-demand managed cache generation adapter must implement claimCacheGeneration, rotateCacheGeneration, renewCacheGenerationClaim, and releaseCacheGenerationClaim together`
567
+ );
568
+ }
569
+ if (!adapter.claimCacheGeneration) return;
570
+ const claim = await adapter.claimCacheGeneration(this.collectionId);
571
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
572
+ await adapter.releaseCacheGenerationClaim?.(claim.claimId);
573
+ return;
574
+ }
575
+ this.cacheGenerationClaim = claim;
576
+ this.scheduleCacheClaimRenewal(claim);
577
+ this.attachCoordinatorSubscription();
578
+ }
579
+ clearCacheClaimRenewal() {
580
+ if (this.cacheClaimRenewTimer !== null) {
581
+ clearTimeout(this.cacheClaimRenewTimer);
582
+ this.cacheClaimRenewTimer = null;
583
+ }
584
+ }
585
+ scheduleCacheClaimRenewal(claim) {
586
+ this.clearCacheClaimRenewal();
587
+ if (claim.expiresAtMs === void 0 || !this.persistence.adapter.renewCacheGenerationClaim) {
588
+ return;
589
+ }
590
+ const remaining = claim.expiresAtMs - this.cacheGenerationNow();
591
+ const delay = remaining <= 0 ? 1e3 : Math.max(
592
+ 1,
593
+ Math.min(CACHE_CLAIM_RENEW_WINDOW_MS, Math.floor(remaining / 2))
594
+ );
595
+ const lifecycleGeneration = this.lifecycleGeneration;
596
+ this.cacheClaimRenewTimer = setTimeout(() => {
597
+ this.cacheClaimRenewTimer = null;
598
+ void this.ensureCacheClaimActive().catch(() => {
599
+ if (lifecycleGeneration === this.lifecycleGeneration && this.cacheGenerationClaim === claim) {
600
+ this.scheduleCacheClaimRenewal(claim);
601
+ }
602
+ });
603
+ }, delay);
604
+ }
605
+ async ensureCacheClaimActive() {
606
+ if (this.cacheClaimRecovery) {
607
+ await this.cacheClaimRecovery;
608
+ return;
609
+ }
610
+ if (await this.renewCacheClaim(this.persistence.adapter)) return;
611
+ if (!this.cacheClaimRecovery) {
612
+ const recovery = this.restartSourceAfterScopedRecovery === void 0 ? this.reclaimStartupCacheGeneration() : this.startScopedRecovery();
613
+ this.cacheClaimRecovery = recovery;
614
+ void recovery.finally(() => {
615
+ if (this.cacheClaimRecovery === recovery) {
616
+ this.cacheClaimRecovery = null;
617
+ }
618
+ }).catch(() => void 0);
619
+ }
620
+ await this.cacheClaimRecovery;
621
+ }
622
+ scheduleCacheClaimRecovery(lifecycleGeneration) {
623
+ queueMicrotask(() => {
624
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
625
+ void this.ensureCacheClaimActive().then(
626
+ () => this.applyMutex.run(() => this.flushQueuedTxCommittedUnsafe())
627
+ ).catch((error) => {
628
+ this.markTerminalFailure(error, lifecycleGeneration);
629
+ });
630
+ });
631
+ }
632
+ async reclaimStartupCacheGeneration() {
633
+ const lifecycleGeneration = this.lifecycleGeneration;
634
+ const previous = this.cacheGenerationClaim;
635
+ if (!previous) return;
636
+ if (!this.persistence.adapter.rotateCacheGeneration) {
637
+ throw new errors.InvalidPersistedCollectionConfigError(
638
+ `A managed on-demand cache adapter must rotate expired startup claims`
639
+ );
640
+ }
641
+ this.clearCacheClaimRenewal();
642
+ this.coordinatorUnsubscribe?.();
643
+ this.coordinatorUnsubscribe = null;
644
+ const rotated = await this.persistence.adapter.rotateCacheGeneration(
645
+ this.collectionId,
646
+ previous.claimId,
647
+ void 0,
648
+ previous.storageCollectionId
649
+ );
650
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
651
+ await this.persistence.adapter.releaseCacheGenerationClaim?.(
652
+ rotated.claimId
653
+ );
654
+ return;
655
+ }
656
+ this.cacheGenerationClaim = rotated;
657
+ this.startupCacheGenerationRotated = true;
658
+ this.resetSequence++;
459
659
  this.latestTerm = 0;
460
660
  this.latestSeq = 0;
461
661
  this.latestRowVersion = 0;
462
662
  this.localTerm = 1;
463
663
  this.localSeq = 0;
464
664
  this.localRowVersion = 0;
665
+ this.persistedResumeGeneration = void 0;
666
+ this.persistedKeySetEvidence = { status: `incompatible` };
667
+ this.scheduleCacheClaimRenewal(rotated);
668
+ this.attachCoordinatorSubscription();
669
+ }
670
+ async renewCacheClaim(adapter) {
671
+ const claim = this.cacheGenerationClaim;
672
+ if (!claim || !adapter.renewCacheGenerationClaim || this.scopedRecoveryPending)
673
+ return true;
674
+ if (claim.expiresAtMs !== void 0 && claim.expiresAtMs - this.cacheGenerationNow() > CACHE_CLAIM_RENEW_WINDOW_MS) {
675
+ if (this.cacheClaimRenewTimer === null) {
676
+ this.scheduleCacheClaimRenewal(claim);
677
+ }
678
+ return true;
679
+ }
680
+ const lifecycleGeneration = this.lifecycleGeneration;
681
+ const expiresAtMs = await adapter.renewCacheGenerationClaim(
682
+ claim.storageCollectionId,
683
+ claim.claimId
684
+ );
685
+ if (lifecycleGeneration !== this.lifecycleGeneration || this.cacheGenerationClaim?.claimId !== claim.claimId || this.cacheGenerationClaim.storageCollectionId !== claim.storageCollectionId) {
686
+ return true;
687
+ }
688
+ if (expiresAtMs === void 0) {
689
+ return false;
690
+ }
691
+ this.cacheGenerationClaim = { ...claim, expiresAtMs };
692
+ this.scheduleCacheClaimRenewal(this.cacheGenerationClaim);
693
+ return true;
465
694
  }
466
695
  setSyncControls(syncControls) {
467
696
  this.advanceLifecycle();
@@ -473,6 +702,12 @@ class PersistedCollectionRuntime {
473
702
  commit: commit ? (signal) => this.trackAppliedReceipt(commit(signal)) : null
474
703
  };
475
704
  }
705
+ setSourceRestartAfterScopedRecovery(restart) {
706
+ this.restartSourceAfterScopedRecovery = restart ?? null;
707
+ }
708
+ clearSourceRestartAfterScopedRecovery() {
709
+ this.restartSourceAfterScopedRecovery = void 0;
710
+ }
476
711
  reportSyncError(error) {
477
712
  const markError = this.syncControls.markError;
478
713
  if (this.syncErrorReported) return this.reportedSyncError;
@@ -487,8 +722,9 @@ class PersistedCollectionRuntime {
487
722
  }
488
723
  registerRemoteSubsetOwner(owner) {
489
724
  this.remoteSubsetOwnerUnsubscribe?.();
725
+ this.remoteSubsetOwner = owner;
490
726
  this.remoteSubsetOwnerUnsubscribe = this.persistence.coordinator.registerRemoteSubsetOwner(
491
- this.collectionId,
727
+ this.storageCollectionId,
492
728
  owner
493
729
  );
494
730
  }
@@ -513,6 +749,23 @@ class PersistedCollectionRuntime {
513
749
  )
514
750
  );
515
751
  }
752
+ // An ensure reply settles a follower's load (law 7). A source's loadSubset
753
+ // must await every commit receipt that establishes its result, so the
754
+ // leader broadcasts those commits before it replies on the same channel.
755
+ // They are already queued on the apply mutex; settle after they show here.
756
+ async applyLeaderCommitsBeforeReply(options) {
757
+ try {
758
+ if (this.hasExpiredCacheClaim() || this.cacheClaimRecovery) {
759
+ await this.ensureCacheClaimActive();
760
+ }
761
+ await this.applyMutex.run(() => this.flushQueuedTxCommittedUnsafe());
762
+ this.throwIfTerminal();
763
+ } catch (error) {
764
+ if (this.getCurrentTerminalFailure()) this.unloadSubset(options);
765
+ else this.queueRemoteSubsetEnsure(options);
766
+ throw error;
767
+ }
768
+ }
516
769
  clearSyncControls() {
517
770
  this.syncControls = {
518
771
  begin: null,
@@ -571,17 +824,46 @@ class PersistedCollectionRuntime {
571
824
  isApplyingInternally() {
572
825
  return this.internalApplyDepth > 0;
573
826
  }
574
- setCollection(collection) {
827
+ isScopedRecoveryPending() {
828
+ return this.scopedRecoveryPending;
829
+ }
830
+ hasBoundCollection() {
831
+ return this.boundCollection !== null;
832
+ }
833
+ setCollection(collection, syncControls) {
834
+ if (collection.config[db.collectionOptionsClaim] !== this.optionsClaim) {
835
+ throw new errors.InvalidPersistedCollectionConfigError(
836
+ `Persisted sync belongs to its options Collection`
837
+ );
838
+ }
839
+ if (this.boundCollection && this.boundCollection !== collection) {
840
+ throw new errors.InvalidPersistedCollectionConfigError(
841
+ `Persisted options cannot own more than one Collection`
842
+ );
843
+ }
844
+ const previousCollection = this.collection;
845
+ const previousControls = this.syncControls;
846
+ this.setSyncControls(syncControls);
575
847
  if (this.collection === collection) {
848
+ this.boundCollection ??= collection;
576
849
  return;
577
850
  }
578
851
  this.collection = collection;
579
- this.attachCoordinatorSubscription();
852
+ try {
853
+ if (!this.persistence.adapter.claimCacheGeneration || this.mode !== `sync-present` || this.syncMode !== `on-demand`) {
854
+ this.attachCoordinatorSubscription();
855
+ }
856
+ } catch (error) {
857
+ this.collection = previousCollection;
858
+ this.syncControls = previousControls;
859
+ throw error;
860
+ }
861
+ this.boundCollection ??= collection;
580
862
  }
581
863
  getLeadershipState() {
582
864
  return {
583
865
  nodeId: this.persistence.coordinator.getNodeId(),
584
- isLeader: this.persistence.coordinator.isLeader(this.collectionId)
866
+ isLeader: this.persistence.coordinator.isLeader(this.storageCollectionId)
585
867
  };
586
868
  }
587
869
  runInHydrationScope(task, adapter = this.persistence.adapter) {
@@ -596,6 +878,7 @@ class PersistedCollectionRuntime {
596
878
  return this.startPromise;
597
879
  }
598
880
  const lifecycleGeneration = this.lifecycleGeneration;
881
+ this.startupCacheGenerationRotated = false;
599
882
  const isRestart = this.hasAttemptedStartup;
600
883
  this.hasAttemptedStartup = true;
601
884
  let resolveStartupMetadata;
@@ -606,63 +889,98 @@ class PersistedCollectionRuntime {
606
889
  });
607
890
  void this.startupMetadataPromise.catch(() => void 0);
608
891
  this.startPromise = (async () => {
892
+ await this.claimCacheGeneration(lifecycleGeneration);
893
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
894
+ resolveStartupMetadata();
895
+ return;
896
+ }
897
+ let metadataExpired = false;
609
898
  const loadStartupMetadata = async (adapter) => {
610
899
  if (lifecycleGeneration !== this.lifecycleGeneration) {
611
900
  resolveStartupMetadata();
612
901
  return false;
613
902
  }
614
903
  try {
615
- await this.loadStartupMetadataInternal(lifecycleGeneration, adapter);
904
+ if (!await this.loadStartupMetadataInternal(
905
+ lifecycleGeneration,
906
+ adapter
907
+ )) {
908
+ metadataExpired = true;
909
+ return false;
910
+ }
616
911
  return lifecycleGeneration === this.lifecycleGeneration;
617
912
  } catch (error) {
913
+ if (this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
914
+ metadataExpired = true;
915
+ return false;
916
+ }
618
917
  rejectStartupMetadata(error);
619
918
  throw error;
620
919
  }
621
920
  };
622
921
  let startup;
623
922
  const scheduleStartupAsOneHydrate = this.persistence.adapter.runInHydrationScope !== void 0 && (!isRestart || (this.persistence.adapter.isHydrationScopeScheduled?.() ?? true));
624
- if (scheduleStartupAsOneHydrate) {
625
- startup = await this.applyMutex.run(async () => {
626
- const result = await this.runInHydrationScope(async (adapter) => {
627
- if (!await loadStartupMetadata(adapter)) return void 0;
628
- return this.startInternal(
629
- lifecycleGeneration,
630
- adapter,
631
- resolveStartupMetadata
632
- );
923
+ for (; ; ) {
924
+ metadataExpired = false;
925
+ const storageAtAttempt = this.storageCollectionId;
926
+ if (scheduleStartupAsOneHydrate) {
927
+ startup = await this.applyMutex.run(async () => {
928
+ const result = await this.runInHydrationScope(async (adapter) => {
929
+ if (!await loadStartupMetadata(adapter)) return void 0;
930
+ return this.startInternal(
931
+ lifecycleGeneration,
932
+ adapter,
933
+ resolveStartupMetadata
934
+ );
935
+ });
936
+ if (lifecycleGeneration === this.lifecycleGeneration) {
937
+ await this.flushQueuedTxCommittedUnsafe();
938
+ }
939
+ return result;
633
940
  });
634
- if (lifecycleGeneration === this.lifecycleGeneration) {
635
- await this.flushQueuedTxCommittedUnsafe();
636
- }
637
- return result;
638
- });
639
- } else {
640
- if (await loadStartupMetadata(this.persistence.adapter)) {
641
- if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
642
- startup = await this.applyMutex.run(async () => {
643
- const result = await this.runInHydrationScope(
644
- (adapter) => this.startInternal(
645
- lifecycleGeneration,
646
- adapter,
647
- resolveStartupMetadata
648
- )
941
+ } else {
942
+ if (await loadStartupMetadata(this.persistence.adapter)) {
943
+ if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
944
+ startup = await this.applyMutex.run(async () => {
945
+ const result = await this.runInHydrationScope(
946
+ (adapter) => this.startInternal(
947
+ lifecycleGeneration,
948
+ adapter,
949
+ resolveStartupMetadata
950
+ )
951
+ );
952
+ if (lifecycleGeneration === this.lifecycleGeneration) {
953
+ await this.flushQueuedTxCommittedUnsafe();
954
+ }
955
+ return result;
956
+ });
957
+ } else {
958
+ startup = await this.startInternal(
959
+ lifecycleGeneration,
960
+ this.persistence.adapter,
961
+ resolveStartupMetadata
649
962
  );
650
963
  if (lifecycleGeneration === this.lifecycleGeneration) {
651
964
  await this.flushQueuedTxCommittedUnsafe();
652
965
  }
653
- return result;
654
- });
655
- } else {
656
- startup = await this.startInternal(
657
- lifecycleGeneration,
658
- this.persistence.adapter,
659
- resolveStartupMetadata
660
- );
661
- if (lifecycleGeneration === this.lifecycleGeneration) {
662
- await this.flushQueuedTxCommittedUnsafe();
663
966
  }
664
967
  }
665
968
  }
969
+ if (this.cacheGenerationClaim && this.syncMode === `on-demand` && // Hydration may have changed this through loadStartupMetadata().
970
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
971
+ !metadataExpired) {
972
+ await this.ensureCacheClaimActive();
973
+ metadataExpired = storageAtAttempt !== this.storageCollectionId;
974
+ }
975
+ if (!metadataExpired) break;
976
+ await this.ensureCacheClaimActive();
977
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
978
+ resolveStartupMetadata();
979
+ return;
980
+ }
981
+ }
982
+ if (this.cacheGenerationClaim && this.syncMode === `on-demand` && lifecycleGeneration === this.lifecycleGeneration) {
983
+ resolveStartupMetadata();
666
984
  }
667
985
  if (startup !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
668
986
  await this.requestCoordinatorPersistedIndexes(
@@ -698,6 +1016,7 @@ class PersistedCollectionRuntime {
698
1016
  await this.ensureStarted();
699
1017
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
700
1018
  if (this.syncMode !== `on-demand`) return;
1019
+ if (this.scopedRecovery) return;
701
1020
  const appliedCursor = await this.applyMutex.run(async () => {
702
1021
  const result = await this.runInHydrationScope(
703
1022
  (adapter) => this.hydrateBaseline(lifecycleGeneration, adapter)
@@ -707,6 +1026,10 @@ class PersistedCollectionRuntime {
707
1026
  }
708
1027
  return result;
709
1028
  });
1029
+ if (appliedCursor === false) {
1030
+ await this.ensureCacheClaimActive();
1031
+ return;
1032
+ }
710
1033
  if (appliedCursor !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
711
1034
  await this.waitForAppliedReceiptsAfter(appliedCursor, `accepted`);
712
1035
  }
@@ -723,14 +1046,32 @@ class PersistedCollectionRuntime {
723
1046
  this.resumeCertificationPromise = (async () => {
724
1047
  await this.ensureStarted();
725
1048
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
726
- const snapshot = await this.persistence.adapter.loadResumeSnapshot(
727
- this.collectionId,
728
- {
729
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
730
- includeRows: false
731
- }
732
- );
733
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
1049
+ if (this.scopedRecovery) return;
1050
+ const storageCollectionId = this.storageCollectionId;
1051
+ const claimId = this.cacheGenerationClaim?.claimId;
1052
+ const certificationIsCurrent = () => lifecycleGeneration === this.lifecycleGeneration && !this.scopedRecovery && storageCollectionId === this.storageCollectionId && claimId === this.cacheGenerationClaim?.claimId;
1053
+ let snapshot;
1054
+ try {
1055
+ snapshot = await this.persistence.adapter.loadResumeSnapshot(
1056
+ this.storageTargetFor(storageCollectionId, claimId),
1057
+ {
1058
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1059
+ includeRows: false,
1060
+ cacheGenerationClaimId: claimId
1061
+ }
1062
+ );
1063
+ } catch (error) {
1064
+ if (!certificationIsCurrent()) return;
1065
+ if (!claimId || !this.hasExpiredCacheClaim()) throw error;
1066
+ await this.ensureCacheClaimActive();
1067
+ return;
1068
+ }
1069
+ if (!certificationIsCurrent()) return;
1070
+ if (claimId && !await this.renewCacheClaim(this.persistence.adapter)) {
1071
+ await this.ensureCacheClaimActive();
1072
+ return;
1073
+ }
1074
+ if (!certificationIsCurrent()) return;
734
1075
  this.bindResumeSnapshotEvidence(snapshot);
735
1076
  })();
736
1077
  return this.resumeCertificationPromise;
@@ -738,6 +1079,162 @@ class PersistedCollectionRuntime {
738
1079
  getKeySetEvidence() {
739
1080
  return this.persistedKeySetEvidence;
740
1081
  }
1082
+ hasManagedCacheGeneration() {
1083
+ return this.cacheGenerationClaim !== void 0;
1084
+ }
1085
+ hasRotatedStartupCacheGeneration() {
1086
+ return this.startupCacheGenerationRotated;
1087
+ }
1088
+ async startScopedRecovery(resetMetadata) {
1089
+ this.throwIfTerminal();
1090
+ this.clearCacheClaimRenewal();
1091
+ const lifecycleGeneration = this.lifecycleGeneration;
1092
+ const restartSource = this.restartSourceAfterScopedRecovery;
1093
+ this.scopedRecovery = true;
1094
+ this.scopedRecoveryPending = true;
1095
+ const recoverySequence = ++this.scopedRecoverySequence;
1096
+ this.hydratedDemands.clear();
1097
+ this.resetSequence++;
1098
+ let resolveCacheRotated;
1099
+ let rejectCacheRotated;
1100
+ let sourceRestart;
1101
+ try {
1102
+ if (restartSource) {
1103
+ const cacheRotated = new Promise((resolve, reject) => {
1104
+ resolveCacheRotated = resolve;
1105
+ rejectCacheRotated = reject;
1106
+ });
1107
+ void cacheRotated.catch(() => void 0);
1108
+ sourceRestart = Promise.resolve(restartSource(cacheRotated));
1109
+ void sourceRestart.catch(() => void 0);
1110
+ }
1111
+ await this.applyMutex.run(async () => {
1112
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1113
+ const claim = this.cacheGenerationClaim;
1114
+ if (claim && this.persistence.adapter.rotateCacheGeneration) {
1115
+ const previousStorageId = claim.storageCollectionId;
1116
+ const rotated = await this.persistence.adapter.rotateCacheGeneration(
1117
+ this.collectionId,
1118
+ claim.claimId,
1119
+ resetMetadata,
1120
+ previousStorageId
1121
+ );
1122
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
1123
+ await this.persistence.adapter.releaseCacheGenerationClaim?.(
1124
+ rotated.claimId
1125
+ );
1126
+ throw new db.SyncTransactionAbortedError();
1127
+ }
1128
+ for (const options of this.activeSubsets.values()) {
1129
+ void this.persistence.coordinator.requestReleaseRemoteSubset(
1130
+ previousStorageId,
1131
+ this.rotatedAcquisitions.get(options) ?? options
1132
+ ).catch(() => void 0);
1133
+ if (options.signal?.aborted) {
1134
+ this.rotatedAcquisitions.delete(options);
1135
+ continue;
1136
+ }
1137
+ this.rotatedAcquisitions.set(options, {
1138
+ ...options,
1139
+ refetch: true
1140
+ });
1141
+ }
1142
+ this.remoteSubsetOwnerUnsubscribe?.();
1143
+ this.remoteSubsetOwnerUnsubscribe = null;
1144
+ this.coordinatorUnsubscribe?.();
1145
+ this.coordinatorUnsubscribe = null;
1146
+ this.cacheGenerationClaim = rotated;
1147
+ this.latestTerm = 0;
1148
+ this.latestSeq = 0;
1149
+ this.latestRowVersion = 0;
1150
+ this.publicRowVersion = 0;
1151
+ this.resetTermFence = void 0;
1152
+ this.localTerm = 1;
1153
+ this.localSeq = 0;
1154
+ this.localRowVersion = 0;
1155
+ this.queuedTxCommitted.length = 0;
1156
+ this.persistedResumeGeneration = void 0;
1157
+ this.persistedKeySetEvidence = { status: `incompatible` };
1158
+ this.resumeCertificationPromise = null;
1159
+ this.resumeGenerationOwner = /* @__PURE__ */ Symbol(
1160
+ `persisted resume generation owner`
1161
+ );
1162
+ this.attachCoordinatorSubscription();
1163
+ if (this.remoteSubsetOwner) {
1164
+ this.registerRemoteSubsetOwner(this.remoteSubsetOwner);
1165
+ }
1166
+ }
1167
+ if (this.syncControls.begin && this.syncControls.commit) {
1168
+ const applied = this.withInternalApply(() => {
1169
+ this.syncControls.begin?.();
1170
+ this.syncControls.truncate?.({ markReady: false });
1171
+ if (resetMetadata) {
1172
+ this.syncControls.metadata?.collection.set(
1173
+ resetMetadata.key,
1174
+ resetMetadata.value
1175
+ );
1176
+ }
1177
+ return this.syncControls.commit?.() ?? true;
1178
+ });
1179
+ await db.whenSyncAccepted(applied);
1180
+ if (!this.cacheGenerationClaim && recoverySequence === this.scopedRecoverySequence) {
1181
+ this.scopedRecoveryPending = false;
1182
+ }
1183
+ await this.runInHydrationScope(async (adapter) => {
1184
+ if (this.recoverQueuedAfterLocalLoadFailure) {
1185
+ this.recoverQueuedAfterLocalLoadFailure = false;
1186
+ await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
1187
+ lifecycleGeneration,
1188
+ adapter
1189
+ );
1190
+ } else {
1191
+ await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1192
+ }
1193
+ });
1194
+ await this.flushQueuedTxCommittedUnsafe();
1195
+ }
1196
+ if (recoverySequence === this.scopedRecoverySequence) {
1197
+ this.scopedRecoveryPending = false;
1198
+ if (this.cacheGenerationClaim) {
1199
+ const currentIndexes = this.collection?.getIndexMetadata() ?? [];
1200
+ const completedIndexes = await this.bootstrapPersistedIndexes(
1201
+ currentIndexes,
1202
+ this.persistence.adapter
1203
+ );
1204
+ await this.requestCoordinatorPersistedIndexes(
1205
+ currentIndexes,
1206
+ completedIndexes
1207
+ );
1208
+ }
1209
+ }
1210
+ });
1211
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1212
+ resolveCacheRotated?.();
1213
+ await sourceRestart;
1214
+ if (recoverySequence === this.scopedRecoverySequence && this.cacheGenerationClaim) {
1215
+ this.scheduleCacheClaimRenewal(this.cacheGenerationClaim);
1216
+ }
1217
+ if (recoverySequence !== this.scopedRecoverySequence) return;
1218
+ for (const options of this.activeSubsets.values()) {
1219
+ if (options.signal?.aborted) continue;
1220
+ const rotatedOptions = this.rotatedAcquisitions.get(options);
1221
+ if (rotatedOptions) {
1222
+ const ensure = this.persistence.coordinator.requestEnsureRemoteSubset(
1223
+ this.storageCollectionId,
1224
+ rotatedOptions
1225
+ );
1226
+ this.rotatedEnsures.set(options, ensure);
1227
+ void ensure.catch((error) => this.reportSyncError(error));
1228
+ } else {
1229
+ this.refreshScopedSubsets();
1230
+ break;
1231
+ }
1232
+ }
1233
+ } catch (error) {
1234
+ rejectCacheRotated?.(error);
1235
+ throw this.markTerminalFailure(error, lifecycleGeneration);
1236
+ }
1237
+ }
741
1238
  getResumeGenerationOwner() {
742
1239
  return this.resumeGenerationOwner;
743
1240
  }
@@ -746,7 +1243,7 @@ class PersistedCollectionRuntime {
746
1243
  const baseline = {};
747
1244
  this.activeSubsets.set(this.getSubsetKey(baseline), baseline);
748
1245
  const appliedCursor = this.appliedReceiptSequence;
749
- await this.hydrateSubsetUnsafe(
1246
+ const hydrated = await this.hydrateSubsetUnsafe(
750
1247
  baseline,
751
1248
  {
752
1249
  lifecycleGeneration,
@@ -754,6 +1251,7 @@ class PersistedCollectionRuntime {
754
1251
  },
755
1252
  adapter
756
1253
  );
1254
+ if (!hydrated) return false;
757
1255
  return lifecycleGeneration === this.lifecycleGeneration ? appliedCursor : void 0;
758
1256
  }
759
1257
  async ensureStartupMetadataLoaded() {
@@ -773,19 +1271,33 @@ class PersistedCollectionRuntime {
773
1271
  adapter
774
1272
  );
775
1273
  if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
776
- onStartupMetadataLoaded();
1274
+ if (!this.cacheGenerationClaim || this.syncMode !== `on-demand`) {
1275
+ onStartupMetadataLoaded();
1276
+ }
777
1277
  const appliedCursor = this.syncMode !== `on-demand` ? await this.hydrateBaseline(lifecycleGeneration, adapter) : void 0;
778
1278
  return lifecycleGeneration === this.lifecycleGeneration ? {
779
- appliedCursor,
1279
+ appliedCursor: appliedCursor === false ? void 0 : appliedCursor,
780
1280
  indexBootstrapSnapshot,
781
1281
  completedLocalIndexSignatures
782
1282
  } : void 0;
783
1283
  }
784
1284
  async loadStartupMetadataInternal(lifecycleGeneration, adapter) {
785
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
786
- includeRows: false
787
- });
788
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
1285
+ const claimId = this.cacheGenerationClaim?.claimId;
1286
+ const storageCollectionId = this.storageCollectionId;
1287
+ const snapshot = await adapter.loadResumeSnapshot(
1288
+ this.storageTargetFor(storageCollectionId, claimId),
1289
+ {
1290
+ includeRows: false,
1291
+ cacheGenerationClaimId: claimId
1292
+ }
1293
+ );
1294
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1295
+ if (claimId !== this.cacheGenerationClaim?.claimId) return false;
1296
+ if (storageCollectionId !== this.storageCollectionId) return false;
1297
+ if (claimId && !await this.renewCacheClaim(adapter)) return false;
1298
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1299
+ if (claimId !== this.cacheGenerationClaim?.claimId) return false;
1300
+ if (storageCollectionId !== this.storageCollectionId) return false;
789
1301
  this.persistedResumeGeneration = this.getResumeSnapshotGeneration(snapshot);
790
1302
  this.persistedKeySetEvidence = snapshot.keySet;
791
1303
  this.observeStreamPosition(
@@ -797,36 +1309,42 @@ class PersistedCollectionRuntime {
797
1309
  snapshot.collectionMetadata
798
1310
  );
799
1311
  await db.whenSyncAccepted(applied);
1312
+ return true;
800
1313
  }
801
1314
  async loadCollectionMetadataSnapshot(adapter) {
802
1315
  if (!adapter.loadCollectionMetadata) {
803
1316
  return [];
804
1317
  }
805
- return adapter.loadCollectionMetadata(this.collectionId);
1318
+ return adapter.loadCollectionMetadata(this.storageTargetFor(), {
1319
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1320
+ });
806
1321
  }
807
1322
  replaceCollectionMetadataSnapshot(collectionMetadata) {
808
1323
  if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
809
1324
  return true;
810
1325
  }
811
- const nextMetadata = new Map(
812
- collectionMetadata.map(({ key, value }) => [key, value])
813
- );
814
- const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
815
1326
  return this.withInternalApply(() => {
816
1327
  this.syncControls.begin?.();
817
- currentKeys.forEach((key) => {
818
- if (!nextMetadata.has(key)) {
819
- this.syncControls.metadata?.collection.delete(key);
820
- }
821
- });
822
- nextMetadata.forEach((value, key) => {
823
- this.syncControls.metadata?.collection.set(key, value);
824
- });
1328
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
825
1329
  return this.syncControls.commit?.() ?? true;
826
1330
  });
827
1331
  }
828
- async loadSubset(options, upstreamLoadSubset) {
1332
+ applyCollectionMetadataSnapshot(collectionMetadata) {
1333
+ const metadata = this.syncControls.metadata;
1334
+ if (!metadata) return;
1335
+ const next = new Map(
1336
+ collectionMetadata.map(({ key, value }) => [key, value])
1337
+ );
1338
+ for (const { key } of metadata.collection.list()) {
1339
+ if (!next.has(key)) metadata.collection.delete(key);
1340
+ }
1341
+ for (const [key, value] of next) metadata.collection.set(key, value);
1342
+ }
1343
+ async loadSubset(options, upstreamLoadSubset, onHydrationStarted, isDemandRetained) {
829
1344
  this.throwIfTerminal();
1345
+ await this.ensureCacheClaimActive();
1346
+ if (options.signal?.aborted || isDemandRetained && !isDemandRetained())
1347
+ return;
830
1348
  const lifecycleGeneration = this.lifecycleGeneration;
831
1349
  const subsetKey = this.getSubsetKey(options);
832
1350
  const truncateGeneration = this.sourceTruncateGeneration;
@@ -835,25 +1353,52 @@ class PersistedCollectionRuntime {
835
1353
  this.activeSubsets.set(subsetKey, options);
836
1354
  const appliedCursor = this.appliedReceiptSequence;
837
1355
  try {
838
- await this.applyMutex.run(async () => {
839
- try {
840
- await this.runInHydrationScope(
841
- (adapter) => this.hydrateSubsetUnsafe(
842
- options,
843
- {
844
- lifecycleGeneration,
845
- requestLocalLoadFailure: true,
846
- rejectBufferedReplayFailure: true
847
- },
848
- adapter
849
- )
850
- );
851
- } finally {
852
- if (lifecycleGeneration === this.lifecycleGeneration && !this.getCurrentTerminalFailure()) {
853
- await this.flushQueuedTxCommittedUnsafe();
1356
+ const hydrated = await this.applyMutex.run(
1357
+ async () => {
1358
+ if (options.signal?.aborted) return `aborted`;
1359
+ try {
1360
+ return await this.runInHydrationScope(
1361
+ (adapter) => {
1362
+ if (options.signal?.aborted) {
1363
+ return Promise.resolve(`aborted`);
1364
+ }
1365
+ onHydrationStarted?.();
1366
+ return this.hydrateSubsetUnsafe(
1367
+ options,
1368
+ {
1369
+ lifecycleGeneration,
1370
+ requestLocalLoadFailure: true,
1371
+ rejectBufferedReplayFailure: true
1372
+ },
1373
+ adapter
1374
+ );
1375
+ }
1376
+ );
1377
+ } finally {
1378
+ if (lifecycleGeneration === this.lifecycleGeneration && !this.getCurrentTerminalFailure()) {
1379
+ await this.flushQueuedTxCommittedUnsafe();
1380
+ }
854
1381
  }
855
1382
  }
856
- });
1383
+ );
1384
+ if (hydrated === `aborted`) {
1385
+ if (this.activeSubsets.get(subsetKey) === options) {
1386
+ this.activeSubsets.delete(subsetKey);
1387
+ }
1388
+ return;
1389
+ }
1390
+ if (!hydrated) {
1391
+ await this.ensureCacheClaimActive();
1392
+ const rotatedOptions = this.rotatedAcquisitions.get(options);
1393
+ if (rotatedOptions && this.activeSubsets.get(subsetKey) === options && !options.signal?.aborted) {
1394
+ await (this.rotatedEnsures.get(options) ?? this.persistence.coordinator.requestEnsureRemoteSubset(
1395
+ this.storageCollectionId,
1396
+ rotatedOptions
1397
+ ));
1398
+ await this.applyLeaderCommitsBeforeReply(options);
1399
+ }
1400
+ return;
1401
+ }
857
1402
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
858
1403
  await this.waitForAppliedReceiptsAfter(appliedCursor);
859
1404
  if (!routeRemoteDemandDuringHydration && this.canRouteRemoteDemandThroughCoordinator()) {
@@ -866,13 +1411,16 @@ class PersistedCollectionRuntime {
866
1411
  throw error;
867
1412
  }
868
1413
  if (options.signal?.aborted || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
1414
+ if (options.signal?.aborted && this.activeSubsets.get(subsetKey) === options) {
1415
+ this.activeSubsets.delete(subsetKey);
1416
+ }
869
1417
  return;
870
1418
  }
871
1419
  if (this.canRouteRemoteDemandThroughCoordinator()) {
872
1420
  try {
873
1421
  await this.persistence.coordinator.requestEnsureRemoteSubset(
874
- this.collectionId,
875
- options
1422
+ this.storageCollectionId,
1423
+ this.rotatedAcquisitions.get(options) ?? options
876
1424
  );
877
1425
  } catch (error) {
878
1426
  if (options.signal?.aborted || typeof error === `object` && error !== null && `name` in error && error.name === `AbortError`) {
@@ -882,6 +1430,7 @@ class PersistedCollectionRuntime {
882
1430
  this.queueRemoteSubsetEnsure(options);
883
1431
  throw error;
884
1432
  }
1433
+ await this.applyLeaderCommitsBeforeReply(options);
885
1434
  return;
886
1435
  }
887
1436
  if (truncateGeneration === this.sourceTruncateGeneration && this.activeSubsets.get(subsetKey) === options) {
@@ -899,7 +1448,7 @@ class PersistedCollectionRuntime {
899
1448
  loadHydratedSubset(options, upstreamLoadSubset) {
900
1449
  const failure = this.getCurrentTerminalFailure();
901
1450
  if (failure) return Promise.reject(failure.error);
902
- if (!this.startupSettled || this.isHydratingNow() || this.canRouteRemoteDemandThroughCoordinator() || options.refetch === true || !this.hydratedDemands.has(db.getLoadSubsetDemandKey(options))) {
1451
+ if (this.hasExpiredCacheClaim() || !this.startupSettled || this.isHydratingNow() || this.canRouteRemoteDemandThroughCoordinator() || options.refetch === true || !this.hydratedDemands.has(db.getLoadSubsetDemandKey(options))) {
903
1452
  return;
904
1453
  }
905
1454
  this.activeSubsets.set(this.getSubsetKey(options), options);
@@ -939,13 +1488,15 @@ class PersistedCollectionRuntime {
939
1488
  const subsetKey = this.getSubsetKey(options);
940
1489
  this.activeSubsets.delete(subsetKey);
941
1490
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1491
+ const remoteOptions = this.rotatedAcquisitions.get(options) ?? options;
1492
+ this.rotatedAcquisitions.delete(options);
942
1493
  const demandKey = db.getLoadSubsetDemandKey(options);
943
1494
  const stillActive = Array.from(this.activeSubsets.values()).some(
944
1495
  (active) => db.getLoadSubsetDemandKey(active) === demandKey
945
1496
  );
946
1497
  if (!stillActive) this.hydratedDemands.delete(demandKey);
947
1498
  if (this.mode === `sync-present`) {
948
- void this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
1499
+ void this.persistence.coordinator.requestReleaseRemoteSubset(this.storageCollectionId, remoteOptions).catch((error) => {
949
1500
  this.reportSyncError(error);
950
1501
  });
951
1502
  }
@@ -962,9 +1513,10 @@ class PersistedCollectionRuntime {
962
1513
  }
963
1514
  async forceReloadSubset(options) {
964
1515
  this.throwIfTerminal();
1516
+ await this.ensureCacheClaimActive();
965
1517
  const lifecycleGeneration = this.lifecycleGeneration;
966
- await this.applyMutex.run(async () => {
967
- await this.runInHydrationScope(
1518
+ const hydrated = await this.applyMutex.run(async () => {
1519
+ const result = await this.runInHydrationScope(
968
1520
  (adapter) => this.hydrateSubsetUnsafe(
969
1521
  options,
970
1522
  {
@@ -977,7 +1529,9 @@ class PersistedCollectionRuntime {
977
1529
  if (lifecycleGeneration === this.lifecycleGeneration) {
978
1530
  await this.flushQueuedTxCommittedUnsafe();
979
1531
  }
1532
+ return result;
980
1533
  });
1534
+ if (!hydrated) await this.ensureCacheClaimActive();
981
1535
  }
982
1536
  queueHydrationBufferedTransaction(transaction) {
983
1537
  const failure = this.getCurrentTerminalFailure();
@@ -985,11 +1539,13 @@ class PersistedCollectionRuntime {
985
1539
  transaction.rejectApplied?.(failure.error);
986
1540
  return;
987
1541
  }
1542
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
988
1543
  this.queuedHydrationTransactions.push(transaction);
989
1544
  }
990
1545
  applyHydrationBufferedTransaction(transaction) {
991
1546
  const failure = this.getCurrentTerminalFailure();
992
1547
  if (failure) return Promise.reject(failure.error);
1548
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
993
1549
  return this.applyMutex.run(async () => {
994
1550
  await this.applyBufferedSyncTransactionUnsafe(transaction);
995
1551
  });
@@ -1094,7 +1650,10 @@ class PersistedCollectionRuntime {
1094
1650
  for (const options of this.activeSubsets.values()) {
1095
1651
  attempt(() => {
1096
1652
  remoteReleases.push(
1097
- this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
1653
+ this.persistence.coordinator.requestReleaseRemoteSubset(
1654
+ this.storageCollectionId,
1655
+ this.rotatedAcquisitions.get(options) ?? options
1656
+ ).catch((error) => {
1098
1657
  this.reportSyncError(error);
1099
1658
  })
1100
1659
  );
@@ -1105,6 +1664,7 @@ class PersistedCollectionRuntime {
1105
1664
  this.coordinatorUnsubscribe = null;
1106
1665
  attempt(() => this.remoteSubsetOwnerUnsubscribe?.());
1107
1666
  this.remoteSubsetOwnerUnsubscribe = null;
1667
+ this.remoteSubsetOwner = void 0;
1108
1668
  attempt(() => this.indexAddedUnsubscribe?.());
1109
1669
  this.indexAddedUnsubscribe = null;
1110
1670
  attempt(() => this.indexRemovedUnsubscribe?.());
@@ -1114,6 +1674,16 @@ class PersistedCollectionRuntime {
1114
1674
  this.remoteEnsureRetryTimer = null;
1115
1675
  }
1116
1676
  this.pendingRemoteSubsetEnsures.clear();
1677
+ this.rotatedAcquisitions.clear();
1678
+ for (const [options, storageId] of this.scopedRefreshes) {
1679
+ remoteReleases.push(
1680
+ this.persistence.coordinator.requestReleaseRemoteSubset(
1681
+ storageId,
1682
+ options
1683
+ )
1684
+ );
1685
+ }
1686
+ this.scopedRefreshes.clear();
1117
1687
  this.activeSubsets.clear();
1118
1688
  this.hydratedDemands.clear();
1119
1689
  for (const transaction of this.queuedHydrationTransactions) {
@@ -1123,6 +1693,15 @@ class PersistedCollectionRuntime {
1123
1693
  this.queuedTxCommitted.length = 0;
1124
1694
  this.clearSyncControls();
1125
1695
  this.collection = null;
1696
+ const claim = this.cacheGenerationClaim;
1697
+ this.cacheGenerationClaim = void 0;
1698
+ this.clearCacheClaimRenewal();
1699
+ this.cacheClaimRecovery = null;
1700
+ if (claim && this.persistence.adapter.releaseCacheGenerationClaim) {
1701
+ remoteReleases.push(
1702
+ this.persistence.adapter.releaseCacheGenerationClaim(claim.claimId)
1703
+ );
1704
+ }
1126
1705
  return Promise.all(remoteReleases).then(() => {
1127
1706
  if (failures.length === 1) throw failures[0];
1128
1707
  if (failures.length > 1) {
@@ -1134,8 +1713,16 @@ class PersistedCollectionRuntime {
1134
1713
  }
1135
1714
  advanceLifecycle() {
1136
1715
  this.lifecycleGeneration++;
1716
+ this.clearCacheClaimRenewal();
1717
+ this.cacheClaimRecovery = null;
1718
+ this.scopedRecovery = false;
1719
+ this.scopedRecoveryPending = false;
1720
+ this.scopedRecoverySequence++;
1721
+ this.recoverQueuedAfterLocalLoadFailure = false;
1137
1722
  this.persistedReadiness?.set({ status: `loading` });
1138
1723
  this.startupSettled = false;
1724
+ this.publicRowVersion = 0;
1725
+ this.resetTermFence = void 0;
1139
1726
  this.hydratedDemands.clear();
1140
1727
  this.startupMetadataPromise = null;
1141
1728
  this.startPromise = null;
@@ -1161,25 +1748,44 @@ class PersistedCollectionRuntime {
1161
1748
  return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
1162
1749
  }
1163
1750
  loadSubsetRowsUnsafe(options, adapter) {
1164
- return adapter.loadSubset(this.collectionId, options, {
1165
- requiredIndexSignatures: this.getRequiredIndexSignatures()
1751
+ if (this.scopedRecovery) return Promise.resolve([]);
1752
+ return adapter.loadSubset(this.storageTargetFor(), options, {
1753
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1754
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1166
1755
  });
1167
1756
  }
1168
1757
  async scanPersistedRowsUnsafe(options) {
1169
1758
  if (!this.persistence.adapter.scanRows) {
1170
1759
  return [];
1171
1760
  }
1172
- return this.persistence.adapter.scanRows(
1173
- this.collectionId,
1174
- options
1175
- );
1761
+ return this.persistence.adapter.scanRows(this.storageTargetFor(), options, {
1762
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1763
+ });
1176
1764
  }
1177
1765
  async scanPersistedRows(options) {
1178
- this.throwIfTerminal();
1179
- return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options));
1766
+ for (; ; ) {
1767
+ this.throwIfTerminal();
1768
+ await this.ensureCacheClaimActive();
1769
+ const claimId = this.cacheGenerationClaim?.claimId;
1770
+ const storageCollectionId = this.storageCollectionId;
1771
+ const rows = await this.applyMutex.run(
1772
+ () => this.scanPersistedRowsUnsafe(options)
1773
+ );
1774
+ if (this.scopedRecoveryPending) {
1775
+ throw new db.SyncTransactionAbortedError();
1776
+ }
1777
+ if (claimId !== this.cacheGenerationClaim?.claimId || storageCollectionId !== this.storageCollectionId) {
1778
+ continue;
1779
+ }
1780
+ if (claimId && !await this.renewCacheClaim(this.persistence.adapter)) {
1781
+ continue;
1782
+ }
1783
+ return rows;
1784
+ }
1180
1785
  }
1181
1786
  async hydrateSubsetUnsafe(options, config, adapter) {
1182
1787
  let rowsLoaded = false;
1788
+ let boundRowVersion;
1183
1789
  let replayFailure;
1184
1790
  const resetSequence = this.resetSequence;
1185
1791
  this.hydrationSequence++;
@@ -1190,38 +1796,68 @@ class PersistedCollectionRuntime {
1190
1796
  this.hydratingGeneration = config.lifecycleGeneration;
1191
1797
  try {
1192
1798
  let rows;
1193
- if (config.bindKeySetEvidence) {
1194
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
1195
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1196
- includeRows: true
1197
- });
1799
+ let bindResumeSnapshot;
1800
+ if (config.bindKeySetEvidence && !this.scopedRecovery) {
1801
+ const snapshot = await adapter.loadResumeSnapshot(
1802
+ this.storageTargetFor(),
1803
+ {
1804
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1805
+ includeRows: true,
1806
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1807
+ }
1808
+ );
1198
1809
  rows = snapshot.rows;
1199
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1200
- if (resetSequence !== this.resetSequence) return;
1201
- this.bindResumeSnapshotEvidence(snapshot);
1810
+ if (config.lifecycleGeneration !== this.lifecycleGeneration)
1811
+ return true;
1812
+ if (resetSequence !== this.resetSequence) return true;
1813
+ bindResumeSnapshot = () => this.bindResumeSnapshotEvidence(snapshot);
1814
+ boundRowVersion = snapshot.latestRowVersion;
1202
1815
  } else {
1203
1816
  rows = await this.loadSubsetRowsUnsafe(options, adapter);
1204
1817
  }
1818
+ const complete = this.syncMode === `on-demand` && !this.scopedRecovery && options.where === void 0 && options.limit === void 0 && options.offset === void 0 && options.cursor === void 0;
1819
+ const validationRows = complete ? rows : void 0;
1205
1820
  rowsLoaded = true;
1206
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1207
- if (resetSequence !== this.resetSequence) return;
1821
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1822
+ if (resetSequence !== this.resetSequence) return true;
1823
+ if (this.cacheGenerationClaim && !await this.renewCacheClaim(adapter)) {
1824
+ return false;
1825
+ }
1826
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1827
+ if (resetSequence !== this.resetSequence) return true;
1828
+ bindResumeSnapshot?.();
1208
1829
  if (!config.bindKeySetEvidence || this.persistedKeySetEvidence?.status !== `incompatible`) {
1209
1830
  for (const row of rows) {
1210
1831
  hydrationContext.suppliedRowKeys.add(row.key);
1211
1832
  }
1212
- const applied = this.applyRowsToCollection(rows);
1833
+ const applied = this.applyRowsToCollection(rows, validationRows);
1213
1834
  await db.whenSyncAccepted(applied);
1835
+ if (boundRowVersion !== void 0) {
1836
+ this.publicRowVersion = Math.max(
1837
+ this.publicRowVersion,
1838
+ boundRowVersion
1839
+ );
1840
+ }
1214
1841
  }
1215
1842
  } finally {
1216
1843
  if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === config.lifecycleGeneration) {
1217
1844
  this.hydratingGeneration = null;
1218
1845
  }
1219
1846
  }
1220
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1221
- replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1222
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1847
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1848
+ if (!this.scopedRecoveryPending) {
1849
+ replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1850
+ }
1851
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1223
1852
  } catch (error) {
1853
+ if (config.requestLocalLoadFailure && !rowsLoaded && this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
1854
+ return false;
1855
+ }
1224
1856
  if (config.requestLocalLoadFailure && !rowsLoaded) {
1857
+ if (this.scopedRecoveryPending) {
1858
+ this.recoverQueuedAfterLocalLoadFailure = true;
1859
+ throw error;
1860
+ }
1225
1861
  this.hydratingGeneration = config.lifecycleGeneration;
1226
1862
  try {
1227
1863
  await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
@@ -1250,6 +1886,7 @@ class PersistedCollectionRuntime {
1250
1886
  if (replayFailure && config.rejectBufferedReplayFailure) {
1251
1887
  throw replayFailure.reason;
1252
1888
  }
1889
+ return true;
1253
1890
  }
1254
1891
  async recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(lifecycleGeneration, adapter) {
1255
1892
  let snapshotRows;
@@ -1284,18 +1921,15 @@ class PersistedCollectionRuntime {
1284
1921
  const key = operation.key;
1285
1922
  if (operation.type === `update` && transaction.partialUpdateOperationIndexes.has(index) && getPresence(key) === `unknown`) {
1286
1923
  if (snapshotRows === void 0) {
1287
- const snapshot = await adapter.loadResumeSnapshot(
1288
- this.collectionId,
1289
- {
1290
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1291
- includeRows: true
1292
- }
1293
- );
1924
+ const snapshot = this.scopedRecovery ? void 0 : await adapter.loadResumeSnapshot(this.storageTargetFor(), {
1925
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1926
+ includeRows: true,
1927
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1928
+ });
1294
1929
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1295
1930
  if (transaction.signal?.aborted) break;
1296
- snapshotRows = new Map(
1297
- snapshot.rows.map((row) => [row.key, row])
1298
- );
1931
+ const rows = snapshot && !this.scopedRecovery ? snapshot.rows : [];
1932
+ snapshotRows = new Map(rows.map((row) => [row.key, row]));
1299
1933
  }
1300
1934
  const baseline = snapshotRows.get(key);
1301
1935
  if (baseline === void 0) {
@@ -1345,22 +1979,65 @@ class PersistedCollectionRuntime {
1345
1979
  }
1346
1980
  this.queuedHydrationTransactions.length = 0;
1347
1981
  }
1348
- applyRowsToCollection(rows) {
1982
+ applyRowsToCollection(rows, validationRows, collectionMetadata) {
1349
1983
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
1350
1984
  return true;
1351
1985
  }
1352
1986
  return this.withInternalApply(() => {
1353
1987
  this.syncControls.begin?.();
1988
+ if (validationRows) {
1989
+ const durableRows = new Map(validationRows.map((row) => [row.key, row]));
1990
+ for (const key of this.collection?.base.keys() ?? []) {
1991
+ if (this.collection?._hasHydratedKey(key)) continue;
1992
+ const durable = durableRows.get(key);
1993
+ if (!durable) {
1994
+ this.syncControls.write?.({ type: `delete`, key });
1995
+ } else {
1996
+ const valueChanged = !db.equalPersistedSnapshotValues(
1997
+ this.collection?.base.get(key),
1998
+ durable.value
1999
+ );
2000
+ const metadataChanged = !db.equalPersistedSnapshotValues(
2001
+ this.syncControls.metadata?.row.get(key),
2002
+ durable.metadata
2003
+ );
2004
+ if (valueChanged || metadataChanged && durable.metadata !== void 0) {
2005
+ if (valueChanged) {
2006
+ this.syncControls.write?.({ type: `delete`, key });
2007
+ }
2008
+ this.syncControls.write?.({
2009
+ type: `update`,
2010
+ value: durable.value,
2011
+ metadata: durable.metadata
2012
+ });
2013
+ }
2014
+ if (metadataChanged && durable.metadata === void 0) {
2015
+ this.syncControls.metadata?.row.delete(key);
2016
+ }
2017
+ }
2018
+ }
2019
+ for (const key of this.collection?._syncedRowMetadataKeysOutsideBase() ?? []) {
2020
+ this.syncControls.metadata?.row.delete(key);
2021
+ }
2022
+ }
1354
2023
  for (const row of rows) {
1355
- if (this.collection?._hasHydratedKey(row.key)) {
2024
+ if (validationRows && this.collection?.base.has(row.key) || this.collection?._hasHydratedKey(row.key)) {
1356
2025
  continue;
1357
2026
  }
2027
+ if (this.collection?.base.has(row.key) && !db.equalPersistedSnapshotValues(
2028
+ this.collection.base.get(row.key),
2029
+ row.value
2030
+ )) {
2031
+ this.syncControls.write?.({ type: `delete`, key: row.key });
2032
+ }
1358
2033
  this.syncControls.write?.({
1359
2034
  type: `update`,
1360
2035
  value: row.value,
1361
2036
  metadata: row.metadata
1362
2037
  });
1363
2038
  }
2039
+ if (collectionMetadata)
2040
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1364
2041
  return this.syncControls.commit?.() ?? true;
1365
2042
  });
1366
2043
  }
@@ -1374,7 +2051,9 @@ class PersistedCollectionRuntime {
1374
2051
  }
1375
2052
  isExpectedResumeGeneration(generation) {
1376
2053
  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;
2054
+ return expected !== void 0 && expected.latestRowVersion === generation.latestRowVersion && expected.resetEpoch === generation.resetEpoch && (expected.latestTerm === generation.latestTerm && expected.latestSeq === generation.latestSeq || // A no-write election reserves a new term at sequence zero. It does
2055
+ // not change the persisted rows or the source resume cursor.
2056
+ generation.latestTerm > expected.latestTerm && generation.latestSeq === 0);
1378
2057
  }
1379
2058
  bindResumeSnapshotEvidence(snapshot) {
1380
2059
  const generation = this.getResumeSnapshotGeneration(snapshot);
@@ -1387,35 +2066,69 @@ class PersistedCollectionRuntime {
1387
2066
  );
1388
2067
  this.persistedKeySetEvidence = this.isExpectedResumeGeneration(generation) || mayAcceptUnownedGeneration ? snapshot.keySet : { status: `incompatible` };
1389
2068
  }
1390
- replaceCollectionSnapshot(rows, collectionMetadata) {
2069
+ replaceCollectionSnapshot(rows, collectionMetadata, replayDemand) {
1391
2070
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
1392
2071
  return true;
1393
2072
  }
1394
- const nextMetadata = new Map(
1395
- collectionMetadata.map(({ key, value }) => [key, value])
2073
+ const base = this.collection?.base;
2074
+ const hydratedKeys = new Set(
2075
+ Array.from(base?.keys() ?? []).filter(
2076
+ (key) => this.collection?._hasHydratedKey(key) === true
2077
+ )
1396
2078
  );
1397
- const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
2079
+ const rowKeys = new Set(rows.map((row) => row.key));
2080
+ const followsLeader = this.canRouteRemoteDemandThroughCoordinator() && !this.persistence.coordinator.isLeader(this.storageCollectionId);
2081
+ const truncate = hydratedKeys.size === 0 && (replayDemand || !followsLeader);
1398
2082
  return this.withInternalApply(() => {
1399
2083
  this.syncControls.begin?.();
1400
- this.syncControls.truncate?.();
2084
+ if (truncate) {
2085
+ this.syncControls.truncate?.();
2086
+ } else {
2087
+ for (const key of base?.keys() ?? []) {
2088
+ if (!hydratedKeys.has(key) && !rowKeys.has(key)) {
2089
+ this.syncControls.write?.({ type: `delete`, key });
2090
+ }
2091
+ }
2092
+ for (const key of this.collection?._syncedRowMetadataKeysOutsideBase() ?? []) {
2093
+ this.syncControls.metadata?.row.delete(key);
2094
+ }
2095
+ }
1401
2096
  for (const row of rows) {
2097
+ if (hydratedKeys.has(row.key)) continue;
2098
+ if (!truncate && base?.has(row.key)) {
2099
+ this.syncControls.write?.({ type: `delete`, key: row.key });
2100
+ }
1402
2101
  this.syncControls.write?.({
1403
2102
  type: `update`,
1404
2103
  value: row.value,
1405
2104
  metadata: row.metadata
1406
2105
  });
1407
2106
  }
1408
- currentKeys.forEach((key) => {
1409
- if (!nextMetadata.has(key)) {
1410
- this.syncControls.metadata?.collection.delete(key);
1411
- }
1412
- });
1413
- nextMetadata.forEach((value, key) => {
1414
- this.syncControls.metadata?.collection.set(key, value);
1415
- });
2107
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1416
2108
  return this.syncControls.commit?.() ?? true;
1417
2109
  });
1418
2110
  }
2111
+ matchesCollectionSnapshot(rows, collectionMetadata) {
2112
+ if (!this.collection || !this.syncControls.metadata) return false;
2113
+ const base = this.collection.base;
2114
+ if (base.size !== rows.size) return false;
2115
+ for (const [key, row] of rows) {
2116
+ if (!base.has(key) || !db.equalPersistedSnapshotValues(base.get(key), row.value) || !db.equalPersistedSnapshotValues(
2117
+ this.syncControls.metadata.row.get(key),
2118
+ row.metadata
2119
+ )) {
2120
+ return false;
2121
+ }
2122
+ }
2123
+ const currentMetadata = this.syncControls.metadata.collection.list();
2124
+ if (currentMetadata.length !== collectionMetadata.length) return false;
2125
+ const expectedMetadata = new Map(
2126
+ collectionMetadata.map(({ key, value }) => [key, value])
2127
+ );
2128
+ return currentMetadata.every(
2129
+ ({ key, value }) => expectedMetadata.has(key) && db.equalPersistedSnapshotValues(expectedMetadata.get(key), value)
2130
+ );
2131
+ }
1419
2132
  async flushQueuedHydrationTransactionsUnsafe(adapter) {
1420
2133
  let operationFailure;
1421
2134
  while (this.queuedHydrationTransactions.length > 0) {
@@ -1443,6 +2156,31 @@ class PersistedCollectionRuntime {
1443
2156
  async applyBufferedSyncTransactionUnsafe(transaction, scopedAdapter) {
1444
2157
  this.throwIfTerminal();
1445
2158
  this.throwIfLifecycleReplaced(transaction.lifecycleGeneration);
2159
+ if (transaction.admittedStorageCollectionId !== void 0 && transaction.admittedStorageCollectionId !== this.storageCollectionId) {
2160
+ const error = new db.SyncTransactionAbortedError();
2161
+ if (!transaction.rejectApplied) throw error;
2162
+ transaction.rejectApplied(error);
2163
+ return { applied: false };
2164
+ }
2165
+ if (!transaction.internal && this.scopedRecoveryPending) {
2166
+ const error = new db.SyncTransactionAbortedError();
2167
+ if (!transaction.rejectApplied) throw error;
2168
+ transaction.rejectApplied(error);
2169
+ return { applied: false };
2170
+ }
2171
+ if (!transaction.internal && this.cacheGenerationClaim && (scopedAdapter ?? this.persistence.adapter).renewCacheGenerationClaim && !await this.renewCacheClaim(scopedAdapter ?? this.persistence.adapter)) {
2172
+ const error = new db.SyncTransactionAbortedError();
2173
+ queueMicrotask(() => {
2174
+ if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return;
2175
+ if (this.scopedRecoveryPending) return;
2176
+ void this.startScopedRecovery().catch((failure) => {
2177
+ this.markTerminalFailure(failure, transaction.lifecycleGeneration);
2178
+ });
2179
+ });
2180
+ if (!transaction.rejectApplied) throw error;
2181
+ transaction.rejectApplied(error);
2182
+ return { applied: false };
2183
+ }
1446
2184
  const abortedBeforeApplication = transaction.signal?.aborted === true;
1447
2185
  let abortedDuringApplication = false;
1448
2186
  let applicationReturned = false;
@@ -1500,16 +2238,56 @@ class PersistedCollectionRuntime {
1500
2238
  if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
1501
2239
  return;
1502
2240
  }
2241
+ const anchor = this.persistedResumeGeneration && {
2242
+ latestRowVersion: this.latestRowVersion,
2243
+ resetEpoch: this.persistedResumeGeneration.resetEpoch
2244
+ };
1503
2245
  const streamPosition = this.nextLocalStreamPosition();
1504
2246
  const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
1505
- const response = await this.persistence.coordinator.requestApplyCommittedTx(
1506
- this.collectionId,
1507
- tx,
1508
- // A hydration-scoped adapter is already inside the shared scheduler.
1509
- // Ordinary source commits pass no scoped adapter so the coordinator
1510
- // enters that scheduler before taking the database-wide writer lock.
1511
- scopedAdapter
1512
- );
2247
+ const storageCollectionId = this.storageCollectionId;
2248
+ let response;
2249
+ try {
2250
+ response = await this.persistence.coordinator.requestApplyCommittedTx(
2251
+ storageCollectionId,
2252
+ tx,
2253
+ // A hydration-scoped adapter is already inside the shared scheduler.
2254
+ // Ordinary source commits enter it before taking the writer lock.
2255
+ scopedAdapter
2256
+ );
2257
+ } catch (error) {
2258
+ if (!(error instanceof errors.IndeterminateCommitError) || !anchor || !this.persistence.coordinator.reconcileCommittedTx) {
2259
+ throw error;
2260
+ }
2261
+ let reconciled;
2262
+ try {
2263
+ reconciled = await this.persistence.coordinator.reconcileCommittedTx(
2264
+ storageCollectionId,
2265
+ tx,
2266
+ anchor,
2267
+ scopedAdapter
2268
+ );
2269
+ } catch (reconcileError) {
2270
+ if (reconcileError instanceof errors.PersistedCollectionDurabilityError) {
2271
+ throw reconcileError;
2272
+ }
2273
+ throw error;
2274
+ }
2275
+ if (!reconciled.ok) {
2276
+ if (reconciled.code === `PERSISTENCE_ERROR`) {
2277
+ throw new errors.PersistedCollectionDurabilityError(
2278
+ `Failed to reconcile collection "${this.collectionId}": ${reconciled.error}`,
2279
+ {
2280
+ cause: reconciled,
2281
+ code: reconciled.sourceCode ?? reconciled.code,
2282
+ path: reconciled.path
2283
+ }
2284
+ );
2285
+ }
2286
+ throw error;
2287
+ }
2288
+ this.recordDurableSourceCommit(transaction, reconciled.committed);
2289
+ return;
2290
+ }
1513
2291
  if (!response.ok) {
1514
2292
  if (response.code === `PERSISTENCE_ERROR`) {
1515
2293
  const error = new errors.PersistedCollectionDurabilityError(
@@ -1526,18 +2304,24 @@ class PersistedCollectionRuntime {
1526
2304
  `failed to apply external sync transaction through coordinator: ${response.error}`
1527
2305
  );
1528
2306
  }
2307
+ this.recordDurableSourceCommit(transaction, {
2308
+ term: response.term,
2309
+ seq: response.seq,
2310
+ rowVersion: response.latestRowVersion
2311
+ });
2312
+ }
2313
+ recordDurableSourceCommit(transaction, position) {
1529
2314
  if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return;
1530
- this.observeStreamPosition(
1531
- response.term,
1532
- response.seq,
1533
- response.latestRowVersion
1534
- );
2315
+ this.observeStreamPosition(position.term, position.seq, position.rowVersion);
2316
+ if (position.rowVersion === this.publicRowVersion + 1) {
2317
+ this.publicRowVersion = position.rowVersion;
2318
+ }
1535
2319
  if (transaction.expectedResumeGenerationOwner === this.resumeGenerationOwner && this.persistedResumeGeneration !== void 0) {
1536
2320
  this.persistedResumeGeneration = {
1537
2321
  ...this.persistedResumeGeneration,
1538
- latestTerm: response.term,
1539
- latestSeq: response.seq,
1540
- latestRowVersion: response.latestRowVersion
2322
+ latestTerm: position.term,
2323
+ latestSeq: position.seq,
2324
+ latestRowVersion: position.rowVersion
1541
2325
  };
1542
2326
  }
1543
2327
  }
@@ -1545,7 +2329,10 @@ class PersistedCollectionRuntime {
1545
2329
  this.throwIfTerminal();
1546
2330
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1547
2331
  try {
1548
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
2332
+ await this.persistence.adapter.applyCommittedTx(
2333
+ this.storageTargetFor(),
2334
+ tx
2335
+ );
1549
2336
  } catch (error) {
1550
2337
  throw this.markTerminalFailure(
1551
2338
  errors.toPersistedCollectionDurabilityError(this.collectionId, error),
@@ -1560,6 +2347,7 @@ class PersistedCollectionRuntime {
1560
2347
  term: streamPosition.term,
1561
2348
  seq: streamPosition.seq,
1562
2349
  rowVersion: streamPosition.rowVersion,
2350
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1563
2351
  truncate: transaction.truncate,
1564
2352
  mutations: transaction.operations.map(
1565
2353
  (operation) => operation.type === `update` ? {
@@ -1594,6 +2382,7 @@ class PersistedCollectionRuntime {
1594
2382
  term: streamPosition.term,
1595
2383
  seq: streamPosition.seq,
1596
2384
  rowVersion: streamPosition.rowVersion,
2385
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1597
2386
  mutations: mutations.map((mutation) => {
1598
2387
  if (mutation.type === `delete`) {
1599
2388
  return {
@@ -1653,14 +2442,16 @@ class PersistedCollectionRuntime {
1653
2442
  async persistCollectionMutationsUnsafe(mutations, lifecycleGeneration) {
1654
2443
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1655
2444
  if (this.persistence.coordinator.requestApplyLocalMutations) {
2445
+ const target = this.storageTargetFor();
1656
2446
  const envelopeMutations = mutations.map(
1657
2447
  (mutation) => toPersistedMutationEnvelope(
1658
2448
  mutation
1659
2449
  )
1660
2450
  );
1661
2451
  const response = await this.persistence.coordinator.requestApplyLocalMutations(
1662
- this.collectionId,
1663
- envelopeMutations
2452
+ this.storageCollectionId,
2453
+ envelopeMutations,
2454
+ target.kind === `managed` ? target.claimId : void 0
1664
2455
  );
1665
2456
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1666
2457
  if (!response.ok) {
@@ -1757,12 +2548,12 @@ class PersistedCollectionRuntime {
1757
2548
  const envelope = {
1758
2549
  v: 1,
1759
2550
  dbName: this.dbName,
1760
- collectionId: this.collectionId,
2551
+ collectionId: this.storageCollectionId,
1761
2552
  senderId: this.persistence.coordinator.getNodeId(),
1762
2553
  ts: Date.now(),
1763
2554
  payload: txCommitted
1764
2555
  };
1765
- this.persistence.coordinator.publish(this.collectionId, envelope);
2556
+ this.persistence.coordinator.publish(this.storageCollectionId, envelope);
1766
2557
  }
1767
2558
  observeStreamPosition(term, seq, rowVersion) {
1768
2559
  if (term > this.latestTerm || term === this.latestTerm && seq > this.latestSeq) {
@@ -1804,7 +2595,7 @@ class PersistedCollectionRuntime {
1804
2595
  if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1805
2596
  return false;
1806
2597
  }
1807
- return !this.persistence.coordinator.isLeader(this.collectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
2598
+ return !this.persistence.coordinator.isLeader(this.storageCollectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
1808
2599
  }
1809
2600
  queueRemoteSubsetEnsure(options) {
1810
2601
  if (options.signal?.aborted || !this.canRouteRemoteDemandThroughCoordinator() || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
@@ -1843,8 +2634,8 @@ class PersistedCollectionRuntime {
1843
2634
  }
1844
2635
  try {
1845
2636
  await this.persistence.coordinator.requestEnsureRemoteSubset(
1846
- this.collectionId,
1847
- options
2637
+ this.storageCollectionId,
2638
+ this.rotatedAcquisitions.get(options) ?? options
1848
2639
  );
1849
2640
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1850
2641
  } catch (error) {
@@ -1862,18 +2653,19 @@ class PersistedCollectionRuntime {
1862
2653
  return;
1863
2654
  }
1864
2655
  this.persistence.coordinator.setAdapterForCollection?.(
1865
- this.collectionId,
1866
- this.persistence.adapter
2656
+ this.storageCollectionId,
2657
+ this.persistence.adapter,
2658
+ this.cacheGenerationClaim?.claimId
1867
2659
  );
1868
2660
  this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
1869
- this.collectionId,
2661
+ this.storageCollectionId,
1870
2662
  (message) => {
1871
2663
  this.onCoordinatorMessage(message);
1872
2664
  }
1873
2665
  );
1874
2666
  }
1875
2667
  onCoordinatorMessage(message) {
1876
- if (message.collectionId !== this.collectionId) {
2668
+ if (message.collectionId !== this.storageCollectionId) {
1877
2669
  return;
1878
2670
  }
1879
2671
  if (this.getCurrentTerminalFailure()) {
@@ -1882,17 +2674,24 @@ class PersistedCollectionRuntime {
1882
2674
  const { payload } = message;
1883
2675
  const lifecycleGeneration = this.lifecycleGeneration;
1884
2676
  const isSelf = message.senderId === this.persistence.coordinator.getNodeId();
2677
+ const claimExpired = this.hasExpiredCacheClaim();
2678
+ if (claimExpired && !isTxCommittedPayload(payload)) {
2679
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
2680
+ return;
2681
+ }
1885
2682
  if (isTxCommittedPayload(payload)) {
1886
- if (this.isHydratingNow()) {
1887
- this.queuedTxCommitted.push({
1888
- txCommitted: payload,
1889
- lifecycleGeneration
2683
+ this.queuedTxCommitted.push({
2684
+ txCommitted: payload,
2685
+ lifecycleGeneration,
2686
+ storageCollectionId: message.collectionId
2687
+ });
2688
+ if (claimExpired) {
2689
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
2690
+ } else if (!this.isHydratingNow()) {
2691
+ void this.applyMutex.run(() => this.flushQueuedTxCommittedUnsafe()).catch((error) => {
2692
+ this.markTerminalFailure(error, lifecycleGeneration);
1890
2693
  });
1891
- return;
1892
2694
  }
1893
- void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload, lifecycleGeneration)).catch((error) => {
1894
- this.markTerminalFailure(error, lifecycleGeneration);
1895
- });
1896
2695
  return;
1897
2696
  }
1898
2697
  if (isSelf) {
@@ -1900,9 +2699,18 @@ class PersistedCollectionRuntime {
1900
2699
  }
1901
2700
  if (isCollectionResetPayload(payload)) {
1902
2701
  void this.applyMutex.run(async () => {
1903
- await this.runInHydrationScope(
1904
- (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
1905
- );
2702
+ if (message.collectionId !== this.storageCollectionId) return;
2703
+ await this.runInHydrationScope(async (adapter) => {
2704
+ await this.truncateAndReloadUnsafe(adapter, lifecycleGeneration);
2705
+ await this.rebaseDurableStreamFromSnapshotUnsafe(
2706
+ adapter,
2707
+ lifecycleGeneration
2708
+ );
2709
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2710
+ this.publicRowVersion = 0;
2711
+ this.resetTermFence = this.latestTerm;
2712
+ }
2713
+ });
1906
2714
  if (lifecycleGeneration === this.lifecycleGeneration) {
1907
2715
  await this.flushQueuedTxCommittedUnsafe();
1908
2716
  }
@@ -1913,33 +2721,63 @@ class PersistedCollectionRuntime {
1913
2721
  }
1914
2722
  async flushQueuedTxCommittedUnsafe() {
1915
2723
  while (this.queuedTxCommitted.length > 0) {
2724
+ if (this.hasExpiredCacheClaim()) {
2725
+ this.scheduleCacheClaimRecovery(this.lifecycleGeneration);
2726
+ return;
2727
+ }
1916
2728
  const queued = this.queuedTxCommitted.shift();
1917
2729
  if (!queued) {
1918
2730
  continue;
1919
2731
  }
1920
- await this.processCommittedTxUnsafe(
1921
- queued.txCommitted,
1922
- queued.lifecycleGeneration
1923
- );
2732
+ if (queued.storageCollectionId !== this.storageCollectionId) continue;
2733
+ try {
2734
+ await this.processCommittedTxUnsafe(
2735
+ queued.txCommitted,
2736
+ queued.lifecycleGeneration
2737
+ );
2738
+ } catch (error) {
2739
+ throw this.markTerminalFailure(error, queued.lifecycleGeneration);
2740
+ }
1924
2741
  }
1925
2742
  }
1926
2743
  async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
1927
2744
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1928
- if (txCommitted.term < this.latestTerm) {
1929
- return;
2745
+ if (this.resetTermFence !== void 0) {
2746
+ if (txCommitted.term <= this.resetTermFence) {
2747
+ await this.runInHydrationScope(
2748
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2749
+ );
2750
+ return;
2751
+ }
1930
2752
  }
1931
- if (txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
2753
+ if (txCommitted.requiresFullReload && txCommitted.reconciliationReload) {
2754
+ await this.invalidateFromCommittedTxUnsafe(
2755
+ txCommitted,
2756
+ this.persistence.adapter
2757
+ );
2758
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2759
+ this.observeStreamPosition(
2760
+ txCommitted.term,
2761
+ txCommitted.seq,
2762
+ txCommitted.latestRowVersion
2763
+ );
2764
+ this.publicRowVersion = Math.max(
2765
+ this.publicRowVersion,
2766
+ txCommitted.latestRowVersion
2767
+ );
2768
+ await this.flushQueuedTxCommittedUnsafe();
2769
+ }
1932
2770
  return;
1933
2771
  }
2772
+ if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
2773
+ const observedButUnpublished = txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq;
1934
2774
  const hasGapInCurrentTerm = txCommitted.term === this.latestTerm && txCommitted.seq > this.latestSeq + 1;
1935
2775
  const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
1936
- const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
2776
+ const hasGap = observedButUnpublished || txCommitted.latestRowVersion > this.publicRowVersion + 1 || hasGapInCurrentTerm || hasGapAcrossTerms;
1937
2777
  if (hasGap) {
1938
- await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
2778
+ if (!await this.recoverFromSeqGapUnsafe(lifecycleGeneration)) return;
1939
2779
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1940
- if (txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
1941
- return;
1942
- }
2780
+ if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
1943
2781
  }
1944
2782
  this.observeStreamPosition(
1945
2783
  txCommitted.term,
@@ -1951,23 +2789,49 @@ class PersistedCollectionRuntime {
1951
2789
  this.persistence.adapter
1952
2790
  );
1953
2791
  if (lifecycleGeneration === this.lifecycleGeneration) {
2792
+ this.publicRowVersion = Math.max(
2793
+ this.publicRowVersion,
2794
+ txCommitted.latestRowVersion
2795
+ );
1954
2796
  await this.flushQueuedTxCommittedUnsafe();
1955
2797
  }
1956
2798
  }
1957
- async recoverFromSeqGapUnsafe(lifecycleGeneration) {
2799
+ async rebaseDurableStreamFromSnapshotUnsafe(adapter, lifecycleGeneration) {
2800
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2801
+ const snapshot = await adapter.loadResumeSnapshot(this.storageTargetFor(), {
2802
+ includeRows: false,
2803
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
2804
+ });
1958
2805
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2806
+ this.latestRowVersion = snapshot.latestRowVersion;
2807
+ this.localRowVersion = snapshot.latestRowVersion;
2808
+ this.observeStreamPosition(
2809
+ snapshot.latestTerm,
2810
+ snapshot.latestSeq,
2811
+ snapshot.latestRowVersion
2812
+ );
2813
+ }
2814
+ async recoverFromSeqGapUnsafe(lifecycleGeneration) {
2815
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1959
2816
  if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
1960
2817
  let pullResponse;
1961
2818
  try {
1962
2819
  pullResponse = await this.persistence.coordinator.pullSince(
1963
- this.collectionId,
1964
- this.latestRowVersion
2820
+ this.storageCollectionId,
2821
+ this.publicRowVersion,
2822
+ void 0,
2823
+ this.cacheGenerationClaim?.claimId
1965
2824
  );
1966
2825
  } catch (error) {
1967
2826
  console.warn(`Failed pullSince recovery attempt:`, error);
1968
2827
  }
2828
+ if (this.scopedRecoveryPending) return false;
2829
+ if (this.cacheGenerationClaim && !await this.renewCacheClaim(this.persistence.adapter)) {
2830
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
2831
+ return false;
2832
+ }
1969
2833
  if (pullResponse) {
1970
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2834
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1971
2835
  if (pullResponse.ok) {
1972
2836
  this.observeStreamPosition(
1973
2837
  pullResponse.latestTerm,
@@ -1978,14 +2842,22 @@ class PersistedCollectionRuntime {
1978
2842
  await this.runInHydrationScope(
1979
2843
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1980
2844
  );
1981
- return;
2845
+ this.publicRowVersion = Math.max(
2846
+ this.publicRowVersion,
2847
+ pullResponse.latestRowVersion
2848
+ );
2849
+ return true;
1982
2850
  }
1983
2851
  const deltas = pullResponse.deltas;
1984
2852
  if (!deltas) {
1985
2853
  await this.runInHydrationScope(
1986
2854
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1987
2855
  );
1988
- return;
2856
+ this.publicRowVersion = Math.max(
2857
+ this.publicRowVersion,
2858
+ pullResponse.latestRowVersion
2859
+ );
2860
+ return true;
1989
2861
  }
1990
2862
  await this.runInHydrationScope(async (adapter) => {
1991
2863
  for (const delta of deltas) {
@@ -2008,25 +2880,69 @@ class PersistedCollectionRuntime {
2008
2880
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2009
2881
  }
2010
2882
  });
2011
- return;
2883
+ this.publicRowVersion = Math.max(
2884
+ this.publicRowVersion,
2885
+ pullResponse.latestRowVersion
2886
+ );
2887
+ return true;
2012
2888
  }
2013
2889
  }
2014
2890
  }
2015
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2891
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2016
2892
  await this.runInHydrationScope(
2017
- (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
2893
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2894
+ );
2895
+ this.publicRowVersion = Math.max(
2896
+ this.publicRowVersion,
2897
+ this.latestRowVersion
2018
2898
  );
2019
2899
  if (this.mode === `sync-present`) {
2020
2900
  for (const options of this.activeSubsets.values()) {
2021
2901
  this.queueRemoteSubsetEnsure(options);
2022
2902
  }
2023
2903
  }
2904
+ return true;
2905
+ }
2906
+ refreshScopedSubsets() {
2907
+ if (!this.remoteSubsetOwner) return;
2908
+ const lifecycleGeneration = this.lifecycleGeneration;
2909
+ const storageId = this.storageCollectionId;
2910
+ for (const [subsetKey, options] of this.activeSubsets) {
2911
+ if (options.signal?.aborted) continue;
2912
+ this.hydratedDemands.delete(db.getLoadSubsetDemandKey(options));
2913
+ const refreshOptions = { ...options, refetch: true };
2914
+ this.scopedRefreshes.set(refreshOptions, storageId);
2915
+ void Promise.resolve().then(() => {
2916
+ if (lifecycleGeneration !== this.lifecycleGeneration || this.activeSubsets.get(subsetKey) !== options) {
2917
+ return;
2918
+ }
2919
+ return this.persistence.coordinator.requestEnsureRemoteSubset(
2920
+ storageId,
2921
+ refreshOptions
2922
+ );
2923
+ }).catch((error) => {
2924
+ if (lifecycleGeneration === this.lifecycleGeneration && !options.signal?.aborted) {
2925
+ this.reportSyncError(error);
2926
+ }
2927
+ }).finally(() => {
2928
+ if (!this.scopedRefreshes.delete(refreshOptions)) return;
2929
+ void this.persistence.coordinator.requestReleaseRemoteSubset(storageId, refreshOptions).catch((error) => {
2930
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2931
+ this.reportSyncError(error);
2932
+ }
2933
+ });
2934
+ });
2935
+ }
2024
2936
  }
2025
2937
  async truncateAndReloadUnsafe(adapter, lifecycleGeneration = this.lifecycleGeneration) {
2026
2938
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2939
+ if (this.scopedRecovery) {
2940
+ this.refreshScopedSubsets();
2941
+ return;
2942
+ }
2027
2943
  this.hydratedDemands.clear();
2028
2944
  this.resetSequence++;
2029
- if (this.syncControls.begin && this.syncControls.commit) {
2945
+ if (this.syncMode === `eager` && this.syncControls.begin && this.syncControls.commit) {
2030
2946
  const applied = this.withInternalApply(() => {
2031
2947
  this.syncControls.begin?.();
2032
2948
  this.syncControls.truncate?.();
@@ -2035,15 +2951,24 @@ class PersistedCollectionRuntime {
2035
2951
  await db.whenSyncAccepted(applied);
2036
2952
  }
2037
2953
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2038
- await this.reloadActiveSubsetsUnsafe(adapter);
2954
+ await this.reloadActiveSubsetsUnsafe(
2955
+ adapter,
2956
+ false,
2957
+ this.syncMode === `eager`,
2958
+ true
2959
+ );
2039
2960
  }
2040
2961
  async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
2041
- const reloadActiveSubsets = () => this.runInHydrationScope(
2042
- (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
2962
+ if (this.scopedRecovery) {
2963
+ this.refreshScopedSubsets();
2964
+ return;
2965
+ }
2966
+ const reloadActiveSubsets = (skipUnchangedReplace = false) => this.runInHydrationScope(
2967
+ (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter, skipUnchangedReplace),
2043
2968
  adapter
2044
2969
  );
2045
2970
  if (txCommitted.requiresFullReload) {
2046
- await reloadActiveSubsets();
2971
+ await reloadActiveSubsets(txCommitted.reconciliationReload === true);
2047
2972
  return;
2048
2973
  }
2049
2974
  const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
@@ -2054,6 +2979,21 @@ class PersistedCollectionRuntime {
2054
2979
  const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
2055
2980
  (opt) => opt.limit != null || opt.offset != null || opt.cursor != null
2056
2981
  );
2982
+ const changedKeys = new Set(txCommitted.changedRows.map((row) => row.key));
2983
+ const hasRepeatedChange = changedKeys.size !== txCommitted.changedRows.length;
2984
+ const hasChangedAndDeletedKey = txCommitted.deletedKeys.some(
2985
+ (key) => changedKeys.has(key)
2986
+ );
2987
+ const hasChangedPublicKey = txCommitted.changedRows.some(
2988
+ (row) => this.collection?.base.has(row.key)
2989
+ );
2990
+ const hasUnseenDemandedKey = this.activeSubsets.size > 0 && txCommitted.changedRows.some(
2991
+ (row) => !this.collection?.base.has(row.key) && !this.collection?._hasHydratedKey(row.key)
2992
+ );
2993
+ if (hasRepeatedChange || hasChangedAndDeletedKey || hasChangedPublicKey || this.syncMode === `on-demand` && hasUnseenDemandedKey) {
2994
+ await reloadActiveSubsets();
2995
+ return;
2996
+ }
2057
2997
  if (!hasPaginatedSubset || changedKeyCount === 0) {
2058
2998
  await this.applyTargetedInvalidationUnsafe(txCommitted);
2059
2999
  return;
@@ -2070,20 +3010,33 @@ class PersistedCollectionRuntime {
2070
3010
  key: changedKey,
2071
3011
  value: newValue
2072
3012
  } of txCommitted.changedRows) {
2073
- const matchesAnySubset = subsetEvaluators.some((evaluator) => {
2074
- if (!evaluator) return true;
2075
- return db.toBooleanPredicate(evaluator(newValue));
2076
- });
2077
- if (matchesAnySubset) {
2078
- this.syncControls.write?.({ type: `update`, value: newValue });
2079
- } else if (this.collection?.get(changedKey) !== void 0) {
2080
- this.syncControls.write?.({ type: `delete`, key: changedKey });
3013
+ const key = changedKey;
3014
+ if (this.collection?._hasHydratedKey(key)) continue;
3015
+ const previous = this.collection?.base.get(key);
3016
+ if (previous === void 0 && this.activeSubsets.size === 0) continue;
3017
+ const completeValue = {
3018
+ ...previous,
3019
+ ...newValue
3020
+ };
3021
+ const currentlyDemanded = matchesAnySubset(
3022
+ completeValue,
3023
+ subsetEvaluators
3024
+ );
3025
+ const previouslyOutsideDemand = previous !== void 0 && !matchesAnySubset(previous, subsetEvaluators);
3026
+ if (currentlyDemanded || previouslyOutsideDemand) {
3027
+ this.syncControls.write?.({ type: `update`, value: completeValue });
3028
+ } else if (previous !== void 0) {
3029
+ this.syncControls.write?.({ type: `delete`, key });
2081
3030
  }
2082
3031
  }
2083
3032
  for (const deletedKey of txCommitted.deletedKeys) {
2084
- this.syncControls.write?.({ type: `delete`, key: deletedKey });
3033
+ const key = deletedKey;
3034
+ if (!this.collection?._hasHydratedKey(key)) {
3035
+ this.syncControls.write?.({ type: `delete`, key });
3036
+ }
2085
3037
  }
2086
3038
  txCommitted.rowMetadataMutations?.forEach((mutation) => {
3039
+ if (this.collection?._hasHydratedKey(mutation.key)) return;
2087
3040
  if (mutation.type === `delete`) {
2088
3041
  this.syncControls.metadata?.row.delete(mutation.key);
2089
3042
  } else {
@@ -2107,10 +3060,25 @@ class PersistedCollectionRuntime {
2107
3060
  });
2108
3061
  await db.whenSyncAccepted(applied);
2109
3062
  }
2110
- async reloadActiveSubsetsUnsafe(adapter) {
3063
+ async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false, restoreResetBaseline = false, replayDemand = false) {
3064
+ if (this.scopedRecovery) {
3065
+ this.refreshScopedSubsets();
3066
+ return;
3067
+ }
2111
3068
  const lifecycleGeneration = this.lifecycleGeneration;
2112
3069
  const truncateGeneration = this.sourceTruncateGeneration;
2113
- const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
3070
+ const activeSubsetOptions = Array.from(this.activeSubsets.values());
3071
+ if (restoreResetBaseline && activeSubsetOptions.length === 0) {
3072
+ activeSubsetOptions.push({});
3073
+ }
3074
+ const evaluators = activeSubsetOptions.map(
3075
+ (options) => options.where ? db.compileSingleRowExpression(options.where) : null
3076
+ );
3077
+ const outsideDemandKeys = activeSubsetOptions.some(
3078
+ (options) => options.limit != null || options.offset != null || options.cursor != null
3079
+ ) ? [] : Array.from(this.collection?.base ?? []).filter(
3080
+ ([key, row]) => !this.collection?._hasHydratedKey(key) && !matchesAnySubset(row, evaluators)
3081
+ ).map(([key]) => key);
2114
3082
  this.hydratedDemands.clear();
2115
3083
  this.hydrationSequence++;
2116
3084
  const hydrationContext = { suppliedRowKeys: /* @__PURE__ */ new Set() };
@@ -2118,30 +3086,91 @@ class PersistedCollectionRuntime {
2118
3086
  this.hydratingGeneration = lifecycleGeneration;
2119
3087
  try {
2120
3088
  const mergedRows = /* @__PURE__ */ new Map();
2121
- const collectionMetadata = await this.loadCollectionMetadataSnapshot(adapter);
2122
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2123
- for (const options of activeSubsetOptions) {
2124
- const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter);
3089
+ let validationSnapshot;
3090
+ let collectionMetadata = [];
3091
+ try {
3092
+ validationSnapshot = (activeSubsetOptions.length === 0 || outsideDemandKeys.length > 0) && this.collection?.base.size ? await adapter.loadResumeSnapshot(this.storageTargetFor(), {
3093
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
3094
+ includeRows: true,
3095
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
3096
+ }) : void 0;
3097
+ collectionMetadata = validationSnapshot ? validationSnapshot.collectionMetadata : await this.loadCollectionMetadataSnapshot(adapter);
2125
3098
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2126
- for (const row of subsetRows) {
2127
- mergedRows.set(row.key, {
2128
- value: row.value,
2129
- metadata: row.metadata
2130
- });
3099
+ for (const options of activeSubsetOptions) {
3100
+ const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter);
3101
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
3102
+ for (const row of subsetRows) {
3103
+ mergedRows.set(row.key, {
3104
+ value: row.value,
3105
+ metadata: row.metadata
3106
+ });
3107
+ }
3108
+ }
3109
+ if (outsideDemandKeys.length > 0 && validationSnapshot) {
3110
+ const durableRows = new Map(
3111
+ validationSnapshot.rows.map((row) => [row.key, row])
3112
+ );
3113
+ for (const key of outsideDemandKeys) {
3114
+ if (mergedRows.has(key)) continue;
3115
+ const row = durableRows.get(key);
3116
+ if (row) {
3117
+ mergedRows.set(key, {
3118
+ value: row.value,
3119
+ metadata: row.metadata
3120
+ });
3121
+ }
3122
+ }
3123
+ }
3124
+ } catch (error) {
3125
+ if (this.scopedRecovery) return;
3126
+ if (!this.cacheGenerationClaim || !this.hasExpiredCacheClaim()) {
3127
+ throw error;
2131
3128
  }
3129
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
3130
+ return;
3131
+ }
3132
+ if (this.isScopedRecoveryPending()) return;
3133
+ if (this.cacheGenerationClaim && !await this.renewCacheClaim(adapter)) {
3134
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
3135
+ return;
2132
3136
  }
3137
+ if (this.isScopedRecoveryPending() || lifecycleGeneration !== this.lifecycleGeneration)
3138
+ return;
2133
3139
  for (const key of mergedRows.keys()) {
2134
3140
  hydrationContext.suppliedRowKeys.add(key);
2135
3141
  }
2136
- const applied = this.replaceCollectionSnapshot(
2137
- Array.from(mergedRows.entries()).map(([key, row]) => ({
2138
- key,
2139
- value: row.value,
2140
- metadata: row.metadata
2141
- })),
2142
- collectionMetadata
2143
- );
2144
- await db.whenSyncAccepted(applied);
3142
+ if (activeSubsetOptions.length === 0) {
3143
+ await db.whenSyncAccepted(
3144
+ this.applyRowsToCollection(
3145
+ [],
3146
+ validationSnapshot?.rows ?? [],
3147
+ collectionMetadata
3148
+ )
3149
+ );
3150
+ } else if (skipUnchangedReplace && this.matchesCollectionSnapshot(mergedRows, collectionMetadata)) {
3151
+ const orphanKeys = this.collection?._syncedRowMetadataKeysOutsideBase() ?? [];
3152
+ if (orphanKeys.length > 0) {
3153
+ const applied = this.withInternalApply(() => {
3154
+ this.syncControls.begin?.();
3155
+ for (const key of orphanKeys) {
3156
+ this.syncControls.metadata?.row.delete(key);
3157
+ }
3158
+ return this.syncControls.commit?.() ?? true;
3159
+ });
3160
+ await db.whenSyncAccepted(applied);
3161
+ }
3162
+ } else {
3163
+ const applied = this.replaceCollectionSnapshot(
3164
+ Array.from(mergedRows.entries()).map(([key, row]) => ({
3165
+ key,
3166
+ value: row.value,
3167
+ metadata: row.metadata
3168
+ })),
3169
+ collectionMetadata,
3170
+ replayDemand
3171
+ );
3172
+ await db.whenSyncAccepted(applied);
3173
+ }
2145
3174
  } finally {
2146
3175
  if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === lifecycleGeneration) {
2147
3176
  this.hydratingGeneration = null;
@@ -2166,7 +3195,12 @@ class PersistedCollectionRuntime {
2166
3195
  return;
2167
3196
  }
2168
3197
  this.indexAddedUnsubscribe = this.collection.on(`index:added`, (event) => {
2169
- void this.ensurePersistedIndex(event.index);
3198
+ void this.ensurePersistedIndex(event.index).catch((error) => {
3199
+ console.warn(
3200
+ `Failed to ensure persisted index after index:added:`,
3201
+ error
3202
+ );
3203
+ });
2170
3204
  });
2171
3205
  this.indexRemovedUnsubscribe = this.collection.on(
2172
3206
  `index:removed`,
@@ -2182,20 +3216,69 @@ class PersistedCollectionRuntime {
2182
3216
  }
2183
3217
  const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
2184
3218
  const completedLocalIndexSignatures = /* @__PURE__ */ new Set();
3219
+ const lifecycleGeneration = this.lifecycleGeneration;
2185
3220
  for (const metadata of indexMetadata) {
2186
- if (await this.ensureLocalPersistedIndex(metadata, adapter)) {
3221
+ const storageCollectionId = this.storageCollectionId;
3222
+ const claimId = this.cacheGenerationClaim?.claimId;
3223
+ const ensured = await this.ensureLocalPersistedIndex(metadata, adapter);
3224
+ if (ensured && await this.reconcilePersistedIndex(
3225
+ metadata,
3226
+ adapter,
3227
+ storageCollectionId,
3228
+ claimId,
3229
+ lifecycleGeneration,
3230
+ true
3231
+ )) {
2187
3232
  completedLocalIndexSignatures.add(metadata.signature);
2188
3233
  }
2189
3234
  }
2190
3235
  return completedLocalIndexSignatures;
2191
3236
  }
2192
3237
  async requestCoordinatorPersistedIndexes(indexMetadata, completedLocalIndexSignatures) {
3238
+ const lifecycleGeneration = this.lifecycleGeneration;
2193
3239
  for (const metadata of indexMetadata) {
3240
+ if (!this.hasCurrentIndex(metadata)) continue;
3241
+ const storageCollectionId = this.storageCollectionId;
3242
+ const claimId = this.cacheGenerationClaim?.claimId;
2194
3243
  await this.requestCoordinatorPersistedIndex(
2195
3244
  metadata,
2196
3245
  completedLocalIndexSignatures.has(metadata.signature)
2197
3246
  );
3247
+ await this.reconcilePersistedIndex(
3248
+ metadata,
3249
+ this.persistence.adapter,
3250
+ storageCollectionId,
3251
+ claimId,
3252
+ lifecycleGeneration,
3253
+ true
3254
+ );
3255
+ }
3256
+ }
3257
+ /** Settle a late index operation against the Collection's current declaration. */
3258
+ async reconcilePersistedIndex(indexMetadata, adapter, storageCollectionId, claimId, lifecycleGeneration, indexPresent) {
3259
+ while (lifecycleGeneration === this.lifecycleGeneration && storageCollectionId === this.storageCollectionId) {
3260
+ const declared = this.hasCurrentIndex(indexMetadata);
3261
+ if (declared === indexPresent) return declared;
3262
+ if (declared) {
3263
+ if (!await this.ensureLocalPersistedIndex(indexMetadata, adapter)) {
3264
+ return false;
3265
+ }
3266
+ } else {
3267
+ if (!adapter.markIndexRemoved) return false;
3268
+ try {
3269
+ await adapter.markIndexRemoved(
3270
+ this.storageTargetFor(storageCollectionId, claimId),
3271
+ indexMetadata.signature,
3272
+ { cacheGenerationClaimId: claimId }
3273
+ );
3274
+ } catch (error) {
3275
+ console.warn(`Failed to mark persisted index removed:`, error);
3276
+ return false;
3277
+ }
3278
+ }
3279
+ indexPresent = declared;
2198
3280
  }
3281
+ return false;
2199
3282
  }
2200
3283
  buildPersistedIndexSpec(index) {
2201
3284
  return {
@@ -2208,19 +3291,36 @@ class PersistedCollectionRuntime {
2208
3291
  };
2209
3292
  }
2210
3293
  async ensurePersistedIndex(indexMetadata, adapter = this.persistence.adapter) {
3294
+ if (this.scopedRecoveryPending) return;
3295
+ const lifecycleGeneration = this.lifecycleGeneration;
3296
+ await this.ensureCacheClaimActive();
3297
+ if (lifecycleGeneration !== this.lifecycleGeneration || !this.hasCurrentIndex(indexMetadata))
3298
+ return;
2211
3299
  const completedLocally = await this.ensureLocalPersistedIndex(
2212
3300
  indexMetadata,
2213
3301
  adapter
2214
3302
  );
3303
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
3304
+ if (!this.hasCurrentIndex(indexMetadata)) {
3305
+ await this.markIndexRemoved(indexMetadata);
3306
+ return;
3307
+ }
2215
3308
  await this.requestCoordinatorPersistedIndex(indexMetadata, completedLocally);
3309
+ if (lifecycleGeneration === this.lifecycleGeneration && !this.hasCurrentIndex(indexMetadata)) {
3310
+ await this.markIndexRemoved(indexMetadata);
3311
+ }
3312
+ }
3313
+ hasCurrentIndex(indexMetadata) {
3314
+ return this.collection?.getIndexMetadata().some((current) => current.signature === indexMetadata.signature) ?? false;
2216
3315
  }
2217
3316
  async ensureLocalPersistedIndex(indexMetadata, adapter) {
2218
3317
  try {
2219
3318
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2220
3319
  await adapter.ensureIndex(
2221
- this.collectionId,
3320
+ this.storageTargetFor(),
2222
3321
  indexMetadata.signature,
2223
- spec
3322
+ spec,
3323
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2224
3324
  );
2225
3325
  return true;
2226
3326
  } catch (error) {
@@ -2232,11 +3332,12 @@ class PersistedCollectionRuntime {
2232
3332
  try {
2233
3333
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2234
3334
  await this.persistence.coordinator.requestEnsurePersistedIndex(
2235
- this.collectionId,
3335
+ this.storageCollectionId,
2236
3336
  indexMetadata.signature,
2237
3337
  spec,
2238
3338
  completedLocally ? this.persistence.adapter : void 0,
2239
- completedLocally
3339
+ completedLocally,
3340
+ this.cacheGenerationClaim?.claimId
2240
3341
  );
2241
3342
  } catch (error) {
2242
3343
  console.warn(
@@ -2250,10 +3351,19 @@ class PersistedCollectionRuntime {
2250
3351
  return;
2251
3352
  }
2252
3353
  try {
3354
+ if (this.scopedRecoveryPending) return;
3355
+ const lifecycleGeneration = this.lifecycleGeneration;
3356
+ await this.ensureCacheClaimActive();
3357
+ if (lifecycleGeneration !== this.lifecycleGeneration || this.hasCurrentIndex(indexMetadata))
3358
+ return;
2253
3359
  await this.persistence.adapter.markIndexRemoved(
2254
- this.collectionId,
2255
- indexMetadata.signature
3360
+ this.storageTargetFor(),
3361
+ indexMetadata.signature,
3362
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2256
3363
  );
3364
+ if (lifecycleGeneration === this.lifecycleGeneration && this.hasCurrentIndex(indexMetadata)) {
3365
+ await this.ensurePersistedIndex(indexMetadata);
3366
+ }
2257
3367
  } catch (error) {
2258
3368
  console.warn(`Failed to mark persisted index removed:`, error);
2259
3369
  }
@@ -2456,20 +3566,26 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2456
3566
  throw error;
2457
3567
  }
2458
3568
  };
2459
- runtime.setSyncControls({
2460
- begin: params.begin,
2461
- write: params.write,
2462
- commit: params.commit,
2463
- truncate: params.truncate,
2464
- metadata: params.metadata ?? null,
2465
- markError: params.markError
2466
- });
2467
3569
  runtime.setCollection(
2468
- params.collection
3570
+ params.collection,
3571
+ {
3572
+ begin: params.begin,
3573
+ write: params.write,
3574
+ commit: params.commit,
3575
+ truncate: params.truncate,
3576
+ metadata: params.metadata ?? null,
3577
+ markError: params.markError
3578
+ }
2469
3579
  );
2470
3580
  const persistenceCapability = {
2471
3581
  protocol: db.SYNC_PERSISTENCE_PROTOCOL,
2472
3582
  version: db.SYNC_PERSISTENCE_VERSION,
3583
+ get managedCacheGeneration() {
3584
+ return runtime.hasManagedCacheGeneration();
3585
+ },
3586
+ get startupCacheGenerationRotated() {
3587
+ return runtime.hasRotatedStartupCacheGeneration();
3588
+ },
2473
3589
  reserveCommitTurn: () => {
2474
3590
  const openTransaction = getOpenTransaction();
2475
3591
  if (openTransaction && !openTransaction.internal)
@@ -2483,6 +3599,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2483
3599
  throw runtime.reportSyncError(error);
2484
3600
  }
2485
3601
  },
3602
+ startScopedRecovery: (resetMetadata) => startupState.cleanedUp ? Promise.resolve() : runtime.startScopedRecovery(resetMetadata),
2486
3603
  scanPersistedRows: (options) => startupState.cleanedUp ? Promise.resolve([]) : runtime.scanPersistedRows(options),
2487
3604
  resumeSnapshot: {
2488
3605
  certify: async () => {
@@ -2506,6 +3623,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2506
3623
  }
2507
3624
  }
2508
3625
  };
3626
+ let sourceSession = 0;
2509
3627
  const wrappedParams = {
2510
3628
  ...params,
2511
3629
  markError: reportUpstreamError,
@@ -2777,6 +3895,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2777
3895
  settlePublicationAdmissionWaiters(openTransaction, error);
2778
3896
  return createHandledRejection(error);
2779
3897
  }
3898
+ if (runtime.isScopedRecoveryPending()) {
3899
+ settlePendingTransaction(openTransaction);
3900
+ const error = new db.SyncTransactionAbortedError();
3901
+ settlePublicationAdmissionWaiters(openTransaction, error);
3902
+ return createHandledRejection(error);
3903
+ }
2780
3904
  try {
2781
3905
  bindToCurrentHydration(openTransaction);
2782
3906
  assertHydrationSequenceCurrent(openTransaction);
@@ -2851,48 +3975,150 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2851
3975
  return visibleAfterDurable(openTransaction, applied);
2852
3976
  }
2853
3977
  };
3978
+ const sourceParamsFor = (session) => {
3979
+ const active = () => !startupState.cleanedUp && session === sourceSession;
3980
+ const metadata = wrappedParams.metadata;
3981
+ return {
3982
+ ...wrappedParams,
3983
+ markReady: () => {
3984
+ if (active()) wrappedParams.markReady();
3985
+ },
3986
+ markError: (error) => {
3987
+ if (active()) wrappedParams.markError(error);
3988
+ },
3989
+ begin: () => {
3990
+ if (active()) wrappedParams.begin();
3991
+ },
3992
+ write: (message) => {
3993
+ if (active()) wrappedParams.write(message);
3994
+ },
3995
+ truncate: () => {
3996
+ if (active()) wrappedParams.truncate();
3997
+ },
3998
+ commit: (signal) => active() ? wrappedParams.commit(signal) : true,
3999
+ metadata: metadata && {
4000
+ ...metadata,
4001
+ row: {
4002
+ get: (key) => active() ? metadata.row.get(key) : void 0,
4003
+ set: (key, value) => {
4004
+ if (active()) metadata.row.set(key, value);
4005
+ },
4006
+ delete: (key) => {
4007
+ if (active()) metadata.row.delete(key);
4008
+ }
4009
+ },
4010
+ collection: {
4011
+ get: (key) => active() ? metadata.collection.get(key) : void 0,
4012
+ set: (key, value) => {
4013
+ if (active()) metadata.collection.set(key, value);
4014
+ },
4015
+ delete: (key) => {
4016
+ if (active()) metadata.collection.delete(key);
4017
+ },
4018
+ list: (prefix) => active() ? metadata.collection.list(prefix) : []
4019
+ },
4020
+ persistence: {
4021
+ ...metadata.persistence,
4022
+ reserveCommitTurn: () => {
4023
+ if (active()) metadata.persistence.reserveCommitTurn();
4024
+ },
4025
+ hydrateBaseline: () => active() ? metadata.persistence.hydrateBaseline() : Promise.resolve(),
4026
+ startScopedRecovery: (resetMetadata) => active() ? metadata.persistence.startScopedRecovery?.(
4027
+ resetMetadata
4028
+ ) ?? Promise.resolve() : Promise.resolve(),
4029
+ scanPersistedRows: (options) => active() ? metadata.persistence.scanPersistedRows(options) : Promise.resolve([]),
4030
+ resumeSnapshot: {
4031
+ certify: () => active() ? metadata.persistence.resumeSnapshot.certify() : Promise.resolve(),
4032
+ getKeySetEvidence: () => active() ? metadata.persistence.resumeSnapshot.getKeySetEvidence() : void 0,
4033
+ expectCurrentCommit: () => {
4034
+ if (active())
4035
+ metadata.persistence.resumeSnapshot.expectCurrentCommit();
4036
+ }
4037
+ }
4038
+ }
4039
+ }
4040
+ };
4041
+ };
2854
4042
  let sourceResult = {};
2855
4043
  let sourceResultSettled = false;
2856
4044
  let activeSourceSyncEntry;
2857
- fullStartPromise = runtime.ensureStarted();
2858
- void fullStartPromise.catch(() => void 0);
2859
- sourceResultPromise = (async () => {
2860
- await runtime.ensureStartupMetadataLoaded();
2861
- if (startupState.cleanedUp) {
2862
- sourceResultSettled = true;
2863
- return sourceResult;
2864
- }
4045
+ let sourceCleanupPromise;
4046
+ let customRestartSequence = 0;
4047
+ const enterSourceSession = () => {
4048
+ const session = ++sourceSession;
2865
4049
  let settleSourceSyncEntry;
2866
4050
  activeSourceSyncEntry = new Promise((resolve) => {
2867
4051
  settleSourceSyncEntry = resolve;
2868
4052
  });
2869
4053
  try {
2870
4054
  sourceResult = normalizeSyncFnResult(
2871
- sourceSyncConfig.sync(wrappedParams)
4055
+ sourceSyncConfig.sync(
4056
+ runtime.hasManagedCacheGeneration() ? sourceParamsFor(session) : wrappedParams
4057
+ )
2872
4058
  );
4059
+ if (!startupState.cleanedUp && sourceResult.loadSubset) {
4060
+ const ownerSource = sourceResult;
4061
+ const loadSubset = async (options) => {
4062
+ if (startupState.cleanedUp || session !== sourceSession) {
4063
+ throw new Error(`persisted sync source is no longer active`);
4064
+ }
4065
+ await ownerSource.loadSubset?.(
4066
+ options
4067
+ );
4068
+ };
4069
+ runtime.registerRemoteSubsetOwner(
4070
+ Object.assign(loadSubset, {
4071
+ unloadSubset: (options) => ownerSource.unloadSubset?.(
4072
+ options
4073
+ ),
4074
+ onError: (error) => {
4075
+ if (session === sourceSession) reportUpstreamError(error);
4076
+ }
4077
+ })
4078
+ );
4079
+ }
2873
4080
  } finally {
2874
4081
  activeSourceSyncEntry = void 0;
2875
4082
  settleSourceSyncEntry();
2876
4083
  }
2877
- if (!startupState.cleanedUp && sourceResult.loadSubset) {
2878
- const loadSubset = async (options) => {
2879
- if (startupState.cleanedUp) {
2880
- throw new Error(`persisted sync source is no longer active`);
2881
- }
2882
- await sourceResult.loadSubset?.(
2883
- options
2884
- );
2885
- };
2886
- runtime.registerRemoteSubsetOwner(
2887
- Object.assign(loadSubset, {
2888
- unloadSubset: (options) => sourceResult.unloadSubset?.(
2889
- options
2890
- ),
2891
- onError: reportUpstreamError
2892
- })
4084
+ };
4085
+ const restartCustomSource = (cacheRotated) => {
4086
+ const restartSequence = ++customRestartSequence;
4087
+ if (!sourceCleanupPromise) {
4088
+ sourceSession++;
4089
+ const abortError = new db.SyncTransactionAbortedError();
4090
+ for (const transaction of transactionStack) {
4091
+ settlePublicationAdmissionWaiters(transaction, abortError);
4092
+ }
4093
+ transactionStack.length = 0;
4094
+ const retiredResult = sourceResult;
4095
+ sourceCleanupPromise = Promise.resolve().then(
4096
+ () => retiredResult.cleanup?.()
4097
+ );
4098
+ }
4099
+ return Promise.all([sourceCleanupPromise, cacheRotated]).then(() => {
4100
+ if (startupState.cleanedUp || restartSequence !== customRestartSequence)
4101
+ return;
4102
+ sourceCleanupPromise = void 0;
4103
+ enterSourceSession();
4104
+ runtime.setSourceRestartAfterScopedRecovery(
4105
+ sourceResult.restartAfterScopedRecovery ?? (runtime.hasManagedCacheGeneration() ? restartCustomSource : void 0)
2893
4106
  );
4107
+ });
4108
+ };
4109
+ fullStartPromise = runtime.ensureStarted();
4110
+ void fullStartPromise.catch(() => void 0);
4111
+ sourceResultPromise = (async () => {
4112
+ await runtime.ensureStartupMetadataLoaded();
4113
+ if (startupState.cleanedUp) {
4114
+ sourceResultSettled = true;
4115
+ return sourceResult;
2894
4116
  }
4117
+ enterSourceSession();
2895
4118
  sourceResultSettled = true;
4119
+ runtime.setSourceRestartAfterScopedRecovery(
4120
+ sourceResult.restartAfterScopedRecovery ?? (runtime.hasManagedCacheGeneration() ? restartCustomSource : void 0)
4121
+ );
2896
4122
  return sourceResult;
2897
4123
  })();
2898
4124
  resolveSourceResultAssigned();
@@ -2902,6 +4128,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2902
4128
  return {
2903
4129
  cleanup: async () => {
2904
4130
  startupState.cleanedUp = true;
4131
+ runtime.clearSourceRestartAfterScopedRecovery();
2905
4132
  const sourceSyncEntry = activeSourceSyncEntry;
2906
4133
  const cleanupError = new db.SyncTransactionAbortedError();
2907
4134
  for (const acquisition of acquisitions.values()) {
@@ -2915,7 +4142,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2915
4142
  transactionStack.length = 0;
2916
4143
  let sourceCleanup;
2917
4144
  try {
2918
- sourceCleanup = sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.());
4145
+ sourceCleanup = sourceCleanupPromise ?? (sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.()));
2919
4146
  } catch (error) {
2920
4147
  sourceCleanup = Promise.reject(error);
2921
4148
  }
@@ -2942,31 +4169,63 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2942
4169
  loadSubset: (options) => {
2943
4170
  const acquisition = { forwarded: false };
2944
4171
  acquisitions.set(options, acquisition);
2945
- const forwardUpstream = (resolvedSourceResult, loadOptions) => {
4172
+ const forwardUpstream = (loadOptions) => {
2946
4173
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
2947
4174
  return true;
2948
4175
  }
2949
- if (!resolvedSourceResult.loadSubset) return true;
4176
+ const currentSource = sourceResult;
4177
+ if (!currentSource.loadSubset) return true;
2950
4178
  acquisition.forwarded = true;
4179
+ acquisition.source = currentSource;
2951
4180
  try {
2952
- return resolvedSourceResult.loadSubset(loadOptions);
4181
+ return currentSource.loadSubset(loadOptions);
2953
4182
  } catch (error) {
2954
4183
  acquisition.forwarded = false;
4184
+ acquisition.source = void 0;
2955
4185
  throw error;
2956
4186
  }
2957
4187
  };
2958
4188
  const openTransaction = getOpenTransaction();
2959
4189
  const needsPublicationAdmission = openTransaction?.reservesCommitTurn === true;
4190
+ let hydrationStarted = false;
4191
+ const raceRetainedLoad = (work2, canAbortPromptly = () => true) => {
4192
+ const signal = options.signal;
4193
+ const abortable = !signal ? work2 : new Promise((resolve, reject) => {
4194
+ const abortError = () => signal.reason ?? new db.SyncTransactionAbortedError();
4195
+ const onAbort = () => {
4196
+ if (canAbortPromptly()) reject(abortError());
4197
+ };
4198
+ signal.addEventListener(`abort`, onAbort, { once: true });
4199
+ if (signal.aborted) onAbort();
4200
+ void work2.then(
4201
+ () => {
4202
+ signal.removeEventListener(`abort`, onAbort);
4203
+ if (signal.aborted) reject(abortError());
4204
+ else resolve();
4205
+ },
4206
+ (error) => {
4207
+ signal.removeEventListener(`abort`, onAbort);
4208
+ reject(signal.aborted ? abortError() : error);
4209
+ }
4210
+ );
4211
+ });
4212
+ return abortable.catch((error) => {
4213
+ if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
4214
+ return;
4215
+ }
4216
+ throw error;
4217
+ });
4218
+ };
2960
4219
  const hydrated = sourceResultSettled && !needsPublicationAdmission ? runtime.loadHydratedSubset(
2961
4220
  options,
2962
- (loadOptions) => forwardUpstream(sourceResult, loadOptions)
4221
+ (loadOptions) => forwardUpstream(loadOptions)
2963
4222
  ) : void 0;
2964
4223
  if (hydrated !== void 0) {
2965
- return hydrated;
4224
+ return hydrated === true ? true : raceRetainedLoad(hydrated);
2966
4225
  }
2967
- return (async () => {
4226
+ const work = (async () => {
2968
4227
  await fullStartPromise;
2969
- const resolvedSourceResult = await sourceResultPromise;
4228
+ await sourceResultPromise;
2970
4229
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
2971
4230
  return;
2972
4231
  }
@@ -2983,9 +4242,14 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2983
4242
  }
2984
4243
  return runtime.loadSubset(
2985
4244
  options,
2986
- (loadOptions) => forwardUpstream(resolvedSourceResult, loadOptions)
4245
+ (loadOptions) => forwardUpstream(loadOptions),
4246
+ () => {
4247
+ hydrationStarted = true;
4248
+ },
4249
+ () => !startupState.cleanedUp && acquisitions.get(options) === acquisition
2987
4250
  );
2988
4251
  })();
4252
+ return raceRetainedLoad(work, () => !hydrationStarted);
2989
4253
  },
2990
4254
  unloadSubset: (options) => {
2991
4255
  const acquisition = acquisitions.get(options);
@@ -2993,7 +4257,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2993
4257
  acquisitions.delete(options);
2994
4258
  runtime.unloadSubset(
2995
4259
  options,
2996
- acquisition?.forwarded ? sourceResult.unloadSubset : void 0
4260
+ acquisition?.forwarded ? acquisition.source?.unloadSubset : void 0
2997
4261
  );
2998
4262
  }
2999
4263
  };
@@ -3003,16 +4267,16 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3003
4267
  function createLoopbackSyncConfig(runtime) {
3004
4268
  return {
3005
4269
  sync: (params) => {
3006
- runtime.setSyncControls({
3007
- begin: params.begin,
3008
- write: params.write,
3009
- commit: params.commit,
3010
- truncate: params.truncate,
3011
- metadata: params.metadata ?? null,
3012
- markError: params.markError
3013
- });
3014
4270
  runtime.setCollection(
3015
- params.collection
4271
+ params.collection,
4272
+ {
4273
+ begin: params.begin,
4274
+ write: params.write,
4275
+ commit: params.commit,
4276
+ truncate: params.truncate,
4277
+ metadata: params.metadata ?? null,
4278
+ markError: params.markError
4279
+ }
3016
4280
  );
3017
4281
  void runtime.ensureStarted().then(() => {
3018
4282
  if (runtime.getCurrentTerminalFailure()) return;
@@ -3042,6 +4306,19 @@ function persistedCollectionOptions(options) {
3042
4306
  );
3043
4307
  }
3044
4308
  const persistedReadinessTracker = networkTimeoutMs !== void 0 ? new PersistedReadinessTracker(networkTimeoutMs) : void 0;
4309
+ let claimed = false;
4310
+ let claimedRuntime;
4311
+ const claimOptions = () => {
4312
+ if (claimed) {
4313
+ throw new errors.InvalidPersistedCollectionConfigError(
4314
+ `Persisted options can create only one Collection. Create fresh options for each Collection.`
4315
+ );
4316
+ }
4317
+ claimed = true;
4318
+ return () => {
4319
+ if (!claimedRuntime?.hasBoundCollection()) claimed = false;
4320
+ };
4321
+ };
3045
4322
  if (hasOwnSyncKey(options)) {
3046
4323
  if (!isValidSyncConfig(options.sync)) {
3047
4324
  throw new errors.InvalidSyncConfigError(
@@ -3068,8 +4345,10 @@ function persistedCollectionOptions(options) {
3068
4345
  persistence2,
3069
4346
  syncOptions.syncMode ?? `eager`,
3070
4347
  collectionId2,
3071
- persistedReadinessTracker
4348
+ persistedReadinessTracker,
4349
+ claimOptions
3072
4350
  );
4351
+ claimedRuntime = runtime2;
3073
4352
  const result2 = {
3074
4353
  ...syncOptions,
3075
4354
  id: collectionId2,
@@ -3086,7 +4365,8 @@ function persistedCollectionOptions(options) {
3086
4365
  () => persistedCollectionOptions({
3087
4366
  ...options,
3088
4367
  id: collectionId2
3089
- })
4368
+ }),
4369
+ claimOptions
3090
4370
  );
3091
4371
  }
3092
4372
  const {
@@ -3109,8 +4389,10 @@ function persistedCollectionOptions(options) {
3109
4389
  persistence,
3110
4390
  localOnlyOptions.syncMode ?? `eager`,
3111
4391
  localOnlyOptions.id ?? DEFAULT_DB_NAME,
3112
- persistedReadinessTracker
4392
+ persistedReadinessTracker,
4393
+ claimOptions
3113
4394
  );
4395
+ claimedRuntime = runtime;
3114
4396
  const wrappedOnInsert = async (params) => {
3115
4397
  const lifecycleGeneration = runtime.getLifecycleGeneration();
3116
4398
  const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
@@ -3174,7 +4456,8 @@ function persistedCollectionOptions(options) {
3174
4456
  () => persistedCollectionOptions({
3175
4457
  ...options,
3176
4458
  id: collectionId
3177
- })
4459
+ }),
4460
+ claimOptions
3178
4461
  );
3179
4462
  }
3180
4463
  function encodePersistedStorageKey(key) {
@@ -3236,8 +4519,11 @@ exports.SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY = SQLITE_DRIVER_SHARED_LOGIC
3236
4519
  exports.SingleProcessCoordinator = SingleProcessCoordinator;
3237
4520
  exports.createPersistedTableName = createPersistedTableName;
3238
4521
  exports.decodePersistedStorageKey = decodePersistedStorageKey;
4522
+ exports.eagerStorageTarget = eagerStorageTarget;
3239
4523
  exports.encodePersistedStorageKey = encodePersistedStorageKey;
3240
4524
  exports.forwardSQLiteDriverSharedLogicalScheduling = forwardSQLiteDriverSharedLogicalScheduling;
4525
+ exports.managedStorageTarget = managedStorageTarget;
3241
4526
  exports.persistedCollectionOptions = persistedCollectionOptions;
4527
+ exports.resolvePersistedStorageTarget = resolvePersistedStorageTarget;
3242
4528
  exports.validatePersistedCollectionCoordinator = validatePersistedCollectionCoordinator;
3243
4529
  //# sourceMappingURL=persisted.cjs.map