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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +32 -3
  2. package/dist/cjs/broadcast-coordinator.cjs +282 -43
  3. package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
  4. package/dist/cjs/broadcast-coordinator.d.cts +25 -8
  5. package/dist/cjs/committed-tx-anchor.cjs +9 -0
  6. package/dist/cjs/committed-tx-anchor.cjs.map +1 -0
  7. package/dist/cjs/committed-tx-anchor.d.cts +1 -0
  8. package/dist/cjs/index.cjs +4 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/persisted.cjs +1599 -313
  11. package/dist/cjs/persisted.cjs.map +1 -1
  12. package/dist/cjs/persisted.d.cts +106 -13
  13. package/dist/cjs/remote-subset-wire.cjs +11 -0
  14. package/dist/cjs/remote-subset-wire.cjs.map +1 -1
  15. package/dist/cjs/remote-subset-wire.d.cts +1 -0
  16. package/dist/cjs/sqlite-core-adapter.cjs +705 -64
  17. package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
  18. package/dist/cjs/sqlite-core-adapter.d.cts +53 -10
  19. package/dist/esm/broadcast-coordinator.d.ts +25 -8
  20. package/dist/esm/broadcast-coordinator.js +283 -44
  21. package/dist/esm/broadcast-coordinator.js.map +1 -1
  22. package/dist/esm/committed-tx-anchor.d.ts +1 -0
  23. package/dist/esm/committed-tx-anchor.js +9 -0
  24. package/dist/esm/committed-tx-anchor.js.map +1 -0
  25. package/dist/esm/index.js +6 -2
  26. package/dist/esm/persisted.d.ts +106 -13
  27. package/dist/esm/persisted.js +1601 -315
  28. package/dist/esm/persisted.js.map +1 -1
  29. package/dist/esm/remote-subset-wire.d.ts +1 -0
  30. package/dist/esm/remote-subset-wire.js +11 -0
  31. package/dist/esm/remote-subset-wire.js.map +1 -1
  32. package/dist/esm/sqlite-core-adapter.d.ts +53 -10
  33. package/dist/esm/sqlite-core-adapter.js +708 -67
  34. package/dist/esm/sqlite-core-adapter.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/broadcast-coordinator.ts +415 -62
  37. package/src/committed-tx-anchor.ts +13 -0
  38. package/src/persisted.ts +2283 -370
  39. package/src/remote-subset-wire.ts +12 -0
  40. package/src/sqlite-core-adapter.ts +891 -68
@@ -1,8 +1,60 @@
1
- import { safeRandomUUID, withCollectionConfigFactory, whenSyncAccepted, SyncTransactionAbortedError, getLoadSubsetDemandKey, compileSingleRowExpression, toBooleanPredicate, SYNC_PERSISTENCE_VERSION, SYNC_PERSISTENCE_PROTOCOL, PERSISTED_READINESS, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError, withAcceptedReceipt } from "@tanstack/db";
2
- import { InvalidPersistedCollectionConfigError, DuplicateRemoteSubsetOwnerError, toPersistedCollectionDurabilityError, InvalidPersistedStorageKeyEncodingError, InvalidPersistedStorageKeyError, InvalidSyncConfigError, InvalidPersistedCollectionCoordinatorError, PersistedCollectionDurabilityError, InvalidPersistenceAdapterError } from "./errors.js";
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
+ 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;
@@ -391,7 +446,7 @@ function isTxCommittedPayload(payload) {
391
446
  return false;
392
447
  }
393
448
  if (payload.requiresFullReload) {
394
- return true;
449
+ return payload.reconciliationReload === void 0 || payload.reconciliationReload === true;
395
450
  }
396
451
  return Array.isArray(payload.changedRows) && Array.isArray(payload.deletedKeys) && (payload.rowMetadataMutations === void 0 || Array.isArray(payload.rowMetadataMutations)) && (payload.collectionMetadataMutations === void 0 || Array.isArray(payload.collectionMetadataMutations));
397
452
  }
@@ -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,15 +511,184 @@ 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;
524
+ this.latestTerm = 0;
525
+ this.latestSeq = 0;
526
+ this.latestRowVersion = 0;
527
+ this.publicRowVersion = 0;
528
+ this.localTerm = 1;
529
+ this.localSeq = 0;
530
+ this.localRowVersion = 0;
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++;
457
657
  this.latestTerm = 0;
458
658
  this.latestSeq = 0;
459
659
  this.latestRowVersion = 0;
460
660
  this.localTerm = 1;
461
661
  this.localSeq = 0;
462
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;
463
692
  }
464
693
  setSyncControls(syncControls) {
465
694
  this.advanceLifecycle();
@@ -471,6 +700,12 @@ class PersistedCollectionRuntime {
471
700
  commit: commit ? (signal) => this.trackAppliedReceipt(commit(signal)) : null
472
701
  };
473
702
  }
703
+ setSourceRestartAfterScopedRecovery(restart) {
704
+ this.restartSourceAfterScopedRecovery = restart ?? null;
705
+ }
706
+ clearSourceRestartAfterScopedRecovery() {
707
+ this.restartSourceAfterScopedRecovery = void 0;
708
+ }
474
709
  reportSyncError(error) {
475
710
  const markError = this.syncControls.markError;
476
711
  if (this.syncErrorReported) return this.reportedSyncError;
@@ -485,8 +720,9 @@ class PersistedCollectionRuntime {
485
720
  }
486
721
  registerRemoteSubsetOwner(owner) {
487
722
  this.remoteSubsetOwnerUnsubscribe?.();
723
+ this.remoteSubsetOwner = owner;
488
724
  this.remoteSubsetOwnerUnsubscribe = this.persistence.coordinator.registerRemoteSubsetOwner(
489
- this.collectionId,
725
+ this.storageCollectionId,
490
726
  owner
491
727
  );
492
728
  }
@@ -511,6 +747,23 @@ class PersistedCollectionRuntime {
511
747
  )
512
748
  );
513
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
+ }
514
767
  clearSyncControls() {
515
768
  this.syncControls = {
516
769
  begin: null,
@@ -569,17 +822,46 @@ class PersistedCollectionRuntime {
569
822
  isApplyingInternally() {
570
823
  return this.internalApplyDepth > 0;
571
824
  }
572
- 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);
573
845
  if (this.collection === collection) {
846
+ this.boundCollection ??= collection;
574
847
  return;
575
848
  }
576
849
  this.collection = collection;
577
- 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;
578
860
  }
579
861
  getLeadershipState() {
580
862
  return {
581
863
  nodeId: this.persistence.coordinator.getNodeId(),
582
- isLeader: this.persistence.coordinator.isLeader(this.collectionId)
864
+ isLeader: this.persistence.coordinator.isLeader(this.storageCollectionId)
583
865
  };
584
866
  }
585
867
  runInHydrationScope(task, adapter = this.persistence.adapter) {
@@ -594,6 +876,7 @@ class PersistedCollectionRuntime {
594
876
  return this.startPromise;
595
877
  }
596
878
  const lifecycleGeneration = this.lifecycleGeneration;
879
+ this.startupCacheGenerationRotated = false;
597
880
  const isRestart = this.hasAttemptedStartup;
598
881
  this.hasAttemptedStartup = true;
599
882
  let resolveStartupMetadata;
@@ -604,63 +887,98 @@ class PersistedCollectionRuntime {
604
887
  });
605
888
  void this.startupMetadataPromise.catch(() => void 0);
606
889
  this.startPromise = (async () => {
890
+ await this.claimCacheGeneration(lifecycleGeneration);
891
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
892
+ resolveStartupMetadata();
893
+ return;
894
+ }
895
+ let metadataExpired = false;
607
896
  const loadStartupMetadata = async (adapter) => {
608
897
  if (lifecycleGeneration !== this.lifecycleGeneration) {
609
898
  resolveStartupMetadata();
610
899
  return false;
611
900
  }
612
901
  try {
613
- await this.loadStartupMetadataInternal(lifecycleGeneration, adapter);
902
+ if (!await this.loadStartupMetadataInternal(
903
+ lifecycleGeneration,
904
+ adapter
905
+ )) {
906
+ metadataExpired = true;
907
+ return false;
908
+ }
614
909
  return lifecycleGeneration === this.lifecycleGeneration;
615
910
  } catch (error) {
911
+ if (this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
912
+ metadataExpired = true;
913
+ return false;
914
+ }
616
915
  rejectStartupMetadata(error);
617
916
  throw error;
618
917
  }
619
918
  };
620
919
  let startup;
621
920
  const scheduleStartupAsOneHydrate = this.persistence.adapter.runInHydrationScope !== void 0 && (!isRestart || (this.persistence.adapter.isHydrationScopeScheduled?.() ?? true));
622
- if (scheduleStartupAsOneHydrate) {
623
- startup = await this.applyMutex.run(async () => {
624
- const result = await this.runInHydrationScope(async (adapter) => {
625
- if (!await loadStartupMetadata(adapter)) return void 0;
626
- return this.startInternal(
627
- lifecycleGeneration,
628
- adapter,
629
- resolveStartupMetadata
630
- );
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;
631
938
  });
632
- if (lifecycleGeneration === this.lifecycleGeneration) {
633
- await this.flushQueuedTxCommittedUnsafe();
634
- }
635
- return result;
636
- });
637
- } else {
638
- if (await loadStartupMetadata(this.persistence.adapter)) {
639
- if (this.persistence.adapter.isHydrationScopeScheduled?.()) {
640
- startup = await this.applyMutex.run(async () => {
641
- const result = await this.runInHydrationScope(
642
- (adapter) => this.startInternal(
643
- lifecycleGeneration,
644
- adapter,
645
- resolveStartupMetadata
646
- )
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
647
960
  );
648
961
  if (lifecycleGeneration === this.lifecycleGeneration) {
649
962
  await this.flushQueuedTxCommittedUnsafe();
650
963
  }
651
- return result;
652
- });
653
- } else {
654
- startup = await this.startInternal(
655
- lifecycleGeneration,
656
- this.persistence.adapter,
657
- resolveStartupMetadata
658
- );
659
- if (lifecycleGeneration === this.lifecycleGeneration) {
660
- await this.flushQueuedTxCommittedUnsafe();
661
964
  }
662
965
  }
663
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();
664
982
  }
665
983
  if (startup !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
666
984
  await this.requestCoordinatorPersistedIndexes(
@@ -696,6 +1014,7 @@ class PersistedCollectionRuntime {
696
1014
  await this.ensureStarted();
697
1015
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
698
1016
  if (this.syncMode !== `on-demand`) return;
1017
+ if (this.scopedRecovery) return;
699
1018
  const appliedCursor = await this.applyMutex.run(async () => {
700
1019
  const result = await this.runInHydrationScope(
701
1020
  (adapter) => this.hydrateBaseline(lifecycleGeneration, adapter)
@@ -705,6 +1024,10 @@ class PersistedCollectionRuntime {
705
1024
  }
706
1025
  return result;
707
1026
  });
1027
+ if (appliedCursor === false) {
1028
+ await this.ensureCacheClaimActive();
1029
+ return;
1030
+ }
708
1031
  if (appliedCursor !== void 0 && lifecycleGeneration === this.lifecycleGeneration) {
709
1032
  await this.waitForAppliedReceiptsAfter(appliedCursor, `accepted`);
710
1033
  }
@@ -721,14 +1044,32 @@ class PersistedCollectionRuntime {
721
1044
  this.resumeCertificationPromise = (async () => {
722
1045
  await this.ensureStarted();
723
1046
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
724
- const snapshot = await this.persistence.adapter.loadResumeSnapshot(
725
- this.collectionId,
726
- {
727
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
728
- includeRows: false
729
- }
730
- );
731
- 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;
732
1073
  this.bindResumeSnapshotEvidence(snapshot);
733
1074
  })();
734
1075
  return this.resumeCertificationPromise;
@@ -736,6 +1077,162 @@ class PersistedCollectionRuntime {
736
1077
  getKeySetEvidence() {
737
1078
  return this.persistedKeySetEvidence;
738
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
+ }
739
1236
  getResumeGenerationOwner() {
740
1237
  return this.resumeGenerationOwner;
741
1238
  }
@@ -744,7 +1241,7 @@ class PersistedCollectionRuntime {
744
1241
  const baseline = {};
745
1242
  this.activeSubsets.set(this.getSubsetKey(baseline), baseline);
746
1243
  const appliedCursor = this.appliedReceiptSequence;
747
- await this.hydrateSubsetUnsafe(
1244
+ const hydrated = await this.hydrateSubsetUnsafe(
748
1245
  baseline,
749
1246
  {
750
1247
  lifecycleGeneration,
@@ -752,6 +1249,7 @@ class PersistedCollectionRuntime {
752
1249
  },
753
1250
  adapter
754
1251
  );
1252
+ if (!hydrated) return false;
755
1253
  return lifecycleGeneration === this.lifecycleGeneration ? appliedCursor : void 0;
756
1254
  }
757
1255
  async ensureStartupMetadataLoaded() {
@@ -771,19 +1269,33 @@ class PersistedCollectionRuntime {
771
1269
  adapter
772
1270
  );
773
1271
  if (lifecycleGeneration !== this.lifecycleGeneration) return void 0;
774
- onStartupMetadataLoaded();
1272
+ if (!this.cacheGenerationClaim || this.syncMode !== `on-demand`) {
1273
+ onStartupMetadataLoaded();
1274
+ }
775
1275
  const appliedCursor = this.syncMode !== `on-demand` ? await this.hydrateBaseline(lifecycleGeneration, adapter) : void 0;
776
1276
  return lifecycleGeneration === this.lifecycleGeneration ? {
777
- appliedCursor,
1277
+ appliedCursor: appliedCursor === false ? void 0 : appliedCursor,
778
1278
  indexBootstrapSnapshot,
779
1279
  completedLocalIndexSignatures
780
1280
  } : void 0;
781
1281
  }
782
1282
  async loadStartupMetadataInternal(lifecycleGeneration, adapter) {
783
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
784
- includeRows: false
785
- });
786
- 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;
787
1299
  this.persistedResumeGeneration = this.getResumeSnapshotGeneration(snapshot);
788
1300
  this.persistedKeySetEvidence = snapshot.keySet;
789
1301
  this.observeStreamPosition(
@@ -795,36 +1307,42 @@ class PersistedCollectionRuntime {
795
1307
  snapshot.collectionMetadata
796
1308
  );
797
1309
  await whenSyncAccepted(applied);
1310
+ return true;
798
1311
  }
799
1312
  async loadCollectionMetadataSnapshot(adapter) {
800
1313
  if (!adapter.loadCollectionMetadata) {
801
1314
  return [];
802
1315
  }
803
- return adapter.loadCollectionMetadata(this.collectionId);
1316
+ return adapter.loadCollectionMetadata(this.storageTargetFor(), {
1317
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1318
+ });
804
1319
  }
805
1320
  replaceCollectionMetadataSnapshot(collectionMetadata) {
806
1321
  if (!this.syncControls.begin || !this.syncControls.commit || !this.syncControls.metadata) {
807
1322
  return true;
808
1323
  }
809
- const nextMetadata = new Map(
810
- collectionMetadata.map(({ key, value }) => [key, value])
811
- );
812
- const currentKeys = this.syncControls.metadata.collection.list().map(({ key }) => key);
813
1324
  return this.withInternalApply(() => {
814
1325
  this.syncControls.begin?.();
815
- currentKeys.forEach((key) => {
816
- if (!nextMetadata.has(key)) {
817
- this.syncControls.metadata?.collection.delete(key);
818
- }
819
- });
820
- nextMetadata.forEach((value, key) => {
821
- this.syncControls.metadata?.collection.set(key, value);
822
- });
1326
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
823
1327
  return this.syncControls.commit?.() ?? true;
824
1328
  });
825
1329
  }
826
- 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) {
827
1342
  this.throwIfTerminal();
1343
+ await this.ensureCacheClaimActive();
1344
+ if (options.signal?.aborted || isDemandRetained && !isDemandRetained())
1345
+ return;
828
1346
  const lifecycleGeneration = this.lifecycleGeneration;
829
1347
  const subsetKey = this.getSubsetKey(options);
830
1348
  const truncateGeneration = this.sourceTruncateGeneration;
@@ -833,25 +1351,52 @@ class PersistedCollectionRuntime {
833
1351
  this.activeSubsets.set(subsetKey, options);
834
1352
  const appliedCursor = this.appliedReceiptSequence;
835
1353
  try {
836
- await this.applyMutex.run(async () => {
837
- try {
838
- await this.runInHydrationScope(
839
- (adapter) => this.hydrateSubsetUnsafe(
840
- options,
841
- {
842
- lifecycleGeneration,
843
- requestLocalLoadFailure: true,
844
- rejectBufferedReplayFailure: true
845
- },
846
- adapter
847
- )
848
- );
849
- } finally {
850
- if (lifecycleGeneration === this.lifecycleGeneration && !this.getCurrentTerminalFailure()) {
851
- 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
+ }
852
1379
  }
853
1380
  }
854
- });
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
+ }
855
1400
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
856
1401
  await this.waitForAppliedReceiptsAfter(appliedCursor);
857
1402
  if (!routeRemoteDemandDuringHydration && this.canRouteRemoteDemandThroughCoordinator()) {
@@ -864,13 +1409,16 @@ class PersistedCollectionRuntime {
864
1409
  throw error;
865
1410
  }
866
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
+ }
867
1415
  return;
868
1416
  }
869
1417
  if (this.canRouteRemoteDemandThroughCoordinator()) {
870
1418
  try {
871
1419
  await this.persistence.coordinator.requestEnsureRemoteSubset(
872
- this.collectionId,
873
- options
1420
+ this.storageCollectionId,
1421
+ this.rotatedAcquisitions.get(options) ?? options
874
1422
  );
875
1423
  } catch (error) {
876
1424
  if (options.signal?.aborted || typeof error === `object` && error !== null && `name` in error && error.name === `AbortError`) {
@@ -880,6 +1428,7 @@ class PersistedCollectionRuntime {
880
1428
  this.queueRemoteSubsetEnsure(options);
881
1429
  throw error;
882
1430
  }
1431
+ await this.applyLeaderCommitsBeforeReply(options);
883
1432
  return;
884
1433
  }
885
1434
  if (truncateGeneration === this.sourceTruncateGeneration && this.activeSubsets.get(subsetKey) === options) {
@@ -897,7 +1446,7 @@ class PersistedCollectionRuntime {
897
1446
  loadHydratedSubset(options, upstreamLoadSubset) {
898
1447
  const failure = this.getCurrentTerminalFailure();
899
1448
  if (failure) return Promise.reject(failure.error);
900
- 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))) {
901
1450
  return;
902
1451
  }
903
1452
  this.activeSubsets.set(this.getSubsetKey(options), options);
@@ -937,13 +1486,15 @@ class PersistedCollectionRuntime {
937
1486
  const subsetKey = this.getSubsetKey(options);
938
1487
  this.activeSubsets.delete(subsetKey);
939
1488
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1489
+ const remoteOptions = this.rotatedAcquisitions.get(options) ?? options;
1490
+ this.rotatedAcquisitions.delete(options);
940
1491
  const demandKey = getLoadSubsetDemandKey(options);
941
1492
  const stillActive = Array.from(this.activeSubsets.values()).some(
942
1493
  (active) => getLoadSubsetDemandKey(active) === demandKey
943
1494
  );
944
1495
  if (!stillActive) this.hydratedDemands.delete(demandKey);
945
1496
  if (this.mode === `sync-present`) {
946
- void this.persistence.coordinator.requestReleaseRemoteSubset(this.collectionId, options).catch((error) => {
1497
+ void this.persistence.coordinator.requestReleaseRemoteSubset(this.storageCollectionId, remoteOptions).catch((error) => {
947
1498
  this.reportSyncError(error);
948
1499
  });
949
1500
  }
@@ -960,9 +1511,10 @@ class PersistedCollectionRuntime {
960
1511
  }
961
1512
  async forceReloadSubset(options) {
962
1513
  this.throwIfTerminal();
1514
+ await this.ensureCacheClaimActive();
963
1515
  const lifecycleGeneration = this.lifecycleGeneration;
964
- await this.applyMutex.run(async () => {
965
- await this.runInHydrationScope(
1516
+ const hydrated = await this.applyMutex.run(async () => {
1517
+ const result = await this.runInHydrationScope(
966
1518
  (adapter) => this.hydrateSubsetUnsafe(
967
1519
  options,
968
1520
  {
@@ -975,7 +1527,9 @@ class PersistedCollectionRuntime {
975
1527
  if (lifecycleGeneration === this.lifecycleGeneration) {
976
1528
  await this.flushQueuedTxCommittedUnsafe();
977
1529
  }
1530
+ return result;
978
1531
  });
1532
+ if (!hydrated) await this.ensureCacheClaimActive();
979
1533
  }
980
1534
  queueHydrationBufferedTransaction(transaction) {
981
1535
  const failure = this.getCurrentTerminalFailure();
@@ -983,11 +1537,13 @@ class PersistedCollectionRuntime {
983
1537
  transaction.rejectApplied?.(failure.error);
984
1538
  return;
985
1539
  }
1540
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
986
1541
  this.queuedHydrationTransactions.push(transaction);
987
1542
  }
988
1543
  applyHydrationBufferedTransaction(transaction) {
989
1544
  const failure = this.getCurrentTerminalFailure();
990
1545
  if (failure) return Promise.reject(failure.error);
1546
+ transaction.admittedStorageCollectionId = this.storageCollectionId;
991
1547
  return this.applyMutex.run(async () => {
992
1548
  await this.applyBufferedSyncTransactionUnsafe(transaction);
993
1549
  });
@@ -1092,7 +1648,10 @@ class PersistedCollectionRuntime {
1092
1648
  for (const options of this.activeSubsets.values()) {
1093
1649
  attempt(() => {
1094
1650
  remoteReleases.push(
1095
- 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) => {
1096
1655
  this.reportSyncError(error);
1097
1656
  })
1098
1657
  );
@@ -1103,6 +1662,7 @@ class PersistedCollectionRuntime {
1103
1662
  this.coordinatorUnsubscribe = null;
1104
1663
  attempt(() => this.remoteSubsetOwnerUnsubscribe?.());
1105
1664
  this.remoteSubsetOwnerUnsubscribe = null;
1665
+ this.remoteSubsetOwner = void 0;
1106
1666
  attempt(() => this.indexAddedUnsubscribe?.());
1107
1667
  this.indexAddedUnsubscribe = null;
1108
1668
  attempt(() => this.indexRemovedUnsubscribe?.());
@@ -1112,6 +1672,16 @@ class PersistedCollectionRuntime {
1112
1672
  this.remoteEnsureRetryTimer = null;
1113
1673
  }
1114
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();
1115
1685
  this.activeSubsets.clear();
1116
1686
  this.hydratedDemands.clear();
1117
1687
  for (const transaction of this.queuedHydrationTransactions) {
@@ -1121,6 +1691,15 @@ class PersistedCollectionRuntime {
1121
1691
  this.queuedTxCommitted.length = 0;
1122
1692
  this.clearSyncControls();
1123
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
+ }
1124
1703
  return Promise.all(remoteReleases).then(() => {
1125
1704
  if (failures.length === 1) throw failures[0];
1126
1705
  if (failures.length > 1) {
@@ -1132,8 +1711,16 @@ class PersistedCollectionRuntime {
1132
1711
  }
1133
1712
  advanceLifecycle() {
1134
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;
1135
1720
  this.persistedReadiness?.set({ status: `loading` });
1136
1721
  this.startupSettled = false;
1722
+ this.publicRowVersion = 0;
1723
+ this.resetTermFence = void 0;
1137
1724
  this.hydratedDemands.clear();
1138
1725
  this.startupMetadataPromise = null;
1139
1726
  this.startPromise = null;
@@ -1159,25 +1746,44 @@ class PersistedCollectionRuntime {
1159
1746
  return this.collection.getIndexMetadata().map((metadata) => metadata.signature);
1160
1747
  }
1161
1748
  loadSubsetRowsUnsafe(options, adapter) {
1162
- return adapter.loadSubset(this.collectionId, options, {
1163
- 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
1164
1753
  });
1165
1754
  }
1166
1755
  async scanPersistedRowsUnsafe(options) {
1167
1756
  if (!this.persistence.adapter.scanRows) {
1168
1757
  return [];
1169
1758
  }
1170
- return this.persistence.adapter.scanRows(
1171
- this.collectionId,
1172
- options
1173
- );
1759
+ return this.persistence.adapter.scanRows(this.storageTargetFor(), options, {
1760
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
1761
+ });
1174
1762
  }
1175
1763
  async scanPersistedRows(options) {
1176
- this.throwIfTerminal();
1177
- 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
+ }
1178
1783
  }
1179
1784
  async hydrateSubsetUnsafe(options, config, adapter) {
1180
1785
  let rowsLoaded = false;
1786
+ let boundRowVersion;
1181
1787
  let replayFailure;
1182
1788
  const resetSequence = this.resetSequence;
1183
1789
  this.hydrationSequence++;
@@ -1188,38 +1794,68 @@ class PersistedCollectionRuntime {
1188
1794
  this.hydratingGeneration = config.lifecycleGeneration;
1189
1795
  try {
1190
1796
  let rows;
1191
- if (config.bindKeySetEvidence) {
1192
- const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
1193
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1194
- includeRows: true
1195
- });
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
+ );
1196
1807
  rows = snapshot.rows;
1197
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1198
- if (resetSequence !== this.resetSequence) return;
1199
- this.bindResumeSnapshotEvidence(snapshot);
1808
+ if (config.lifecycleGeneration !== this.lifecycleGeneration)
1809
+ return true;
1810
+ if (resetSequence !== this.resetSequence) return true;
1811
+ bindResumeSnapshot = () => this.bindResumeSnapshotEvidence(snapshot);
1812
+ boundRowVersion = snapshot.latestRowVersion;
1200
1813
  } else {
1201
1814
  rows = await this.loadSubsetRowsUnsafe(options, adapter);
1202
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;
1203
1818
  rowsLoaded = true;
1204
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1205
- 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?.();
1206
1827
  if (!config.bindKeySetEvidence || this.persistedKeySetEvidence?.status !== `incompatible`) {
1207
1828
  for (const row of rows) {
1208
1829
  hydrationContext.suppliedRowKeys.add(row.key);
1209
1830
  }
1210
- const applied = this.applyRowsToCollection(rows);
1831
+ const applied = this.applyRowsToCollection(rows, validationRows);
1211
1832
  await whenSyncAccepted(applied);
1833
+ if (boundRowVersion !== void 0) {
1834
+ this.publicRowVersion = Math.max(
1835
+ this.publicRowVersion,
1836
+ boundRowVersion
1837
+ );
1838
+ }
1212
1839
  }
1213
1840
  } finally {
1214
1841
  if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === config.lifecycleGeneration) {
1215
1842
  this.hydratingGeneration = null;
1216
1843
  }
1217
1844
  }
1218
- if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1219
- replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter);
1220
- 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;
1221
1850
  } catch (error) {
1851
+ if (config.requestLocalLoadFailure && !rowsLoaded && this.cacheGenerationClaim && this.hasExpiredCacheClaim()) {
1852
+ return false;
1853
+ }
1222
1854
  if (config.requestLocalLoadFailure && !rowsLoaded) {
1855
+ if (this.scopedRecoveryPending) {
1856
+ this.recoverQueuedAfterLocalLoadFailure = true;
1857
+ throw error;
1858
+ }
1223
1859
  this.hydratingGeneration = config.lifecycleGeneration;
1224
1860
  try {
1225
1861
  await this.recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(
@@ -1248,6 +1884,7 @@ class PersistedCollectionRuntime {
1248
1884
  if (replayFailure && config.rejectBufferedReplayFailure) {
1249
1885
  throw replayFailure.reason;
1250
1886
  }
1887
+ return true;
1251
1888
  }
1252
1889
  async recoverBufferedTransactionsAfterLocalLoadFailureUnsafe(lifecycleGeneration, adapter) {
1253
1890
  let snapshotRows;
@@ -1282,18 +1919,15 @@ class PersistedCollectionRuntime {
1282
1919
  const key = operation.key;
1283
1920
  if (operation.type === `update` && transaction.partialUpdateOperationIndexes.has(index) && getPresence(key) === `unknown`) {
1284
1921
  if (snapshotRows === void 0) {
1285
- const snapshot = await adapter.loadResumeSnapshot(
1286
- this.collectionId,
1287
- {
1288
- requiredIndexSignatures: this.getRequiredIndexSignatures(),
1289
- includeRows: true
1290
- }
1291
- );
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
+ });
1292
1927
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1293
1928
  if (transaction.signal?.aborted) break;
1294
- snapshotRows = new Map(
1295
- snapshot.rows.map((row) => [row.key, row])
1296
- );
1929
+ const rows = snapshot && !this.scopedRecovery ? snapshot.rows : [];
1930
+ snapshotRows = new Map(rows.map((row) => [row.key, row]));
1297
1931
  }
1298
1932
  const baseline = snapshotRows.get(key);
1299
1933
  if (baseline === void 0) {
@@ -1343,22 +1977,65 @@ class PersistedCollectionRuntime {
1343
1977
  }
1344
1978
  this.queuedHydrationTransactions.length = 0;
1345
1979
  }
1346
- applyRowsToCollection(rows) {
1980
+ applyRowsToCollection(rows, validationRows, collectionMetadata) {
1347
1981
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit) {
1348
1982
  return true;
1349
1983
  }
1350
1984
  return this.withInternalApply(() => {
1351
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
+ }
1352
2021
  for (const row of rows) {
1353
- if (this.collection?._hasHydratedKey(row.key)) {
2022
+ if (validationRows && this.collection?.base.has(row.key) || this.collection?._hasHydratedKey(row.key)) {
1354
2023
  continue;
1355
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
+ }
1356
2031
  this.syncControls.write?.({
1357
2032
  type: `update`,
1358
2033
  value: row.value,
1359
2034
  metadata: row.metadata
1360
2035
  });
1361
2036
  }
2037
+ if (collectionMetadata)
2038
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1362
2039
  return this.syncControls.commit?.() ?? true;
1363
2040
  });
1364
2041
  }
@@ -1372,7 +2049,9 @@ class PersistedCollectionRuntime {
1372
2049
  }
1373
2050
  isExpectedResumeGeneration(generation) {
1374
2051
  const expected = this.persistedResumeGeneration;
1375
- return expected !== void 0 && expected.latestTerm === generation.latestTerm && expected.latestSeq === generation.latestSeq && expected.latestRowVersion === generation.latestRowVersion && expected.resetEpoch === generation.resetEpoch;
2052
+ return expected !== void 0 && expected.latestRowVersion === generation.latestRowVersion && expected.resetEpoch === generation.resetEpoch && (expected.latestTerm === generation.latestTerm && expected.latestSeq === generation.latestSeq || // A no-write election reserves a new term at sequence zero. It does
2053
+ // not change the persisted rows or the source resume cursor.
2054
+ generation.latestTerm > expected.latestTerm && generation.latestSeq === 0);
1376
2055
  }
1377
2056
  bindResumeSnapshotEvidence(snapshot) {
1378
2057
  const generation = this.getResumeSnapshotGeneration(snapshot);
@@ -1385,35 +2064,69 @@ class PersistedCollectionRuntime {
1385
2064
  );
1386
2065
  this.persistedKeySetEvidence = this.isExpectedResumeGeneration(generation) || mayAcceptUnownedGeneration ? snapshot.keySet : { status: `incompatible` };
1387
2066
  }
1388
- replaceCollectionSnapshot(rows, collectionMetadata) {
2067
+ replaceCollectionSnapshot(rows, collectionMetadata, replayDemand) {
1389
2068
  if (!this.syncControls.begin || !this.syncControls.write || !this.syncControls.commit || !this.syncControls.metadata) {
1390
2069
  return true;
1391
2070
  }
1392
- const nextMetadata = new Map(
1393
- 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
+ )
1394
2076
  );
1395
- 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);
1396
2080
  return this.withInternalApply(() => {
1397
2081
  this.syncControls.begin?.();
1398
- 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
+ }
1399
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
+ }
1400
2099
  this.syncControls.write?.({
1401
2100
  type: `update`,
1402
2101
  value: row.value,
1403
2102
  metadata: row.metadata
1404
2103
  });
1405
2104
  }
1406
- currentKeys.forEach((key) => {
1407
- if (!nextMetadata.has(key)) {
1408
- this.syncControls.metadata?.collection.delete(key);
1409
- }
1410
- });
1411
- nextMetadata.forEach((value, key) => {
1412
- this.syncControls.metadata?.collection.set(key, value);
1413
- });
2105
+ this.applyCollectionMetadataSnapshot(collectionMetadata);
1414
2106
  return this.syncControls.commit?.() ?? true;
1415
2107
  });
1416
2108
  }
2109
+ matchesCollectionSnapshot(rows, collectionMetadata) {
2110
+ if (!this.collection || !this.syncControls.metadata) return false;
2111
+ const base = this.collection.base;
2112
+ if (base.size !== rows.size) return false;
2113
+ for (const [key, row] of rows) {
2114
+ if (!base.has(key) || !equalPersistedSnapshotValues(base.get(key), row.value) || !equalPersistedSnapshotValues(
2115
+ this.syncControls.metadata.row.get(key),
2116
+ row.metadata
2117
+ )) {
2118
+ return false;
2119
+ }
2120
+ }
2121
+ const currentMetadata = this.syncControls.metadata.collection.list();
2122
+ if (currentMetadata.length !== collectionMetadata.length) return false;
2123
+ const expectedMetadata = new Map(
2124
+ collectionMetadata.map(({ key, value }) => [key, value])
2125
+ );
2126
+ return currentMetadata.every(
2127
+ ({ key, value }) => expectedMetadata.has(key) && equalPersistedSnapshotValues(expectedMetadata.get(key), value)
2128
+ );
2129
+ }
1417
2130
  async flushQueuedHydrationTransactionsUnsafe(adapter) {
1418
2131
  let operationFailure;
1419
2132
  while (this.queuedHydrationTransactions.length > 0) {
@@ -1441,6 +2154,31 @@ class PersistedCollectionRuntime {
1441
2154
  async applyBufferedSyncTransactionUnsafe(transaction, scopedAdapter) {
1442
2155
  this.throwIfTerminal();
1443
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
+ }
1444
2182
  const abortedBeforeApplication = transaction.signal?.aborted === true;
1445
2183
  let abortedDuringApplication = false;
1446
2184
  let applicationReturned = false;
@@ -1498,16 +2236,56 @@ class PersistedCollectionRuntime {
1498
2236
  if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
1499
2237
  return;
1500
2238
  }
2239
+ const anchor = this.persistedResumeGeneration && {
2240
+ latestRowVersion: this.latestRowVersion,
2241
+ resetEpoch: this.persistedResumeGeneration.resetEpoch
2242
+ };
1501
2243
  const streamPosition = this.nextLocalStreamPosition();
1502
2244
  const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
1503
- const response = await this.persistence.coordinator.requestApplyCommittedTx(
1504
- this.collectionId,
1505
- tx,
1506
- // A hydration-scoped adapter is already inside the shared scheduler.
1507
- // Ordinary source commits pass no scoped adapter so the coordinator
1508
- // enters that scheduler before taking the database-wide writer lock.
1509
- scopedAdapter
1510
- );
2245
+ const storageCollectionId = this.storageCollectionId;
2246
+ let response;
2247
+ try {
2248
+ response = await this.persistence.coordinator.requestApplyCommittedTx(
2249
+ storageCollectionId,
2250
+ tx,
2251
+ // A hydration-scoped adapter is already inside the shared scheduler.
2252
+ // Ordinary source commits enter it before taking the writer lock.
2253
+ scopedAdapter
2254
+ );
2255
+ } catch (error) {
2256
+ if (!(error instanceof IndeterminateCommitError) || !anchor || !this.persistence.coordinator.reconcileCommittedTx) {
2257
+ throw error;
2258
+ }
2259
+ let reconciled;
2260
+ try {
2261
+ reconciled = await this.persistence.coordinator.reconcileCommittedTx(
2262
+ storageCollectionId,
2263
+ tx,
2264
+ anchor,
2265
+ scopedAdapter
2266
+ );
2267
+ } catch (reconcileError) {
2268
+ if (reconcileError instanceof PersistedCollectionDurabilityError) {
2269
+ throw reconcileError;
2270
+ }
2271
+ throw error;
2272
+ }
2273
+ if (!reconciled.ok) {
2274
+ if (reconciled.code === `PERSISTENCE_ERROR`) {
2275
+ throw new PersistedCollectionDurabilityError(
2276
+ `Failed to reconcile collection "${this.collectionId}": ${reconciled.error}`,
2277
+ {
2278
+ cause: reconciled,
2279
+ code: reconciled.sourceCode ?? reconciled.code,
2280
+ path: reconciled.path
2281
+ }
2282
+ );
2283
+ }
2284
+ throw error;
2285
+ }
2286
+ this.recordDurableSourceCommit(transaction, reconciled.committed);
2287
+ return;
2288
+ }
1511
2289
  if (!response.ok) {
1512
2290
  if (response.code === `PERSISTENCE_ERROR`) {
1513
2291
  const error = new PersistedCollectionDurabilityError(
@@ -1524,18 +2302,24 @@ class PersistedCollectionRuntime {
1524
2302
  `failed to apply external sync transaction through coordinator: ${response.error}`
1525
2303
  );
1526
2304
  }
2305
+ this.recordDurableSourceCommit(transaction, {
2306
+ term: response.term,
2307
+ seq: response.seq,
2308
+ rowVersion: response.latestRowVersion
2309
+ });
2310
+ }
2311
+ recordDurableSourceCommit(transaction, position) {
1527
2312
  if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return;
1528
- this.observeStreamPosition(
1529
- response.term,
1530
- response.seq,
1531
- response.latestRowVersion
1532
- );
2313
+ this.observeStreamPosition(position.term, position.seq, position.rowVersion);
2314
+ if (position.rowVersion === this.publicRowVersion + 1) {
2315
+ this.publicRowVersion = position.rowVersion;
2316
+ }
1533
2317
  if (transaction.expectedResumeGenerationOwner === this.resumeGenerationOwner && this.persistedResumeGeneration !== void 0) {
1534
2318
  this.persistedResumeGeneration = {
1535
2319
  ...this.persistedResumeGeneration,
1536
- latestTerm: response.term,
1537
- latestSeq: response.seq,
1538
- latestRowVersion: response.latestRowVersion
2320
+ latestTerm: position.term,
2321
+ latestSeq: position.seq,
2322
+ latestRowVersion: position.rowVersion
1539
2323
  };
1540
2324
  }
1541
2325
  }
@@ -1543,7 +2327,10 @@ class PersistedCollectionRuntime {
1543
2327
  this.throwIfTerminal();
1544
2328
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1545
2329
  try {
1546
- await this.persistence.adapter.applyCommittedTx(this.collectionId, tx);
2330
+ await this.persistence.adapter.applyCommittedTx(
2331
+ this.storageTargetFor(),
2332
+ tx
2333
+ );
1547
2334
  } catch (error) {
1548
2335
  throw this.markTerminalFailure(
1549
2336
  toPersistedCollectionDurabilityError(this.collectionId, error),
@@ -1558,6 +2345,7 @@ class PersistedCollectionRuntime {
1558
2345
  term: streamPosition.term,
1559
2346
  seq: streamPosition.seq,
1560
2347
  rowVersion: streamPosition.rowVersion,
2348
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1561
2349
  truncate: transaction.truncate,
1562
2350
  mutations: transaction.operations.map(
1563
2351
  (operation) => operation.type === `update` ? {
@@ -1592,6 +2380,7 @@ class PersistedCollectionRuntime {
1592
2380
  term: streamPosition.term,
1593
2381
  seq: streamPosition.seq,
1594
2382
  rowVersion: streamPosition.rowVersion,
2383
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId,
1595
2384
  mutations: mutations.map((mutation) => {
1596
2385
  if (mutation.type === `delete`) {
1597
2386
  return {
@@ -1651,14 +2440,16 @@ class PersistedCollectionRuntime {
1651
2440
  async persistCollectionMutationsUnsafe(mutations, lifecycleGeneration) {
1652
2441
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1653
2442
  if (this.persistence.coordinator.requestApplyLocalMutations) {
2443
+ const target = this.storageTargetFor();
1654
2444
  const envelopeMutations = mutations.map(
1655
2445
  (mutation) => toPersistedMutationEnvelope(
1656
2446
  mutation
1657
2447
  )
1658
2448
  );
1659
2449
  const response = await this.persistence.coordinator.requestApplyLocalMutations(
1660
- this.collectionId,
1661
- envelopeMutations
2450
+ this.storageCollectionId,
2451
+ envelopeMutations,
2452
+ target.kind === `managed` ? target.claimId : void 0
1662
2453
  );
1663
2454
  this.throwIfLifecycleReplaced(lifecycleGeneration);
1664
2455
  if (!response.ok) {
@@ -1755,12 +2546,12 @@ class PersistedCollectionRuntime {
1755
2546
  const envelope = {
1756
2547
  v: 1,
1757
2548
  dbName: this.dbName,
1758
- collectionId: this.collectionId,
2549
+ collectionId: this.storageCollectionId,
1759
2550
  senderId: this.persistence.coordinator.getNodeId(),
1760
2551
  ts: Date.now(),
1761
2552
  payload: txCommitted
1762
2553
  };
1763
- this.persistence.coordinator.publish(this.collectionId, envelope);
2554
+ this.persistence.coordinator.publish(this.storageCollectionId, envelope);
1764
2555
  }
1765
2556
  observeStreamPosition(term, seq, rowVersion) {
1766
2557
  if (term > this.latestTerm || term === this.latestTerm && seq > this.latestSeq) {
@@ -1802,7 +2593,7 @@ class PersistedCollectionRuntime {
1802
2593
  if (this.getCurrentTerminalFailure() || this.mode !== `sync-present` || this.persistence.coordinator instanceof SingleProcessCoordinator) {
1803
2594
  return false;
1804
2595
  }
1805
- return !this.persistence.coordinator.isLeader(this.collectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
2596
+ return !this.persistence.coordinator.isLeader(this.storageCollectionId) || this.remoteSubsetOwnerUnsubscribe !== null;
1806
2597
  }
1807
2598
  queueRemoteSubsetEnsure(options) {
1808
2599
  if (options.signal?.aborted || !this.canRouteRemoteDemandThroughCoordinator() || this.activeSubsets.get(this.getSubsetKey(options)) !== options) {
@@ -1841,8 +2632,8 @@ class PersistedCollectionRuntime {
1841
2632
  }
1842
2633
  try {
1843
2634
  await this.persistence.coordinator.requestEnsureRemoteSubset(
1844
- this.collectionId,
1845
- options
2635
+ this.storageCollectionId,
2636
+ this.rotatedAcquisitions.get(options) ?? options
1846
2637
  );
1847
2638
  this.pendingRemoteSubsetEnsures.delete(subsetKey);
1848
2639
  } catch (error) {
@@ -1860,18 +2651,19 @@ class PersistedCollectionRuntime {
1860
2651
  return;
1861
2652
  }
1862
2653
  this.persistence.coordinator.setAdapterForCollection?.(
1863
- this.collectionId,
1864
- this.persistence.adapter
2654
+ this.storageCollectionId,
2655
+ this.persistence.adapter,
2656
+ this.cacheGenerationClaim?.claimId
1865
2657
  );
1866
2658
  this.coordinatorUnsubscribe = this.persistence.coordinator.subscribe(
1867
- this.collectionId,
2659
+ this.storageCollectionId,
1868
2660
  (message) => {
1869
2661
  this.onCoordinatorMessage(message);
1870
2662
  }
1871
2663
  );
1872
2664
  }
1873
2665
  onCoordinatorMessage(message) {
1874
- if (message.collectionId !== this.collectionId) {
2666
+ if (message.collectionId !== this.storageCollectionId) {
1875
2667
  return;
1876
2668
  }
1877
2669
  if (this.getCurrentTerminalFailure()) {
@@ -1880,17 +2672,24 @@ class PersistedCollectionRuntime {
1880
2672
  const { payload } = message;
1881
2673
  const lifecycleGeneration = this.lifecycleGeneration;
1882
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
+ }
1883
2680
  if (isTxCommittedPayload(payload)) {
1884
- if (this.isHydratingNow()) {
1885
- this.queuedTxCommitted.push({
1886
- txCommitted: payload,
1887
- 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);
1888
2691
  });
1889
- return;
1890
2692
  }
1891
- void this.applyMutex.run(() => this.processCommittedTxUnsafe(payload, lifecycleGeneration)).catch((error) => {
1892
- this.markTerminalFailure(error, lifecycleGeneration);
1893
- });
1894
2693
  return;
1895
2694
  }
1896
2695
  if (isSelf) {
@@ -1898,9 +2697,18 @@ class PersistedCollectionRuntime {
1898
2697
  }
1899
2698
  if (isCollectionResetPayload(payload)) {
1900
2699
  void this.applyMutex.run(async () => {
1901
- await this.runInHydrationScope(
1902
- (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
1903
- );
2700
+ if (message.collectionId !== this.storageCollectionId) return;
2701
+ await this.runInHydrationScope(async (adapter) => {
2702
+ await this.truncateAndReloadUnsafe(adapter, lifecycleGeneration);
2703
+ await this.rebaseDurableStreamFromSnapshotUnsafe(
2704
+ adapter,
2705
+ lifecycleGeneration
2706
+ );
2707
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2708
+ this.publicRowVersion = 0;
2709
+ this.resetTermFence = this.latestTerm;
2710
+ }
2711
+ });
1904
2712
  if (lifecycleGeneration === this.lifecycleGeneration) {
1905
2713
  await this.flushQueuedTxCommittedUnsafe();
1906
2714
  }
@@ -1911,33 +2719,63 @@ class PersistedCollectionRuntime {
1911
2719
  }
1912
2720
  async flushQueuedTxCommittedUnsafe() {
1913
2721
  while (this.queuedTxCommitted.length > 0) {
2722
+ if (this.hasExpiredCacheClaim()) {
2723
+ this.scheduleCacheClaimRecovery(this.lifecycleGeneration);
2724
+ return;
2725
+ }
1914
2726
  const queued = this.queuedTxCommitted.shift();
1915
2727
  if (!queued) {
1916
2728
  continue;
1917
2729
  }
1918
- await this.processCommittedTxUnsafe(
1919
- queued.txCommitted,
1920
- queued.lifecycleGeneration
1921
- );
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
+ }
1922
2739
  }
1923
2740
  }
1924
2741
  async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
1925
2742
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1926
- if (txCommitted.term < this.latestTerm) {
1927
- return;
2743
+ if (this.resetTermFence !== void 0) {
2744
+ if (txCommitted.term <= this.resetTermFence) {
2745
+ await this.runInHydrationScope(
2746
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2747
+ );
2748
+ return;
2749
+ }
1928
2750
  }
1929
- if (txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
2751
+ if (txCommitted.requiresFullReload && txCommitted.reconciliationReload) {
2752
+ await this.invalidateFromCommittedTxUnsafe(
2753
+ txCommitted,
2754
+ this.persistence.adapter
2755
+ );
2756
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2757
+ this.observeStreamPosition(
2758
+ txCommitted.term,
2759
+ txCommitted.seq,
2760
+ txCommitted.latestRowVersion
2761
+ );
2762
+ this.publicRowVersion = Math.max(
2763
+ this.publicRowVersion,
2764
+ txCommitted.latestRowVersion
2765
+ );
2766
+ await this.flushQueuedTxCommittedUnsafe();
2767
+ }
1930
2768
  return;
1931
2769
  }
2770
+ if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
2771
+ const observedButUnpublished = txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq;
1932
2772
  const hasGapInCurrentTerm = txCommitted.term === this.latestTerm && txCommitted.seq > this.latestSeq + 1;
1933
2773
  const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
1934
- const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
2774
+ const hasGap = observedButUnpublished || txCommitted.latestRowVersion > this.publicRowVersion + 1 || hasGapInCurrentTerm || hasGapAcrossTerms;
1935
2775
  if (hasGap) {
1936
- await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
2776
+ if (!await this.recoverFromSeqGapUnsafe(lifecycleGeneration)) return;
1937
2777
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1938
- if (txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
1939
- return;
1940
- }
2778
+ if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
1941
2779
  }
1942
2780
  this.observeStreamPosition(
1943
2781
  txCommitted.term,
@@ -1949,23 +2787,49 @@ class PersistedCollectionRuntime {
1949
2787
  this.persistence.adapter
1950
2788
  );
1951
2789
  if (lifecycleGeneration === this.lifecycleGeneration) {
2790
+ this.publicRowVersion = Math.max(
2791
+ this.publicRowVersion,
2792
+ txCommitted.latestRowVersion
2793
+ );
1952
2794
  await this.flushQueuedTxCommittedUnsafe();
1953
2795
  }
1954
2796
  }
1955
- async recoverFromSeqGapUnsafe(lifecycleGeneration) {
2797
+ async rebaseDurableStreamFromSnapshotUnsafe(adapter, lifecycleGeneration) {
2798
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2799
+ const snapshot = await adapter.loadResumeSnapshot(this.storageTargetFor(), {
2800
+ includeRows: false,
2801
+ cacheGenerationClaimId: this.cacheGenerationClaim?.claimId
2802
+ });
1956
2803
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2804
+ this.latestRowVersion = snapshot.latestRowVersion;
2805
+ this.localRowVersion = snapshot.latestRowVersion;
2806
+ this.observeStreamPosition(
2807
+ snapshot.latestTerm,
2808
+ snapshot.latestSeq,
2809
+ snapshot.latestRowVersion
2810
+ );
2811
+ }
2812
+ async recoverFromSeqGapUnsafe(lifecycleGeneration) {
2813
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1957
2814
  if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
1958
2815
  let pullResponse;
1959
2816
  try {
1960
2817
  pullResponse = await this.persistence.coordinator.pullSince(
1961
- this.collectionId,
1962
- this.latestRowVersion
2818
+ this.storageCollectionId,
2819
+ this.publicRowVersion,
2820
+ void 0,
2821
+ this.cacheGenerationClaim?.claimId
1963
2822
  );
1964
2823
  } catch (error) {
1965
2824
  console.warn(`Failed pullSince recovery attempt:`, error);
1966
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
+ }
1967
2831
  if (pullResponse) {
1968
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2832
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
1969
2833
  if (pullResponse.ok) {
1970
2834
  this.observeStreamPosition(
1971
2835
  pullResponse.latestTerm,
@@ -1976,14 +2840,22 @@ class PersistedCollectionRuntime {
1976
2840
  await this.runInHydrationScope(
1977
2841
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1978
2842
  );
1979
- return;
2843
+ this.publicRowVersion = Math.max(
2844
+ this.publicRowVersion,
2845
+ pullResponse.latestRowVersion
2846
+ );
2847
+ return true;
1980
2848
  }
1981
2849
  const deltas = pullResponse.deltas;
1982
2850
  if (!deltas) {
1983
2851
  await this.runInHydrationScope(
1984
2852
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1985
2853
  );
1986
- return;
2854
+ this.publicRowVersion = Math.max(
2855
+ this.publicRowVersion,
2856
+ pullResponse.latestRowVersion
2857
+ );
2858
+ return true;
1987
2859
  }
1988
2860
  await this.runInHydrationScope(async (adapter) => {
1989
2861
  for (const delta of deltas) {
@@ -2006,25 +2878,69 @@ class PersistedCollectionRuntime {
2006
2878
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2007
2879
  }
2008
2880
  });
2009
- return;
2881
+ this.publicRowVersion = Math.max(
2882
+ this.publicRowVersion,
2883
+ pullResponse.latestRowVersion
2884
+ );
2885
+ return true;
2010
2886
  }
2011
2887
  }
2012
2888
  }
2013
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2889
+ if (lifecycleGeneration !== this.lifecycleGeneration) return false;
2014
2890
  await this.runInHydrationScope(
2015
- (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
2891
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2892
+ );
2893
+ this.publicRowVersion = Math.max(
2894
+ this.publicRowVersion,
2895
+ this.latestRowVersion
2016
2896
  );
2017
2897
  if (this.mode === `sync-present`) {
2018
2898
  for (const options of this.activeSubsets.values()) {
2019
2899
  this.queueRemoteSubsetEnsure(options);
2020
2900
  }
2021
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
+ }
2022
2934
  }
2023
2935
  async truncateAndReloadUnsafe(adapter, lifecycleGeneration = this.lifecycleGeneration) {
2024
2936
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2937
+ if (this.scopedRecovery) {
2938
+ this.refreshScopedSubsets();
2939
+ return;
2940
+ }
2025
2941
  this.hydratedDemands.clear();
2026
2942
  this.resetSequence++;
2027
- if (this.syncControls.begin && this.syncControls.commit) {
2943
+ if (this.syncMode === `eager` && this.syncControls.begin && this.syncControls.commit) {
2028
2944
  const applied = this.withInternalApply(() => {
2029
2945
  this.syncControls.begin?.();
2030
2946
  this.syncControls.truncate?.();
@@ -2033,15 +2949,24 @@ class PersistedCollectionRuntime {
2033
2949
  await whenSyncAccepted(applied);
2034
2950
  }
2035
2951
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2036
- await this.reloadActiveSubsetsUnsafe(adapter);
2952
+ await this.reloadActiveSubsetsUnsafe(
2953
+ adapter,
2954
+ false,
2955
+ this.syncMode === `eager`,
2956
+ true
2957
+ );
2037
2958
  }
2038
2959
  async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
2039
- const reloadActiveSubsets = () => this.runInHydrationScope(
2040
- (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
2960
+ if (this.scopedRecovery) {
2961
+ this.refreshScopedSubsets();
2962
+ return;
2963
+ }
2964
+ const reloadActiveSubsets = (skipUnchangedReplace = false) => this.runInHydrationScope(
2965
+ (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter, skipUnchangedReplace),
2041
2966
  adapter
2042
2967
  );
2043
2968
  if (txCommitted.requiresFullReload) {
2044
- await reloadActiveSubsets();
2969
+ await reloadActiveSubsets(txCommitted.reconciliationReload === true);
2045
2970
  return;
2046
2971
  }
2047
2972
  const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
@@ -2052,6 +2977,21 @@ class PersistedCollectionRuntime {
2052
2977
  const hasPaginatedSubset = Array.from(this.activeSubsets.values()).some(
2053
2978
  (opt) => opt.limit != null || opt.offset != null || opt.cursor != null
2054
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
+ }
2055
2995
  if (!hasPaginatedSubset || changedKeyCount === 0) {
2056
2996
  await this.applyTargetedInvalidationUnsafe(txCommitted);
2057
2997
  return;
@@ -2068,20 +3008,33 @@ class PersistedCollectionRuntime {
2068
3008
  key: changedKey,
2069
3009
  value: newValue
2070
3010
  } of txCommitted.changedRows) {
2071
- const matchesAnySubset = subsetEvaluators.some((evaluator) => {
2072
- if (!evaluator) return true;
2073
- return toBooleanPredicate(evaluator(newValue));
2074
- });
2075
- if (matchesAnySubset) {
2076
- this.syncControls.write?.({ type: `update`, value: newValue });
2077
- } else if (this.collection?.get(changedKey) !== void 0) {
2078
- 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 });
2079
3028
  }
2080
3029
  }
2081
3030
  for (const deletedKey of txCommitted.deletedKeys) {
2082
- 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
+ }
2083
3035
  }
2084
3036
  txCommitted.rowMetadataMutations?.forEach((mutation) => {
3037
+ if (this.collection?._hasHydratedKey(mutation.key)) return;
2085
3038
  if (mutation.type === `delete`) {
2086
3039
  this.syncControls.metadata?.row.delete(mutation.key);
2087
3040
  } else {
@@ -2105,10 +3058,25 @@ class PersistedCollectionRuntime {
2105
3058
  });
2106
3059
  await whenSyncAccepted(applied);
2107
3060
  }
2108
- async reloadActiveSubsetsUnsafe(adapter) {
3061
+ async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false, restoreResetBaseline = false, replayDemand = false) {
3062
+ if (this.scopedRecovery) {
3063
+ this.refreshScopedSubsets();
3064
+ return;
3065
+ }
2109
3066
  const lifecycleGeneration = this.lifecycleGeneration;
2110
3067
  const truncateGeneration = this.sourceTruncateGeneration;
2111
- 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);
2112
3080
  this.hydratedDemands.clear();
2113
3081
  this.hydrationSequence++;
2114
3082
  const hydrationContext = { suppliedRowKeys: /* @__PURE__ */ new Set() };
@@ -2116,30 +3084,91 @@ class PersistedCollectionRuntime {
2116
3084
  this.hydratingGeneration = lifecycleGeneration;
2117
3085
  try {
2118
3086
  const mergedRows = /* @__PURE__ */ new Map();
2119
- const collectionMetadata = await this.loadCollectionMetadataSnapshot(adapter);
2120
- if (lifecycleGeneration !== this.lifecycleGeneration) return;
2121
- for (const options of activeSubsetOptions) {
2122
- 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);
2123
3096
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2124
- for (const row of subsetRows) {
2125
- mergedRows.set(row.key, {
2126
- value: row.value,
2127
- metadata: row.metadata
2128
- });
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;
2129
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;
2130
3134
  }
3135
+ if (this.isScopedRecoveryPending() || lifecycleGeneration !== this.lifecycleGeneration)
3136
+ return;
2131
3137
  for (const key of mergedRows.keys()) {
2132
3138
  hydrationContext.suppliedRowKeys.add(key);
2133
3139
  }
2134
- const applied = this.replaceCollectionSnapshot(
2135
- Array.from(mergedRows.entries()).map(([key, row]) => ({
2136
- key,
2137
- value: row.value,
2138
- metadata: row.metadata
2139
- })),
2140
- collectionMetadata
2141
- );
2142
- await whenSyncAccepted(applied);
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)) {
3149
+ const orphanKeys = this.collection?._syncedRowMetadataKeysOutsideBase() ?? [];
3150
+ if (orphanKeys.length > 0) {
3151
+ const applied = this.withInternalApply(() => {
3152
+ this.syncControls.begin?.();
3153
+ for (const key of orphanKeys) {
3154
+ this.syncControls.metadata?.row.delete(key);
3155
+ }
3156
+ return this.syncControls.commit?.() ?? true;
3157
+ });
3158
+ await whenSyncAccepted(applied);
3159
+ }
3160
+ } else {
3161
+ const applied = this.replaceCollectionSnapshot(
3162
+ Array.from(mergedRows.entries()).map(([key, row]) => ({
3163
+ key,
3164
+ value: row.value,
3165
+ metadata: row.metadata
3166
+ })),
3167
+ collectionMetadata,
3168
+ replayDemand
3169
+ );
3170
+ await whenSyncAccepted(applied);
3171
+ }
2143
3172
  } finally {
2144
3173
  if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === lifecycleGeneration) {
2145
3174
  this.hydratingGeneration = null;
@@ -2164,7 +3193,12 @@ class PersistedCollectionRuntime {
2164
3193
  return;
2165
3194
  }
2166
3195
  this.indexAddedUnsubscribe = this.collection.on(`index:added`, (event) => {
2167
- 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
+ });
2168
3202
  });
2169
3203
  this.indexRemovedUnsubscribe = this.collection.on(
2170
3204
  `index:removed`,
@@ -2180,20 +3214,69 @@ class PersistedCollectionRuntime {
2180
3214
  }
2181
3215
  const indexMetadata = indexMetadataSnapshot ?? collection?.getIndexMetadata() ?? [];
2182
3216
  const completedLocalIndexSignatures = /* @__PURE__ */ new Set();
3217
+ const lifecycleGeneration = this.lifecycleGeneration;
2183
3218
  for (const metadata of indexMetadata) {
2184
- 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
+ )) {
2185
3230
  completedLocalIndexSignatures.add(metadata.signature);
2186
3231
  }
2187
3232
  }
2188
3233
  return completedLocalIndexSignatures;
2189
3234
  }
2190
3235
  async requestCoordinatorPersistedIndexes(indexMetadata, completedLocalIndexSignatures) {
3236
+ const lifecycleGeneration = this.lifecycleGeneration;
2191
3237
  for (const metadata of indexMetadata) {
3238
+ if (!this.hasCurrentIndex(metadata)) continue;
3239
+ const storageCollectionId = this.storageCollectionId;
3240
+ const claimId = this.cacheGenerationClaim?.claimId;
2192
3241
  await this.requestCoordinatorPersistedIndex(
2193
3242
  metadata,
2194
3243
  completedLocalIndexSignatures.has(metadata.signature)
2195
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;
2196
3278
  }
3279
+ return false;
2197
3280
  }
2198
3281
  buildPersistedIndexSpec(index) {
2199
3282
  return {
@@ -2206,19 +3289,36 @@ class PersistedCollectionRuntime {
2206
3289
  };
2207
3290
  }
2208
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;
2209
3297
  const completedLocally = await this.ensureLocalPersistedIndex(
2210
3298
  indexMetadata,
2211
3299
  adapter
2212
3300
  );
3301
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
3302
+ if (!this.hasCurrentIndex(indexMetadata)) {
3303
+ await this.markIndexRemoved(indexMetadata);
3304
+ return;
3305
+ }
2213
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;
2214
3313
  }
2215
3314
  async ensureLocalPersistedIndex(indexMetadata, adapter) {
2216
3315
  try {
2217
3316
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2218
3317
  await adapter.ensureIndex(
2219
- this.collectionId,
3318
+ this.storageTargetFor(),
2220
3319
  indexMetadata.signature,
2221
- spec
3320
+ spec,
3321
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2222
3322
  );
2223
3323
  return true;
2224
3324
  } catch (error) {
@@ -2230,11 +3330,12 @@ class PersistedCollectionRuntime {
2230
3330
  try {
2231
3331
  const spec = this.buildPersistedIndexSpec(indexMetadata);
2232
3332
  await this.persistence.coordinator.requestEnsurePersistedIndex(
2233
- this.collectionId,
3333
+ this.storageCollectionId,
2234
3334
  indexMetadata.signature,
2235
3335
  spec,
2236
3336
  completedLocally ? this.persistence.adapter : void 0,
2237
- completedLocally
3337
+ completedLocally,
3338
+ this.cacheGenerationClaim?.claimId
2238
3339
  );
2239
3340
  } catch (error) {
2240
3341
  console.warn(
@@ -2248,10 +3349,19 @@ class PersistedCollectionRuntime {
2248
3349
  return;
2249
3350
  }
2250
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;
2251
3357
  await this.persistence.adapter.markIndexRemoved(
2252
- this.collectionId,
2253
- indexMetadata.signature
3358
+ this.storageTargetFor(),
3359
+ indexMetadata.signature,
3360
+ { cacheGenerationClaimId: this.cacheGenerationClaim?.claimId }
2254
3361
  );
3362
+ if (lifecycleGeneration === this.lifecycleGeneration && this.hasCurrentIndex(indexMetadata)) {
3363
+ await this.ensurePersistedIndex(indexMetadata);
3364
+ }
2255
3365
  } catch (error) {
2256
3366
  console.warn(`Failed to mark persisted index removed:`, error);
2257
3367
  }
@@ -2454,20 +3564,26 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2454
3564
  throw error;
2455
3565
  }
2456
3566
  };
2457
- runtime.setSyncControls({
2458
- begin: params.begin,
2459
- write: params.write,
2460
- commit: params.commit,
2461
- truncate: params.truncate,
2462
- metadata: params.metadata ?? null,
2463
- markError: params.markError
2464
- });
2465
3567
  runtime.setCollection(
2466
- 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
+ }
2467
3577
  );
2468
3578
  const persistenceCapability = {
2469
3579
  protocol: SYNC_PERSISTENCE_PROTOCOL,
2470
3580
  version: SYNC_PERSISTENCE_VERSION,
3581
+ get managedCacheGeneration() {
3582
+ return runtime.hasManagedCacheGeneration();
3583
+ },
3584
+ get startupCacheGenerationRotated() {
3585
+ return runtime.hasRotatedStartupCacheGeneration();
3586
+ },
2471
3587
  reserveCommitTurn: () => {
2472
3588
  const openTransaction = getOpenTransaction();
2473
3589
  if (openTransaction && !openTransaction.internal)
@@ -2481,6 +3597,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2481
3597
  throw runtime.reportSyncError(error);
2482
3598
  }
2483
3599
  },
3600
+ startScopedRecovery: (resetMetadata) => startupState.cleanedUp ? Promise.resolve() : runtime.startScopedRecovery(resetMetadata),
2484
3601
  scanPersistedRows: (options) => startupState.cleanedUp ? Promise.resolve([]) : runtime.scanPersistedRows(options),
2485
3602
  resumeSnapshot: {
2486
3603
  certify: async () => {
@@ -2504,6 +3621,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2504
3621
  }
2505
3622
  }
2506
3623
  };
3624
+ let sourceSession = 0;
2507
3625
  const wrappedParams = {
2508
3626
  ...params,
2509
3627
  markError: reportUpstreamError,
@@ -2775,6 +3893,12 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2775
3893
  settlePublicationAdmissionWaiters(openTransaction, error);
2776
3894
  return createHandledRejection(error);
2777
3895
  }
3896
+ if (runtime.isScopedRecoveryPending()) {
3897
+ settlePendingTransaction(openTransaction);
3898
+ const error = new SyncTransactionAbortedError();
3899
+ settlePublicationAdmissionWaiters(openTransaction, error);
3900
+ return createHandledRejection(error);
3901
+ }
2778
3902
  try {
2779
3903
  bindToCurrentHydration(openTransaction);
2780
3904
  assertHydrationSequenceCurrent(openTransaction);
@@ -2849,48 +3973,150 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2849
3973
  return visibleAfterDurable(openTransaction, applied);
2850
3974
  }
2851
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
+ };
2852
4040
  let sourceResult = {};
2853
4041
  let sourceResultSettled = false;
2854
4042
  let activeSourceSyncEntry;
2855
- fullStartPromise = runtime.ensureStarted();
2856
- void fullStartPromise.catch(() => void 0);
2857
- sourceResultPromise = (async () => {
2858
- await runtime.ensureStartupMetadataLoaded();
2859
- if (startupState.cleanedUp) {
2860
- sourceResultSettled = true;
2861
- return sourceResult;
2862
- }
4043
+ let sourceCleanupPromise;
4044
+ let customRestartSequence = 0;
4045
+ const enterSourceSession = () => {
4046
+ const session = ++sourceSession;
2863
4047
  let settleSourceSyncEntry;
2864
4048
  activeSourceSyncEntry = new Promise((resolve) => {
2865
4049
  settleSourceSyncEntry = resolve;
2866
4050
  });
2867
4051
  try {
2868
4052
  sourceResult = normalizeSyncFnResult(
2869
- sourceSyncConfig.sync(wrappedParams)
4053
+ sourceSyncConfig.sync(
4054
+ runtime.hasManagedCacheGeneration() ? sourceParamsFor(session) : wrappedParams
4055
+ )
2870
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
+ }
2871
4078
  } finally {
2872
4079
  activeSourceSyncEntry = void 0;
2873
4080
  settleSourceSyncEntry();
2874
4081
  }
2875
- if (!startupState.cleanedUp && sourceResult.loadSubset) {
2876
- const loadSubset = async (options) => {
2877
- if (startupState.cleanedUp) {
2878
- throw new Error(`persisted sync source is no longer active`);
2879
- }
2880
- await sourceResult.loadSubset?.(
2881
- options
2882
- );
2883
- };
2884
- runtime.registerRemoteSubsetOwner(
2885
- Object.assign(loadSubset, {
2886
- unloadSubset: (options) => sourceResult.unloadSubset?.(
2887
- options
2888
- ),
2889
- onError: reportUpstreamError
2890
- })
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)
2891
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;
2892
4114
  }
4115
+ enterSourceSession();
2893
4116
  sourceResultSettled = true;
4117
+ runtime.setSourceRestartAfterScopedRecovery(
4118
+ sourceResult.restartAfterScopedRecovery ?? (runtime.hasManagedCacheGeneration() ? restartCustomSource : void 0)
4119
+ );
2894
4120
  return sourceResult;
2895
4121
  })();
2896
4122
  resolveSourceResultAssigned();
@@ -2900,6 +4126,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2900
4126
  return {
2901
4127
  cleanup: async () => {
2902
4128
  startupState.cleanedUp = true;
4129
+ runtime.clearSourceRestartAfterScopedRecovery();
2903
4130
  const sourceSyncEntry = activeSourceSyncEntry;
2904
4131
  const cleanupError = new SyncTransactionAbortedError();
2905
4132
  for (const acquisition of acquisitions.values()) {
@@ -2913,7 +4140,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2913
4140
  transactionStack.length = 0;
2914
4141
  let sourceCleanup;
2915
4142
  try {
2916
- sourceCleanup = sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.());
4143
+ sourceCleanup = sourceCleanupPromise ?? (sourceSyncEntry ? sourceSyncEntry.then(() => sourceResult.cleanup?.()) : Promise.resolve(sourceResult.cleanup?.()));
2917
4144
  } catch (error) {
2918
4145
  sourceCleanup = Promise.reject(error);
2919
4146
  }
@@ -2940,31 +4167,63 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2940
4167
  loadSubset: (options) => {
2941
4168
  const acquisition = { forwarded: false };
2942
4169
  acquisitions.set(options, acquisition);
2943
- const forwardUpstream = (resolvedSourceResult, loadOptions) => {
4170
+ const forwardUpstream = (loadOptions) => {
2944
4171
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
2945
4172
  return true;
2946
4173
  }
2947
- if (!resolvedSourceResult.loadSubset) return true;
4174
+ const currentSource = sourceResult;
4175
+ if (!currentSource.loadSubset) return true;
2948
4176
  acquisition.forwarded = true;
4177
+ acquisition.source = currentSource;
2949
4178
  try {
2950
- return resolvedSourceResult.loadSubset(loadOptions);
4179
+ return currentSource.loadSubset(loadOptions);
2951
4180
  } catch (error) {
2952
4181
  acquisition.forwarded = false;
4182
+ acquisition.source = void 0;
2953
4183
  throw error;
2954
4184
  }
2955
4185
  };
2956
4186
  const openTransaction = getOpenTransaction();
2957
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
+ };
2958
4217
  const hydrated = sourceResultSettled && !needsPublicationAdmission ? runtime.loadHydratedSubset(
2959
4218
  options,
2960
- (loadOptions) => forwardUpstream(sourceResult, loadOptions)
4219
+ (loadOptions) => forwardUpstream(loadOptions)
2961
4220
  ) : void 0;
2962
4221
  if (hydrated !== void 0) {
2963
- return hydrated;
4222
+ return hydrated === true ? true : raceRetainedLoad(hydrated);
2964
4223
  }
2965
- return (async () => {
4224
+ const work = (async () => {
2966
4225
  await fullStartPromise;
2967
- const resolvedSourceResult = await sourceResultPromise;
4226
+ await sourceResultPromise;
2968
4227
  if (startupState.cleanedUp || acquisitions.get(options) !== acquisition) {
2969
4228
  return;
2970
4229
  }
@@ -2981,9 +4240,14 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2981
4240
  }
2982
4241
  return runtime.loadSubset(
2983
4242
  options,
2984
- (loadOptions) => forwardUpstream(resolvedSourceResult, loadOptions)
4243
+ (loadOptions) => forwardUpstream(loadOptions),
4244
+ () => {
4245
+ hydrationStarted = true;
4246
+ },
4247
+ () => !startupState.cleanedUp && acquisitions.get(options) === acquisition
2985
4248
  );
2986
4249
  })();
4250
+ return raceRetainedLoad(work, () => !hydrationStarted);
2987
4251
  },
2988
4252
  unloadSubset: (options) => {
2989
4253
  const acquisition = acquisitions.get(options);
@@ -2991,7 +4255,7 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
2991
4255
  acquisitions.delete(options);
2992
4256
  runtime.unloadSubset(
2993
4257
  options,
2994
- acquisition?.forwarded ? sourceResult.unloadSubset : void 0
4258
+ acquisition?.forwarded ? acquisition.source?.unloadSubset : void 0
2995
4259
  );
2996
4260
  }
2997
4261
  };
@@ -3001,16 +4265,16 @@ function createWrappedSyncConfig(sourceSyncConfig, runtime, persistedReadiness)
3001
4265
  function createLoopbackSyncConfig(runtime) {
3002
4266
  return {
3003
4267
  sync: (params) => {
3004
- runtime.setSyncControls({
3005
- begin: params.begin,
3006
- write: params.write,
3007
- commit: params.commit,
3008
- truncate: params.truncate,
3009
- metadata: params.metadata ?? null,
3010
- markError: params.markError
3011
- });
3012
4268
  runtime.setCollection(
3013
- 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
+ }
3014
4278
  );
3015
4279
  void runtime.ensureStarted().then(() => {
3016
4280
  if (runtime.getCurrentTerminalFailure()) return;
@@ -3040,6 +4304,19 @@ function persistedCollectionOptions(options) {
3040
4304
  );
3041
4305
  }
3042
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
+ };
3043
4320
  if (hasOwnSyncKey(options)) {
3044
4321
  if (!isValidSyncConfig(options.sync)) {
3045
4322
  throw new InvalidSyncConfigError(
@@ -3066,8 +4343,10 @@ function persistedCollectionOptions(options) {
3066
4343
  persistence2,
3067
4344
  syncOptions.syncMode ?? `eager`,
3068
4345
  collectionId2,
3069
- persistedReadinessTracker
4346
+ persistedReadinessTracker,
4347
+ claimOptions
3070
4348
  );
4349
+ claimedRuntime = runtime2;
3071
4350
  const result2 = {
3072
4351
  ...syncOptions,
3073
4352
  id: collectionId2,
@@ -3084,7 +4363,8 @@ function persistedCollectionOptions(options) {
3084
4363
  () => persistedCollectionOptions({
3085
4364
  ...options,
3086
4365
  id: collectionId2
3087
- })
4366
+ }),
4367
+ claimOptions
3088
4368
  );
3089
4369
  }
3090
4370
  const {
@@ -3107,8 +4387,10 @@ function persistedCollectionOptions(options) {
3107
4387
  persistence,
3108
4388
  localOnlyOptions.syncMode ?? `eager`,
3109
4389
  localOnlyOptions.id ?? DEFAULT_DB_NAME,
3110
- persistedReadinessTracker
4390
+ persistedReadinessTracker,
4391
+ claimOptions
3111
4392
  );
4393
+ claimedRuntime = runtime;
3112
4394
  const wrappedOnInsert = async (params) => {
3113
4395
  const lifecycleGeneration = runtime.getLifecycleGeneration();
3114
4396
  const handlerResult = localOnlyOptions.onInsert ? await localOnlyOptions.onInsert(
@@ -3172,7 +4454,8 @@ function persistedCollectionOptions(options) {
3172
4454
  () => persistedCollectionOptions({
3173
4455
  ...options,
3174
4456
  id: collectionId
3175
- })
4457
+ }),
4458
+ claimOptions
3176
4459
  );
3177
4460
  }
3178
4461
  function encodePersistedStorageKey(key) {
@@ -3235,9 +4518,12 @@ export {
3235
4518
  SingleProcessCoordinator,
3236
4519
  createPersistedTableName,
3237
4520
  decodePersistedStorageKey,
4521
+ eagerStorageTarget,
3238
4522
  encodePersistedStorageKey,
3239
4523
  forwardSQLiteDriverSharedLogicalScheduling,
4524
+ managedStorageTarget,
3240
4525
  persistedCollectionOptions,
4526
+ resolvePersistedStorageTarget,
3241
4527
  validatePersistedCollectionCoordinator
3242
4528
  };
3243
4529
  //# sourceMappingURL=persisted.js.map