@tanstack/db-sqlite-persistence-core 0.4.8 → 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.
@@ -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;
@@ -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,9 +513,16 @@ 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;
459
526
  this.latestTerm = 0;
460
527
  this.latestSeq = 0;
461
528
  this.latestRowVersion = 0;
@@ -464,6 +531,167 @@ class PersistedCollectionRuntime {
464
531
  this.localSeq = 0;
465
532
  this.localRowVersion = 0;
466
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++;
659
+ this.latestTerm = 0;
660
+ this.latestSeq = 0;
661
+ this.latestRowVersion = 0;
662
+ this.localTerm = 1;
663
+ this.localSeq = 0;
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;
694
+ }
467
695
  setSyncControls(syncControls) {
468
696
  this.advanceLifecycle();
469
697
  this.syncErrorReported = false;
@@ -474,6 +702,12 @@ class PersistedCollectionRuntime {
474
702
  commit: commit ? (signal) => this.trackAppliedReceipt(commit(signal)) : null
475
703
  };
476
704
  }
705
+ setSourceRestartAfterScopedRecovery(restart) {
706
+ this.restartSourceAfterScopedRecovery = restart ?? null;
707
+ }
708
+ clearSourceRestartAfterScopedRecovery() {
709
+ this.restartSourceAfterScopedRecovery = void 0;
710
+ }
477
711
  reportSyncError(error) {
478
712
  const markError = this.syncControls.markError;
479
713
  if (this.syncErrorReported) return this.reportedSyncError;
@@ -488,8 +722,9 @@ class PersistedCollectionRuntime {
488
722
  }
489
723
  registerRemoteSubsetOwner(owner) {
490
724
  this.remoteSubsetOwnerUnsubscribe?.();
725
+ this.remoteSubsetOwner = owner;
491
726
  this.remoteSubsetOwnerUnsubscribe = this.persistence.coordinator.registerRemoteSubsetOwner(
492
- this.collectionId,
727
+ this.storageCollectionId,
493
728
  owner
494
729
  );
495
730
  }
@@ -514,6 +749,23 @@ class PersistedCollectionRuntime {
514
749
  )
515
750
  );
516
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
+ }
517
769
  clearSyncControls() {
518
770
  this.syncControls = {
519
771
  begin: null,
@@ -572,17 +824,46 @@ class PersistedCollectionRuntime {
572
824
  isApplyingInternally() {
573
825
  return this.internalApplyDepth > 0;
574
826
  }
575
- 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);
576
847
  if (this.collection === collection) {
848
+ this.boundCollection ??= collection;
577
849
  return;
578
850
  }
579
851
  this.collection = collection;
580
- 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;
581
862
  }
582
863
  getLeadershipState() {
583
864
  return {
584
865
  nodeId: this.persistence.coordinator.getNodeId(),
585
- isLeader: this.persistence.coordinator.isLeader(this.collectionId)
866
+ isLeader: this.persistence.coordinator.isLeader(this.storageCollectionId)
586
867
  };
587
868
  }
588
869
  runInHydrationScope(task, adapter = this.persistence.adapter) {
@@ -597,6 +878,7 @@ class PersistedCollectionRuntime {
597
878
  return this.startPromise;
598
879
  }
599
880
  const lifecycleGeneration = this.lifecycleGeneration;
881
+ this.startupCacheGenerationRotated = false;
600
882
  const isRestart = this.hasAttemptedStartup;
601
883
  this.hasAttemptedStartup = true;
602
884
  let resolveStartupMetadata;
@@ -607,63 +889,98 @@ class PersistedCollectionRuntime {
607
889
  });
608
890
  void this.startupMetadataPromise.catch(() => void 0);
609
891
  this.startPromise = (async () => {
892
+ await this.claimCacheGeneration(lifecycleGeneration);
893
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
894
+ resolveStartupMetadata();
895
+ return;
896
+ }
897
+ let metadataExpired = false;
610
898
  const loadStartupMetadata = async (adapter) => {
611
899
  if (lifecycleGeneration !== this.lifecycleGeneration) {
612
900
  resolveStartupMetadata();
613
901
  return false;
614
902
  }
615
903
  try {
616
- await this.loadStartupMetadataInternal(lifecycleGeneration, adapter);
904
+ if (!await this.loadStartupMetadataInternal(
905
+ lifecycleGeneration,
906
+ adapter
907
+ )) {
908
+ metadataExpired = true;
909
+ return false;
910
+ }
617
911
  return lifecycleGeneration === this.lifecycleGeneration;
618
912
  } catch (error) {
913
+ if (this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
914
+ metadataExpired = true;
915
+ return false;
916
+ }
619
917
  rejectStartupMetadata(error);
620
918
  throw error;
621
919
  }
622
920
  };
623
921
  let startup;
624
922
  const scheduleStartupAsOneHydrate = this.persistence.adapter.runInHydrationScope !== void 0 && (!isRestart || (this.persistence.adapter.isHydrationScopeScheduled?.() ?? true));
625
- if (scheduleStartupAsOneHydrate) {
626
- startup = await this.applyMutex.run(async () => {
627
- const result = await this.runInHydrationScope(async (adapter) => {
628
- if (!await loadStartupMetadata(adapter)) return void 0;
629
- return this.startInternal(
630
- lifecycleGeneration,
631
- adapter,
632
- resolveStartupMetadata
633
- );
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;
634
940
  });
635
- if (lifecycleGeneration === this.lifecycleGeneration) {
636
- await this.flushQueuedTxCommittedUnsafe();
637
- }
638
- return result;
639
- });
640
- } else {
641
- if (await loadStartupMetadata(this.persistence.adapter)) {
642
- if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
643
- startup = await this.applyMutex.run(async () => {
644
- const result = await this.runInHydrationScope(
645
- (adapter) => this.startInternal(
646
- lifecycleGeneration,
647
- adapter,
648
- resolveStartupMetadata
649
- )
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
650
962
  );
651
963
  if (lifecycleGeneration === this.lifecycleGeneration) {
652
964
  await this.flushQueuedTxCommittedUnsafe();
653
965
  }
654
- return result;
655
- });
656
- } else {
657
- startup = await this.startInternal(
658
- lifecycleGeneration,
659
- this.persistence.adapter,
660
- resolveStartupMetadata
661
- );
662
- if (lifecycleGeneration === this.lifecycleGeneration) {
663
- await this.flushQueuedTxCommittedUnsafe();
664
966
  }
665
967
  }
666
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();
667
984
  }
668
985
  if (startup !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
669
986
  await this.requestCoordinatorPersistedIndexes(
@@ -699,6 +1016,7 @@ class PersistedCollectionRuntime {
699
1016
  await this.ensureStarted();
700
1017
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
701
1018
  if (this.syncMode !== `on-demand`) return;
1019
+ if (this.scopedRecovery) return;
702
1020
  const appliedCursor = await this.applyMutex.run(async () => {
703
1021
  const result = await this.runInHydrationScope(
704
1022
  (adapter) => this.hydrateBaseline(lifecycleGeneration, adapter)
@@ -708,6 +1026,10 @@ class PersistedCollectionRuntime {
708
1026
  }
709
1027
  return result;
710
1028
  });
1029
+ if (appliedCursor === false) {
1030
+ await this.ensureCacheClaimActive();
1031
+ return;
1032
+ }
711
1033
  if (appliedCursor !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
712
1034
  await this.waitForAppliedReceiptsAfter(appliedCursor, `accepted`);
713
1035
  }
@@ -724,14 +1046,32 @@ class PersistedCollectionRuntime {
724
1046
  this.resumeCertificationPromise = (async () => {
725
1047
  await this.ensureStarted();
726
1048
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
727
- const snapshot = await this.persistence.adapter.loadResumeSnapshot(
728
- this.collectionId,
729
- {
730
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
731
- includeRows: false
732
- }
733
- );
734
- 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;
735
1075
  this.bindResumeSnapshotEvidence(snapshot);
736
1076
  })();
737
1077
  return this.resumeCertificationPromise;
@@ -739,6 +1079,162 @@ class PersistedCollectionRuntime {
739
1079
  getKeySetEvidence() {
740
1080
  return this.persistedKeySetEvidence;
741
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
+ }
742
1238
  getResumeGenerationOwner() {
743
1239
  return this.resumeGenerationOwner;
744
1240
  }
@@ -747,7 +1243,7 @@ class PersistedCollectionRuntime {
747
1243
  const baseline = {};
748
1244
  this.activeSubsets.set(this.getSubsetKey(baseline), baseline);
749
1245
  const appliedCursor = this.appliedReceiptSequence;
750
- await this.hydrateSubsetUnsafe(
1246
+ const hydrated = await this.hydrateSubsetUnsafe(
751
1247
  baseline,
752
1248
  {
753
1249
  lifecycleGeneration,
@@ -755,6 +1251,7 @@ class PersistedCollectionRuntime {
755
1251
  },
756
1252
  adapter
757
1253
  );
1254
+ if (!hydrated) return false;
758
1255
  return lifecycleGeneration === this.lifecycleGeneration ? appliedCursor : void 0;
759
1256
  }
760
1257
  async ensureStartupMetadataLoaded() {
@@ -774,19 +1271,33 @@ class PersistedCollectionRuntime {
774
1271
  adapter
775
1272
  );
776
1273
  if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
777
- onStartupMetadataLoaded();
1274
+ if (!this.cacheGenerationClaim || this.syncMode !== `on-demand`) {
1275
+ onStartupMetadataLoaded();
1276
+ }
778
1277
  const appliedCursor = this.syncMode !== `on-demand` ? await this.hydrateBaseline(lifecycleGeneration, adapter) : void 0;
779
1278
  return lifecycleGeneration === this.lifecycleGeneration ? {
780
- appliedCursor,
1279
+ appliedCursor: appliedCursor === false ? void 0 : appliedCursor,
781
1280
  indexBootstrapSnapshot,
782
1281
  completedLocalIndexSignatures
783
1282
  } : void 0;
784
1283
  }
785
1284
  async loadStartupMetadataInternal(lifecycleGeneration, adapter) {
786
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
787
- includeRows: false
788
- });
789
- 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;
790
1301
  this.persistedResumeGeneration = this.getResumeSnapshotGeneration(snapshot);
791
1302
  this.persistedKeySetEvidence = snapshot.keySet;
792
1303
  this.observeStreamPosition(
@@ -798,36 +1309,42 @@ class PersistedCollectionRuntime {
798
1309
  snapshot.collectionMetadata
799
1310
  );
800
1311
  await db.whenSyncAccepted(applied);
1312
+ return true;
801
1313
  }
802
1314
  async loadCollectionMetadataSnapshot(adapter) {
803
1315
  if (!adapter.loadCollectionMetadata) {
804
1316
  return [];
805
1317
  }
806
- return adapter.loadCollectionMetadata(this.collectionId);
1318
+ return adapter.loadCollectionMetadata(this.storageTargetFor(), {
1319
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1320
+ });
807
1321
  }
808
1322
  replaceCollectionMetadataSnapshot(collectionMetadata) {
809
1323
  if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
810
1324
  return true;
811
1325
  }
812
- const nextMetadata = new Map(
813
- collectionMetadata.map(({ key, value }) => [key, value])
814
- );
815
- const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
816
1326
  return this.withInternalApply(() => {
817
1327
  this.syncControls.begin?.();
818
- currentKeys.forEach((key) => {
819
- if (!nextMetadata.has(key)) {
820
- this.syncControls.metadata?.collection.delete(key);
821
- }
822
- });
823
- nextMetadata.forEach((value, key) => {
824
- this.syncControls.metadata?.collection.set(key, value);
825
- });
1328
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
826
1329
  return this.syncControls.commit?.() ?? true;
827
1330
  });
828
1331
  }
829
- 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) {
830
1344
  this.throwIfTerminal();
1345
+ await this.ensureCacheClaimActive();
1346
+ if (options.signal?.aborted || isDemandRetained && !isDemandRetained())
1347
+ return;
831
1348
  const lifecycleGeneration = this.lifecycleGeneration;
832
1349
  const subsetKey = this.getSubsetKey(options);
833
1350
  const truncateGeneration = this.sourceTruncateGeneration;
@@ -836,25 +1353,52 @@ class PersistedCollectionRuntime {
836
1353
  this.activeSubsets.set(subsetKey, options);
837
1354
  const appliedCursor = this.appliedReceiptSequence;
838
1355
  try {
839
- await this.applyMutex.run(async () => {
840
- try {
841
- await this.runInHydrationScope(
842
- (adapter) => this.hydrateSubsetUnsafe(
843
- options,
844
- {
845
- lifecycleGeneration,
846
- requestLocalLoadFailure: true,
847
- rejectBufferedReplayFailure: true
848
- },
849
- adapter
850
- )
851
- );
852
- } finally {
853
- if (lifecycleGeneration === this.lifecycleGeneration && !this.getCurrentTerminalFailure()) {
854
- 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
+ }
855
1381
  }
856
1382
  }
857
- });
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
+ }
858
1402
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
859
1403
  await this.waitForAppliedReceiptsAfter(appliedCursor);
860
1404
  if (!routeRemoteDemandDuringHydration && this.canRouteRemoteDemandThroughCoordinator()) {
@@ -867,13 +1411,16 @@ class PersistedCollectionRuntime {
867
1411
  throw error;
868
1412
  }
869
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
+ }
870
1417
  return;
871
1418
  }
872
1419
  if (this.canRouteRemoteDemandThroughCoordinator()) {
873
1420
  try {
874
1421
  await this.persistence.coordinator.requestEnsureRemoteSubset(
875
- this.collectionId,
876
- options
1422
+ this.storageCollectionId,
1423
+ this.rotatedAcquisitions.get(options) ?? options
877
1424
  );
878
1425
  } catch (error) {
879
1426
  if (options.signal?.aborted || typeof error === `object` && error !== null && `name` in error && error.name === `AbortError`) {
@@ -883,6 +1430,7 @@ class PersistedCollectionRuntime {
883
1430
  this.queueRemoteSubsetEnsure(options);
884
1431
  throw error;
885
1432
  }
1433
+ await this.applyLeaderCommitsBeforeReply(options);
886
1434
  return;
887
1435
  }
888
1436
  if (truncateGeneration === this.sourceTruncateGeneration && this.activeSubsets.get(subsetKey) === options) {
@@ -900,7 +1448,7 @@ class PersistedCollectionRuntime {
900
1448
  loadHydratedSubset(options, upstreamLoadSubset) {
901
1449
  const failure = this.getCurrentTerminalFailure();
902
1450
  if (failure) return Promise.reject(failure.error);
903
- 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))) {
904
1452
  return;
905
1453
  }
906
1454
  this.activeSubsets.set(this.getSubsetKey(options), options);
@@ -940,13 +1488,15 @@ class PersistedCollectionRuntime {
940
1488
  const subsetKey = this.getSubsetKey(options);
941
1489
  this.activeSubsets.delete(subsetKey);
942
1490
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1491
+ const remoteOptions = this.rotatedAcquisitions.get(options) ?? options;
1492
+ this.rotatedAcquisitions.delete(options);
943
1493
  const demandKey = db.getLoadSubsetDemandKey(options);
944
1494
  const stillActive = Array.from(this.activeSubsets.values()).some(
945
1495
  (active) => db.getLoadSubsetDemandKey(active) === demandKey
946
1496
  );
947
1497
  if (!stillActive) this.hydratedDemands.delete(demandKey);
948
1498
  if (this.mode === `sync-present`) {
949
- void this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
1499
+ void this.persistence.coordinator.requestReleaseRemoteSubset(this.storageCollectionId, remoteOptions).catch((error) => {
950
1500
  this.reportSyncError(error);
951
1501
  });
952
1502
  }
@@ -963,9 +1513,10 @@ class PersistedCollectionRuntime {
963
1513
  }
964
1514
  async forceReloadSubset(options) {
965
1515
  this.throwIfTerminal();
1516
+ await this.ensureCacheClaimActive();
966
1517
  const lifecycleGeneration = this.lifecycleGeneration;
967
- await this.applyMutex.run(async () => {
968
- await this.runInHydrationScope(
1518
+ const hydrated = await this.applyMutex.run(async () => {
1519
+ const result = await this.runInHydrationScope(
969
1520
  (adapter) => this.hydrateSubsetUnsafe(
970
1521
  options,
971
1522
  {
@@ -978,7 +1529,9 @@ class PersistedCollectionRuntime {
978
1529
  if (lifecycleGeneration === this.lifecycleGeneration) {
979
1530
  await this.flushQueuedTxCommittedUnsafe();
980
1531
  }
1532
+ return result;
981
1533
  });
1534
+ if (!hydrated) await this.ensureCacheClaimActive();
982
1535
  }
983
1536
  queueHydrationBufferedTransaction(transaction) {
984
1537
  const failure = this.getCurrentTerminalFailure();
@@ -986,11 +1539,13 @@ class PersistedCollectionRuntime {
986
1539
  transaction.rejectApplied?.(failure.error);
987
1540
  return;
988
1541
  }
1542
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
989
1543
  this.queuedHydrationTransactions.push(transaction);
990
1544
  }
991
1545
  applyHydrationBufferedTransaction(transaction) {
992
1546
  const failure = this.getCurrentTerminalFailure();
993
1547
  if (failure) return Promise.reject(failure.error);
1548
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
994
1549
  return this.applyMutex.run(async () => {
995
1550
  await this.applyBufferedSyncTransactionUnsafe(transaction);
996
1551
  });
@@ -1095,7 +1650,10 @@ class PersistedCollectionRuntime {
1095
1650
  for (const options of this.activeSubsets.values()) {
1096
1651
  attempt(() => {
1097
1652
  remoteReleases.push(
1098
- 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) => {
1099
1657
  this.reportSyncError(error);
1100
1658
  })
1101
1659
  );
@@ -1106,6 +1664,7 @@ class PersistedCollectionRuntime {
1106
1664
  this.coordinatorUnsubscribe = null;
1107
1665
  attempt(() => this.remoteSubsetOwnerUnsubscribe?.());
1108
1666
  this.remoteSubsetOwnerUnsubscribe = null;
1667
+ this.remoteSubsetOwner = void 0;
1109
1668
  attempt(() => this.indexAddedUnsubscribe?.());
1110
1669
  this.indexAddedUnsubscribe = null;
1111
1670
  attempt(() => this.indexRemovedUnsubscribe?.());
@@ -1115,6 +1674,16 @@ class PersistedCollectionRuntime {
1115
1674
  this.remoteEnsureRetryTimer = null;
1116
1675
  }
1117
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();
1118
1687
  this.activeSubsets.clear();
1119
1688
  this.hydratedDemands.clear();
1120
1689
  for (const transaction of this.queuedHydrationTransactions) {
@@ -1124,6 +1693,15 @@ class PersistedCollectionRuntime {
1124
1693
  this.queuedTxCommitted.length = 0;
1125
1694
  this.clearSyncControls();
1126
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
+ }
1127
1705
  return Promise.all(remoteReleases).then(() => {
1128
1706
  if (failures.length === 1) throw failures[0];
1129
1707
  if (failures.length > 1) {
@@ -1135,6 +1713,12 @@ class PersistedCollectionRuntime {
1135
1713
  }
1136
1714
  advanceLifecycle() {
1137
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;
1138
1722
  this.persistedReadiness?.set({ status: `loading` });
1139
1723
  this.startupSettled = false;
1140
1724
  this.publicRowVersion = 0;
@@ -1164,22 +1748,40 @@ class PersistedCollectionRuntime {
1164
1748
  return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
1165
1749
  }
1166
1750
  loadSubsetRowsUnsafe(options, adapter) {
1167
- return adapter.loadSubset(this.collectionId, options, {
1168
- 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
1169
1755
  });
1170
1756
  }
1171
1757
  async scanPersistedRowsUnsafe(options) {
1172
1758
  if (!this.persistence.adapter.scanRows) {
1173
1759
  return [];
1174
1760
  }
1175
- return this.persistence.adapter.scanRows(
1176
- this.collectionId,
1177
- options
1178
- );
1761
+ return this.persistence.adapter.scanRows(this.storageTargetFor(), options, {
1762
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1763
+ });
1179
1764
  }
1180
1765
  async scanPersistedRows(options) {
1181
- this.throwIfTerminal();
1182
- 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
+ }
1183
1785
  }
1184
1786
  async hydrateSubsetUnsafe(options, config, adapter) {
1185
1787
  let rowsLoaded = false;
@@ -1194,27 +1796,41 @@ class PersistedCollectionRuntime {
1194
1796
  this.hydratingGeneration = config.lifecycleGeneration;
1195
1797
  try {
1196
1798
  let rows;
1197
- if (config.bindKeySetEvidence) {
1198
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
1199
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1200
- includeRows: true
1201
- });
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
+ );
1202
1809
  rows = snapshot.rows;
1203
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1204
- if (resetSequence !== this.resetSequence) return;
1205
- this.bindResumeSnapshotEvidence(snapshot);
1810
+ if (config.lifecycleGeneration !== this.lifecycleGeneration)
1811
+ return true;
1812
+ if (resetSequence !== this.resetSequence) return true;
1813
+ bindResumeSnapshot = () => this.bindResumeSnapshotEvidence(snapshot);
1206
1814
  boundRowVersion = snapshot.latestRowVersion;
1207
1815
  } else {
1208
1816
  rows = await this.loadSubsetRowsUnsafe(options, adapter);
1209
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;
1210
1820
  rowsLoaded = true;
1211
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1212
- 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?.();
1213
1829
  if (!config.bindKeySetEvidence || this.persistedKeySetEvidence?.status !== `incompatible`) {
1214
1830
  for (const row of rows) {
1215
1831
  hydrationContext.suppliedRowKeys.add(row.key);
1216
1832
  }
1217
- const applied = this.applyRowsToCollection(rows);
1833
+ const applied = this.applyRowsToCollection(rows, validationRows);
1218
1834
  await db.whenSyncAccepted(applied);
1219
1835
  if (boundRowVersion !== void 0) {
1220
1836
  this.publicRowVersion = Math.max(
@@ -1228,11 +1844,20 @@ class PersistedCollectionRuntime {
1228
1844
  this.hydratingGeneration = null;
1229
1845
  }
1230
1846
  }
1231
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1232
- replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1233
- 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;
1234
1852
  } catch (error) {
1853
+ if (config.requestLocalLoadFailure && !rowsLoaded && this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
1854
+ return false;
1855
+ }
1235
1856
  if (config.requestLocalLoadFailure && !rowsLoaded) {
1857
+ if (this.scopedRecoveryPending) {
1858
+ this.recoverQueuedAfterLocalLoadFailure = true;
1859
+ throw error;
1860
+ }
1236
1861
  this.hydratingGeneration = config.lifecycleGeneration;
1237
1862
  try {
1238
1863
  await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
@@ -1261,6 +1886,7 @@ class PersistedCollectionRuntime {
1261
1886
  if (replayFailure && config.rejectBufferedReplayFailure) {
1262
1887
  throw replayFailure.reason;
1263
1888
  }
1889
+ return true;
1264
1890
  }
1265
1891
  async recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(lifecycleGeneration, adapter) {
1266
1892
  let snapshotRows;
@@ -1295,18 +1921,15 @@ class PersistedCollectionRuntime {
1295
1921
  const key = operation.key;
1296
1922
  if (operation.type === `update` && transaction.partialUpdateOperationIndexes.has(index) && getPresence(key) === `unknown`) {
1297
1923
  if (snapshotRows === void 0) {
1298
- const snapshot = await adapter.loadResumeSnapshot(
1299
- this.collectionId,
1300
- {
1301
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1302
- includeRows: true
1303
- }
1304
- );
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
+ });
1305
1929
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1306
1930
  if (transaction.signal?.aborted) break;
1307
- snapshotRows = new Map(
1308
- snapshot.rows.map((row) => [row.key, row])
1309
- );
1931
+ const rows = snapshot && !this.scopedRecovery ? snapshot.rows : [];
1932
+ snapshotRows = new Map(rows.map((row) => [row.key, row]));
1310
1933
  }
1311
1934
  const baseline = snapshotRows.get(key);
1312
1935
  if (baseline === void 0) {
@@ -1356,22 +1979,65 @@ class PersistedCollectionRuntime {
1356
1979
  }
1357
1980
  this.queuedHydrationTransactions.length = 0;
1358
1981
  }
1359
- applyRowsToCollection(rows) {
1982
+ applyRowsToCollection(rows, validationRows, collectionMetadata) {
1360
1983
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
1361
1984
  return true;
1362
1985
  }
1363
1986
  return this.withInternalApply(() => {
1364
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
+ }
1365
2023
  for (const row of rows) {
1366
- if (this.collection?._hasHydratedKey(row.key)) {
2024
+ if (validationRows && this.collection?.base.has(row.key) || this.collection?._hasHydratedKey(row.key)) {
1367
2025
  continue;
1368
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
+ }
1369
2033
  this.syncControls.write?.({
1370
2034
  type: `update`,
1371
2035
  value: row.value,
1372
2036
  metadata: row.metadata
1373
2037
  });
1374
2038
  }
2039
+ if (collectionMetadata)
2040
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1375
2041
  return this.syncControls.commit?.() ?? true;
1376
2042
  });
1377
2043
  }
@@ -1400,32 +2066,45 @@ class PersistedCollectionRuntime {
1400
2066
  );
1401
2067
  this.persistedKeySetEvidence = this.isExpectedResumeGeneration(generation) || mayAcceptUnownedGeneration ? snapshot.keySet : { status: `incompatible` };
1402
2068
  }
1403
- replaceCollectionSnapshot(rows, collectionMetadata) {
2069
+ replaceCollectionSnapshot(rows, collectionMetadata, replayDemand) {
1404
2070
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
1405
2071
  return true;
1406
2072
  }
1407
- const nextMetadata = new Map(
1408
- 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
+ )
1409
2078
  );
1410
- 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);
1411
2082
  return this.withInternalApply(() => {
1412
2083
  this.syncControls.begin?.();
1413
- 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
+ }
1414
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
+ }
1415
2101
  this.syncControls.write?.({
1416
2102
  type: `update`,
1417
2103
  value: row.value,
1418
2104
  metadata: row.metadata
1419
2105
  });
1420
2106
  }
1421
- currentKeys.forEach((key) => {
1422
- if (!nextMetadata.has(key)) {
1423
- this.syncControls.metadata?.collection.delete(key);
1424
- }
1425
- });
1426
- nextMetadata.forEach((value, key) => {
1427
- this.syncControls.metadata?.collection.set(key, value);
1428
- });
2107
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1429
2108
  return this.syncControls.commit?.() ?? true;
1430
2109
  });
1431
2110
  }
@@ -1477,6 +2156,31 @@ class PersistedCollectionRuntime {
1477
2156
  async applyBufferedSyncTransactionUnsafe(transaction, scopedAdapter) {
1478
2157
  this.throwIfTerminal();
1479
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
+ }
1480
2184
  const abortedBeforeApplication = transaction.signal?.aborted === true;
1481
2185
  let abortedDuringApplication = false;
1482
2186
  let applicationReturned = false;
@@ -1540,10 +2244,11 @@ class PersistedCollectionRuntime {
1540
2244
  };
1541
2245
  const streamPosition = this.nextLocalStreamPosition();
1542
2246
  const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
2247
+ const storageCollectionId = this.storageCollectionId;
1543
2248
  let response;
1544
2249
  try {
1545
2250
  response = await this.persistence.coordinator.requestApplyCommittedTx(
1546
- this.collectionId,
2251
+ storageCollectionId,
1547
2252
  tx,
1548
2253
  // A hydration-scoped adapter is already inside the shared scheduler.
1549
2254
  // Ordinary source commits enter it before taking the writer lock.
@@ -1556,7 +2261,7 @@ class PersistedCollectionRuntime {
1556
2261
  let reconciled;
1557
2262
  try {
1558
2263
  reconciled = await this.persistence.coordinator.reconcileCommittedTx(
1559
- this.collectionId,
2264
+ storageCollectionId,
1560
2265
  tx,
1561
2266
  anchor,
1562
2267
  scopedAdapter
@@ -1624,7 +2329,10 @@ class PersistedCollectionRuntime {
1624
2329
  this.throwIfTerminal();
1625
2330
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1626
2331
  try {
1627
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
2332
+ await this.persistence.adapter.applyCommittedTx(
2333
+ this.storageTargetFor(),
2334
+ tx
2335
+ );
1628
2336
  } catch (error) {
1629
2337
  throw this.markTerminalFailure(
1630
2338
  errors.toPersistedCollectionDurabilityError(this.collectionId, error),
@@ -1639,6 +2347,7 @@ class PersistedCollectionRuntime {
1639
2347
  term: streamPosition.term,
1640
2348
  seq: streamPosition.seq,
1641
2349
  rowVersion: streamPosition.rowVersion,
2350
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1642
2351
  truncate: transaction.truncate,
1643
2352
  mutations: transaction.operations.map(
1644
2353
  (operation) => operation.type === `update` ? {
@@ -1673,6 +2382,7 @@ class PersistedCollectionRuntime {
1673
2382
  term: streamPosition.term,
1674
2383
  seq: streamPosition.seq,
1675
2384
  rowVersion: streamPosition.rowVersion,
2385
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1676
2386
  mutations: mutations.map((mutation) => {
1677
2387
  if (mutation.type === `delete`) {
1678
2388
  return {
@@ -1732,14 +2442,16 @@ class PersistedCollectionRuntime {
1732
2442
  async persistCollectionMutationsUnsafe(mutations, lifecycleGeneration) {
1733
2443
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1734
2444
  if (this.persistence.coordinator.requestApplyLocalMutations) {
2445
+ const target = this.storageTargetFor();
1735
2446
  const envelopeMutations = mutations.map(
1736
2447
  (mutation) => toPersistedMutationEnvelope(
1737
2448
  mutation
1738
2449
  )
1739
2450
  );
1740
2451
  const response = await this.persistence.coordinator.requestApplyLocalMutations(
1741
- this.collectionId,
1742
- envelopeMutations
2452
+ this.storageCollectionId,
2453
+ envelopeMutations,
2454
+ target.kind === `managed` ? target.claimId : void 0
1743
2455
  );
1744
2456
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1745
2457
  if (!response.ok) {
@@ -1836,12 +2548,12 @@ class PersistedCollectionRuntime {
1836
2548
  const envelope = {
1837
2549
  v: 1,
1838
2550
  dbName: this.dbName,
1839
- collectionId: this.collectionId,
2551
+ collectionId: this.storageCollectionId,
1840
2552
  senderId: this.persistence.coordinator.getNodeId(),
1841
2553
  ts: Date.now(),
1842
2554
  payload: txCommitted
1843
2555
  };
1844
- this.persistence.coordinator.publish(this.collectionId, envelope);
2556
+ this.persistence.coordinator.publish(this.storageCollectionId, envelope);
1845
2557
  }
1846
2558
  observeStreamPosition(term, seq, rowVersion) {
1847
2559
  if (term > this.latestTerm || term === this.latestTerm && seq > this.latestSeq) {
@@ -1883,7 +2595,7 @@ class PersistedCollectionRuntime {
1883
2595
  if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1884
2596
  return false;
1885
2597
  }
1886
- return !this.persistence.coordinator.isLeader(this.collectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
2598
+ return !this.persistence.coordinator.isLeader(this.storageCollectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
1887
2599
  }
1888
2600
  queueRemoteSubsetEnsure(options) {
1889
2601
  if (options.signal?.aborted || !this.canRouteRemoteDemandThroughCoordinator() || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
@@ -1922,8 +2634,8 @@ class PersistedCollectionRuntime {
1922
2634
  }
1923
2635
  try {
1924
2636
  await this.persistence.coordinator.requestEnsureRemoteSubset(
1925
- this.collectionId,
1926
- options
2637
+ this.storageCollectionId,
2638
+ this.rotatedAcquisitions.get(options) ?? options
1927
2639
  );
1928
2640
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1929
2641
  } catch (error) {
@@ -1941,18 +2653,19 @@ class PersistedCollectionRuntime {
1941
2653
  return;
1942
2654
  }
1943
2655
  this.persistence.coordinator.setAdapterForCollection?.(
1944
- this.collectionId,
1945
- this.persistence.adapter
2656
+ this.storageCollectionId,
2657
+ this.persistence.adapter,
2658
+ this.cacheGenerationClaim?.claimId
1946
2659
  );
1947
2660
  this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
1948
- this.collectionId,
2661
+ this.storageCollectionId,
1949
2662
  (message) => {
1950
2663
  this.onCoordinatorMessage(message);
1951
2664
  }
1952
2665
  );
1953
2666
  }
1954
2667
  onCoordinatorMessage(message) {
1955
- if (message.collectionId !== this.collectionId) {
2668
+ if (message.collectionId !== this.storageCollectionId) {
1956
2669
  return;
1957
2670
  }
1958
2671
  if (this.getCurrentTerminalFailure()) {
@@ -1961,17 +2674,24 @@ class PersistedCollectionRuntime {
1961
2674
  const { payload } = message;
1962
2675
  const lifecycleGeneration = this.lifecycleGeneration;
1963
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
+ }
1964
2682
  if (isTxCommittedPayload(payload)) {
1965
- if (this.isHydratingNow()) {
1966
- this.queuedTxCommitted.push({
1967
- txCommitted: payload,
1968
- 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);
1969
2693
  });
1970
- return;
1971
2694
  }
1972
- void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload, lifecycleGeneration)).catch((error) => {
1973
- this.markTerminalFailure(error, lifecycleGeneration);
1974
- });
1975
2695
  return;
1976
2696
  }
1977
2697
  if (isSelf) {
@@ -1979,6 +2699,7 @@ class PersistedCollectionRuntime {
1979
2699
  }
1980
2700
  if (isCollectionResetPayload(payload)) {
1981
2701
  void this.applyMutex.run(async () => {
2702
+ if (message.collectionId !== this.storageCollectionId) return;
1982
2703
  await this.runInHydrationScope(async (adapter) => {
1983
2704
  await this.truncateAndReloadUnsafe(adapter, lifecycleGeneration);
1984
2705
  await this.rebaseDurableStreamFromSnapshotUnsafe(
@@ -2000,14 +2721,23 @@ class PersistedCollectionRuntime {
2000
2721
  }
2001
2722
  async flushQueuedTxCommittedUnsafe() {
2002
2723
  while (this.queuedTxCommitted.length > 0) {
2724
+ if (this.hasExpiredCacheClaim()) {
2725
+ this.scheduleCacheClaimRecovery(this.lifecycleGeneration);
2726
+ return;
2727
+ }
2003
2728
  const queued = this.queuedTxCommitted.shift();
2004
2729
  if (!queued) {
2005
2730
  continue;
2006
2731
  }
2007
- await this.processCommittedTxUnsafe(
2008
- queued.txCommitted,
2009
- queued.lifecycleGeneration
2010
- );
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
+ }
2011
2741
  }
2012
2742
  }
2013
2743
  async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
@@ -2045,7 +2775,7 @@ class PersistedCollectionRuntime {
2045
2775
  const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
2046
2776
  const hasGap = observedButUnpublished || txCommitted.latestRowVersion > this.publicRowVersion + 1 || hasGapInCurrentTerm || hasGapAcrossTerms;
2047
2777
  if (hasGap) {
2048
- await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
2778
+ if (!await this.recoverFromSeqGapUnsafe(lifecycleGeneration)) return;
2049
2779
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2050
2780
  if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
2051
2781
  }
@@ -2068,8 +2798,9 @@ class PersistedCollectionRuntime {
2068
2798
  }
2069
2799
  async rebaseDurableStreamFromSnapshotUnsafe(adapter, lifecycleGeneration) {
2070
2800
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2071
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
2072
- includeRows: false
2801
+ const snapshot = await adapter.loadResumeSnapshot(this.storageTargetFor(), {
2802
+ includeRows: false,
2803
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
2073
2804
  });
2074
2805
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2075
2806
  this.latestRowVersion = snapshot.latestRowVersion;
@@ -2081,19 +2812,26 @@ class PersistedCollectionRuntime {
2081
2812
  );
2082
2813
  }
2083
2814
  async recoverFromSeqGapUnsafe(lifecycleGeneration) {
2084
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2815
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2085
2816
  if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
2086
2817
  let pullResponse;
2087
2818
  try {
2088
2819
  pullResponse = await this.persistence.coordinator.pullSince(
2089
- this.collectionId,
2090
- this.publicRowVersion
2820
+ this.storageCollectionId,
2821
+ this.publicRowVersion,
2822
+ void 0,
2823
+ this.cacheGenerationClaim?.claimId
2091
2824
  );
2092
2825
  } catch (error) {
2093
2826
  console.warn(`Failed pullSince recovery attempt:`, error);
2094
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
+ }
2095
2833
  if (pullResponse) {
2096
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2834
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2097
2835
  if (pullResponse.ok) {
2098
2836
  this.observeStreamPosition(
2099
2837
  pullResponse.latestTerm,
@@ -2108,7 +2846,7 @@ class PersistedCollectionRuntime {
2108
2846
  this.publicRowVersion,
2109
2847
  pullResponse.latestRowVersion
2110
2848
  );
2111
- return;
2849
+ return true;
2112
2850
  }
2113
2851
  const deltas = pullResponse.deltas;
2114
2852
  if (!deltas) {
@@ -2119,7 +2857,7 @@ class PersistedCollectionRuntime {
2119
2857
  this.publicRowVersion,
2120
2858
  pullResponse.latestRowVersion
2121
2859
  );
2122
- return;
2860
+ return true;
2123
2861
  }
2124
2862
  await this.runInHydrationScope(async (adapter) => {
2125
2863
  for (const delta of deltas) {
@@ -2146,11 +2884,11 @@ class PersistedCollectionRuntime {
2146
2884
  this.publicRowVersion,
2147
2885
  pullResponse.latestRowVersion
2148
2886
  );
2149
- return;
2887
+ return true;
2150
2888
  }
2151
2889
  }
2152
2890
  }
2153
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2891
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2154
2892
  await this.runInHydrationScope(
2155
2893
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2156
2894
  );
@@ -2163,12 +2901,48 @@ class PersistedCollectionRuntime {
2163
2901
  this.queueRemoteSubsetEnsure(options);
2164
2902
  }
2165
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
+ }
2166
2936
  }
2167
2937
  async truncateAndReloadUnsafe(adapter, lifecycleGeneration = this.lifecycleGeneration) {
2168
2938
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2939
+ if (this.scopedRecovery) {
2940
+ this.refreshScopedSubsets();
2941
+ return;
2942
+ }
2169
2943
  this.hydratedDemands.clear();
2170
2944
  this.resetSequence++;
2171
- if (this.syncControls.begin && this.syncControls.commit) {
2945
+ if (this.syncMode === `eager` && this.syncControls.begin && this.syncControls.commit) {
2172
2946
  const applied = this.withInternalApply(() => {
2173
2947
  this.syncControls.begin?.();
2174
2948
  this.syncControls.truncate?.();
@@ -2177,9 +2951,18 @@ class PersistedCollectionRuntime {
2177
2951
  await db.whenSyncAccepted(applied);
2178
2952
  }
2179
2953
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2180
- await this.reloadActiveSubsetsUnsafe(adapter);
2954
+ await this.reloadActiveSubsetsUnsafe(
2955
+ adapter,
2956
+ false,
2957
+ this.syncMode === `eager`,
2958
+ true
2959
+ );
2181
2960
  }
2182
2961
  async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
2962
+ if (this.scopedRecovery) {
2963
+ this.refreshScopedSubsets();
2964
+ return;
2965
+ }
2183
2966
  const reloadActiveSubsets = (skipUnchangedReplace = false) => this.runInHydrationScope(
2184
2967
  (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter, skipUnchangedReplace),
2185
2968
  adapter
@@ -2196,6 +2979,21 @@ class PersistedCollectionRuntime {
2196
2979
  const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
2197
2980
  (opt) => opt.limit != null || opt.offset != null || opt.cursor != null
2198
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
+ }
2199
2997
  if (!hasPaginatedSubset || changedKeyCount === 0) {
2200
2998
  await this.applyTargetedInvalidationUnsafe(txCommitted);
2201
2999
  return;
@@ -2212,20 +3010,33 @@ class PersistedCollectionRuntime {
2212
3010
  key: changedKey,
2213
3011
  value: newValue
2214
3012
  } of txCommitted.changedRows) {
2215
- const matchesAnySubset = subsetEvaluators.some((evaluator) => {
2216
- if (!evaluator) return true;
2217
- return db.toBooleanPredicate(evaluator(newValue));
2218
- });
2219
- if (matchesAnySubset) {
2220
- this.syncControls.write?.({ type: `update`, value: newValue });
2221
- } else if (this.collection?.get(changedKey) !== void 0) {
2222
- 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 });
2223
3030
  }
2224
3031
  }
2225
3032
  for (const deletedKey of txCommitted.deletedKeys) {
2226
- 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
+ }
2227
3037
  }
2228
3038
  txCommitted.rowMetadataMutations?.forEach((mutation) => {
3039
+ if (this.collection?._hasHydratedKey(mutation.key)) return;
2229
3040
  if (mutation.type === `delete`) {
2230
3041
  this.syncControls.metadata?.row.delete(mutation.key);
2231
3042
  } else {
@@ -2249,10 +3060,25 @@ class PersistedCollectionRuntime {
2249
3060
  });
2250
3061
  await db.whenSyncAccepted(applied);
2251
3062
  }
2252
- async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false) {
3063
+ async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false, restoreResetBaseline = false, replayDemand = false) {
3064
+ if (this.scopedRecovery) {
3065
+ this.refreshScopedSubsets();
3066
+ return;
3067
+ }
2253
3068
  const lifecycleGeneration = this.lifecycleGeneration;
2254
3069
  const truncateGeneration = this.sourceTruncateGeneration;
2255
- 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);
2256
3082
  this.hydratedDemands.clear();
2257
3083
  this.hydrationSequence++;
2258
3084
  const hydrationContext = { suppliedRowKeys: /* @__PURE__ */ new Set() };
@@ -2260,22 +3086,68 @@ class PersistedCollectionRuntime {
2260
3086
  this.hydratingGeneration = lifecycleGeneration;
2261
3087
  try {
2262
3088
  const mergedRows = /* @__PURE__ */ new Map();
2263
- const collectionMetadata = await this.loadCollectionMetadataSnapshot(adapter);
2264
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2265
- for (const options of activeSubsetOptions) {
2266
- 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);
2267
3098
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2268
- for (const row of subsetRows) {
2269
- mergedRows.set(row.key, {
2270
- value: row.value,
2271
- metadata: row.metadata
2272
- });
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;
2273
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;
2274
3136
  }
3137
+ if (this.isScopedRecoveryPending() || lifecycleGeneration !== this.lifecycleGeneration)
3138
+ return;
2275
3139
  for (const key of mergedRows.keys()) {
2276
3140
  hydrationContext.suppliedRowKeys.add(key);
2277
3141
  }
2278
- if (skipUnchangedReplace && this.matchesCollectionSnapshot(mergedRows, collectionMetadata)) {
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)) {
2279
3151
  const orphanKeys = this.collection?._syncedRowMetadataKeysOutsideBase() ?? [];
2280
3152
  if (orphanKeys.length > 0) {
2281
3153
  const applied = this.withInternalApply(() => {
@@ -2294,7 +3166,8 @@ class PersistedCollectionRuntime {
2294
3166
  value: row.value,
2295
3167
  metadata: row.metadata
2296
3168
  })),
2297
- collectionMetadata
3169
+ collectionMetadata,
3170
+ replayDemand
2298
3171
  );
2299
3172
  await db.whenSyncAccepted(applied);
2300
3173
  }
@@ -2322,7 +3195,12 @@ class PersistedCollectionRuntime {
2322
3195
  return;
2323
3196
  }
2324
3197
  this.indexAddedUnsubscribe = this.collection.on(`index:added`, (event) => {
2325
- 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
+ });
2326
3204
  });
2327
3205
  this.indexRemovedUnsubscribe = this.collection.on(
2328
3206
  `index:removed`,
@@ -2338,20 +3216,69 @@ class PersistedCollectionRuntime {
2338
3216
  }
2339
3217
  const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
2340
3218
  const completedLocalIndexSignatures = /* @__PURE__ */ new Set();
3219
+ const lifecycleGeneration = this.lifecycleGeneration;
2341
3220
  for (const metadata of indexMetadata) {
2342
- 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
+ )) {
2343
3232
  completedLocalIndexSignatures.add(metadata.signature);
2344
3233
  }
2345
3234
  }
2346
3235
  return completedLocalIndexSignatures;
2347
3236
  }
2348
3237
  async requestCoordinatorPersistedIndexes(indexMetadata, completedLocalIndexSignatures) {
3238
+ const lifecycleGeneration = this.lifecycleGeneration;
2349
3239
  for (const metadata of indexMetadata) {
3240
+ if (!this.hasCurrentIndex(metadata)) continue;
3241
+ const storageCollectionId = this.storageCollectionId;
3242
+ const claimId = this.cacheGenerationClaim?.claimId;
2350
3243
  await this.requestCoordinatorPersistedIndex(
2351
3244
  metadata,
2352
3245
  completedLocalIndexSignatures.has(metadata.signature)
2353
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;
2354
3280
  }
3281
+ return false;
2355
3282
  }
2356
3283
  buildPersistedIndexSpec(index) {
2357
3284
  return {
@@ -2364,19 +3291,36 @@ class PersistedCollectionRuntime {
2364
3291
  };
2365
3292
  }
2366
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;
2367
3299
  const completedLocally = await this.ensureLocalPersistedIndex(
2368
3300
  indexMetadata,
2369
3301
  adapter
2370
3302
  );
3303
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
3304
+ if (!this.hasCurrentIndex(indexMetadata)) {
3305
+ await this.markIndexRemoved(indexMetadata);
3306
+ return;
3307
+ }
2371
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;
2372
3315
  }
2373
3316
  async ensureLocalPersistedIndex(indexMetadata, adapter) {
2374
3317
  try {
2375
3318
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2376
3319
  await adapter.ensureIndex(
2377
- this.collectionId,
3320
+ this.storageTargetFor(),
2378
3321
  indexMetadata.signature,
2379
- spec
3322
+ spec,
3323
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2380
3324
  );
2381
3325
  return true;
2382
3326
  } catch (error) {
@@ -2388,11 +3332,12 @@ class PersistedCollectionRuntime {
2388
3332
  try {
2389
3333
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2390
3334
  await this.persistence.coordinator.requestEnsurePersistedIndex(
2391
- this.collectionId,
3335
+ this.storageCollectionId,
2392
3336
  indexMetadata.signature,
2393
3337
  spec,
2394
3338
  completedLocally ? this.persistence.adapter : void 0,
2395
- completedLocally
3339
+ completedLocally,
3340
+ this.cacheGenerationClaim?.claimId
2396
3341
  );
2397
3342
  } catch (error) {
2398
3343
  console.warn(
@@ -2406,10 +3351,19 @@ class PersistedCollectionRuntime {
2406
3351
  return;
2407
3352
  }
2408
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;
2409
3359
  await this.persistence.adapter.markIndexRemoved(
2410
- this.collectionId,
2411
- indexMetadata.signature
3360
+ this.storageTargetFor(),
3361
+ indexMetadata.signature,
3362
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2412
3363
  );
3364
+ if (lifecycleGeneration === this.lifecycleGeneration && this.hasCurrentIndex(indexMetadata)) {
3365
+ await this.ensurePersistedIndex(indexMetadata);
3366
+ }
2413
3367
  } catch (error) {
2414
3368
  console.warn(`Failed to mark persisted index removed:`, error);
2415
3369
  }
@@ -2612,20 +3566,26 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2612
3566
  throw error;
2613
3567
  }
2614
3568
  };
2615
- runtime.setSyncControls({
2616
- begin: params.begin,
2617
- write: params.write,
2618
- commit: params.commit,
2619
- truncate: params.truncate,
2620
- metadata: params.metadata ?? null,
2621
- markError: params.markError
2622
- });
2623
3569
  runtime.setCollection(
2624
- 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
+ }
2625
3579
  );
2626
3580
  const persistenceCapability = {
2627
3581
  protocol: db.SYNC_PERSISTENCE_PROTOCOL,
2628
3582
  version: db.SYNC_PERSISTENCE_VERSION,
3583
+ get managedCacheGeneration() {
3584
+ return runtime.hasManagedCacheGeneration();
3585
+ },
3586
+ get startupCacheGenerationRotated() {
3587
+ return runtime.hasRotatedStartupCacheGeneration();
3588
+ },
2629
3589
  reserveCommitTurn: () => {
2630
3590
  const openTransaction = getOpenTransaction();
2631
3591
  if (openTransaction && !openTransaction.internal)
@@ -2639,6 +3599,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2639
3599
  throw runtime.reportSyncError(error);
2640
3600
  }
2641
3601
  },
3602
+ startScopedRecovery: (resetMetadata) => startupState.cleanedUp ? Promise.resolve() : runtime.startScopedRecovery(resetMetadata),
2642
3603
  scanPersistedRows: (options) => startupState.cleanedUp ? Promise.resolve([]) : runtime.scanPersistedRows(options),
2643
3604
  resumeSnapshot: {
2644
3605
  certify: async () => {
@@ -2662,6 +3623,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2662
3623
  }
2663
3624
  }
2664
3625
  };
3626
+ let sourceSession = 0;
2665
3627
  const wrappedParams = {
2666
3628
  ...params,
2667
3629
  markError: reportUpstreamError,
@@ -2933,6 +3895,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2933
3895
  settlePublicationAdmissionWaiters(openTransaction, error);
2934
3896
  return createHandledRejection(error);
2935
3897
  }
3898
+ if (runtime.isScopedRecoveryPending()) {
3899
+ settlePendingTransaction(openTransaction);
3900
+ const error = new db.SyncTransactionAbortedError();
3901
+ settlePublicationAdmissionWaiters(openTransaction, error);
3902
+ return createHandledRejection(error);
3903
+ }
2936
3904
  try {
2937
3905
  bindToCurrentHydration(openTransaction);
2938
3906
  assertHydrationSequenceCurrent(openTransaction);
@@ -3007,48 +3975,150 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3007
3975
  return visibleAfterDurable(openTransaction, applied);
3008
3976
  }
3009
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
+ };
3010
4042
  let sourceResult = {};
3011
4043
  let sourceResultSettled = false;
3012
4044
  let activeSourceSyncEntry;
3013
- fullStartPromise = runtime.ensureStarted();
3014
- void fullStartPromise.catch(() => void 0);
3015
- sourceResultPromise = (async () => {
3016
- await runtime.ensureStartupMetadataLoaded();
3017
- if (startupState.cleanedUp) {
3018
- sourceResultSettled = true;
3019
- return sourceResult;
3020
- }
4045
+ let sourceCleanupPromise;
4046
+ let customRestartSequence = 0;
4047
+ const enterSourceSession = () => {
4048
+ const session = ++sourceSession;
3021
4049
  let settleSourceSyncEntry;
3022
4050
  activeSourceSyncEntry = new Promise((resolve) => {
3023
4051
  settleSourceSyncEntry = resolve;
3024
4052
  });
3025
4053
  try {
3026
4054
  sourceResult = normalizeSyncFnResult(
3027
- sourceSyncConfig.sync(wrappedParams)
4055
+ sourceSyncConfig.sync(
4056
+ runtime.hasManagedCacheGeneration() ? sourceParamsFor(session) : wrappedParams
4057
+ )
3028
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
+ }
3029
4080
  } finally {
3030
4081
  activeSourceSyncEntry = void 0;
3031
4082
  settleSourceSyncEntry();
3032
4083
  }
3033
- if (!startupState.cleanedUp && sourceResult.loadSubset) {
3034
- const loadSubset = async (options) => {
3035
- if (startupState.cleanedUp) {
3036
- throw new Error(`persisted sync source is no longer active`);
3037
- }
3038
- await sourceResult.loadSubset?.(
3039
- options
3040
- );
3041
- };
3042
- runtime.registerRemoteSubsetOwner(
3043
- Object.assign(loadSubset, {
3044
- unloadSubset: (options) => sourceResult.unloadSubset?.(
3045
- options
3046
- ),
3047
- onError: reportUpstreamError
3048
- })
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)
3049
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;
3050
4116
  }
4117
+ enterSourceSession();
3051
4118
  sourceResultSettled = true;
4119
+ runtime.setSourceRestartAfterScopedRecovery(
4120
+ sourceResult.restartAfterScopedRecovery ?? (runtime.hasManagedCacheGeneration() ? restartCustomSource : void 0)
4121
+ );
3052
4122
  return sourceResult;
3053
4123
  })();
3054
4124
  resolveSourceResultAssigned();
@@ -3058,6 +4128,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3058
4128
  return {
3059
4129
  cleanup: async () => {
3060
4130
  startupState.cleanedUp = true;
4131
+ runtime.clearSourceRestartAfterScopedRecovery();
3061
4132
  const sourceSyncEntry = activeSourceSyncEntry;
3062
4133
  const cleanupError = new db.SyncTransactionAbortedError();
3063
4134
  for (const acquisition of acquisitions.values()) {
@@ -3071,7 +4142,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3071
4142
  transactionStack.length = 0;
3072
4143
  let sourceCleanup;
3073
4144
  try {
3074
- sourceCleanup = sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.());
4145
+ sourceCleanup = sourceCleanupPromise ?? (sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.()));
3075
4146
  } catch (error) {
3076
4147
  sourceCleanup = Promise.reject(error);
3077
4148
  }
@@ -3098,31 +4169,63 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3098
4169
  loadSubset: (options) => {
3099
4170
  const acquisition = { forwarded: false };
3100
4171
  acquisitions.set(options, acquisition);
3101
- const forwardUpstream = (resolvedSourceResult, loadOptions) => {
4172
+ const forwardUpstream = (loadOptions) => {
3102
4173
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
3103
4174
  return true;
3104
4175
  }
3105
- if (!resolvedSourceResult.loadSubset) return true;
4176
+ const currentSource = sourceResult;
4177
+ if (!currentSource.loadSubset) return true;
3106
4178
  acquisition.forwarded = true;
4179
+ acquisition.source = currentSource;
3107
4180
  try {
3108
- return resolvedSourceResult.loadSubset(loadOptions);
4181
+ return currentSource.loadSubset(loadOptions);
3109
4182
  } catch (error) {
3110
4183
  acquisition.forwarded = false;
4184
+ acquisition.source = void 0;
3111
4185
  throw error;
3112
4186
  }
3113
4187
  };
3114
4188
  const openTransaction = getOpenTransaction();
3115
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
+ };
3116
4219
  const hydrated = sourceResultSettled && !needsPublicationAdmission ? runtime.loadHydratedSubset(
3117
4220
  options,
3118
- (loadOptions) => forwardUpstream(sourceResult, loadOptions)
4221
+ (loadOptions) => forwardUpstream(loadOptions)
3119
4222
  ) : void 0;
3120
4223
  if (hydrated !== void 0) {
3121
- return hydrated;
4224
+ return hydrated === true ? true : raceRetainedLoad(hydrated);
3122
4225
  }
3123
- return (async () => {
4226
+ const work = (async () => {
3124
4227
  await fullStartPromise;
3125
- const resolvedSourceResult = await sourceResultPromise;
4228
+ await sourceResultPromise;
3126
4229
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
3127
4230
  return;
3128
4231
  }
@@ -3139,9 +4242,14 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3139
4242
  }
3140
4243
  return runtime.loadSubset(
3141
4244
  options,
3142
- (loadOptions) => forwardUpstream(resolvedSourceResult, loadOptions)
4245
+ (loadOptions) => forwardUpstream(loadOptions),
4246
+ () => {
4247
+ hydrationStarted = true;
4248
+ },
4249
+ () => !startupState.cleanedUp && acquisitions.get(options) === acquisition
3143
4250
  );
3144
4251
  })();
4252
+ return raceRetainedLoad(work, () => !hydrationStarted);
3145
4253
  },
3146
4254
  unloadSubset: (options) => {
3147
4255
  const acquisition = acquisitions.get(options);
@@ -3149,7 +4257,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3149
4257
  acquisitions.delete(options);
3150
4258
  runtime.unloadSubset(
3151
4259
  options,
3152
- acquisition?.forwarded ? sourceResult.unloadSubset : void 0
4260
+ acquisition?.forwarded ? acquisition.source?.unloadSubset : void 0
3153
4261
  );
3154
4262
  }
3155
4263
  };
@@ -3159,16 +4267,16 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3159
4267
  function createLoopbackSyncConfig(runtime) {
3160
4268
  return {
3161
4269
  sync: (params) => {
3162
- runtime.setSyncControls({
3163
- begin: params.begin,
3164
- write: params.write,
3165
- commit: params.commit,
3166
- truncate: params.truncate,
3167
- metadata: params.metadata ?? null,
3168
- markError: params.markError
3169
- });
3170
4270
  runtime.setCollection(
3171
- 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
+ }
3172
4280
  );
3173
4281
  void runtime.ensureStarted().then(() => {
3174
4282
  if (runtime.getCurrentTerminalFailure()) return;
@@ -3198,6 +4306,19 @@ function persistedCollectionOptions(options) {
3198
4306
  );
3199
4307
  }
3200
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
+ };
3201
4322
  if (hasOwnSyncKey(options)) {
3202
4323
  if (!isValidSyncConfig(options.sync)) {
3203
4324
  throw new errors.InvalidSyncConfigError(
@@ -3224,8 +4345,10 @@ function persistedCollectionOptions(options) {
3224
4345
  persistence2,
3225
4346
  syncOptions.syncMode ?? `eager`,
3226
4347
  collectionId2,
3227
- persistedReadinessTracker
4348
+ persistedReadinessTracker,
4349
+ claimOptions
3228
4350
  );
4351
+ claimedRuntime = runtime2;
3229
4352
  const result2 = {
3230
4353
  ...syncOptions,
3231
4354
  id: collectionId2,
@@ -3242,7 +4365,8 @@ function persistedCollectionOptions(options) {
3242
4365
  () => persistedCollectionOptions({
3243
4366
  ...options,
3244
4367
  id: collectionId2
3245
- })
4368
+ }),
4369
+ claimOptions
3246
4370
  );
3247
4371
  }
3248
4372
  const {
@@ -3265,8 +4389,10 @@ function persistedCollectionOptions(options) {
3265
4389
  persistence,
3266
4390
  localOnlyOptions.syncMode ?? `eager`,
3267
4391
  localOnlyOptions.id ?? DEFAULT_DB_NAME,
3268
- persistedReadinessTracker
4392
+ persistedReadinessTracker,
4393
+ claimOptions
3269
4394
  );
4395
+ claimedRuntime = runtime;
3270
4396
  const wrappedOnInsert = async (params) => {
3271
4397
  const lifecycleGeneration = runtime.getLifecycleGeneration();
3272
4398
  const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
@@ -3330,7 +4456,8 @@ function persistedCollectionOptions(options) {
3330
4456
  () => persistedCollectionOptions({
3331
4457
  ...options,
3332
4458
  id: collectionId
3333
- })
4459
+ }),
4460
+ claimOptions
3334
4461
  );
3335
4462
  }
3336
4463
  function encodePersistedStorageKey(key) {
@@ -3392,8 +4519,11 @@ exports.SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY = SQLITE_DRIVER_SHARED_LOGIC
3392
4519
  exports.SingleProcessCoordinator = SingleProcessCoordinator;
3393
4520
  exports.createPersistedTableName = createPersistedTableName;
3394
4521
  exports.decodePersistedStorageKey = decodePersistedStorageKey;
4522
+ exports.eagerStorageTarget = eagerStorageTarget;
3395
4523
  exports.encodePersistedStorageKey = encodePersistedStorageKey;
3396
4524
  exports.forwardSQLiteDriverSharedLogicalScheduling = forwardSQLiteDriverSharedLogicalScheduling;
4525
+ exports.managedStorageTarget = managedStorageTarget;
3397
4526
  exports.persistedCollectionOptions = persistedCollectionOptions;
4527
+ exports.resolvePersistedStorageTarget = resolvePersistedStorageTarget;
3398
4528
  exports.validatePersistedCollectionCoordinator = validatePersistedCollectionCoordinator;
3399
4529
  //# sourceMappingURL=persisted.cjs.map