@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.
@@ -1,8 +1,60 @@
1
- import { safeRandomUUID, withCollectionConfigFactory, whenSyncAccepted, SyncTransactionAbortedError, getLoadSubsetDemandKey, equalPersistedSnapshotValues, compileSingleRowExpression, toBooleanPredicate, SYNC_PERSISTENCE_VERSION, SYNC_PERSISTENCE_PROTOCOL, PERSISTED_READINESS, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError, withAcceptedReceipt } from "@tanstack/db";
1
+ import { safeRandomUUID, withCollectionConfigFactory, whenSyncAccepted, SyncTransactionAbortedError, collectionOptionsClaim, getLoadSubsetDemandKey, equalPersistedSnapshotValues, compileSingleRowExpression, PERSISTED_READINESS, toBooleanPredicate, SYNC_PERSISTENCE_VERSION, SYNC_PERSISTENCE_PROTOCOL, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError, withAcceptedReceipt } from "@tanstack/db";
2
2
  import { InvalidPersistedCollectionConfigError, DuplicateRemoteSubsetOwnerError, toPersistedCollectionDurabilityError, InvalidPersistedStorageKeyEncodingError, InvalidPersistedStorageKeyError, InvalidSyncConfigError, InvalidPersistedCollectionCoordinatorError, PersistedCollectionDurabilityError, IndeterminateCommitError, InvalidPersistenceAdapterError } from "./errors.js";
3
3
  import { serializeSQLiteBigInt, serializeSQLiteTemporal } from "./sqlite-value.js";
4
4
  import { toTransportedLoadSubsetOptions, toProcessLocalLoadSubsetOptions, validateRemoteSubsetOptions } from "./remote-subset-wire.js";
5
5
  import { reportRemoteSubsetOwnerError, unloadRemoteSubsetOwner } from "./remote-subset-owner.js";
6
+ function eagerStorageTarget(collectionId) {
7
+ return { kind: `eager`, collectionId };
8
+ }
9
+ function managedStorageTarget(storageCollectionId, claimId) {
10
+ return { kind: `managed`, storageCollectionId, claimId };
11
+ }
12
+ function resolvePersistedStorageTarget(target, legacyClaimId) {
13
+ const input = target;
14
+ if (typeof input !== `object` || input === null) {
15
+ throw new InvalidPersistedCollectionConfigError(
16
+ `SQLite persistence requires an explicit eager or managed storage target`
17
+ );
18
+ }
19
+ const fields = input;
20
+ if (fields.kind === `eager`) {
21
+ if (typeof fields.collectionId !== `string` || fields.collectionId.trim().length === 0) {
22
+ throw new InvalidPersistedCollectionConfigError(
23
+ `An eager storage target requires a nonempty collectionId`
24
+ );
25
+ }
26
+ if (legacyClaimId !== void 0 || `claimId` in fields || `storageCollectionId` in fields) {
27
+ throw new InvalidPersistedCollectionConfigError(
28
+ `An eager storage target cannot carry a persisted cache claim`
29
+ );
30
+ }
31
+ return { collectionId: fields.collectionId };
32
+ }
33
+ if (fields.kind !== `managed`) {
34
+ throw new InvalidPersistedCollectionConfigError(
35
+ `Unknown SQLite persistence storage target kind`
36
+ );
37
+ }
38
+ if (typeof fields.storageCollectionId !== `string` || fields.storageCollectionId.trim().length === 0 || typeof fields.claimId !== `string` || fields.claimId.trim().length === 0) {
39
+ throw new InvalidPersistedCollectionConfigError(
40
+ `A managed storage target requires a storageCollectionId and persisted cache claim`
41
+ );
42
+ }
43
+ if (`collectionId` in fields) {
44
+ throw new InvalidPersistedCollectionConfigError(
45
+ `A managed storage target cannot carry an eager collectionId`
46
+ );
47
+ }
48
+ if (legacyClaimId !== void 0 && legacyClaimId !== fields.claimId) {
49
+ throw new InvalidPersistedCollectionConfigError(
50
+ `The persisted cache claim does not match its storage target`
51
+ );
52
+ }
53
+ return {
54
+ collectionId: fields.storageCollectionId,
55
+ claimId: fields.claimId
56
+ };
57
+ }
6
58
  const SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY = /* @__PURE__ */ Symbol.for(
7
59
  `tanstack-db.sqlite-driver-supports-shared-logical-scheduling`
8
60
  );
@@ -44,6 +96,7 @@ class PersistedReadinessTracker {
44
96
  }
45
97
  }
46
98
  const DEFAULT_NETWORK_TIMEOUT_MS = 3e3;
99
+ const CACHE_CLAIM_RENEW_WINDOW_MS = 6e4;
47
100
  const MAX_TIMER_TIMEOUT_MS = 2147483647;
48
101
  function getNetworkTimeoutMs(initialRender) {
49
102
  if (initialRender === void 0) return void 0;
@@ -144,10 +197,6 @@ class SingleProcessCoordinator {
144
197
  if (acquisitions.size === 0) {
145
198
  this.remoteSubsetAcquisitions.delete(collectionId);
146
199
  }
147
- try {
148
- await acquisition.load;
149
- } catch {
150
- }
151
200
  await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options);
152
201
  }
153
202
  registerRemoteSubsetOwner(collectionId, owner) {
@@ -161,13 +210,9 @@ class SingleProcessCoordinator {
161
210
  this.remoteSubsetAcquisitions.delete(collectionId);
162
211
  this.remoteSubsetOwners.delete(collectionId);
163
212
  for (const acquisition of acquisitions?.values() ?? []) {
164
- void (async () => {
165
- try {
166
- await acquisition.load;
167
- } catch {
168
- }
169
- await unloadRemoteSubsetOwner(owner, acquisition.options);
170
- })().catch(() => void 0);
213
+ void unloadRemoteSubsetOwner(owner, acquisition.options).catch(
214
+ () => void 0
215
+ );
171
216
  }
172
217
  };
173
218
  }
@@ -184,7 +229,10 @@ class SingleProcessCoordinator {
184
229
  );
185
230
  }
186
231
  try {
187
- await adapter.applyCommittedTx(collectionId, tx);
232
+ await adapter.applyCommittedTx(
233
+ tx.cacheGenerationClaimId ? managedStorageTarget(collectionId, tx.cacheGenerationClaimId) : eagerStorageTarget(collectionId),
234
+ tx
235
+ );
188
236
  } catch (error) {
189
237
  throw toPersistedCollectionDurabilityError(collectionId, error);
190
238
  }
@@ -317,6 +365,13 @@ class ApplyMutex {
317
365
  return this.reserve().run(task);
318
366
  }
319
367
  }
368
+ function matchesAnySubset(value, evaluators) {
369
+ return evaluators.some(
370
+ (evaluator) => !evaluator || toBooleanPredicate(
371
+ evaluator(value)
372
+ )
373
+ );
374
+ }
320
375
  function toStableSerializable(value) {
321
376
  if (value == null) {
322
377
  return value;
@@ -409,23 +464,28 @@ function toPersistedMutationEnvelope(mutation) {
409
464
  };
410
465
  }
411
466
  class PersistedCollectionRuntime {
412
- constructor(mode, collectionId, persistence, syncMode, dbName, persistedReadiness) {
467
+ constructor(mode, collectionId, persistence, syncMode, dbName, persistedReadiness, optionsClaim) {
413
468
  this.mode = mode;
414
469
  this.collectionId = collectionId;
415
470
  this.persistence = persistence;
416
471
  this.syncMode = syncMode;
417
472
  this.dbName = dbName;
418
473
  this.persistedReadiness = persistedReadiness;
474
+ this.optionsClaim = optionsClaim;
419
475
  this.applyMutex = new ApplyMutex();
420
476
  this.activeSubsets = /* @__PURE__ */ new Map();
421
477
  this.hydrationSequence = 0;
422
478
  this.resetSequence = 0;
423
479
  this.pendingRemoteSubsetEnsures = /* @__PURE__ */ new Map();
480
+ this.scopedRefreshes = /* @__PURE__ */ new Map();
481
+ this.rotatedAcquisitions = /* @__PURE__ */ new Map();
482
+ this.rotatedEnsures = /* @__PURE__ */ new WeakMap();
424
483
  this.queuedHydrationTransactions = [];
425
484
  this.queuedTxCommitted = [];
426
485
  this.requestIds = /* @__PURE__ */ new WeakMap();
427
486
  this.hydratedDemands = /* @__PURE__ */ new Set();
428
487
  this.collection = null;
488
+ this.boundCollection = null;
429
489
  this.syncControls = {
430
490
  begin: null,
431
491
  write: null,
@@ -451,9 +511,16 @@ class PersistedCollectionRuntime {
451
511
  this.indexAddedUnsubscribe = null;
452
512
  this.indexRemovedUnsubscribe = null;
453
513
  this.remoteEnsureRetryTimer = null;
514
+ this.cacheClaimRenewTimer = null;
515
+ this.cacheClaimRecovery = null;
454
516
  this.nextRequestId = 0;
455
517
  this.startupSettled = false;
456
518
  this.sourceTruncateGeneration = 0;
519
+ this.scopedRecovery = false;
520
+ this.scopedRecoveryPending = false;
521
+ this.scopedRecoverySequence = 0;
522
+ this.recoverQueuedAfterLocalLoadFailure = false;
523
+ this.startupCacheGenerationRotated = false;
457
524
  this.latestTerm = 0;
458
525
  this.latestSeq = 0;
459
526
  this.latestRowVersion = 0;
@@ -462,6 +529,167 @@ class PersistedCollectionRuntime {
462
529
  this.localSeq = 0;
463
530
  this.localRowVersion = 0;
464
531
  }
532
+ get storageCollectionId() {
533
+ return this.cacheGenerationClaim?.storageCollectionId ?? this.collectionId;
534
+ }
535
+ storageTargetFor(storageCollectionId = this.storageCollectionId, claimId = this.cacheGenerationClaim?.claimId) {
536
+ if (this.mode === `sync-present` && this.syncMode === `on-demand` && this.persistence.adapter.claimCacheGeneration) {
537
+ if (!claimId) {
538
+ throw new InvalidPersistedCollectionConfigError(
539
+ `A managed on-demand cache operation requires a persisted cache claim`
540
+ );
541
+ }
542
+ return { kind: `managed`, storageCollectionId, claimId };
543
+ }
544
+ return { kind: `eager`, collectionId: storageCollectionId };
545
+ }
546
+ cacheGenerationNow() {
547
+ return this.persistence.adapter.getCacheGenerationNow?.() ?? Date.now();
548
+ }
549
+ hasExpiredCacheClaim() {
550
+ const expiresAtMs = this.cacheGenerationClaim?.expiresAtMs;
551
+ return expiresAtMs !== void 0 && this.cacheGenerationNow() >= expiresAtMs;
552
+ }
553
+ async claimCacheGeneration(lifecycleGeneration) {
554
+ if (this.mode !== `sync-present` || this.syncMode !== `on-demand`) return;
555
+ const adapter = this.persistence.adapter;
556
+ const capabilities = [
557
+ adapter.claimCacheGeneration,
558
+ adapter.rotateCacheGeneration,
559
+ adapter.renewCacheGenerationClaim,
560
+ adapter.releaseCacheGenerationClaim
561
+ ];
562
+ if (capabilities.some(Boolean) && !capabilities.every(Boolean)) {
563
+ throw new InvalidPersistedCollectionConfigError(
564
+ `An on-demand managed cache generation adapter must implement claimCacheGeneration, rotateCacheGeneration, renewCacheGenerationClaim, and releaseCacheGenerationClaim together`
565
+ );
566
+ }
567
+ if (!adapter.claimCacheGeneration) return;
568
+ const claim = await adapter.claimCacheGeneration(this.collectionId);
569
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
570
+ await adapter.releaseCacheGenerationClaim?.(claim.claimId);
571
+ return;
572
+ }
573
+ this.cacheGenerationClaim = claim;
574
+ this.scheduleCacheClaimRenewal(claim);
575
+ this.attachCoordinatorSubscription();
576
+ }
577
+ clearCacheClaimRenewal() {
578
+ if (this.cacheClaimRenewTimer !== null) {
579
+ clearTimeout(this.cacheClaimRenewTimer);
580
+ this.cacheClaimRenewTimer = null;
581
+ }
582
+ }
583
+ scheduleCacheClaimRenewal(claim) {
584
+ this.clearCacheClaimRenewal();
585
+ if (claim.expiresAtMs === void 0 || !this.persistence.adapter.renewCacheGenerationClaim) {
586
+ return;
587
+ }
588
+ const remaining = claim.expiresAtMs - this.cacheGenerationNow();
589
+ const delay = remaining <= 0 ? 1e3 : Math.max(
590
+ 1,
591
+ Math.min(CACHE_CLAIM_RENEW_WINDOW_MS, Math.floor(remaining / 2))
592
+ );
593
+ const lifecycleGeneration = this.lifecycleGeneration;
594
+ this.cacheClaimRenewTimer = setTimeout(() => {
595
+ this.cacheClaimRenewTimer = null;
596
+ void this.ensureCacheClaimActive().catch(() => {
597
+ if (lifecycleGeneration === this.lifecycleGeneration && this.cacheGenerationClaim === claim) {
598
+ this.scheduleCacheClaimRenewal(claim);
599
+ }
600
+ });
601
+ }, delay);
602
+ }
603
+ async ensureCacheClaimActive() {
604
+ if (this.cacheClaimRecovery) {
605
+ await this.cacheClaimRecovery;
606
+ return;
607
+ }
608
+ if (await this.renewCacheClaim(this.persistence.adapter)) return;
609
+ if (!this.cacheClaimRecovery) {
610
+ const recovery = this.restartSourceAfterScopedRecovery === void 0 ? this.reclaimStartupCacheGeneration() : this.startScopedRecovery();
611
+ this.cacheClaimRecovery = recovery;
612
+ void recovery.finally(() => {
613
+ if (this.cacheClaimRecovery === recovery) {
614
+ this.cacheClaimRecovery = null;
615
+ }
616
+ }).catch(() => void 0);
617
+ }
618
+ await this.cacheClaimRecovery;
619
+ }
620
+ scheduleCacheClaimRecovery(lifecycleGeneration) {
621
+ queueMicrotask(() => {
622
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
623
+ void this.ensureCacheClaimActive().then(
624
+ () => this.applyMutex.run(() => this.flushQueuedTxCommittedUnsafe())
625
+ ).catch((error) => {
626
+ this.markTerminalFailure(error, lifecycleGeneration);
627
+ });
628
+ });
629
+ }
630
+ async reclaimStartupCacheGeneration() {
631
+ const lifecycleGeneration = this.lifecycleGeneration;
632
+ const previous = this.cacheGenerationClaim;
633
+ if (!previous) return;
634
+ if (!this.persistence.adapter.rotateCacheGeneration) {
635
+ throw new InvalidPersistedCollectionConfigError(
636
+ `A managed on-demand cache adapter must rotate expired startup claims`
637
+ );
638
+ }
639
+ this.clearCacheClaimRenewal();
640
+ this.coordinatorUnsubscribe?.();
641
+ this.coordinatorUnsubscribe = null;
642
+ const rotated = await this.persistence.adapter.rotateCacheGeneration(
643
+ this.collectionId,
644
+ previous.claimId,
645
+ void 0,
646
+ previous.storageCollectionId
647
+ );
648
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
649
+ await this.persistence.adapter.releaseCacheGenerationClaim?.(
650
+ rotated.claimId
651
+ );
652
+ return;
653
+ }
654
+ this.cacheGenerationClaim = rotated;
655
+ this.startupCacheGenerationRotated = true;
656
+ this.resetSequence++;
657
+ this.latestTerm = 0;
658
+ this.latestSeq = 0;
659
+ this.latestRowVersion = 0;
660
+ this.localTerm = 1;
661
+ this.localSeq = 0;
662
+ this.localRowVersion = 0;
663
+ this.persistedResumeGeneration = void 0;
664
+ this.persistedKeySetEvidence = { status: `incompatible` };
665
+ this.scheduleCacheClaimRenewal(rotated);
666
+ this.attachCoordinatorSubscription();
667
+ }
668
+ async renewCacheClaim(adapter) {
669
+ const claim = this.cacheGenerationClaim;
670
+ if (!claim || !adapter.renewCacheGenerationClaim || this.scopedRecoveryPending)
671
+ return true;
672
+ if (claim.expiresAtMs !== void 0 && claim.expiresAtMs - this.cacheGenerationNow() > CACHE_CLAIM_RENEW_WINDOW_MS) {
673
+ if (this.cacheClaimRenewTimer === null) {
674
+ this.scheduleCacheClaimRenewal(claim);
675
+ }
676
+ return true;
677
+ }
678
+ const lifecycleGeneration = this.lifecycleGeneration;
679
+ const expiresAtMs = await adapter.renewCacheGenerationClaim(
680
+ claim.storageCollectionId,
681
+ claim.claimId
682
+ );
683
+ if (lifecycleGeneration !== this.lifecycleGeneration || this.cacheGenerationClaim?.claimId !== claim.claimId || this.cacheGenerationClaim.storageCollectionId !== claim.storageCollectionId) {
684
+ return true;
685
+ }
686
+ if (expiresAtMs === void 0) {
687
+ return false;
688
+ }
689
+ this.cacheGenerationClaim = { ...claim, expiresAtMs };
690
+ this.scheduleCacheClaimRenewal(this.cacheGenerationClaim);
691
+ return true;
692
+ }
465
693
  setSyncControls(syncControls) {
466
694
  this.advanceLifecycle();
467
695
  this.syncErrorReported = false;
@@ -472,6 +700,12 @@ class PersistedCollectionRuntime {
472
700
  commit: commit ? (signal) => this.trackAppliedReceipt(commit(signal)) : null
473
701
  };
474
702
  }
703
+ setSourceRestartAfterScopedRecovery(restart) {
704
+ this.restartSourceAfterScopedRecovery = restart ?? null;
705
+ }
706
+ clearSourceRestartAfterScopedRecovery() {
707
+ this.restartSourceAfterScopedRecovery = void 0;
708
+ }
475
709
  reportSyncError(error) {
476
710
  const markError = this.syncControls.markError;
477
711
  if (this.syncErrorReported) return this.reportedSyncError;
@@ -486,8 +720,9 @@ class PersistedCollectionRuntime {
486
720
  }
487
721
  registerRemoteSubsetOwner(owner) {
488
722
  this.remoteSubsetOwnerUnsubscribe?.();
723
+ this.remoteSubsetOwner = owner;
489
724
  this.remoteSubsetOwnerUnsubscribe = this.persistence.coordinator.registerRemoteSubsetOwner(
490
- this.collectionId,
725
+ this.storageCollectionId,
491
726
  owner
492
727
  );
493
728
  }
@@ -512,6 +747,23 @@ class PersistedCollectionRuntime {
512
747
  )
513
748
  );
514
749
  }
750
+ // An ensure reply settles a follower's load (law 7). A source's loadSubset
751
+ // must await every commit receipt that establishes its result, so the
752
+ // leader broadcasts those commits before it replies on the same channel.
753
+ // They are already queued on the apply mutex; settle after they show here.
754
+ async applyLeaderCommitsBeforeReply(options) {
755
+ try {
756
+ if (this.hasExpiredCacheClaim() || this.cacheClaimRecovery) {
757
+ await this.ensureCacheClaimActive();
758
+ }
759
+ await this.applyMutex.run(() => this.flushQueuedTxCommittedUnsafe());
760
+ this.throwIfTerminal();
761
+ } catch (error) {
762
+ if (this.getCurrentTerminalFailure()) this.unloadSubset(options);
763
+ else this.queueRemoteSubsetEnsure(options);
764
+ throw error;
765
+ }
766
+ }
515
767
  clearSyncControls() {
516
768
  this.syncControls = {
517
769
  begin: null,
@@ -570,17 +822,46 @@ class PersistedCollectionRuntime {
570
822
  isApplyingInternally() {
571
823
  return this.internalApplyDepth > 0;
572
824
  }
573
- setCollection(collection) {
825
+ isScopedRecoveryPending() {
826
+ return this.scopedRecoveryPending;
827
+ }
828
+ hasBoundCollection() {
829
+ return this.boundCollection !== null;
830
+ }
831
+ setCollection(collection, syncControls) {
832
+ if (collection.config[collectionOptionsClaim] !== this.optionsClaim) {
833
+ throw new InvalidPersistedCollectionConfigError(
834
+ `Persisted sync belongs to its options Collection`
835
+ );
836
+ }
837
+ if (this.boundCollection && this.boundCollection !== collection) {
838
+ throw new InvalidPersistedCollectionConfigError(
839
+ `Persisted options cannot own more than one Collection`
840
+ );
841
+ }
842
+ const previousCollection = this.collection;
843
+ const previousControls = this.syncControls;
844
+ this.setSyncControls(syncControls);
574
845
  if (this.collection === collection) {
846
+ this.boundCollection ??= collection;
575
847
  return;
576
848
  }
577
849
  this.collection = collection;
578
- this.attachCoordinatorSubscription();
850
+ try {
851
+ if (!this.persistence.adapter.claimCacheGeneration || this.mode !== `sync-present` || this.syncMode !== `on-demand`) {
852
+ this.attachCoordinatorSubscription();
853
+ }
854
+ } catch (error) {
855
+ this.collection = previousCollection;
856
+ this.syncControls = previousControls;
857
+ throw error;
858
+ }
859
+ this.boundCollection ??= collection;
579
860
  }
580
861
  getLeadershipState() {
581
862
  return {
582
863
  nodeId: this.persistence.coordinator.getNodeId(),
583
- isLeader: this.persistence.coordinator.isLeader(this.collectionId)
864
+ isLeader: this.persistence.coordinator.isLeader(this.storageCollectionId)
584
865
  };
585
866
  }
586
867
  runInHydrationScope(task, adapter = this.persistence.adapter) {
@@ -595,6 +876,7 @@ class PersistedCollectionRuntime {
595
876
  return this.startPromise;
596
877
  }
597
878
  const lifecycleGeneration = this.lifecycleGeneration;
879
+ this.startupCacheGenerationRotated = false;
598
880
  const isRestart = this.hasAttemptedStartup;
599
881
  this.hasAttemptedStartup = true;
600
882
  let resolveStartupMetadata;
@@ -605,63 +887,98 @@ class PersistedCollectionRuntime {
605
887
  });
606
888
  void this.startupMetadataPromise.catch(() => void 0);
607
889
  this.startPromise = (async () => {
890
+ await this.claimCacheGeneration(lifecycleGeneration);
891
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
892
+ resolveStartupMetadata();
893
+ return;
894
+ }
895
+ let metadataExpired = false;
608
896
  const loadStartupMetadata = async (adapter) => {
609
897
  if (lifecycleGeneration !== this.lifecycleGeneration) {
610
898
  resolveStartupMetadata();
611
899
  return false;
612
900
  }
613
901
  try {
614
- await this.loadStartupMetadataInternal(lifecycleGeneration, adapter);
902
+ if (!await this.loadStartupMetadataInternal(
903
+ lifecycleGeneration,
904
+ adapter
905
+ )) {
906
+ metadataExpired = true;
907
+ return false;
908
+ }
615
909
  return lifecycleGeneration === this.lifecycleGeneration;
616
910
  } catch (error) {
911
+ if (this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
912
+ metadataExpired = true;
913
+ return false;
914
+ }
617
915
  rejectStartupMetadata(error);
618
916
  throw error;
619
917
  }
620
918
  };
621
919
  let startup;
622
920
  const scheduleStartupAsOneHydrate = this.persistence.adapter.runInHydrationScope !== void 0 && (!isRestart || (this.persistence.adapter.isHydrationScopeScheduled?.() ?? true));
623
- if (scheduleStartupAsOneHydrate) {
624
- startup = await this.applyMutex.run(async () => {
625
- const result = await this.runInHydrationScope(async (adapter) => {
626
- if (!await loadStartupMetadata(adapter)) return void 0;
627
- return this.startInternal(
628
- lifecycleGeneration,
629
- adapter,
630
- resolveStartupMetadata
631
- );
921
+ for (; ; ) {
922
+ metadataExpired = false;
923
+ const storageAtAttempt = this.storageCollectionId;
924
+ if (scheduleStartupAsOneHydrate) {
925
+ startup = await this.applyMutex.run(async () => {
926
+ const result = await this.runInHydrationScope(async (adapter) => {
927
+ if (!await loadStartupMetadata(adapter)) return void 0;
928
+ return this.startInternal(
929
+ lifecycleGeneration,
930
+ adapter,
931
+ resolveStartupMetadata
932
+ );
933
+ });
934
+ if (lifecycleGeneration === this.lifecycleGeneration) {
935
+ await this.flushQueuedTxCommittedUnsafe();
936
+ }
937
+ return result;
632
938
  });
633
- if (lifecycleGeneration === this.lifecycleGeneration) {
634
- await this.flushQueuedTxCommittedUnsafe();
635
- }
636
- return result;
637
- });
638
- } else {
639
- if (await loadStartupMetadata(this.persistence.adapter)) {
640
- if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
641
- startup = await this.applyMutex.run(async () => {
642
- const result = await this.runInHydrationScope(
643
- (adapter) => this.startInternal(
644
- lifecycleGeneration,
645
- adapter,
646
- resolveStartupMetadata
647
- )
939
+ } else {
940
+ if (await loadStartupMetadata(this.persistence.adapter)) {
941
+ if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
942
+ startup = await this.applyMutex.run(async () => {
943
+ const result = await this.runInHydrationScope(
944
+ (adapter) => this.startInternal(
945
+ lifecycleGeneration,
946
+ adapter,
947
+ resolveStartupMetadata
948
+ )
949
+ );
950
+ if (lifecycleGeneration === this.lifecycleGeneration) {
951
+ await this.flushQueuedTxCommittedUnsafe();
952
+ }
953
+ return result;
954
+ });
955
+ } else {
956
+ startup = await this.startInternal(
957
+ lifecycleGeneration,
958
+ this.persistence.adapter,
959
+ resolveStartupMetadata
648
960
  );
649
961
  if (lifecycleGeneration === this.lifecycleGeneration) {
650
962
  await this.flushQueuedTxCommittedUnsafe();
651
963
  }
652
- return result;
653
- });
654
- } else {
655
- startup = await this.startInternal(
656
- lifecycleGeneration,
657
- this.persistence.adapter,
658
- resolveStartupMetadata
659
- );
660
- if (lifecycleGeneration === this.lifecycleGeneration) {
661
- await this.flushQueuedTxCommittedUnsafe();
662
964
  }
663
965
  }
664
966
  }
967
+ if (this.cacheGenerationClaim && this.syncMode === `on-demand` && // Hydration may have changed this through loadStartupMetadata().
968
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
969
+ !metadataExpired) {
970
+ await this.ensureCacheClaimActive();
971
+ metadataExpired = storageAtAttempt !== this.storageCollectionId;
972
+ }
973
+ if (!metadataExpired) break;
974
+ await this.ensureCacheClaimActive();
975
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
976
+ resolveStartupMetadata();
977
+ return;
978
+ }
979
+ }
980
+ if (this.cacheGenerationClaim && this.syncMode === `on-demand` && lifecycleGeneration === this.lifecycleGeneration) {
981
+ resolveStartupMetadata();
665
982
  }
666
983
  if (startup !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
667
984
  await this.requestCoordinatorPersistedIndexes(
@@ -697,6 +1014,7 @@ class PersistedCollectionRuntime {
697
1014
  await this.ensureStarted();
698
1015
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
699
1016
  if (this.syncMode !== `on-demand`) return;
1017
+ if (this.scopedRecovery) return;
700
1018
  const appliedCursor = await this.applyMutex.run(async () => {
701
1019
  const result = await this.runInHydrationScope(
702
1020
  (adapter) => this.hydrateBaseline(lifecycleGeneration, adapter)
@@ -706,6 +1024,10 @@ class PersistedCollectionRuntime {
706
1024
  }
707
1025
  return result;
708
1026
  });
1027
+ if (appliedCursor === false) {
1028
+ await this.ensureCacheClaimActive();
1029
+ return;
1030
+ }
709
1031
  if (appliedCursor !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
710
1032
  await this.waitForAppliedReceiptsAfter(appliedCursor, `accepted`);
711
1033
  }
@@ -722,14 +1044,32 @@ class PersistedCollectionRuntime {
722
1044
  this.resumeCertificationPromise = (async () => {
723
1045
  await this.ensureStarted();
724
1046
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
725
- const snapshot = await this.persistence.adapter.loadResumeSnapshot(
726
- this.collectionId,
727
- {
728
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
729
- includeRows: false
730
- }
731
- );
732
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
1047
+ if (this.scopedRecovery) return;
1048
+ const storageCollectionId = this.storageCollectionId;
1049
+ const claimId = this.cacheGenerationClaim?.claimId;
1050
+ const certificationIsCurrent = () => lifecycleGeneration === this.lifecycleGeneration && !this.scopedRecovery && storageCollectionId === this.storageCollectionId && claimId === this.cacheGenerationClaim?.claimId;
1051
+ let snapshot;
1052
+ try {
1053
+ snapshot = await this.persistence.adapter.loadResumeSnapshot(
1054
+ this.storageTargetFor(storageCollectionId, claimId),
1055
+ {
1056
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1057
+ includeRows: false,
1058
+ cacheGenerationClaimId: claimId
1059
+ }
1060
+ );
1061
+ } catch (error) {
1062
+ if (!certificationIsCurrent()) return;
1063
+ if (!claimId || !this.hasExpiredCacheClaim()) throw error;
1064
+ await this.ensureCacheClaimActive();
1065
+ return;
1066
+ }
1067
+ if (!certificationIsCurrent()) return;
1068
+ if (claimId && !await this.renewCacheClaim(this.persistence.adapter)) {
1069
+ await this.ensureCacheClaimActive();
1070
+ return;
1071
+ }
1072
+ if (!certificationIsCurrent()) return;
733
1073
  this.bindResumeSnapshotEvidence(snapshot);
734
1074
  })();
735
1075
  return this.resumeCertificationPromise;
@@ -737,6 +1077,162 @@ class PersistedCollectionRuntime {
737
1077
  getKeySetEvidence() {
738
1078
  return this.persistedKeySetEvidence;
739
1079
  }
1080
+ hasManagedCacheGeneration() {
1081
+ return this.cacheGenerationClaim !== void 0;
1082
+ }
1083
+ hasRotatedStartupCacheGeneration() {
1084
+ return this.startupCacheGenerationRotated;
1085
+ }
1086
+ async startScopedRecovery(resetMetadata) {
1087
+ this.throwIfTerminal();
1088
+ this.clearCacheClaimRenewal();
1089
+ const lifecycleGeneration = this.lifecycleGeneration;
1090
+ const restartSource = this.restartSourceAfterScopedRecovery;
1091
+ this.scopedRecovery = true;
1092
+ this.scopedRecoveryPending = true;
1093
+ const recoverySequence = ++this.scopedRecoverySequence;
1094
+ this.hydratedDemands.clear();
1095
+ this.resetSequence++;
1096
+ let resolveCacheRotated;
1097
+ let rejectCacheRotated;
1098
+ let sourceRestart;
1099
+ try {
1100
+ if (restartSource) {
1101
+ const cacheRotated = new Promise((resolve, reject) => {
1102
+ resolveCacheRotated = resolve;
1103
+ rejectCacheRotated = reject;
1104
+ });
1105
+ void cacheRotated.catch(() => void 0);
1106
+ sourceRestart = Promise.resolve(restartSource(cacheRotated));
1107
+ void sourceRestart.catch(() => void 0);
1108
+ }
1109
+ await this.applyMutex.run(async () => {
1110
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1111
+ const claim = this.cacheGenerationClaim;
1112
+ if (claim && this.persistence.adapter.rotateCacheGeneration) {
1113
+ const previousStorageId = claim.storageCollectionId;
1114
+ const rotated = await this.persistence.adapter.rotateCacheGeneration(
1115
+ this.collectionId,
1116
+ claim.claimId,
1117
+ resetMetadata,
1118
+ previousStorageId
1119
+ );
1120
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
1121
+ await this.persistence.adapter.releaseCacheGenerationClaim?.(
1122
+ rotated.claimId
1123
+ );
1124
+ throw new SyncTransactionAbortedError();
1125
+ }
1126
+ for (const options of this.activeSubsets.values()) {
1127
+ void this.persistence.coordinator.requestReleaseRemoteSubset(
1128
+ previousStorageId,
1129
+ this.rotatedAcquisitions.get(options) ?? options
1130
+ ).catch(() => void 0);
1131
+ if (options.signal?.aborted) {
1132
+ this.rotatedAcquisitions.delete(options);
1133
+ continue;
1134
+ }
1135
+ this.rotatedAcquisitions.set(options, {
1136
+ ...options,
1137
+ refetch: true
1138
+ });
1139
+ }
1140
+ this.remoteSubsetOwnerUnsubscribe?.();
1141
+ this.remoteSubsetOwnerUnsubscribe = null;
1142
+ this.coordinatorUnsubscribe?.();
1143
+ this.coordinatorUnsubscribe = null;
1144
+ this.cacheGenerationClaim = rotated;
1145
+ this.latestTerm = 0;
1146
+ this.latestSeq = 0;
1147
+ this.latestRowVersion = 0;
1148
+ this.publicRowVersion = 0;
1149
+ this.resetTermFence = void 0;
1150
+ this.localTerm = 1;
1151
+ this.localSeq = 0;
1152
+ this.localRowVersion = 0;
1153
+ this.queuedTxCommitted.length = 0;
1154
+ this.persistedResumeGeneration = void 0;
1155
+ this.persistedKeySetEvidence = { status: `incompatible` };
1156
+ this.resumeCertificationPromise = null;
1157
+ this.resumeGenerationOwner = /* @__PURE__ */ Symbol(
1158
+ `persisted resume generation owner`
1159
+ );
1160
+ this.attachCoordinatorSubscription();
1161
+ if (this.remoteSubsetOwner) {
1162
+ this.registerRemoteSubsetOwner(this.remoteSubsetOwner);
1163
+ }
1164
+ }
1165
+ if (this.syncControls.begin && this.syncControls.commit) {
1166
+ const applied = this.withInternalApply(() => {
1167
+ this.syncControls.begin?.();
1168
+ this.syncControls.truncate?.({ markReady: false });
1169
+ if (resetMetadata) {
1170
+ this.syncControls.metadata?.collection.set(
1171
+ resetMetadata.key,
1172
+ resetMetadata.value
1173
+ );
1174
+ }
1175
+ return this.syncControls.commit?.() ?? true;
1176
+ });
1177
+ await whenSyncAccepted(applied);
1178
+ if (!this.cacheGenerationClaim && recoverySequence === this.scopedRecoverySequence) {
1179
+ this.scopedRecoveryPending = false;
1180
+ }
1181
+ await this.runInHydrationScope(async (adapter) => {
1182
+ if (this.recoverQueuedAfterLocalLoadFailure) {
1183
+ this.recoverQueuedAfterLocalLoadFailure = false;
1184
+ await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
1185
+ lifecycleGeneration,
1186
+ adapter
1187
+ );
1188
+ } else {
1189
+ await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1190
+ }
1191
+ });
1192
+ await this.flushQueuedTxCommittedUnsafe();
1193
+ }
1194
+ if (recoverySequence === this.scopedRecoverySequence) {
1195
+ this.scopedRecoveryPending = false;
1196
+ if (this.cacheGenerationClaim) {
1197
+ const currentIndexes = this.collection?.getIndexMetadata() ?? [];
1198
+ const completedIndexes = await this.bootstrapPersistedIndexes(
1199
+ currentIndexes,
1200
+ this.persistence.adapter
1201
+ );
1202
+ await this.requestCoordinatorPersistedIndexes(
1203
+ currentIndexes,
1204
+ completedIndexes
1205
+ );
1206
+ }
1207
+ }
1208
+ });
1209
+ this.throwIfLifecycleReplaced(lifecycleGeneration);
1210
+ resolveCacheRotated?.();
1211
+ await sourceRestart;
1212
+ if (recoverySequence === this.scopedRecoverySequence && this.cacheGenerationClaim) {
1213
+ this.scheduleCacheClaimRenewal(this.cacheGenerationClaim);
1214
+ }
1215
+ if (recoverySequence !== this.scopedRecoverySequence) return;
1216
+ for (const options of this.activeSubsets.values()) {
1217
+ if (options.signal?.aborted) continue;
1218
+ const rotatedOptions = this.rotatedAcquisitions.get(options);
1219
+ if (rotatedOptions) {
1220
+ const ensure = this.persistence.coordinator.requestEnsureRemoteSubset(
1221
+ this.storageCollectionId,
1222
+ rotatedOptions
1223
+ );
1224
+ this.rotatedEnsures.set(options, ensure);
1225
+ void ensure.catch((error) => this.reportSyncError(error));
1226
+ } else {
1227
+ this.refreshScopedSubsets();
1228
+ break;
1229
+ }
1230
+ }
1231
+ } catch (error) {
1232
+ rejectCacheRotated?.(error);
1233
+ throw this.markTerminalFailure(error, lifecycleGeneration);
1234
+ }
1235
+ }
740
1236
  getResumeGenerationOwner() {
741
1237
  return this.resumeGenerationOwner;
742
1238
  }
@@ -745,7 +1241,7 @@ class PersistedCollectionRuntime {
745
1241
  const baseline = {};
746
1242
  this.activeSubsets.set(this.getSubsetKey(baseline), baseline);
747
1243
  const appliedCursor = this.appliedReceiptSequence;
748
- await this.hydrateSubsetUnsafe(
1244
+ const hydrated = await this.hydrateSubsetUnsafe(
749
1245
  baseline,
750
1246
  {
751
1247
  lifecycleGeneration,
@@ -753,6 +1249,7 @@ class PersistedCollectionRuntime {
753
1249
  },
754
1250
  adapter
755
1251
  );
1252
+ if (!hydrated) return false;
756
1253
  return lifecycleGeneration === this.lifecycleGeneration ? appliedCursor : void 0;
757
1254
  }
758
1255
  async ensureStartupMetadataLoaded() {
@@ -772,19 +1269,33 @@ class PersistedCollectionRuntime {
772
1269
  adapter
773
1270
  );
774
1271
  if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
775
- onStartupMetadataLoaded();
1272
+ if (!this.cacheGenerationClaim || this.syncMode !== `on-demand`) {
1273
+ onStartupMetadataLoaded();
1274
+ }
776
1275
  const appliedCursor = this.syncMode !== `on-demand` ? await this.hydrateBaseline(lifecycleGeneration, adapter) : void 0;
777
1276
  return lifecycleGeneration === this.lifecycleGeneration ? {
778
- appliedCursor,
1277
+ appliedCursor: appliedCursor === false ? void 0 : appliedCursor,
779
1278
  indexBootstrapSnapshot,
780
1279
  completedLocalIndexSignatures
781
1280
  } : void 0;
782
1281
  }
783
1282
  async loadStartupMetadataInternal(lifecycleGeneration, adapter) {
784
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
785
- includeRows: false
786
- });
787
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
1283
+ const claimId = this.cacheGenerationClaim?.claimId;
1284
+ const storageCollectionId = this.storageCollectionId;
1285
+ const snapshot = await adapter.loadResumeSnapshot(
1286
+ this.storageTargetFor(storageCollectionId, claimId),
1287
+ {
1288
+ includeRows: false,
1289
+ cacheGenerationClaimId: claimId
1290
+ }
1291
+ );
1292
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1293
+ if (claimId !== this.cacheGenerationClaim?.claimId) return false;
1294
+ if (storageCollectionId !== this.storageCollectionId) return false;
1295
+ if (claimId && !await this.renewCacheClaim(adapter)) return false;
1296
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1297
+ if (claimId !== this.cacheGenerationClaim?.claimId) return false;
1298
+ if (storageCollectionId !== this.storageCollectionId) return false;
788
1299
  this.persistedResumeGeneration = this.getResumeSnapshotGeneration(snapshot);
789
1300
  this.persistedKeySetEvidence = snapshot.keySet;
790
1301
  this.observeStreamPosition(
@@ -796,36 +1307,42 @@ class PersistedCollectionRuntime {
796
1307
  snapshot.collectionMetadata
797
1308
  );
798
1309
  await whenSyncAccepted(applied);
1310
+ return true;
799
1311
  }
800
1312
  async loadCollectionMetadataSnapshot(adapter) {
801
1313
  if (!adapter.loadCollectionMetadata) {
802
1314
  return [];
803
1315
  }
804
- return adapter.loadCollectionMetadata(this.collectionId);
1316
+ return adapter.loadCollectionMetadata(this.storageTargetFor(), {
1317
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1318
+ });
805
1319
  }
806
1320
  replaceCollectionMetadataSnapshot(collectionMetadata) {
807
1321
  if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
808
1322
  return true;
809
1323
  }
810
- const nextMetadata = new Map(
811
- collectionMetadata.map(({ key, value }) => [key, value])
812
- );
813
- const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
814
1324
  return this.withInternalApply(() => {
815
1325
  this.syncControls.begin?.();
816
- currentKeys.forEach((key) => {
817
- if (!nextMetadata.has(key)) {
818
- this.syncControls.metadata?.collection.delete(key);
819
- }
820
- });
821
- nextMetadata.forEach((value, key) => {
822
- this.syncControls.metadata?.collection.set(key, value);
823
- });
1326
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
824
1327
  return this.syncControls.commit?.() ?? true;
825
1328
  });
826
1329
  }
827
- async loadSubset(options, upstreamLoadSubset) {
1330
+ applyCollectionMetadataSnapshot(collectionMetadata) {
1331
+ const metadata = this.syncControls.metadata;
1332
+ if (!metadata) return;
1333
+ const next = new Map(
1334
+ collectionMetadata.map(({ key, value }) => [key, value])
1335
+ );
1336
+ for (const { key } of metadata.collection.list()) {
1337
+ if (!next.has(key)) metadata.collection.delete(key);
1338
+ }
1339
+ for (const [key, value] of next) metadata.collection.set(key, value);
1340
+ }
1341
+ async loadSubset(options, upstreamLoadSubset, onHydrationStarted, isDemandRetained) {
828
1342
  this.throwIfTerminal();
1343
+ await this.ensureCacheClaimActive();
1344
+ if (options.signal?.aborted || isDemandRetained && !isDemandRetained())
1345
+ return;
829
1346
  const lifecycleGeneration = this.lifecycleGeneration;
830
1347
  const subsetKey = this.getSubsetKey(options);
831
1348
  const truncateGeneration = this.sourceTruncateGeneration;
@@ -834,25 +1351,52 @@ class PersistedCollectionRuntime {
834
1351
  this.activeSubsets.set(subsetKey, options);
835
1352
  const appliedCursor = this.appliedReceiptSequence;
836
1353
  try {
837
- await this.applyMutex.run(async () => {
838
- try {
839
- await this.runInHydrationScope(
840
- (adapter) => this.hydrateSubsetUnsafe(
841
- options,
842
- {
843
- lifecycleGeneration,
844
- requestLocalLoadFailure: true,
845
- rejectBufferedReplayFailure: true
846
- },
847
- adapter
848
- )
849
- );
850
- } finally {
851
- if (lifecycleGeneration === this.lifecycleGeneration && !this.getCurrentTerminalFailure()) {
852
- await this.flushQueuedTxCommittedUnsafe();
1354
+ const hydrated = await this.applyMutex.run(
1355
+ async () => {
1356
+ if (options.signal?.aborted) return `aborted`;
1357
+ try {
1358
+ return await this.runInHydrationScope(
1359
+ (adapter) => {
1360
+ if (options.signal?.aborted) {
1361
+ return Promise.resolve(`aborted`);
1362
+ }
1363
+ onHydrationStarted?.();
1364
+ return this.hydrateSubsetUnsafe(
1365
+ options,
1366
+ {
1367
+ lifecycleGeneration,
1368
+ requestLocalLoadFailure: true,
1369
+ rejectBufferedReplayFailure: true
1370
+ },
1371
+ adapter
1372
+ );
1373
+ }
1374
+ );
1375
+ } finally {
1376
+ if (lifecycleGeneration === this.lifecycleGeneration && !this.getCurrentTerminalFailure()) {
1377
+ await this.flushQueuedTxCommittedUnsafe();
1378
+ }
853
1379
  }
854
1380
  }
855
- });
1381
+ );
1382
+ if (hydrated === `aborted`) {
1383
+ if (this.activeSubsets.get(subsetKey) === options) {
1384
+ this.activeSubsets.delete(subsetKey);
1385
+ }
1386
+ return;
1387
+ }
1388
+ if (!hydrated) {
1389
+ await this.ensureCacheClaimActive();
1390
+ const rotatedOptions = this.rotatedAcquisitions.get(options);
1391
+ if (rotatedOptions && this.activeSubsets.get(subsetKey) === options && !options.signal?.aborted) {
1392
+ await (this.rotatedEnsures.get(options) ?? this.persistence.coordinator.requestEnsureRemoteSubset(
1393
+ this.storageCollectionId,
1394
+ rotatedOptions
1395
+ ));
1396
+ await this.applyLeaderCommitsBeforeReply(options);
1397
+ }
1398
+ return;
1399
+ }
856
1400
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
857
1401
  await this.waitForAppliedReceiptsAfter(appliedCursor);
858
1402
  if (!routeRemoteDemandDuringHydration && this.canRouteRemoteDemandThroughCoordinator()) {
@@ -865,13 +1409,16 @@ class PersistedCollectionRuntime {
865
1409
  throw error;
866
1410
  }
867
1411
  if (options.signal?.aborted || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
1412
+ if (options.signal?.aborted && this.activeSubsets.get(subsetKey) === options) {
1413
+ this.activeSubsets.delete(subsetKey);
1414
+ }
868
1415
  return;
869
1416
  }
870
1417
  if (this.canRouteRemoteDemandThroughCoordinator()) {
871
1418
  try {
872
1419
  await this.persistence.coordinator.requestEnsureRemoteSubset(
873
- this.collectionId,
874
- options
1420
+ this.storageCollectionId,
1421
+ this.rotatedAcquisitions.get(options) ?? options
875
1422
  );
876
1423
  } catch (error) {
877
1424
  if (options.signal?.aborted || typeof error === `object` && error !== null && `name` in error && error.name === `AbortError`) {
@@ -881,6 +1428,7 @@ class PersistedCollectionRuntime {
881
1428
  this.queueRemoteSubsetEnsure(options);
882
1429
  throw error;
883
1430
  }
1431
+ await this.applyLeaderCommitsBeforeReply(options);
884
1432
  return;
885
1433
  }
886
1434
  if (truncateGeneration === this.sourceTruncateGeneration && this.activeSubsets.get(subsetKey) === options) {
@@ -898,7 +1446,7 @@ class PersistedCollectionRuntime {
898
1446
  loadHydratedSubset(options, upstreamLoadSubset) {
899
1447
  const failure = this.getCurrentTerminalFailure();
900
1448
  if (failure) return Promise.reject(failure.error);
901
- if (!this.startupSettled || this.isHydratingNow() || this.canRouteRemoteDemandThroughCoordinator() || options.refetch === true || !this.hydratedDemands.has(getLoadSubsetDemandKey(options))) {
1449
+ if (this.hasExpiredCacheClaim() || !this.startupSettled || this.isHydratingNow() || this.canRouteRemoteDemandThroughCoordinator() || options.refetch === true || !this.hydratedDemands.has(getLoadSubsetDemandKey(options))) {
902
1450
  return;
903
1451
  }
904
1452
  this.activeSubsets.set(this.getSubsetKey(options), options);
@@ -938,13 +1486,15 @@ class PersistedCollectionRuntime {
938
1486
  const subsetKey = this.getSubsetKey(options);
939
1487
  this.activeSubsets.delete(subsetKey);
940
1488
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1489
+ const remoteOptions = this.rotatedAcquisitions.get(options) ?? options;
1490
+ this.rotatedAcquisitions.delete(options);
941
1491
  const demandKey = getLoadSubsetDemandKey(options);
942
1492
  const stillActive = Array.from(this.activeSubsets.values()).some(
943
1493
  (active) => getLoadSubsetDemandKey(active) === demandKey
944
1494
  );
945
1495
  if (!stillActive) this.hydratedDemands.delete(demandKey);
946
1496
  if (this.mode === `sync-present`) {
947
- void this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
1497
+ void this.persistence.coordinator.requestReleaseRemoteSubset(this.storageCollectionId, remoteOptions).catch((error) => {
948
1498
  this.reportSyncError(error);
949
1499
  });
950
1500
  }
@@ -961,9 +1511,10 @@ class PersistedCollectionRuntime {
961
1511
  }
962
1512
  async forceReloadSubset(options) {
963
1513
  this.throwIfTerminal();
1514
+ await this.ensureCacheClaimActive();
964
1515
  const lifecycleGeneration = this.lifecycleGeneration;
965
- await this.applyMutex.run(async () => {
966
- await this.runInHydrationScope(
1516
+ const hydrated = await this.applyMutex.run(async () => {
1517
+ const result = await this.runInHydrationScope(
967
1518
  (adapter) => this.hydrateSubsetUnsafe(
968
1519
  options,
969
1520
  {
@@ -976,7 +1527,9 @@ class PersistedCollectionRuntime {
976
1527
  if (lifecycleGeneration === this.lifecycleGeneration) {
977
1528
  await this.flushQueuedTxCommittedUnsafe();
978
1529
  }
1530
+ return result;
979
1531
  });
1532
+ if (!hydrated) await this.ensureCacheClaimActive();
980
1533
  }
981
1534
  queueHydrationBufferedTransaction(transaction) {
982
1535
  const failure = this.getCurrentTerminalFailure();
@@ -984,11 +1537,13 @@ class PersistedCollectionRuntime {
984
1537
  transaction.rejectApplied?.(failure.error);
985
1538
  return;
986
1539
  }
1540
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
987
1541
  this.queuedHydrationTransactions.push(transaction);
988
1542
  }
989
1543
  applyHydrationBufferedTransaction(transaction) {
990
1544
  const failure = this.getCurrentTerminalFailure();
991
1545
  if (failure) return Promise.reject(failure.error);
1546
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
992
1547
  return this.applyMutex.run(async () => {
993
1548
  await this.applyBufferedSyncTransactionUnsafe(transaction);
994
1549
  });
@@ -1093,7 +1648,10 @@ class PersistedCollectionRuntime {
1093
1648
  for (const options of this.activeSubsets.values()) {
1094
1649
  attempt(() => {
1095
1650
  remoteReleases.push(
1096
- this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
1651
+ this.persistence.coordinator.requestReleaseRemoteSubset(
1652
+ this.storageCollectionId,
1653
+ this.rotatedAcquisitions.get(options) ?? options
1654
+ ).catch((error) => {
1097
1655
  this.reportSyncError(error);
1098
1656
  })
1099
1657
  );
@@ -1104,6 +1662,7 @@ class PersistedCollectionRuntime {
1104
1662
  this.coordinatorUnsubscribe = null;
1105
1663
  attempt(() => this.remoteSubsetOwnerUnsubscribe?.());
1106
1664
  this.remoteSubsetOwnerUnsubscribe = null;
1665
+ this.remoteSubsetOwner = void 0;
1107
1666
  attempt(() => this.indexAddedUnsubscribe?.());
1108
1667
  this.indexAddedUnsubscribe = null;
1109
1668
  attempt(() => this.indexRemovedUnsubscribe?.());
@@ -1113,6 +1672,16 @@ class PersistedCollectionRuntime {
1113
1672
  this.remoteEnsureRetryTimer = null;
1114
1673
  }
1115
1674
  this.pendingRemoteSubsetEnsures.clear();
1675
+ this.rotatedAcquisitions.clear();
1676
+ for (const [options, storageId] of this.scopedRefreshes) {
1677
+ remoteReleases.push(
1678
+ this.persistence.coordinator.requestReleaseRemoteSubset(
1679
+ storageId,
1680
+ options
1681
+ )
1682
+ );
1683
+ }
1684
+ this.scopedRefreshes.clear();
1116
1685
  this.activeSubsets.clear();
1117
1686
  this.hydratedDemands.clear();
1118
1687
  for (const transaction of this.queuedHydrationTransactions) {
@@ -1122,6 +1691,15 @@ class PersistedCollectionRuntime {
1122
1691
  this.queuedTxCommitted.length = 0;
1123
1692
  this.clearSyncControls();
1124
1693
  this.collection = null;
1694
+ const claim = this.cacheGenerationClaim;
1695
+ this.cacheGenerationClaim = void 0;
1696
+ this.clearCacheClaimRenewal();
1697
+ this.cacheClaimRecovery = null;
1698
+ if (claim && this.persistence.adapter.releaseCacheGenerationClaim) {
1699
+ remoteReleases.push(
1700
+ this.persistence.adapter.releaseCacheGenerationClaim(claim.claimId)
1701
+ );
1702
+ }
1125
1703
  return Promise.all(remoteReleases).then(() => {
1126
1704
  if (failures.length === 1) throw failures[0];
1127
1705
  if (failures.length > 1) {
@@ -1133,6 +1711,12 @@ class PersistedCollectionRuntime {
1133
1711
  }
1134
1712
  advanceLifecycle() {
1135
1713
  this.lifecycleGeneration++;
1714
+ this.clearCacheClaimRenewal();
1715
+ this.cacheClaimRecovery = null;
1716
+ this.scopedRecovery = false;
1717
+ this.scopedRecoveryPending = false;
1718
+ this.scopedRecoverySequence++;
1719
+ this.recoverQueuedAfterLocalLoadFailure = false;
1136
1720
  this.persistedReadiness?.set({ status: `loading` });
1137
1721
  this.startupSettled = false;
1138
1722
  this.publicRowVersion = 0;
@@ -1162,22 +1746,40 @@ class PersistedCollectionRuntime {
1162
1746
  return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
1163
1747
  }
1164
1748
  loadSubsetRowsUnsafe(options, adapter) {
1165
- return adapter.loadSubset(this.collectionId, options, {
1166
- requiredIndexSignatures: this.getRequiredIndexSignatures()
1749
+ if (this.scopedRecovery) return Promise.resolve([]);
1750
+ return adapter.loadSubset(this.storageTargetFor(), options, {
1751
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1752
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1167
1753
  });
1168
1754
  }
1169
1755
  async scanPersistedRowsUnsafe(options) {
1170
1756
  if (!this.persistence.adapter.scanRows) {
1171
1757
  return [];
1172
1758
  }
1173
- return this.persistence.adapter.scanRows(
1174
- this.collectionId,
1175
- options
1176
- );
1759
+ return this.persistence.adapter.scanRows(this.storageTargetFor(), options, {
1760
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1761
+ });
1177
1762
  }
1178
1763
  async scanPersistedRows(options) {
1179
- this.throwIfTerminal();
1180
- return this.applyMutex.run(() => this.scanPersistedRowsUnsafe(options));
1764
+ for (; ; ) {
1765
+ this.throwIfTerminal();
1766
+ await this.ensureCacheClaimActive();
1767
+ const claimId = this.cacheGenerationClaim?.claimId;
1768
+ const storageCollectionId = this.storageCollectionId;
1769
+ const rows = await this.applyMutex.run(
1770
+ () => this.scanPersistedRowsUnsafe(options)
1771
+ );
1772
+ if (this.scopedRecoveryPending) {
1773
+ throw new SyncTransactionAbortedError();
1774
+ }
1775
+ if (claimId !== this.cacheGenerationClaim?.claimId || storageCollectionId !== this.storageCollectionId) {
1776
+ continue;
1777
+ }
1778
+ if (claimId && !await this.renewCacheClaim(this.persistence.adapter)) {
1779
+ continue;
1780
+ }
1781
+ return rows;
1782
+ }
1181
1783
  }
1182
1784
  async hydrateSubsetUnsafe(options, config, adapter) {
1183
1785
  let rowsLoaded = false;
@@ -1192,27 +1794,41 @@ class PersistedCollectionRuntime {
1192
1794
  this.hydratingGeneration = config.lifecycleGeneration;
1193
1795
  try {
1194
1796
  let rows;
1195
- if (config.bindKeySetEvidence) {
1196
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
1197
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1198
- includeRows: true
1199
- });
1797
+ let bindResumeSnapshot;
1798
+ if (config.bindKeySetEvidence && !this.scopedRecovery) {
1799
+ const snapshot = await adapter.loadResumeSnapshot(
1800
+ this.storageTargetFor(),
1801
+ {
1802
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1803
+ includeRows: true,
1804
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1805
+ }
1806
+ );
1200
1807
  rows = snapshot.rows;
1201
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1202
- if (resetSequence !== this.resetSequence) return;
1203
- this.bindResumeSnapshotEvidence(snapshot);
1808
+ if (config.lifecycleGeneration !== this.lifecycleGeneration)
1809
+ return true;
1810
+ if (resetSequence !== this.resetSequence) return true;
1811
+ bindResumeSnapshot = () => this.bindResumeSnapshotEvidence(snapshot);
1204
1812
  boundRowVersion = snapshot.latestRowVersion;
1205
1813
  } else {
1206
1814
  rows = await this.loadSubsetRowsUnsafe(options, adapter);
1207
1815
  }
1816
+ const complete = this.syncMode === `on-demand` && !this.scopedRecovery && options.where === void 0 && options.limit === void 0 && options.offset === void 0 && options.cursor === void 0;
1817
+ const validationRows = complete ? rows : void 0;
1208
1818
  rowsLoaded = true;
1209
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1210
- if (resetSequence !== this.resetSequence) return;
1819
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1820
+ if (resetSequence !== this.resetSequence) return true;
1821
+ if (this.cacheGenerationClaim && !await this.renewCacheClaim(adapter)) {
1822
+ return false;
1823
+ }
1824
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1825
+ if (resetSequence !== this.resetSequence) return true;
1826
+ bindResumeSnapshot?.();
1211
1827
  if (!config.bindKeySetEvidence || this.persistedKeySetEvidence?.status !== `incompatible`) {
1212
1828
  for (const row of rows) {
1213
1829
  hydrationContext.suppliedRowKeys.add(row.key);
1214
1830
  }
1215
- const applied = this.applyRowsToCollection(rows);
1831
+ const applied = this.applyRowsToCollection(rows, validationRows);
1216
1832
  await whenSyncAccepted(applied);
1217
1833
  if (boundRowVersion !== void 0) {
1218
1834
  this.publicRowVersion = Math.max(
@@ -1226,11 +1842,20 @@ class PersistedCollectionRuntime {
1226
1842
  this.hydratingGeneration = null;
1227
1843
  }
1228
1844
  }
1229
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1230
- replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1231
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1845
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1846
+ if (!this.scopedRecoveryPending) {
1847
+ replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1848
+ }
1849
+ if (config.lifecycleGeneration !== this.lifecycleGeneration) return true;
1232
1850
  } catch (error) {
1851
+ if (config.requestLocalLoadFailure && !rowsLoaded && this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
1852
+ return false;
1853
+ }
1233
1854
  if (config.requestLocalLoadFailure && !rowsLoaded) {
1855
+ if (this.scopedRecoveryPending) {
1856
+ this.recoverQueuedAfterLocalLoadFailure = true;
1857
+ throw error;
1858
+ }
1234
1859
  this.hydratingGeneration = config.lifecycleGeneration;
1235
1860
  try {
1236
1861
  await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
@@ -1259,6 +1884,7 @@ class PersistedCollectionRuntime {
1259
1884
  if (replayFailure && config.rejectBufferedReplayFailure) {
1260
1885
  throw replayFailure.reason;
1261
1886
  }
1887
+ return true;
1262
1888
  }
1263
1889
  async recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(lifecycleGeneration, adapter) {
1264
1890
  let snapshotRows;
@@ -1293,18 +1919,15 @@ class PersistedCollectionRuntime {
1293
1919
  const key = operation.key;
1294
1920
  if (operation.type === `update` && transaction.partialUpdateOperationIndexes.has(index) && getPresence(key) === `unknown`) {
1295
1921
  if (snapshotRows === void 0) {
1296
- const snapshot = await adapter.loadResumeSnapshot(
1297
- this.collectionId,
1298
- {
1299
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1300
- includeRows: true
1301
- }
1302
- );
1922
+ const snapshot = this.scopedRecovery ? void 0 : await adapter.loadResumeSnapshot(this.storageTargetFor(), {
1923
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
1924
+ includeRows: true,
1925
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1926
+ });
1303
1927
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1304
1928
  if (transaction.signal?.aborted) break;
1305
- snapshotRows = new Map(
1306
- snapshot.rows.map((row) => [row.key, row])
1307
- );
1929
+ const rows = snapshot && !this.scopedRecovery ? snapshot.rows : [];
1930
+ snapshotRows = new Map(rows.map((row) => [row.key, row]));
1308
1931
  }
1309
1932
  const baseline = snapshotRows.get(key);
1310
1933
  if (baseline === void 0) {
@@ -1354,22 +1977,65 @@ class PersistedCollectionRuntime {
1354
1977
  }
1355
1978
  this.queuedHydrationTransactions.length = 0;
1356
1979
  }
1357
- applyRowsToCollection(rows) {
1980
+ applyRowsToCollection(rows, validationRows, collectionMetadata) {
1358
1981
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
1359
1982
  return true;
1360
1983
  }
1361
1984
  return this.withInternalApply(() => {
1362
1985
  this.syncControls.begin?.();
1986
+ if (validationRows) {
1987
+ const durableRows = new Map(validationRows.map((row) => [row.key, row]));
1988
+ for (const key of this.collection?.base.keys() ?? []) {
1989
+ if (this.collection?._hasHydratedKey(key)) continue;
1990
+ const durable = durableRows.get(key);
1991
+ if (!durable) {
1992
+ this.syncControls.write?.({ type: `delete`, key });
1993
+ } else {
1994
+ const valueChanged = !equalPersistedSnapshotValues(
1995
+ this.collection?.base.get(key),
1996
+ durable.value
1997
+ );
1998
+ const metadataChanged = !equalPersistedSnapshotValues(
1999
+ this.syncControls.metadata?.row.get(key),
2000
+ durable.metadata
2001
+ );
2002
+ if (valueChanged || metadataChanged && durable.metadata !== void 0) {
2003
+ if (valueChanged) {
2004
+ this.syncControls.write?.({ type: `delete`, key });
2005
+ }
2006
+ this.syncControls.write?.({
2007
+ type: `update`,
2008
+ value: durable.value,
2009
+ metadata: durable.metadata
2010
+ });
2011
+ }
2012
+ if (metadataChanged && durable.metadata === void 0) {
2013
+ this.syncControls.metadata?.row.delete(key);
2014
+ }
2015
+ }
2016
+ }
2017
+ for (const key of this.collection?._syncedRowMetadataKeysOutsideBase() ?? []) {
2018
+ this.syncControls.metadata?.row.delete(key);
2019
+ }
2020
+ }
1363
2021
  for (const row of rows) {
1364
- if (this.collection?._hasHydratedKey(row.key)) {
2022
+ if (validationRows && this.collection?.base.has(row.key) || this.collection?._hasHydratedKey(row.key)) {
1365
2023
  continue;
1366
2024
  }
2025
+ if (this.collection?.base.has(row.key) && !equalPersistedSnapshotValues(
2026
+ this.collection.base.get(row.key),
2027
+ row.value
2028
+ )) {
2029
+ this.syncControls.write?.({ type: `delete`, key: row.key });
2030
+ }
1367
2031
  this.syncControls.write?.({
1368
2032
  type: `update`,
1369
2033
  value: row.value,
1370
2034
  metadata: row.metadata
1371
2035
  });
1372
2036
  }
2037
+ if (collectionMetadata)
2038
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1373
2039
  return this.syncControls.commit?.() ?? true;
1374
2040
  });
1375
2041
  }
@@ -1398,32 +2064,45 @@ class PersistedCollectionRuntime {
1398
2064
  );
1399
2065
  this.persistedKeySetEvidence = this.isExpectedResumeGeneration(generation) || mayAcceptUnownedGeneration ? snapshot.keySet : { status: `incompatible` };
1400
2066
  }
1401
- replaceCollectionSnapshot(rows, collectionMetadata) {
2067
+ replaceCollectionSnapshot(rows, collectionMetadata, replayDemand) {
1402
2068
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
1403
2069
  return true;
1404
2070
  }
1405
- const nextMetadata = new Map(
1406
- collectionMetadata.map(({ key, value }) => [key, value])
2071
+ const base = this.collection?.base;
2072
+ const hydratedKeys = new Set(
2073
+ Array.from(base?.keys() ?? []).filter(
2074
+ (key) => this.collection?._hasHydratedKey(key) === true
2075
+ )
1407
2076
  );
1408
- const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
2077
+ const rowKeys = new Set(rows.map((row) => row.key));
2078
+ const followsLeader = this.canRouteRemoteDemandThroughCoordinator() && !this.persistence.coordinator.isLeader(this.storageCollectionId);
2079
+ const truncate = hydratedKeys.size === 0 && (replayDemand || !followsLeader);
1409
2080
  return this.withInternalApply(() => {
1410
2081
  this.syncControls.begin?.();
1411
- this.syncControls.truncate?.();
2082
+ if (truncate) {
2083
+ this.syncControls.truncate?.();
2084
+ } else {
2085
+ for (const key of base?.keys() ?? []) {
2086
+ if (!hydratedKeys.has(key) && !rowKeys.has(key)) {
2087
+ this.syncControls.write?.({ type: `delete`, key });
2088
+ }
2089
+ }
2090
+ for (const key of this.collection?._syncedRowMetadataKeysOutsideBase() ?? []) {
2091
+ this.syncControls.metadata?.row.delete(key);
2092
+ }
2093
+ }
1412
2094
  for (const row of rows) {
2095
+ if (hydratedKeys.has(row.key)) continue;
2096
+ if (!truncate && base?.has(row.key)) {
2097
+ this.syncControls.write?.({ type: `delete`, key: row.key });
2098
+ }
1413
2099
  this.syncControls.write?.({
1414
2100
  type: `update`,
1415
2101
  value: row.value,
1416
2102
  metadata: row.metadata
1417
2103
  });
1418
2104
  }
1419
- currentKeys.forEach((key) => {
1420
- if (!nextMetadata.has(key)) {
1421
- this.syncControls.metadata?.collection.delete(key);
1422
- }
1423
- });
1424
- nextMetadata.forEach((value, key) => {
1425
- this.syncControls.metadata?.collection.set(key, value);
1426
- });
2105
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1427
2106
  return this.syncControls.commit?.() ?? true;
1428
2107
  });
1429
2108
  }
@@ -1475,6 +2154,31 @@ class PersistedCollectionRuntime {
1475
2154
  async applyBufferedSyncTransactionUnsafe(transaction, scopedAdapter) {
1476
2155
  this.throwIfTerminal();
1477
2156
  this.throwIfLifecycleReplaced(transaction.lifecycleGeneration);
2157
+ if (transaction.admittedStorageCollectionId !== void 0 && transaction.admittedStorageCollectionId !== this.storageCollectionId) {
2158
+ const error = new SyncTransactionAbortedError();
2159
+ if (!transaction.rejectApplied) throw error;
2160
+ transaction.rejectApplied(error);
2161
+ return { applied: false };
2162
+ }
2163
+ if (!transaction.internal && this.scopedRecoveryPending) {
2164
+ const error = new SyncTransactionAbortedError();
2165
+ if (!transaction.rejectApplied) throw error;
2166
+ transaction.rejectApplied(error);
2167
+ return { applied: false };
2168
+ }
2169
+ if (!transaction.internal && this.cacheGenerationClaim && (scopedAdapter ?? this.persistence.adapter).renewCacheGenerationClaim && !await this.renewCacheClaim(scopedAdapter ?? this.persistence.adapter)) {
2170
+ const error = new SyncTransactionAbortedError();
2171
+ queueMicrotask(() => {
2172
+ if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return;
2173
+ if (this.scopedRecoveryPending) return;
2174
+ void this.startScopedRecovery().catch((failure) => {
2175
+ this.markTerminalFailure(failure, transaction.lifecycleGeneration);
2176
+ });
2177
+ });
2178
+ if (!transaction.rejectApplied) throw error;
2179
+ transaction.rejectApplied(error);
2180
+ return { applied: false };
2181
+ }
1478
2182
  const abortedBeforeApplication = transaction.signal?.aborted === true;
1479
2183
  let abortedDuringApplication = false;
1480
2184
  let applicationReturned = false;
@@ -1538,10 +2242,11 @@ class PersistedCollectionRuntime {
1538
2242
  };
1539
2243
  const streamPosition = this.nextLocalStreamPosition();
1540
2244
  const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
2245
+ const storageCollectionId = this.storageCollectionId;
1541
2246
  let response;
1542
2247
  try {
1543
2248
  response = await this.persistence.coordinator.requestApplyCommittedTx(
1544
- this.collectionId,
2249
+ storageCollectionId,
1545
2250
  tx,
1546
2251
  // A hydration-scoped adapter is already inside the shared scheduler.
1547
2252
  // Ordinary source commits enter it before taking the writer lock.
@@ -1554,7 +2259,7 @@ class PersistedCollectionRuntime {
1554
2259
  let reconciled;
1555
2260
  try {
1556
2261
  reconciled = await this.persistence.coordinator.reconcileCommittedTx(
1557
- this.collectionId,
2262
+ storageCollectionId,
1558
2263
  tx,
1559
2264
  anchor,
1560
2265
  scopedAdapter
@@ -1622,7 +2327,10 @@ class PersistedCollectionRuntime {
1622
2327
  this.throwIfTerminal();
1623
2328
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1624
2329
  try {
1625
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
2330
+ await this.persistence.adapter.applyCommittedTx(
2331
+ this.storageTargetFor(),
2332
+ tx
2333
+ );
1626
2334
  } catch (error) {
1627
2335
  throw this.markTerminalFailure(
1628
2336
  toPersistedCollectionDurabilityError(this.collectionId, error),
@@ -1637,6 +2345,7 @@ class PersistedCollectionRuntime {
1637
2345
  term: streamPosition.term,
1638
2346
  seq: streamPosition.seq,
1639
2347
  rowVersion: streamPosition.rowVersion,
2348
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1640
2349
  truncate: transaction.truncate,
1641
2350
  mutations: transaction.operations.map(
1642
2351
  (operation) => operation.type === `update` ? {
@@ -1671,6 +2380,7 @@ class PersistedCollectionRuntime {
1671
2380
  term: streamPosition.term,
1672
2381
  seq: streamPosition.seq,
1673
2382
  rowVersion: streamPosition.rowVersion,
2383
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1674
2384
  mutations: mutations.map((mutation) => {
1675
2385
  if (mutation.type === `delete`) {
1676
2386
  return {
@@ -1730,14 +2440,16 @@ class PersistedCollectionRuntime {
1730
2440
  async persistCollectionMutationsUnsafe(mutations, lifecycleGeneration) {
1731
2441
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1732
2442
  if (this.persistence.coordinator.requestApplyLocalMutations) {
2443
+ const target = this.storageTargetFor();
1733
2444
  const envelopeMutations = mutations.map(
1734
2445
  (mutation) => toPersistedMutationEnvelope(
1735
2446
  mutation
1736
2447
  )
1737
2448
  );
1738
2449
  const response = await this.persistence.coordinator.requestApplyLocalMutations(
1739
- this.collectionId,
1740
- envelopeMutations
2450
+ this.storageCollectionId,
2451
+ envelopeMutations,
2452
+ target.kind === `managed` ? target.claimId : void 0
1741
2453
  );
1742
2454
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1743
2455
  if (!response.ok) {
@@ -1834,12 +2546,12 @@ class PersistedCollectionRuntime {
1834
2546
  const envelope = {
1835
2547
  v: 1,
1836
2548
  dbName: this.dbName,
1837
- collectionId: this.collectionId,
2549
+ collectionId: this.storageCollectionId,
1838
2550
  senderId: this.persistence.coordinator.getNodeId(),
1839
2551
  ts: Date.now(),
1840
2552
  payload: txCommitted
1841
2553
  };
1842
- this.persistence.coordinator.publish(this.collectionId, envelope);
2554
+ this.persistence.coordinator.publish(this.storageCollectionId, envelope);
1843
2555
  }
1844
2556
  observeStreamPosition(term, seq, rowVersion) {
1845
2557
  if (term > this.latestTerm || term === this.latestTerm && seq > this.latestSeq) {
@@ -1881,7 +2593,7 @@ class PersistedCollectionRuntime {
1881
2593
  if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1882
2594
  return false;
1883
2595
  }
1884
- return !this.persistence.coordinator.isLeader(this.collectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
2596
+ return !this.persistence.coordinator.isLeader(this.storageCollectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
1885
2597
  }
1886
2598
  queueRemoteSubsetEnsure(options) {
1887
2599
  if (options.signal?.aborted || !this.canRouteRemoteDemandThroughCoordinator() || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
@@ -1920,8 +2632,8 @@ class PersistedCollectionRuntime {
1920
2632
  }
1921
2633
  try {
1922
2634
  await this.persistence.coordinator.requestEnsureRemoteSubset(
1923
- this.collectionId,
1924
- options
2635
+ this.storageCollectionId,
2636
+ this.rotatedAcquisitions.get(options) ?? options
1925
2637
  );
1926
2638
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1927
2639
  } catch (error) {
@@ -1939,18 +2651,19 @@ class PersistedCollectionRuntime {
1939
2651
  return;
1940
2652
  }
1941
2653
  this.persistence.coordinator.setAdapterForCollection?.(
1942
- this.collectionId,
1943
- this.persistence.adapter
2654
+ this.storageCollectionId,
2655
+ this.persistence.adapter,
2656
+ this.cacheGenerationClaim?.claimId
1944
2657
  );
1945
2658
  this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
1946
- this.collectionId,
2659
+ this.storageCollectionId,
1947
2660
  (message) => {
1948
2661
  this.onCoordinatorMessage(message);
1949
2662
  }
1950
2663
  );
1951
2664
  }
1952
2665
  onCoordinatorMessage(message) {
1953
- if (message.collectionId !== this.collectionId) {
2666
+ if (message.collectionId !== this.storageCollectionId) {
1954
2667
  return;
1955
2668
  }
1956
2669
  if (this.getCurrentTerminalFailure()) {
@@ -1959,17 +2672,24 @@ class PersistedCollectionRuntime {
1959
2672
  const { payload } = message;
1960
2673
  const lifecycleGeneration = this.lifecycleGeneration;
1961
2674
  const isSelf = message.senderId === this.persistence.coordinator.getNodeId();
2675
+ const claimExpired = this.hasExpiredCacheClaim();
2676
+ if (claimExpired && !isTxCommittedPayload(payload)) {
2677
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
2678
+ return;
2679
+ }
1962
2680
  if (isTxCommittedPayload(payload)) {
1963
- if (this.isHydratingNow()) {
1964
- this.queuedTxCommitted.push({
1965
- txCommitted: payload,
1966
- lifecycleGeneration
2681
+ this.queuedTxCommitted.push({
2682
+ txCommitted: payload,
2683
+ lifecycleGeneration,
2684
+ storageCollectionId: message.collectionId
2685
+ });
2686
+ if (claimExpired) {
2687
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
2688
+ } else if (!this.isHydratingNow()) {
2689
+ void this.applyMutex.run(() => this.flushQueuedTxCommittedUnsafe()).catch((error) => {
2690
+ this.markTerminalFailure(error, lifecycleGeneration);
1967
2691
  });
1968
- return;
1969
2692
  }
1970
- void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload, lifecycleGeneration)).catch((error) => {
1971
- this.markTerminalFailure(error, lifecycleGeneration);
1972
- });
1973
2693
  return;
1974
2694
  }
1975
2695
  if (isSelf) {
@@ -1977,6 +2697,7 @@ class PersistedCollectionRuntime {
1977
2697
  }
1978
2698
  if (isCollectionResetPayload(payload)) {
1979
2699
  void this.applyMutex.run(async () => {
2700
+ if (message.collectionId !== this.storageCollectionId) return;
1980
2701
  await this.runInHydrationScope(async (adapter) => {
1981
2702
  await this.truncateAndReloadUnsafe(adapter, lifecycleGeneration);
1982
2703
  await this.rebaseDurableStreamFromSnapshotUnsafe(
@@ -1998,14 +2719,23 @@ class PersistedCollectionRuntime {
1998
2719
  }
1999
2720
  async flushQueuedTxCommittedUnsafe() {
2000
2721
  while (this.queuedTxCommitted.length > 0) {
2722
+ if (this.hasExpiredCacheClaim()) {
2723
+ this.scheduleCacheClaimRecovery(this.lifecycleGeneration);
2724
+ return;
2725
+ }
2001
2726
  const queued = this.queuedTxCommitted.shift();
2002
2727
  if (!queued) {
2003
2728
  continue;
2004
2729
  }
2005
- await this.processCommittedTxUnsafe(
2006
- queued.txCommitted,
2007
- queued.lifecycleGeneration
2008
- );
2730
+ if (queued.storageCollectionId !== this.storageCollectionId) continue;
2731
+ try {
2732
+ await this.processCommittedTxUnsafe(
2733
+ queued.txCommitted,
2734
+ queued.lifecycleGeneration
2735
+ );
2736
+ } catch (error) {
2737
+ throw this.markTerminalFailure(error, queued.lifecycleGeneration);
2738
+ }
2009
2739
  }
2010
2740
  }
2011
2741
  async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
@@ -2043,7 +2773,7 @@ class PersistedCollectionRuntime {
2043
2773
  const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
2044
2774
  const hasGap = observedButUnpublished || txCommitted.latestRowVersion > this.publicRowVersion + 1 || hasGapInCurrentTerm || hasGapAcrossTerms;
2045
2775
  if (hasGap) {
2046
- await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
2776
+ if (!await this.recoverFromSeqGapUnsafe(lifecycleGeneration)) return;
2047
2777
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2048
2778
  if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
2049
2779
  }
@@ -2066,8 +2796,9 @@ class PersistedCollectionRuntime {
2066
2796
  }
2067
2797
  async rebaseDurableStreamFromSnapshotUnsafe(adapter, lifecycleGeneration) {
2068
2798
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2069
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
2070
- includeRows: false
2799
+ const snapshot = await adapter.loadResumeSnapshot(this.storageTargetFor(), {
2800
+ includeRows: false,
2801
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
2071
2802
  });
2072
2803
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2073
2804
  this.latestRowVersion = snapshot.latestRowVersion;
@@ -2079,19 +2810,26 @@ class PersistedCollectionRuntime {
2079
2810
  );
2080
2811
  }
2081
2812
  async recoverFromSeqGapUnsafe(lifecycleGeneration) {
2082
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2813
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2083
2814
  if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
2084
2815
  let pullResponse;
2085
2816
  try {
2086
2817
  pullResponse = await this.persistence.coordinator.pullSince(
2087
- this.collectionId,
2088
- this.publicRowVersion
2818
+ this.storageCollectionId,
2819
+ this.publicRowVersion,
2820
+ void 0,
2821
+ this.cacheGenerationClaim?.claimId
2089
2822
  );
2090
2823
  } catch (error) {
2091
2824
  console.warn(`Failed pullSince recovery attempt:`, error);
2092
2825
  }
2826
+ if (this.scopedRecoveryPending) return false;
2827
+ if (this.cacheGenerationClaim && !await this.renewCacheClaim(this.persistence.adapter)) {
2828
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
2829
+ return false;
2830
+ }
2093
2831
  if (pullResponse) {
2094
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2832
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2095
2833
  if (pullResponse.ok) {
2096
2834
  this.observeStreamPosition(
2097
2835
  pullResponse.latestTerm,
@@ -2106,7 +2844,7 @@ class PersistedCollectionRuntime {
2106
2844
  this.publicRowVersion,
2107
2845
  pullResponse.latestRowVersion
2108
2846
  );
2109
- return;
2847
+ return true;
2110
2848
  }
2111
2849
  const deltas = pullResponse.deltas;
2112
2850
  if (!deltas) {
@@ -2117,7 +2855,7 @@ class PersistedCollectionRuntime {
2117
2855
  this.publicRowVersion,
2118
2856
  pullResponse.latestRowVersion
2119
2857
  );
2120
- return;
2858
+ return true;
2121
2859
  }
2122
2860
  await this.runInHydrationScope(async (adapter) => {
2123
2861
  for (const delta of deltas) {
@@ -2144,11 +2882,11 @@ class PersistedCollectionRuntime {
2144
2882
  this.publicRowVersion,
2145
2883
  pullResponse.latestRowVersion
2146
2884
  );
2147
- return;
2885
+ return true;
2148
2886
  }
2149
2887
  }
2150
2888
  }
2151
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2889
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2152
2890
  await this.runInHydrationScope(
2153
2891
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2154
2892
  );
@@ -2161,12 +2899,48 @@ class PersistedCollectionRuntime {
2161
2899
  this.queueRemoteSubsetEnsure(options);
2162
2900
  }
2163
2901
  }
2902
+ return true;
2903
+ }
2904
+ refreshScopedSubsets() {
2905
+ if (!this.remoteSubsetOwner) return;
2906
+ const lifecycleGeneration = this.lifecycleGeneration;
2907
+ const storageId = this.storageCollectionId;
2908
+ for (const [subsetKey, options] of this.activeSubsets) {
2909
+ if (options.signal?.aborted) continue;
2910
+ this.hydratedDemands.delete(getLoadSubsetDemandKey(options));
2911
+ const refreshOptions = { ...options, refetch: true };
2912
+ this.scopedRefreshes.set(refreshOptions, storageId);
2913
+ void Promise.resolve().then(() => {
2914
+ if (lifecycleGeneration !== this.lifecycleGeneration || this.activeSubsets.get(subsetKey) !== options) {
2915
+ return;
2916
+ }
2917
+ return this.persistence.coordinator.requestEnsureRemoteSubset(
2918
+ storageId,
2919
+ refreshOptions
2920
+ );
2921
+ }).catch((error) => {
2922
+ if (lifecycleGeneration === this.lifecycleGeneration && !options.signal?.aborted) {
2923
+ this.reportSyncError(error);
2924
+ }
2925
+ }).finally(() => {
2926
+ if (!this.scopedRefreshes.delete(refreshOptions)) return;
2927
+ void this.persistence.coordinator.requestReleaseRemoteSubset(storageId, refreshOptions).catch((error) => {
2928
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2929
+ this.reportSyncError(error);
2930
+ }
2931
+ });
2932
+ });
2933
+ }
2164
2934
  }
2165
2935
  async truncateAndReloadUnsafe(adapter, lifecycleGeneration = this.lifecycleGeneration) {
2166
2936
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2937
+ if (this.scopedRecovery) {
2938
+ this.refreshScopedSubsets();
2939
+ return;
2940
+ }
2167
2941
  this.hydratedDemands.clear();
2168
2942
  this.resetSequence++;
2169
- if (this.syncControls.begin && this.syncControls.commit) {
2943
+ if (this.syncMode === `eager` && this.syncControls.begin && this.syncControls.commit) {
2170
2944
  const applied = this.withInternalApply(() => {
2171
2945
  this.syncControls.begin?.();
2172
2946
  this.syncControls.truncate?.();
@@ -2175,9 +2949,18 @@ class PersistedCollectionRuntime {
2175
2949
  await whenSyncAccepted(applied);
2176
2950
  }
2177
2951
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2178
- await this.reloadActiveSubsetsUnsafe(adapter);
2952
+ await this.reloadActiveSubsetsUnsafe(
2953
+ adapter,
2954
+ false,
2955
+ this.syncMode === `eager`,
2956
+ true
2957
+ );
2179
2958
  }
2180
2959
  async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
2960
+ if (this.scopedRecovery) {
2961
+ this.refreshScopedSubsets();
2962
+ return;
2963
+ }
2181
2964
  const reloadActiveSubsets = (skipUnchangedReplace = false) => this.runInHydrationScope(
2182
2965
  (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter, skipUnchangedReplace),
2183
2966
  adapter
@@ -2194,6 +2977,21 @@ class PersistedCollectionRuntime {
2194
2977
  const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
2195
2978
  (opt) => opt.limit != null || opt.offset != null || opt.cursor != null
2196
2979
  );
2980
+ const changedKeys = new Set(txCommitted.changedRows.map((row) => row.key));
2981
+ const hasRepeatedChange = changedKeys.size !== txCommitted.changedRows.length;
2982
+ const hasChangedAndDeletedKey = txCommitted.deletedKeys.some(
2983
+ (key) => changedKeys.has(key)
2984
+ );
2985
+ const hasChangedPublicKey = txCommitted.changedRows.some(
2986
+ (row) => this.collection?.base.has(row.key)
2987
+ );
2988
+ const hasUnseenDemandedKey = this.activeSubsets.size > 0 && txCommitted.changedRows.some(
2989
+ (row) => !this.collection?.base.has(row.key) && !this.collection?._hasHydratedKey(row.key)
2990
+ );
2991
+ if (hasRepeatedChange || hasChangedAndDeletedKey || hasChangedPublicKey || this.syncMode === `on-demand` && hasUnseenDemandedKey) {
2992
+ await reloadActiveSubsets();
2993
+ return;
2994
+ }
2197
2995
  if (!hasPaginatedSubset || changedKeyCount === 0) {
2198
2996
  await this.applyTargetedInvalidationUnsafe(txCommitted);
2199
2997
  return;
@@ -2210,20 +3008,33 @@ class PersistedCollectionRuntime {
2210
3008
  key: changedKey,
2211
3009
  value: newValue
2212
3010
  } of txCommitted.changedRows) {
2213
- const matchesAnySubset = subsetEvaluators.some((evaluator) => {
2214
- if (!evaluator) return true;
2215
- return toBooleanPredicate(evaluator(newValue));
2216
- });
2217
- if (matchesAnySubset) {
2218
- this.syncControls.write?.({ type: `update`, value: newValue });
2219
- } else if (this.collection?.get(changedKey) !== void 0) {
2220
- this.syncControls.write?.({ type: `delete`, key: changedKey });
3011
+ const key = changedKey;
3012
+ if (this.collection?._hasHydratedKey(key)) continue;
3013
+ const previous = this.collection?.base.get(key);
3014
+ if (previous === void 0 && this.activeSubsets.size === 0) continue;
3015
+ const completeValue = {
3016
+ ...previous,
3017
+ ...newValue
3018
+ };
3019
+ const currentlyDemanded = matchesAnySubset(
3020
+ completeValue,
3021
+ subsetEvaluators
3022
+ );
3023
+ const previouslyOutsideDemand = previous !== void 0 && !matchesAnySubset(previous, subsetEvaluators);
3024
+ if (currentlyDemanded || previouslyOutsideDemand) {
3025
+ this.syncControls.write?.({ type: `update`, value: completeValue });
3026
+ } else if (previous !== void 0) {
3027
+ this.syncControls.write?.({ type: `delete`, key });
2221
3028
  }
2222
3029
  }
2223
3030
  for (const deletedKey of txCommitted.deletedKeys) {
2224
- this.syncControls.write?.({ type: `delete`, key: deletedKey });
3031
+ const key = deletedKey;
3032
+ if (!this.collection?._hasHydratedKey(key)) {
3033
+ this.syncControls.write?.({ type: `delete`, key });
3034
+ }
2225
3035
  }
2226
3036
  txCommitted.rowMetadataMutations?.forEach((mutation) => {
3037
+ if (this.collection?._hasHydratedKey(mutation.key)) return;
2227
3038
  if (mutation.type === `delete`) {
2228
3039
  this.syncControls.metadata?.row.delete(mutation.key);
2229
3040
  } else {
@@ -2247,10 +3058,25 @@ class PersistedCollectionRuntime {
2247
3058
  });
2248
3059
  await whenSyncAccepted(applied);
2249
3060
  }
2250
- async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false) {
3061
+ async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false, restoreResetBaseline = false, replayDemand = false) {
3062
+ if (this.scopedRecovery) {
3063
+ this.refreshScopedSubsets();
3064
+ return;
3065
+ }
2251
3066
  const lifecycleGeneration = this.lifecycleGeneration;
2252
3067
  const truncateGeneration = this.sourceTruncateGeneration;
2253
- const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
3068
+ const activeSubsetOptions = Array.from(this.activeSubsets.values());
3069
+ if (restoreResetBaseline && activeSubsetOptions.length === 0) {
3070
+ activeSubsetOptions.push({});
3071
+ }
3072
+ const evaluators = activeSubsetOptions.map(
3073
+ (options) => options.where ? compileSingleRowExpression(options.where) : null
3074
+ );
3075
+ const outsideDemandKeys = activeSubsetOptions.some(
3076
+ (options) => options.limit != null || options.offset != null || options.cursor != null
3077
+ ) ? [] : Array.from(this.collection?.base ?? []).filter(
3078
+ ([key, row]) => !this.collection?._hasHydratedKey(key) && !matchesAnySubset(row, evaluators)
3079
+ ).map(([key]) => key);
2254
3080
  this.hydratedDemands.clear();
2255
3081
  this.hydrationSequence++;
2256
3082
  const hydrationContext = { suppliedRowKeys: /* @__PURE__ */ new Set() };
@@ -2258,22 +3084,68 @@ class PersistedCollectionRuntime {
2258
3084
  this.hydratingGeneration = lifecycleGeneration;
2259
3085
  try {
2260
3086
  const mergedRows = /* @__PURE__ */ new Map();
2261
- const collectionMetadata = await this.loadCollectionMetadataSnapshot(adapter);
2262
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2263
- for (const options of activeSubsetOptions) {
2264
- const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter);
3087
+ let validationSnapshot;
3088
+ let collectionMetadata = [];
3089
+ try {
3090
+ validationSnapshot = (activeSubsetOptions.length === 0 || outsideDemandKeys.length > 0) && this.collection?.base.size ? await adapter.loadResumeSnapshot(this.storageTargetFor(), {
3091
+ requiredIndexSignatures: this.getRequiredIndexSignatures(),
3092
+ includeRows: true,
3093
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
3094
+ }) : void 0;
3095
+ collectionMetadata = validationSnapshot ? validationSnapshot.collectionMetadata : await this.loadCollectionMetadataSnapshot(adapter);
2265
3096
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2266
- for (const row of subsetRows) {
2267
- mergedRows.set(row.key, {
2268
- value: row.value,
2269
- metadata: row.metadata
2270
- });
3097
+ for (const options of activeSubsetOptions) {
3098
+ const subsetRows = await this.loadSubsetRowsUnsafe(options, adapter);
3099
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
3100
+ for (const row of subsetRows) {
3101
+ mergedRows.set(row.key, {
3102
+ value: row.value,
3103
+ metadata: row.metadata
3104
+ });
3105
+ }
3106
+ }
3107
+ if (outsideDemandKeys.length > 0 && validationSnapshot) {
3108
+ const durableRows = new Map(
3109
+ validationSnapshot.rows.map((row) => [row.key, row])
3110
+ );
3111
+ for (const key of outsideDemandKeys) {
3112
+ if (mergedRows.has(key)) continue;
3113
+ const row = durableRows.get(key);
3114
+ if (row) {
3115
+ mergedRows.set(key, {
3116
+ value: row.value,
3117
+ metadata: row.metadata
3118
+ });
3119
+ }
3120
+ }
3121
+ }
3122
+ } catch (error) {
3123
+ if (this.scopedRecovery) return;
3124
+ if (!this.cacheGenerationClaim || !this.hasExpiredCacheClaim()) {
3125
+ throw error;
2271
3126
  }
3127
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
3128
+ return;
3129
+ }
3130
+ if (this.isScopedRecoveryPending()) return;
3131
+ if (this.cacheGenerationClaim && !await this.renewCacheClaim(adapter)) {
3132
+ this.scheduleCacheClaimRecovery(lifecycleGeneration);
3133
+ return;
2272
3134
  }
3135
+ if (this.isScopedRecoveryPending() || lifecycleGeneration !== this.lifecycleGeneration)
3136
+ return;
2273
3137
  for (const key of mergedRows.keys()) {
2274
3138
  hydrationContext.suppliedRowKeys.add(key);
2275
3139
  }
2276
- if (skipUnchangedReplace && this.matchesCollectionSnapshot(mergedRows, collectionMetadata)) {
3140
+ if (activeSubsetOptions.length === 0) {
3141
+ await whenSyncAccepted(
3142
+ this.applyRowsToCollection(
3143
+ [],
3144
+ validationSnapshot?.rows ?? [],
3145
+ collectionMetadata
3146
+ )
3147
+ );
3148
+ } else if (skipUnchangedReplace && this.matchesCollectionSnapshot(mergedRows, collectionMetadata)) {
2277
3149
  const orphanKeys = this.collection?._syncedRowMetadataKeysOutsideBase() ?? [];
2278
3150
  if (orphanKeys.length > 0) {
2279
3151
  const applied = this.withInternalApply(() => {
@@ -2292,7 +3164,8 @@ class PersistedCollectionRuntime {
2292
3164
  value: row.value,
2293
3165
  metadata: row.metadata
2294
3166
  })),
2295
- collectionMetadata
3167
+ collectionMetadata,
3168
+ replayDemand
2296
3169
  );
2297
3170
  await whenSyncAccepted(applied);
2298
3171
  }
@@ -2320,7 +3193,12 @@ class PersistedCollectionRuntime {
2320
3193
  return;
2321
3194
  }
2322
3195
  this.indexAddedUnsubscribe = this.collection.on(`index:added`, (event) => {
2323
- void this.ensurePersistedIndex(event.index);
3196
+ void this.ensurePersistedIndex(event.index).catch((error) => {
3197
+ console.warn(
3198
+ `Failed to ensure persisted index after index:added:`,
3199
+ error
3200
+ );
3201
+ });
2324
3202
  });
2325
3203
  this.indexRemovedUnsubscribe = this.collection.on(
2326
3204
  `index:removed`,
@@ -2336,20 +3214,69 @@ class PersistedCollectionRuntime {
2336
3214
  }
2337
3215
  const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
2338
3216
  const completedLocalIndexSignatures = /* @__PURE__ */ new Set();
3217
+ const lifecycleGeneration = this.lifecycleGeneration;
2339
3218
  for (const metadata of indexMetadata) {
2340
- if (await this.ensureLocalPersistedIndex(metadata, adapter)) {
3219
+ const storageCollectionId = this.storageCollectionId;
3220
+ const claimId = this.cacheGenerationClaim?.claimId;
3221
+ const ensured = await this.ensureLocalPersistedIndex(metadata, adapter);
3222
+ if (ensured && await this.reconcilePersistedIndex(
3223
+ metadata,
3224
+ adapter,
3225
+ storageCollectionId,
3226
+ claimId,
3227
+ lifecycleGeneration,
3228
+ true
3229
+ )) {
2341
3230
  completedLocalIndexSignatures.add(metadata.signature);
2342
3231
  }
2343
3232
  }
2344
3233
  return completedLocalIndexSignatures;
2345
3234
  }
2346
3235
  async requestCoordinatorPersistedIndexes(indexMetadata, completedLocalIndexSignatures) {
3236
+ const lifecycleGeneration = this.lifecycleGeneration;
2347
3237
  for (const metadata of indexMetadata) {
3238
+ if (!this.hasCurrentIndex(metadata)) continue;
3239
+ const storageCollectionId = this.storageCollectionId;
3240
+ const claimId = this.cacheGenerationClaim?.claimId;
2348
3241
  await this.requestCoordinatorPersistedIndex(
2349
3242
  metadata,
2350
3243
  completedLocalIndexSignatures.has(metadata.signature)
2351
3244
  );
3245
+ await this.reconcilePersistedIndex(
3246
+ metadata,
3247
+ this.persistence.adapter,
3248
+ storageCollectionId,
3249
+ claimId,
3250
+ lifecycleGeneration,
3251
+ true
3252
+ );
3253
+ }
3254
+ }
3255
+ /** Settle a late index operation against the Collection's current declaration. */
3256
+ async reconcilePersistedIndex(indexMetadata, adapter, storageCollectionId, claimId, lifecycleGeneration, indexPresent) {
3257
+ while (lifecycleGeneration === this.lifecycleGeneration && storageCollectionId === this.storageCollectionId) {
3258
+ const declared = this.hasCurrentIndex(indexMetadata);
3259
+ if (declared === indexPresent) return declared;
3260
+ if (declared) {
3261
+ if (!await this.ensureLocalPersistedIndex(indexMetadata, adapter)) {
3262
+ return false;
3263
+ }
3264
+ } else {
3265
+ if (!adapter.markIndexRemoved) return false;
3266
+ try {
3267
+ await adapter.markIndexRemoved(
3268
+ this.storageTargetFor(storageCollectionId, claimId),
3269
+ indexMetadata.signature,
3270
+ { cacheGenerationClaimId: claimId }
3271
+ );
3272
+ } catch (error) {
3273
+ console.warn(`Failed to mark persisted index removed:`, error);
3274
+ return false;
3275
+ }
3276
+ }
3277
+ indexPresent = declared;
2352
3278
  }
3279
+ return false;
2353
3280
  }
2354
3281
  buildPersistedIndexSpec(index) {
2355
3282
  return {
@@ -2362,19 +3289,36 @@ class PersistedCollectionRuntime {
2362
3289
  };
2363
3290
  }
2364
3291
  async ensurePersistedIndex(indexMetadata, adapter = this.persistence.adapter) {
3292
+ if (this.scopedRecoveryPending) return;
3293
+ const lifecycleGeneration = this.lifecycleGeneration;
3294
+ await this.ensureCacheClaimActive();
3295
+ if (lifecycleGeneration !== this.lifecycleGeneration || !this.hasCurrentIndex(indexMetadata))
3296
+ return;
2365
3297
  const completedLocally = await this.ensureLocalPersistedIndex(
2366
3298
  indexMetadata,
2367
3299
  adapter
2368
3300
  );
3301
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
3302
+ if (!this.hasCurrentIndex(indexMetadata)) {
3303
+ await this.markIndexRemoved(indexMetadata);
3304
+ return;
3305
+ }
2369
3306
  await this.requestCoordinatorPersistedIndex(indexMetadata, completedLocally);
3307
+ if (lifecycleGeneration === this.lifecycleGeneration && !this.hasCurrentIndex(indexMetadata)) {
3308
+ await this.markIndexRemoved(indexMetadata);
3309
+ }
3310
+ }
3311
+ hasCurrentIndex(indexMetadata) {
3312
+ return this.collection?.getIndexMetadata().some((current) => current.signature === indexMetadata.signature) ?? false;
2370
3313
  }
2371
3314
  async ensureLocalPersistedIndex(indexMetadata, adapter) {
2372
3315
  try {
2373
3316
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2374
3317
  await adapter.ensureIndex(
2375
- this.collectionId,
3318
+ this.storageTargetFor(),
2376
3319
  indexMetadata.signature,
2377
- spec
3320
+ spec,
3321
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2378
3322
  );
2379
3323
  return true;
2380
3324
  } catch (error) {
@@ -2386,11 +3330,12 @@ class PersistedCollectionRuntime {
2386
3330
  try {
2387
3331
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2388
3332
  await this.persistence.coordinator.requestEnsurePersistedIndex(
2389
- this.collectionId,
3333
+ this.storageCollectionId,
2390
3334
  indexMetadata.signature,
2391
3335
  spec,
2392
3336
  completedLocally ? this.persistence.adapter : void 0,
2393
- completedLocally
3337
+ completedLocally,
3338
+ this.cacheGenerationClaim?.claimId
2394
3339
  );
2395
3340
  } catch (error) {
2396
3341
  console.warn(
@@ -2404,10 +3349,19 @@ class PersistedCollectionRuntime {
2404
3349
  return;
2405
3350
  }
2406
3351
  try {
3352
+ if (this.scopedRecoveryPending) return;
3353
+ const lifecycleGeneration = this.lifecycleGeneration;
3354
+ await this.ensureCacheClaimActive();
3355
+ if (lifecycleGeneration !== this.lifecycleGeneration || this.hasCurrentIndex(indexMetadata))
3356
+ return;
2407
3357
  await this.persistence.adapter.markIndexRemoved(
2408
- this.collectionId,
2409
- indexMetadata.signature
3358
+ this.storageTargetFor(),
3359
+ indexMetadata.signature,
3360
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2410
3361
  );
3362
+ if (lifecycleGeneration === this.lifecycleGeneration && this.hasCurrentIndex(indexMetadata)) {
3363
+ await this.ensurePersistedIndex(indexMetadata);
3364
+ }
2411
3365
  } catch (error) {
2412
3366
  console.warn(`Failed to mark persisted index removed:`, error);
2413
3367
  }
@@ -2610,20 +3564,26 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2610
3564
  throw error;
2611
3565
  }
2612
3566
  };
2613
- runtime.setSyncControls({
2614
- begin: params.begin,
2615
- write: params.write,
2616
- commit: params.commit,
2617
- truncate: params.truncate,
2618
- metadata: params.metadata ?? null,
2619
- markError: params.markError
2620
- });
2621
3567
  runtime.setCollection(
2622
- params.collection
3568
+ params.collection,
3569
+ {
3570
+ begin: params.begin,
3571
+ write: params.write,
3572
+ commit: params.commit,
3573
+ truncate: params.truncate,
3574
+ metadata: params.metadata ?? null,
3575
+ markError: params.markError
3576
+ }
2623
3577
  );
2624
3578
  const persistenceCapability = {
2625
3579
  protocol: SYNC_PERSISTENCE_PROTOCOL,
2626
3580
  version: SYNC_PERSISTENCE_VERSION,
3581
+ get managedCacheGeneration() {
3582
+ return runtime.hasManagedCacheGeneration();
3583
+ },
3584
+ get startupCacheGenerationRotated() {
3585
+ return runtime.hasRotatedStartupCacheGeneration();
3586
+ },
2627
3587
  reserveCommitTurn: () => {
2628
3588
  const openTransaction = getOpenTransaction();
2629
3589
  if (openTransaction && !openTransaction.internal)
@@ -2637,6 +3597,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2637
3597
  throw runtime.reportSyncError(error);
2638
3598
  }
2639
3599
  },
3600
+ startScopedRecovery: (resetMetadata) => startupState.cleanedUp ? Promise.resolve() : runtime.startScopedRecovery(resetMetadata),
2640
3601
  scanPersistedRows: (options) => startupState.cleanedUp ? Promise.resolve([]) : runtime.scanPersistedRows(options),
2641
3602
  resumeSnapshot: {
2642
3603
  certify: async () => {
@@ -2660,6 +3621,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2660
3621
  }
2661
3622
  }
2662
3623
  };
3624
+ let sourceSession = 0;
2663
3625
  const wrappedParams = {
2664
3626
  ...params,
2665
3627
  markError: reportUpstreamError,
@@ -2931,6 +3893,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2931
3893
  settlePublicationAdmissionWaiters(openTransaction, error);
2932
3894
  return createHandledRejection(error);
2933
3895
  }
3896
+ if (runtime.isScopedRecoveryPending()) {
3897
+ settlePendingTransaction(openTransaction);
3898
+ const error = new SyncTransactionAbortedError();
3899
+ settlePublicationAdmissionWaiters(openTransaction, error);
3900
+ return createHandledRejection(error);
3901
+ }
2934
3902
  try {
2935
3903
  bindToCurrentHydration(openTransaction);
2936
3904
  assertHydrationSequenceCurrent(openTransaction);
@@ -3005,48 +3973,150 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3005
3973
  return visibleAfterDurable(openTransaction, applied);
3006
3974
  }
3007
3975
  };
3976
+ const sourceParamsFor = (session) => {
3977
+ const active = () => !startupState.cleanedUp && session === sourceSession;
3978
+ const metadata = wrappedParams.metadata;
3979
+ return {
3980
+ ...wrappedParams,
3981
+ markReady: () => {
3982
+ if (active()) wrappedParams.markReady();
3983
+ },
3984
+ markError: (error) => {
3985
+ if (active()) wrappedParams.markError(error);
3986
+ },
3987
+ begin: () => {
3988
+ if (active()) wrappedParams.begin();
3989
+ },
3990
+ write: (message) => {
3991
+ if (active()) wrappedParams.write(message);
3992
+ },
3993
+ truncate: () => {
3994
+ if (active()) wrappedParams.truncate();
3995
+ },
3996
+ commit: (signal) => active() ? wrappedParams.commit(signal) : true,
3997
+ metadata: metadata && {
3998
+ ...metadata,
3999
+ row: {
4000
+ get: (key) => active() ? metadata.row.get(key) : void 0,
4001
+ set: (key, value) => {
4002
+ if (active()) metadata.row.set(key, value);
4003
+ },
4004
+ delete: (key) => {
4005
+ if (active()) metadata.row.delete(key);
4006
+ }
4007
+ },
4008
+ collection: {
4009
+ get: (key) => active() ? metadata.collection.get(key) : void 0,
4010
+ set: (key, value) => {
4011
+ if (active()) metadata.collection.set(key, value);
4012
+ },
4013
+ delete: (key) => {
4014
+ if (active()) metadata.collection.delete(key);
4015
+ },
4016
+ list: (prefix) => active() ? metadata.collection.list(prefix) : []
4017
+ },
4018
+ persistence: {
4019
+ ...metadata.persistence,
4020
+ reserveCommitTurn: () => {
4021
+ if (active()) metadata.persistence.reserveCommitTurn();
4022
+ },
4023
+ hydrateBaseline: () => active() ? metadata.persistence.hydrateBaseline() : Promise.resolve(),
4024
+ startScopedRecovery: (resetMetadata) => active() ? metadata.persistence.startScopedRecovery?.(
4025
+ resetMetadata
4026
+ ) ?? Promise.resolve() : Promise.resolve(),
4027
+ scanPersistedRows: (options) => active() ? metadata.persistence.scanPersistedRows(options) : Promise.resolve([]),
4028
+ resumeSnapshot: {
4029
+ certify: () => active() ? metadata.persistence.resumeSnapshot.certify() : Promise.resolve(),
4030
+ getKeySetEvidence: () => active() ? metadata.persistence.resumeSnapshot.getKeySetEvidence() : void 0,
4031
+ expectCurrentCommit: () => {
4032
+ if (active())
4033
+ metadata.persistence.resumeSnapshot.expectCurrentCommit();
4034
+ }
4035
+ }
4036
+ }
4037
+ }
4038
+ };
4039
+ };
3008
4040
  let sourceResult = {};
3009
4041
  let sourceResultSettled = false;
3010
4042
  let activeSourceSyncEntry;
3011
- fullStartPromise = runtime.ensureStarted();
3012
- void fullStartPromise.catch(() => void 0);
3013
- sourceResultPromise = (async () => {
3014
- await runtime.ensureStartupMetadataLoaded();
3015
- if (startupState.cleanedUp) {
3016
- sourceResultSettled = true;
3017
- return sourceResult;
3018
- }
4043
+ let sourceCleanupPromise;
4044
+ let customRestartSequence = 0;
4045
+ const enterSourceSession = () => {
4046
+ const session = ++sourceSession;
3019
4047
  let settleSourceSyncEntry;
3020
4048
  activeSourceSyncEntry = new Promise((resolve) => {
3021
4049
  settleSourceSyncEntry = resolve;
3022
4050
  });
3023
4051
  try {
3024
4052
  sourceResult = normalizeSyncFnResult(
3025
- sourceSyncConfig.sync(wrappedParams)
4053
+ sourceSyncConfig.sync(
4054
+ runtime.hasManagedCacheGeneration() ? sourceParamsFor(session) : wrappedParams
4055
+ )
3026
4056
  );
4057
+ if (!startupState.cleanedUp && sourceResult.loadSubset) {
4058
+ const ownerSource = sourceResult;
4059
+ const loadSubset = async (options) => {
4060
+ if (startupState.cleanedUp || session !== sourceSession) {
4061
+ throw new Error(`persisted sync source is no longer active`);
4062
+ }
4063
+ await ownerSource.loadSubset?.(
4064
+ options
4065
+ );
4066
+ };
4067
+ runtime.registerRemoteSubsetOwner(
4068
+ Object.assign(loadSubset, {
4069
+ unloadSubset: (options) => ownerSource.unloadSubset?.(
4070
+ options
4071
+ ),
4072
+ onError: (error) => {
4073
+ if (session === sourceSession) reportUpstreamError(error);
4074
+ }
4075
+ })
4076
+ );
4077
+ }
3027
4078
  } finally {
3028
4079
  activeSourceSyncEntry = void 0;
3029
4080
  settleSourceSyncEntry();
3030
4081
  }
3031
- if (!startupState.cleanedUp && sourceResult.loadSubset) {
3032
- const loadSubset = async (options) => {
3033
- if (startupState.cleanedUp) {
3034
- throw new Error(`persisted sync source is no longer active`);
3035
- }
3036
- await sourceResult.loadSubset?.(
3037
- options
3038
- );
3039
- };
3040
- runtime.registerRemoteSubsetOwner(
3041
- Object.assign(loadSubset, {
3042
- unloadSubset: (options) => sourceResult.unloadSubset?.(
3043
- options
3044
- ),
3045
- onError: reportUpstreamError
3046
- })
4082
+ };
4083
+ const restartCustomSource = (cacheRotated) => {
4084
+ const restartSequence = ++customRestartSequence;
4085
+ if (!sourceCleanupPromise) {
4086
+ sourceSession++;
4087
+ const abortError = new SyncTransactionAbortedError();
4088
+ for (const transaction of transactionStack) {
4089
+ settlePublicationAdmissionWaiters(transaction, abortError);
4090
+ }
4091
+ transactionStack.length = 0;
4092
+ const retiredResult = sourceResult;
4093
+ sourceCleanupPromise = Promise.resolve().then(
4094
+ () => retiredResult.cleanup?.()
4095
+ );
4096
+ }
4097
+ return Promise.all([sourceCleanupPromise, cacheRotated]).then(() => {
4098
+ if (startupState.cleanedUp || restartSequence !== customRestartSequence)
4099
+ return;
4100
+ sourceCleanupPromise = void 0;
4101
+ enterSourceSession();
4102
+ runtime.setSourceRestartAfterScopedRecovery(
4103
+ sourceResult.restartAfterScopedRecovery ?? (runtime.hasManagedCacheGeneration() ? restartCustomSource : void 0)
3047
4104
  );
4105
+ });
4106
+ };
4107
+ fullStartPromise = runtime.ensureStarted();
4108
+ void fullStartPromise.catch(() => void 0);
4109
+ sourceResultPromise = (async () => {
4110
+ await runtime.ensureStartupMetadataLoaded();
4111
+ if (startupState.cleanedUp) {
4112
+ sourceResultSettled = true;
4113
+ return sourceResult;
3048
4114
  }
4115
+ enterSourceSession();
3049
4116
  sourceResultSettled = true;
4117
+ runtime.setSourceRestartAfterScopedRecovery(
4118
+ sourceResult.restartAfterScopedRecovery ?? (runtime.hasManagedCacheGeneration() ? restartCustomSource : void 0)
4119
+ );
3050
4120
  return sourceResult;
3051
4121
  })();
3052
4122
  resolveSourceResultAssigned();
@@ -3056,6 +4126,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3056
4126
  return {
3057
4127
  cleanup: async () => {
3058
4128
  startupState.cleanedUp = true;
4129
+ runtime.clearSourceRestartAfterScopedRecovery();
3059
4130
  const sourceSyncEntry = activeSourceSyncEntry;
3060
4131
  const cleanupError = new SyncTransactionAbortedError();
3061
4132
  for (const acquisition of acquisitions.values()) {
@@ -3069,7 +4140,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3069
4140
  transactionStack.length = 0;
3070
4141
  let sourceCleanup;
3071
4142
  try {
3072
- sourceCleanup = sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.());
4143
+ sourceCleanup = sourceCleanupPromise ?? (sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.()));
3073
4144
  } catch (error) {
3074
4145
  sourceCleanup = Promise.reject(error);
3075
4146
  }
@@ -3096,31 +4167,63 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3096
4167
  loadSubset: (options) => {
3097
4168
  const acquisition = { forwarded: false };
3098
4169
  acquisitions.set(options, acquisition);
3099
- const forwardUpstream = (resolvedSourceResult, loadOptions) => {
4170
+ const forwardUpstream = (loadOptions) => {
3100
4171
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
3101
4172
  return true;
3102
4173
  }
3103
- if (!resolvedSourceResult.loadSubset) return true;
4174
+ const currentSource = sourceResult;
4175
+ if (!currentSource.loadSubset) return true;
3104
4176
  acquisition.forwarded = true;
4177
+ acquisition.source = currentSource;
3105
4178
  try {
3106
- return resolvedSourceResult.loadSubset(loadOptions);
4179
+ return currentSource.loadSubset(loadOptions);
3107
4180
  } catch (error) {
3108
4181
  acquisition.forwarded = false;
4182
+ acquisition.source = void 0;
3109
4183
  throw error;
3110
4184
  }
3111
4185
  };
3112
4186
  const openTransaction = getOpenTransaction();
3113
4187
  const needsPublicationAdmission = openTransaction?.reservesCommitTurn === true;
4188
+ let hydrationStarted = false;
4189
+ const raceRetainedLoad = (work2, canAbortPromptly = () => true) => {
4190
+ const signal = options.signal;
4191
+ const abortable = !signal ? work2 : new Promise((resolve, reject) => {
4192
+ const abortError = () => signal.reason ?? new SyncTransactionAbortedError();
4193
+ const onAbort = () => {
4194
+ if (canAbortPromptly()) reject(abortError());
4195
+ };
4196
+ signal.addEventListener(`abort`, onAbort, { once: true });
4197
+ if (signal.aborted) onAbort();
4198
+ void work2.then(
4199
+ () => {
4200
+ signal.removeEventListener(`abort`, onAbort);
4201
+ if (signal.aborted) reject(abortError());
4202
+ else resolve();
4203
+ },
4204
+ (error) => {
4205
+ signal.removeEventListener(`abort`, onAbort);
4206
+ reject(signal.aborted ? abortError() : error);
4207
+ }
4208
+ );
4209
+ });
4210
+ return abortable.catch((error) => {
4211
+ if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
4212
+ return;
4213
+ }
4214
+ throw error;
4215
+ });
4216
+ };
3114
4217
  const hydrated = sourceResultSettled && !needsPublicationAdmission ? runtime.loadHydratedSubset(
3115
4218
  options,
3116
- (loadOptions) => forwardUpstream(sourceResult, loadOptions)
4219
+ (loadOptions) => forwardUpstream(loadOptions)
3117
4220
  ) : void 0;
3118
4221
  if (hydrated !== void 0) {
3119
- return hydrated;
4222
+ return hydrated === true ? true : raceRetainedLoad(hydrated);
3120
4223
  }
3121
- return (async () => {
4224
+ const work = (async () => {
3122
4225
  await fullStartPromise;
3123
- const resolvedSourceResult = await sourceResultPromise;
4226
+ await sourceResultPromise;
3124
4227
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
3125
4228
  return;
3126
4229
  }
@@ -3137,9 +4240,14 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3137
4240
  }
3138
4241
  return runtime.loadSubset(
3139
4242
  options,
3140
- (loadOptions) => forwardUpstream(resolvedSourceResult, loadOptions)
4243
+ (loadOptions) => forwardUpstream(loadOptions),
4244
+ () => {
4245
+ hydrationStarted = true;
4246
+ },
4247
+ () => !startupState.cleanedUp && acquisitions.get(options) === acquisition
3141
4248
  );
3142
4249
  })();
4250
+ return raceRetainedLoad(work, () => !hydrationStarted);
3143
4251
  },
3144
4252
  unloadSubset: (options) => {
3145
4253
  const acquisition = acquisitions.get(options);
@@ -3147,7 +4255,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3147
4255
  acquisitions.delete(options);
3148
4256
  runtime.unloadSubset(
3149
4257
  options,
3150
- acquisition?.forwarded ? sourceResult.unloadSubset : void 0
4258
+ acquisition?.forwarded ? acquisition.source?.unloadSubset : void 0
3151
4259
  );
3152
4260
  }
3153
4261
  };
@@ -3157,16 +4265,16 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3157
4265
  function createLoopbackSyncConfig(runtime) {
3158
4266
  return {
3159
4267
  sync: (params) => {
3160
- runtime.setSyncControls({
3161
- begin: params.begin,
3162
- write: params.write,
3163
- commit: params.commit,
3164
- truncate: params.truncate,
3165
- metadata: params.metadata ?? null,
3166
- markError: params.markError
3167
- });
3168
4268
  runtime.setCollection(
3169
- params.collection
4269
+ params.collection,
4270
+ {
4271
+ begin: params.begin,
4272
+ write: params.write,
4273
+ commit: params.commit,
4274
+ truncate: params.truncate,
4275
+ metadata: params.metadata ?? null,
4276
+ markError: params.markError
4277
+ }
3170
4278
  );
3171
4279
  void runtime.ensureStarted().then(() => {
3172
4280
  if (runtime.getCurrentTerminalFailure()) return;
@@ -3196,6 +4304,19 @@ function persistedCollectionOptions(options) {
3196
4304
  );
3197
4305
  }
3198
4306
  const persistedReadinessTracker = networkTimeoutMs !== void 0 ? new PersistedReadinessTracker(networkTimeoutMs) : void 0;
4307
+ let claimed = false;
4308
+ let claimedRuntime;
4309
+ const claimOptions = () => {
4310
+ if (claimed) {
4311
+ throw new InvalidPersistedCollectionConfigError(
4312
+ `Persisted options can create only one Collection. Create fresh options for each Collection.`
4313
+ );
4314
+ }
4315
+ claimed = true;
4316
+ return () => {
4317
+ if (!claimedRuntime?.hasBoundCollection()) claimed = false;
4318
+ };
4319
+ };
3199
4320
  if (hasOwnSyncKey(options)) {
3200
4321
  if (!isValidSyncConfig(options.sync)) {
3201
4322
  throw new InvalidSyncConfigError(
@@ -3222,8 +4343,10 @@ function persistedCollectionOptions(options) {
3222
4343
  persistence2,
3223
4344
  syncOptions.syncMode ?? `eager`,
3224
4345
  collectionId2,
3225
- persistedReadinessTracker
4346
+ persistedReadinessTracker,
4347
+ claimOptions
3226
4348
  );
4349
+ claimedRuntime = runtime2;
3227
4350
  const result2 = {
3228
4351
  ...syncOptions,
3229
4352
  id: collectionId2,
@@ -3240,7 +4363,8 @@ function persistedCollectionOptions(options) {
3240
4363
  () => persistedCollectionOptions({
3241
4364
  ...options,
3242
4365
  id: collectionId2
3243
- })
4366
+ }),
4367
+ claimOptions
3244
4368
  );
3245
4369
  }
3246
4370
  const {
@@ -3263,8 +4387,10 @@ function persistedCollectionOptions(options) {
3263
4387
  persistence,
3264
4388
  localOnlyOptions.syncMode ?? `eager`,
3265
4389
  localOnlyOptions.id ?? DEFAULT_DB_NAME,
3266
- persistedReadinessTracker
4390
+ persistedReadinessTracker,
4391
+ claimOptions
3267
4392
  );
4393
+ claimedRuntime = runtime;
3268
4394
  const wrappedOnInsert = async (params) => {
3269
4395
  const lifecycleGeneration = runtime.getLifecycleGeneration();
3270
4396
  const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
@@ -3328,7 +4454,8 @@ function persistedCollectionOptions(options) {
3328
4454
  () => persistedCollectionOptions({
3329
4455
  ...options,
3330
4456
  id: collectionId
3331
- })
4457
+ }),
4458
+ claimOptions
3332
4459
  );
3333
4460
  }
3334
4461
  function encodePersistedStorageKey(key) {
@@ -3391,9 +4518,12 @@ export {
3391
4518
  SingleProcessCoordinator,
3392
4519
  createPersistedTableName,
3393
4520
  decodePersistedStorageKey,
4521
+ eagerStorageTarget,
3394
4522
  encodePersistedStorageKey,
3395
4523
  forwardSQLiteDriverSharedLogicalScheduling,
4524
+ managedStorageTarget,
3396
4525
  persistedCollectionOptions,
4526
+ resolvePersistedStorageTarget,
3397
4527
  validatePersistedCollectionCoordinator
3398
4528
  };
3399
4529
  //# sourceMappingURL=persisted.js.map