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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/db-sqlite-persistence-core",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "SQLite persisted collection core for TanStack DB",
5
5
  "author": "TanStack Team",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  ],
51
51
  "dependencies": {
52
52
  "@standard-schema/spec": "^1.1.0",
53
- "@tanstack/db": "0.13.0"
53
+ "@tanstack/db": "0.14.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "typescript": ">=4.7"
@@ -3,6 +3,7 @@ import { isValidCommittedTxAnchor } from './committed-tx-anchor'
3
3
  import {
4
4
  DuplicateRemoteSubsetOwnerError,
5
5
  IndeterminateCommitError,
6
+ InvalidPersistedCollectionConfigError,
6
7
  InvalidPersistenceAdapterError,
7
8
  PersistedCollectionDurabilityError,
8
9
  RetryableRemoteSubsetAcquisitionError,
@@ -13,6 +14,7 @@ import {
13
14
  toTransportedLoadSubsetOptions,
14
15
  } from './remote-subset-wire'
15
16
  import { unloadRemoteSubsetOwner } from './remote-subset-owner'
17
+ import { eagerStorageTarget, managedStorageTarget } from './persisted'
16
18
  import type { LoadSubsetOptions } from '@tanstack/db'
17
19
  import type { IndeterminateCommitRequestType } from './errors'
18
20
  import type { TransportedLoadSubsetOptions } from './remote-subset-wire'
@@ -28,6 +30,7 @@ import type {
28
30
  PersistedIndexSpec,
29
31
  PersistedMutationEnvelope,
30
32
  PersistedRowMetadataMutation,
33
+ PersistedStorageTarget,
31
34
  PersistedTx,
32
35
  PersistenceAdapter,
33
36
  ProtocolEnvelope,
@@ -64,12 +67,14 @@ type RPCRequest =
64
67
  rpcId: string
65
68
  signature: string
66
69
  spec: PersistedIndexSpec
70
+ cacheGenerationClaimId?: string
67
71
  }
68
72
  | {
69
73
  type: `rpc:applyLocalMutations:req`
70
74
  rpcId: string
71
75
  envelopeId: string
72
76
  mutations: Array<PersistedMutationEnvelope>
77
+ cacheGenerationClaimId?: string
73
78
  }
74
79
  | {
75
80
  type: `rpc:applyCommittedTx:req`
@@ -87,6 +92,7 @@ type RPCRequest =
87
92
  type: `rpc:pullSince:req`
88
93
  rpcId: string
89
94
  fromRowVersion: number
95
+ cacheGenerationClaimId?: string
90
96
  }
91
97
 
92
98
  type RPCResponse =
@@ -133,8 +139,9 @@ type CoordinatorAdapter = Pick<
133
139
  | `runInRegularScope`
134
140
  > & {
135
141
  pullSince?: (
136
- collectionId: string,
142
+ target: PersistedStorageTarget,
137
143
  fromRowVersion: number,
144
+ ctx?: { cacheGenerationClaimId?: string },
138
145
  ) => Promise<
139
146
  | {
140
147
  latestRowVersion: number
@@ -147,14 +154,18 @@ type CoordinatorAdapter = Pick<
147
154
  deletedKeys: Array<string | number>
148
155
  }
149
156
  >
150
- getStreamPosition?: (collectionId: string) => Promise<{
157
+ getStreamPosition?: (
158
+ target: PersistedStorageTarget,
159
+ ctx?: { cacheGenerationClaimId?: string },
160
+ ) => Promise<{
151
161
  latestTerm: number
152
162
  latestSeq: number
153
163
  latestRowVersion: number
154
164
  }>
155
165
  reserveLeadershipTerm?: (
156
- collectionId: string,
166
+ target: PersistedStorageTarget,
157
167
  observedTerm: number,
168
+ ctx?: { cacheGenerationClaimId?: string },
158
169
  ) => Promise<{
159
170
  latestTerm: number
160
171
  latestSeq: number
@@ -175,6 +186,7 @@ type ActiveRemoteSubsetAcquisition = {
175
186
  owner: RemoteSubsetOwner
176
187
  options: TransportedLoadSubsetOptions
177
188
  load: Promise<void>
189
+ admitted: Promise<void>
178
190
  transferred: boolean
179
191
  released: boolean
180
192
  terminalRelease: boolean
@@ -259,6 +271,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
259
271
  private readonly coordinatorName: string
260
272
  private defaultAdapter: CoordinatorAdapter | null
261
273
  private readonly collectionAdapters = new Map<string, CoordinatorAdapter>()
274
+ private readonly collectionClaimIds = new Map<string, string>()
262
275
  private readonly remoteSubsetOwners = new Map<string, RemoteSubsetOwner>()
263
276
  private readonly remoteSubsetIds = new Map<
264
277
  string,
@@ -338,9 +351,15 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
338
351
  setAdapterForCollection(
339
352
  collectionId: string,
340
353
  adapter: CoordinatorAdapter,
354
+ cacheGenerationClaimId?: string,
341
355
  ): void {
342
356
  requireDurableElection(adapter)
343
357
  this.collectionAdapters.set(collectionId, adapter)
358
+ if (cacheGenerationClaimId) {
359
+ this.collectionClaimIds.set(collectionId, cacheGenerationClaimId)
360
+ } else {
361
+ this.collectionClaimIds.delete(collectionId)
362
+ }
344
363
  }
345
364
 
346
365
  registerRemoteSubsetOwner(
@@ -523,11 +542,38 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
523
542
  }
524
543
  try {
525
544
  await acquisition.release
545
+ } catch (error) {
546
+ this.scheduleRemoteSubsetReleaseRetry(acquisition, options)
547
+ throw error
526
548
  } finally {
527
549
  acquisition.release = null
528
550
  }
529
551
  }
530
552
 
553
+ private scheduleRemoteSubsetReleaseRetry(
554
+ acquisition: OutboundRemoteSubsetAcquisition,
555
+ options: LoadSubsetOptions,
556
+ ): void {
557
+ const key = remoteSubsetAcquisitionKey(
558
+ acquisition.collectionId,
559
+ acquisition.acquisitionId,
560
+ )
561
+ if (
562
+ this.isDisposed() ||
563
+ this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition ||
564
+ acquisition.retryTimer !== null
565
+ ) {
566
+ return
567
+ }
568
+ acquisition.retryTimer = setTimeout(() => {
569
+ acquisition.retryTimer = null
570
+ void this.requestReleaseRemoteSubset(
571
+ acquisition.collectionId,
572
+ options,
573
+ ).catch(() => undefined)
574
+ }, RPC_RETRY_DELAY_MS)
575
+ }
576
+
531
577
  private async acquireRemoteSubset(
532
578
  acquisition: OutboundRemoteSubsetAcquisition,
533
579
  ): Promise<void> {
@@ -603,7 +649,11 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
603
649
  try {
604
650
  await work
605
651
  acquired = true
606
- this.cancelRemoteSubsetReplayRetry(acquisition)
652
+ // Release may have failed while this acquisition was in flight. In that
653
+ // case the shared timer carries release retry debt, not replay work.
654
+ if (!acquisition.releaseRequested) {
655
+ this.cancelRemoteSubsetReplayRetry(acquisition)
656
+ }
607
657
  } finally {
608
658
  if (acquisition.inFlight === work) acquisition.inFlight = null
609
659
  const current = this.collections.get(acquisition.collectionId)
@@ -636,14 +686,27 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
636
686
  spec: PersistedIndexSpec,
637
687
  scopedAdapter?: HydrationPersistenceAdapter,
638
688
  localEnsureCompleted = false,
689
+ cacheGenerationClaimId?: string,
639
690
  ): Promise<void> {
640
691
  if (this.isLeader(collectionId)) {
641
692
  if (localEnsureCompleted) return
642
- await (scopedAdapter ?? this.requireAdapter(collectionId)).ensureIndex(
643
- collectionId,
644
- signature,
645
- spec,
646
- )
693
+ const adapter = scopedAdapter ?? this.requireAdapter(collectionId)
694
+ if (cacheGenerationClaimId) {
695
+ await adapter.ensureIndex(
696
+ managedStorageTarget(collectionId, cacheGenerationClaimId),
697
+ signature,
698
+ spec,
699
+ {
700
+ cacheGenerationClaimId,
701
+ },
702
+ )
703
+ } else {
704
+ await adapter.ensureIndex(
705
+ eagerStorageTarget(collectionId),
706
+ signature,
707
+ spec,
708
+ )
709
+ }
647
710
  return
648
711
  }
649
712
 
@@ -657,6 +720,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
657
720
  rpcId: safeRandomUUID(),
658
721
  signature,
659
722
  spec,
723
+ cacheGenerationClaimId,
660
724
  })
661
725
 
662
726
  if (!response.ok) {
@@ -669,6 +733,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
669
733
  async requestApplyLocalMutations(
670
734
  collectionId: string,
671
735
  mutations: Array<PersistedMutationEnvelope>,
736
+ cacheGenerationClaimId?: string,
672
737
  ): Promise<ApplyLocalMutationsResponse> {
673
738
  const initialRoute = this.waitForInitialRoute(collectionId)
674
739
  if (initialRoute) await initialRoute
@@ -678,6 +743,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
678
743
  rpcId: safeRandomUUID(),
679
744
  envelopeId: safeRandomUUID(),
680
745
  mutations,
746
+ cacheGenerationClaimId,
681
747
  })
682
748
  }
683
749
 
@@ -686,6 +752,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
686
752
  rpcId: safeRandomUUID(),
687
753
  envelopeId: safeRandomUUID(),
688
754
  mutations,
755
+ cacheGenerationClaimId,
689
756
  })
690
757
  }
691
758
 
@@ -750,6 +817,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
750
817
  collectionId: string,
751
818
  fromRowVersion: number,
752
819
  scopedAdapter?: HydrationPersistenceAdapter,
820
+ cacheGenerationClaimId?: string,
753
821
  ): Promise<PullSinceResponse> {
754
822
  if (this.isLeader(collectionId)) {
755
823
  return this.handlePullSince(
@@ -758,6 +826,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
758
826
  type: `rpc:pullSince:req`,
759
827
  rpcId: safeRandomUUID(),
760
828
  fromRowVersion,
829
+ cacheGenerationClaimId,
761
830
  },
762
831
  scopedAdapter,
763
832
  )
@@ -767,6 +836,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
767
836
  type: `rpc:pullSince:req`,
768
837
  rpcId: safeRandomUUID(),
769
838
  fromRowVersion,
839
+ cacheGenerationClaimId,
770
840
  })
771
841
  }
772
842
 
@@ -805,6 +875,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
805
875
  this.channel.close()
806
876
  this.collections.clear()
807
877
  this.collectionAdapters.clear()
878
+ this.collectionClaimIds.clear()
808
879
  for (const collectionId of this.remoteSubsetOwners.keys()) {
809
880
  this.releaseInboundRemoteSubsetAcquisitions(collectionId)
810
881
  }
@@ -883,9 +954,17 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
883
954
  const reserveTerm = adapter.reserveLeadershipTerm
884
955
  if (!reserveTerm)
885
956
  throw new InvalidPersistenceAdapterError(`reserveLeadershipTerm`)
886
- const pos = await this.withWriterLock(() =>
887
- reserveTerm.call(adapter, collectionId, state.latestTerm),
888
- )
957
+ const pos = await this.withWriterLock(() => {
958
+ const claimId = this.collectionClaimIds.get(collectionId)
959
+ return reserveTerm.call(
960
+ adapter,
961
+ claimId
962
+ ? managedStorageTarget(collectionId, claimId)
963
+ : eagerStorageTarget(collectionId),
964
+ state.latestTerm,
965
+ { cacheGenerationClaimId: claimId },
966
+ )
967
+ })
889
968
  state.latestTerm = pos.latestTerm
890
969
  state.latestSeq = pos.latestSeq
891
970
  state.latestRowVersion = pos.latestRowVersion
@@ -1079,6 +1158,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1079
1158
  this.releaseLeadership(collectionId, state)
1080
1159
  this.collections.delete(collectionId)
1081
1160
  this.collectionAdapters.delete(collectionId)
1161
+ this.collectionClaimIds.delete(collectionId)
1082
1162
  const prefix = `${JSON.stringify([collectionId]).slice(0, -1)},`
1083
1163
  for (const key of this.appliedEnvelopes.keys()) {
1084
1164
  if (key.startsWith(prefix)) this.appliedEnvelopes.delete(key)
@@ -1625,12 +1705,11 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1625
1705
  acquisition.released = true
1626
1706
  acquisition.release = (async () => {
1627
1707
  try {
1628
- await acquisition.load
1629
- } catch {
1630
- // A returned promise transfers the lease even when initial loading fails.
1631
- }
1632
- try {
1708
+ await acquisition.admitted
1633
1709
  if (acquisition.transferred) {
1710
+ // The owner may require unload to settle its load. Invocation of the
1711
+ // owner already transferred this lease; source settlement is not a
1712
+ // prerequisite for giving it the cancellation opportunity.
1634
1713
  await unloadRemoteSubsetOwner(acquisition.owner, acquisition.options)
1635
1714
  }
1636
1715
  } finally {
@@ -1790,6 +1869,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1790
1869
  owner: options.owner,
1791
1870
  options: options.options,
1792
1871
  load: Promise.resolve(),
1872
+ admitted: Promise.resolve(),
1793
1873
  transferred: false,
1794
1874
  released: false,
1795
1875
  terminalRelease: false,
@@ -1797,6 +1877,10 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1797
1877
  }
1798
1878
  this.releasedRemoteSubsetAcquisitionTimes.delete(key)
1799
1879
  this.inboundRemoteSubsetAcquisitions.set(key, acquisition)
1880
+ let resolveAdmission!: () => void
1881
+ acquisition.admitted = new Promise<void>((resolve) => {
1882
+ resolveAdmission = resolve
1883
+ })
1800
1884
  let resolveLoad!: () => void
1801
1885
  let rejectLoad!: (error: unknown) => void
1802
1886
  acquisition.load = new Promise<void>((resolve, reject) => {
@@ -1809,6 +1893,8 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1809
1893
  void Promise.resolve(load).then(resolveLoad, rejectLoad)
1810
1894
  } catch (error) {
1811
1895
  rejectLoad(error)
1896
+ } finally {
1897
+ resolveAdmission()
1812
1898
  }
1813
1899
 
1814
1900
  try {
@@ -1844,11 +1930,26 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1844
1930
  rpcId: string
1845
1931
  signature: string
1846
1932
  spec: PersistedIndexSpec
1933
+ cacheGenerationClaimId?: string
1847
1934
  },
1848
1935
  ): Promise<RPCResponse> {
1849
- await this.withScheduledWriterLock(collectionId, (adapter) =>
1850
- adapter.ensureIndex(collectionId, request.signature, request.spec),
1851
- )
1936
+ await this.withScheduledWriterLock(collectionId, (adapter) => {
1937
+ if (request.cacheGenerationClaimId) {
1938
+ return adapter.ensureIndex(
1939
+ managedStorageTarget(collectionId, request.cacheGenerationClaimId),
1940
+ request.signature,
1941
+ request.spec,
1942
+ {
1943
+ cacheGenerationClaimId: request.cacheGenerationClaimId,
1944
+ },
1945
+ )
1946
+ }
1947
+ return adapter.ensureIndex(
1948
+ eagerStorageTarget(collectionId),
1949
+ request.signature,
1950
+ request.spec,
1951
+ )
1952
+ })
1852
1953
  return {
1853
1954
  type: `rpc:ensurePersistedIndex:res`,
1854
1955
  rpcId: request.rpcId,
@@ -1863,6 +1964,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1863
1964
  rpcId: string
1864
1965
  envelopeId: string
1865
1966
  mutations: Array<PersistedMutationEnvelope>
1967
+ cacheGenerationClaimId?: string
1866
1968
  },
1867
1969
  ): Promise<ApplyLocalMutationsResponse> {
1868
1970
  const envelopeKey = appliedEnvelopeKey(collectionId, request.envelopeId)
@@ -1896,6 +1998,14 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1896
1998
  collectionId: string,
1897
1999
  request: Extract<RPCRequest, { type: `rpc:applyLocalMutations:req` }>,
1898
2000
  ): Promise<ApplyLocalMutationsResponse> {
2001
+ if (
2002
+ this.collectionClaimIds.has(collectionId) &&
2003
+ !request.cacheGenerationClaimId
2004
+ ) {
2005
+ throw new InvalidPersistedCollectionConfigError(
2006
+ `A managed local mutation requires a persisted cache claim`,
2007
+ )
2008
+ }
1899
2009
  const state = this.collections.get(collectionId)
1900
2010
  if (!state || !state.isLeader) {
1901
2011
  return {
@@ -1925,6 +2035,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
1925
2035
  }
1926
2036
  const pendingTx = {
1927
2037
  txId: safeRandomUUID(),
2038
+ cacheGenerationClaimId: request.cacheGenerationClaimId,
1928
2039
  mutations: request.mutations.map((m) => ({
1929
2040
  type: m.type,
1930
2041
  key: m.key,
@@ -2230,7 +2341,12 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
2230
2341
  if (!adapter.reconcileCommittedTx) return { kind: `unknown` as const }
2231
2342
  try {
2232
2343
  const reconciled = await adapter.reconcileCommittedTx(
2233
- collectionId,
2344
+ candidate.cacheGenerationClaimId
2345
+ ? managedStorageTarget(
2346
+ collectionId,
2347
+ candidate.cacheGenerationClaimId,
2348
+ )
2349
+ : eagerStorageTarget(collectionId),
2234
2350
  candidate,
2235
2351
  request.anchor,
2236
2352
  )
@@ -2327,7 +2443,12 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
2327
2443
  )
2328
2444
  if (tx === null) return null
2329
2445
  try {
2330
- await adapter.applyCommittedTx(collectionId, tx)
2446
+ await adapter.applyCommittedTx(
2447
+ tx.cacheGenerationClaimId
2448
+ ? managedStorageTarget(collectionId, tx.cacheGenerationClaimId)
2449
+ : eagerStorageTarget(collectionId),
2450
+ tx,
2451
+ )
2331
2452
  } catch (error) {
2332
2453
  throw toPersistedCollectionDurabilityError(collectionId, error)
2333
2454
  }
@@ -2345,6 +2466,7 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
2345
2466
  type: `rpc:pullSince:req`
2346
2467
  rpcId: string
2347
2468
  fromRowVersion: number
2469
+ cacheGenerationClaimId?: string
2348
2470
  },
2349
2471
  scopedAdapter?: HydrationPersistenceAdapter,
2350
2472
  ): Promise<PullSinceResponse> {
@@ -2363,7 +2485,15 @@ export class BroadcastCollectionCoordinator implements PersistedCollectionCoordi
2363
2485
  }
2364
2486
  }
2365
2487
 
2366
- const result = await adapter.pullSince(collectionId, request.fromRowVersion)
2488
+ const result = await adapter.pullSince(
2489
+ request.cacheGenerationClaimId
2490
+ ? managedStorageTarget(collectionId, request.cacheGenerationClaimId)
2491
+ : eagerStorageTarget(collectionId),
2492
+ request.fromRowVersion,
2493
+ {
2494
+ cacheGenerationClaimId: request.cacheGenerationClaimId,
2495
+ },
2496
+ )
2367
2497
 
2368
2498
  if (result.requiresFullReload) {
2369
2499
  return {