@tanstack/db-sqlite-persistence-core 0.4.3 → 0.4.4

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.3",
3
+ "version": "0.4.4",
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.11.3"
53
+ "@tanstack/db": "0.12.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "typescript": ">=4.7"
package/src/errors.ts CHANGED
@@ -90,8 +90,7 @@ export function toPersistedCollectionDurabilityError(
90
90
  }
91
91
 
92
92
  export type IndeterminateCommitRequestType =
93
- | `rpc:applyLocalMutations:req`
94
- | `rpc:applyCommittedTx:req`
93
+ `rpc:applyLocalMutations:req` | `rpc:applyCommittedTx:req`
95
94
 
96
95
  export type IndeterminateCommitErrorOptions = {
97
96
  collectionId: string
package/src/persisted.ts CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  getLoadSubsetDemandKey,
10
10
  safeRandomUUID,
11
11
  toBooleanPredicate,
12
+ whenSyncAccepted,
13
+ withAcceptedReceipt,
12
14
  withCollectionConfigFactory,
13
15
  } from '@tanstack/db'
14
16
  import {
@@ -289,8 +291,7 @@ export type PersistedRowMetadataMutation<
289
291
  > = { type: `set`; key: TKey; value: unknown } | { type: `delete`; key: TKey }
290
292
 
291
293
  export type PersistedCollectionMetadataMutation =
292
- | { type: `set`; key: string; value: unknown }
293
- | { type: `delete`; key: string }
294
+ { type: `set`; key: string; value: unknown } | { type: `delete`; key: string }
294
295
 
295
296
  export type ReplayableTxDelta<
296
297
  T extends Record<string, unknown> = Record<string, unknown>,
@@ -738,7 +739,7 @@ const DEFAULT_DB_NAME = `tanstack-db`
738
739
  const REMOTE_ENSURE_RETRY_DELAY_MS = 50
739
740
 
740
741
  type SyncControlFns<T extends object, TKey extends string | number> = {
741
- begin: ((options?: { immediate?: boolean }) => void) | null
742
+ begin: (() => void) | null
742
743
  write:
743
744
  | ((
744
745
  message:
@@ -1046,7 +1047,6 @@ type BufferedSyncTransaction<T extends object, TKey extends string | number> = {
1046
1047
  truncate: boolean
1047
1048
  internal: boolean
1048
1049
  lifecycleGeneration: number
1049
- beginOptions?: { immediate?: boolean }
1050
1050
  expectedResumeGenerationOwner?: symbol
1051
1051
  signal?: AbortSignal
1052
1052
  prependHydrationRows: (
@@ -1066,6 +1066,8 @@ type OpenSyncTransaction<T extends object, TKey extends string | number> = Omit<
1066
1066
  applicationReceipt?: SyncAppliedReceipt
1067
1067
  operationKeys: Set<TKey>
1068
1068
  queuedBecauseHydrating: boolean
1069
+ // Set by `reserveCommitTurn`: subset hydration waits for this commit.
1070
+ reservesCommitTurn?: boolean
1069
1071
  hasDependentSuccessor: boolean
1070
1072
  publicationAdmissionWaiters?: Set<{
1071
1073
  resolve: () => void
@@ -1326,15 +1328,13 @@ class PersistedCollectionRuntime<
1326
1328
  private resumeGenerationOwner = Symbol(`persisted resume generation owner`)
1327
1329
  private lifecycleGeneration = 0
1328
1330
  private internalApplyDepth = 0
1329
- private sourcePublicationWaitDepth = 0
1330
1331
  private appliedReceiptSequence = 0
1331
1332
  private syncErrorReported = false
1332
1333
  private reportedSyncError: unknown
1333
1334
  private readonly pendingAppliedReceipts = new Map<number, Promise<void>>()
1334
1335
  private hydratingGeneration: number | null = null
1335
1336
  private terminalFailure:
1336
- | { lifecycleGeneration: number; error: unknown }
1337
- | undefined
1337
+ { lifecycleGeneration: number; error: unknown } | undefined
1338
1338
  private coordinatorUnsubscribe: (() => void) | null = null
1339
1339
  private remoteSubsetOwnerUnsubscribe: (() => void) | null = null
1340
1340
  private indexAddedUnsubscribe: (() => void) | null = null
@@ -1409,10 +1409,20 @@ class PersistedCollectionRuntime<
1409
1409
  return receipt
1410
1410
  }
1411
1411
 
1412
- private async waitForAppliedReceiptsAfter(cursor: number): Promise<void> {
1412
+ // Startup waits for acceptance: a local mutation's handler can wait for
1413
+ // startup, and the rows publish when that mutation settles. Readiness waits
1414
+ // for them in core. A subset load waits for visibility.
1415
+ private async waitForAppliedReceiptsAfter(
1416
+ cursor: number,
1417
+ until: `accepted` | `visible` = `visible`,
1418
+ ): Promise<void> {
1413
1419
  await Promise.all(
1414
1420
  Array.from(this.pendingAppliedReceipts, ([sequence, receipt]) =>
1415
- sequence > cursor ? receipt : undefined,
1421
+ sequence <= cursor
1422
+ ? undefined
1423
+ : until === `accepted`
1424
+ ? whenSyncAccepted(receipt)
1425
+ : receipt,
1416
1426
  ),
1417
1427
  )
1418
1428
  }
@@ -1623,7 +1633,10 @@ class PersistedCollectionRuntime<
1623
1633
  startup.appliedCursor !== undefined &&
1624
1634
  lifecycleGeneration === this.lifecycleGeneration
1625
1635
  ) {
1626
- await this.waitForAppliedReceiptsAfter(startup.appliedCursor)
1636
+ await this.waitForAppliedReceiptsAfter(
1637
+ startup.appliedCursor,
1638
+ `accepted`,
1639
+ )
1627
1640
  }
1628
1641
  }
1629
1642
  if (lifecycleGeneration === this.lifecycleGeneration) {
@@ -1664,7 +1677,7 @@ class PersistedCollectionRuntime<
1664
1677
  appliedCursor !== undefined &&
1665
1678
  lifecycleGeneration === this.lifecycleGeneration
1666
1679
  ) {
1667
- await this.waitForAppliedReceiptsAfter(appliedCursor)
1680
+ await this.waitForAppliedReceiptsAfter(appliedCursor, `accepted`)
1668
1681
  }
1669
1682
  })().catch((error) => {
1670
1683
  throw this.markTerminalFailure(error, lifecycleGeneration)
@@ -1795,7 +1808,7 @@ class PersistedCollectionRuntime<
1795
1808
  const applied = this.replaceCollectionMetadataSnapshot(
1796
1809
  snapshot.collectionMetadata,
1797
1810
  )
1798
- if (applied !== true) await applied
1811
+ await whenSyncAccepted(applied)
1799
1812
  }
1800
1813
 
1801
1814
  private async loadCollectionMetadataSnapshot(
@@ -1827,7 +1840,7 @@ class PersistedCollectionRuntime<
1827
1840
  .map(({ key }) => key)
1828
1841
 
1829
1842
  return this.withInternalApply(() => {
1830
- this.syncControls.begin?.({ immediate: true })
1843
+ this.syncControls.begin?.()
1831
1844
 
1832
1845
  currentKeys.forEach((key) => {
1833
1846
  if (!nextMetadata.has(key)) {
@@ -2072,16 +2085,6 @@ class PersistedCollectionRuntime<
2072
2085
  ): Promise<void> {
2073
2086
  const failure = this.getCurrentTerminalFailure()
2074
2087
  if (failure) return Promise.reject(failure.error)
2075
- if (
2076
- transaction.beginOptions?.immediate &&
2077
- this.sourcePublicationWaitDepth > 0
2078
- ) {
2079
- return Promise.reject(
2080
- new InvalidPersistedCollectionConfigError(
2081
- `immediate persisted source replay cannot enter while an earlier source publication is waiting`,
2082
- ),
2083
- )
2084
- }
2085
2088
  return this.applyMutex.run(async () => {
2086
2089
  await this.applyBufferedSyncTransactionUnsafe(transaction)
2087
2090
  })
@@ -2373,7 +2376,7 @@ class PersistedCollectionRuntime<
2373
2376
  hydrationContext.suppliedRowKeys.add(row.key)
2374
2377
  }
2375
2378
  const applied = this.applyRowsToCollection(rows)
2376
- if (applied !== true) await applied
2379
+ await whenSyncAccepted(applied)
2377
2380
  }
2378
2381
  } finally {
2379
2382
  if (
@@ -2433,8 +2436,7 @@ class PersistedCollectionRuntime<
2433
2436
  adapter: HydrationPersistenceAdapter,
2434
2437
  ): Promise<void> {
2435
2438
  let snapshotRows:
2436
- | Map<TKey, { key: TKey; value: T; metadata?: unknown }>
2437
- | undefined
2439
+ Map<TKey, { key: TKey; value: T; metadata?: unknown }> | undefined
2438
2440
  type RecoveryPresence = `present` | `absent` | `unknown`
2439
2441
  const recoveredPresence = new Map<TKey, RecoveryPresence>()
2440
2442
  let snapshotInvalidatedByTruncate = false
@@ -2570,7 +2572,7 @@ class PersistedCollectionRuntime<
2570
2572
  }
2571
2573
 
2572
2574
  return this.withInternalApply(() => {
2573
- this.syncControls.begin?.({ immediate: true })
2575
+ this.syncControls.begin?.()
2574
2576
 
2575
2577
  for (const row of rows) {
2576
2578
  if (this.collection?._hasHydratedKey(row.key)) {
@@ -2668,7 +2670,7 @@ class PersistedCollectionRuntime<
2668
2670
  .map(({ key }) => key)
2669
2671
 
2670
2672
  return this.withInternalApply(() => {
2671
- this.syncControls.begin?.({ immediate: true })
2673
+ this.syncControls.begin?.()
2672
2674
  this.syncControls.truncate?.()
2673
2675
 
2674
2676
  for (const row of rows) {
@@ -2739,14 +2741,17 @@ class PersistedCollectionRuntime<
2739
2741
  : transaction.applyToCollection()
2740
2742
  applicationReturned = true
2741
2743
  abortedDuringApplication = transaction.signal?.aborted === true
2742
- if (applied !== true) {
2743
- this.sourcePublicationWaitDepth++
2744
- try {
2745
- await applied
2746
- } finally {
2747
- this.sourcePublicationWaitDepth--
2748
- }
2749
- }
2744
+ // Inside the apply mutex, internal work waits only for acceptance: a
2745
+ // local mutation's handler may need this mutex, and the rows publish
2746
+ // when that mutation settles. A source transaction is stored once it is
2747
+ // visible, except one held by a persisting optimistic transaction,
2748
+ // which is stored now so a handler can await its acceptance.
2749
+ if (transaction.internal) await whenSyncAccepted(applied)
2750
+ else if (
2751
+ applied !== true &&
2752
+ !this.collection?._state.hasPersistingTransaction()
2753
+ )
2754
+ await applied
2750
2755
  this.throwIfLifecycleReplaced(transaction.lifecycleGeneration)
2751
2756
 
2752
2757
  if (!transaction.internal) {
@@ -2972,7 +2977,7 @@ class PersistedCollectionRuntime<
2972
2977
  }
2973
2978
 
2974
2979
  const applied = this.withInternalApply(() => {
2975
- this.syncControls.begin?.({ immediate: true })
2980
+ this.syncControls.begin?.()
2976
2981
 
2977
2982
  for (const mutation of mutations) {
2978
2983
  if (mutation.type === `delete`) {
@@ -2991,9 +2996,10 @@ class PersistedCollectionRuntime<
2991
2996
  return this.syncControls.commit?.() ?? true
2992
2997
  })
2993
2998
 
2994
- if (applied !== true) {
2995
- await applied
2996
- }
2999
+ // The mutation handler awaits this confirmation, so wait only for
3000
+ // acceptance: the rows become visible when the mutation settles.
3001
+ const accepted = whenSyncAccepted(applied)
3002
+ if (accepted !== true) await accepted
2997
3003
  }
2998
3004
 
2999
3005
  private filterMutationsForCollection(
@@ -3545,11 +3551,11 @@ class PersistedCollectionRuntime<
3545
3551
  this.resetSequence++
3546
3552
  if (this.syncControls.begin && this.syncControls.commit) {
3547
3553
  const applied = this.withInternalApply(() => {
3548
- this.syncControls.begin?.({ immediate: true })
3554
+ this.syncControls.begin?.()
3549
3555
  this.syncControls.truncate?.()
3550
3556
  return this.syncControls.commit?.() ?? true
3551
3557
  })
3552
- if (applied !== true) await applied
3558
+ await whenSyncAccepted(applied)
3553
3559
  }
3554
3560
 
3555
3561
  if (lifecycleGeneration !== this.lifecycleGeneration) return
@@ -3600,7 +3606,7 @@ class PersistedCollectionRuntime<
3600
3606
  )
3601
3607
 
3602
3608
  const applied = this.withInternalApply(() => {
3603
- this.syncControls.begin?.({ immediate: true })
3609
+ this.syncControls.begin?.()
3604
3610
 
3605
3611
  for (const {
3606
3612
  key: changedKey,
@@ -3646,7 +3652,7 @@ class PersistedCollectionRuntime<
3646
3652
 
3647
3653
  return this.syncControls.commit?.() ?? true
3648
3654
  })
3649
- if (applied !== true) await applied
3655
+ await whenSyncAccepted(applied)
3650
3656
  }
3651
3657
 
3652
3658
  private async reloadActiveSubsetsUnsafe(
@@ -3692,7 +3698,7 @@ class PersistedCollectionRuntime<
3692
3698
  })),
3693
3699
  collectionMetadata,
3694
3700
  )
3695
- if (applied !== true) await applied
3701
+ await whenSyncAccepted(applied)
3696
3702
  } finally {
3697
3703
  if (
3698
3704
  this.activeHydrationContext === hydrationContext &&
@@ -3934,6 +3940,19 @@ function createWrappedSyncConfig<
3934
3940
  ) => {
3935
3941
  removePendingPublicationTransaction(transaction)
3936
3942
  }
3943
+ // A source receipt is accepted once its rows are durable, and resolves
3944
+ // when core makes them visible.
3945
+ const visibleAfterDurable = (
3946
+ transaction: OpenSyncTransaction<T, TKey>,
3947
+ durable: Promise<void>,
3948
+ ): Promise<void> => {
3949
+ const visible = durable.then(async () => {
3950
+ const receipt = transaction.applicationReceipt
3951
+ if (receipt !== undefined && receipt !== true) await receipt
3952
+ })
3953
+ void visible.catch(() => undefined)
3954
+ return withAcceptedReceipt(visible, durable)
3955
+ }
3937
3956
  const settleRuntimeTransaction = (
3938
3957
  transaction: OpenSyncTransaction<T, TKey>,
3939
3958
  applied: Promise<void>,
@@ -4072,14 +4091,7 @@ function createWrappedSyncConfig<
4072
4091
  }
4073
4092
  transaction.deferredHydrationMetadataDeleteKeys.clear()
4074
4093
 
4075
- // A buffered source replay is part of the hydrate that owns it.
4076
- // It must not wait for a mutation whose persistence is queued
4077
- // behind that hydrate's apply mutex.
4078
- params.begin(
4079
- transaction.queuedBecauseHydrating
4080
- ? { immediate: true }
4081
- : transaction.beginOptions,
4082
- )
4094
+ params.begin()
4083
4095
  if (transaction.truncate) params.truncate()
4084
4096
  for (const operation of transaction.operations) {
4085
4097
  if (operation.type === `delete`) {
@@ -4106,14 +4118,10 @@ function createWrappedSyncConfig<
4106
4118
 
4107
4119
  const applied = params.commit(signal)
4108
4120
  transaction.applicationReceipt = applied
4109
- if (applied === true) {
4110
- removePendingPublicationTransaction(transaction)
4111
- } else {
4112
- void applied.then(
4113
- () => removePendingPublicationTransaction(transaction),
4114
- () => removePendingPublicationTransaction(transaction),
4115
- )
4116
- }
4121
+ // Core accepted the transaction, so it is no longer staged here;
4122
+ // the receipt resolves when it is visible.
4123
+ removePendingPublicationTransaction(transaction)
4124
+ if (applied !== true) void applied.catch(() => undefined)
4117
4125
  return applied
4118
4126
  } catch (error) {
4119
4127
  removePendingPublicationTransaction(transaction)
@@ -4135,6 +4143,11 @@ function createWrappedSyncConfig<
4135
4143
  const persistenceCapability: SyncPersistenceCapabilityV1<TKey> = {
4136
4144
  protocol: SYNC_PERSISTENCE_PROTOCOL,
4137
4145
  version: SYNC_PERSISTENCE_VERSION,
4146
+ reserveCommitTurn: () => {
4147
+ const openTransaction = getOpenTransaction()
4148
+ if (openTransaction && !openTransaction.internal)
4149
+ openTransaction.reservesCommitTurn = true
4150
+ },
4138
4151
  hydrateBaseline: async () => {
4139
4152
  if (startupState.cleanedUp) return
4140
4153
  try {
@@ -4192,7 +4205,7 @@ function createWrappedSyncConfig<
4192
4205
  })
4193
4206
  .catch(() => undefined)
4194
4207
  },
4195
- begin: (options?: { immediate?: boolean }) => {
4208
+ begin: () => {
4196
4209
  if (startupState.cleanedUp) return undefined
4197
4210
  const terminalFailure = getTerminalFailure()
4198
4211
  const internal = runtime.isApplyingInternally()
@@ -4205,7 +4218,6 @@ function createWrappedSyncConfig<
4205
4218
  truncate: false,
4206
4219
  internal,
4207
4220
  lifecycleGeneration: runtime.getLifecycleGeneration(),
4208
- beginOptions: options,
4209
4221
  operationKeys: new Set(),
4210
4222
  hasDependentSuccessor: false,
4211
4223
  queuedBecauseHydrating:
@@ -4522,7 +4534,6 @@ function createWrappedSyncConfig<
4522
4534
  truncate: openTransaction.truncate,
4523
4535
  internal: false,
4524
4536
  lifecycleGeneration: openTransaction.lifecycleGeneration,
4525
- beginOptions: openTransaction.beginOptions,
4526
4537
  expectedResumeGenerationOwner:
4527
4538
  openTransaction.expectedResumeGenerationOwner,
4528
4539
  signal,
@@ -4576,7 +4587,7 @@ function createWrappedSyncConfig<
4576
4587
  })
4577
4588
  settlePublicationAdmissionWaiters(openTransaction)
4578
4589
  settleRuntimeTransaction(openTransaction, applied)
4579
- return applied
4590
+ return visibleAfterDurable(openTransaction, applied)
4580
4591
  }
4581
4592
 
4582
4593
  let applied: Promise<void>
@@ -4588,7 +4599,7 @@ function createWrappedSyncConfig<
4588
4599
  }
4589
4600
  settlePublicationAdmissionWaiters(openTransaction)
4590
4601
  settleRuntimeTransaction(openTransaction, applied)
4591
- return applied
4602
+ return visibleAfterDurable(openTransaction, applied)
4592
4603
  },
4593
4604
  }
4594
4605
 
@@ -4728,9 +4739,7 @@ function createWrappedSyncConfig<
4728
4739
 
4729
4740
  const openTransaction = getOpenTransaction()
4730
4741
  const needsPublicationAdmission =
4731
- openTransaction &&
4732
- !openTransaction.internal &&
4733
- openTransaction.beginOptions?.immediate
4742
+ openTransaction?.reservesCommitTurn === true
4734
4743
  const hydrated =
4735
4744
  sourceResultSettled && !needsPublicationAdmission
4736
4745
  ? runtime.loadHydratedSubset(options, (loadOptions) =>
@@ -4751,13 +4760,9 @@ function createWrappedSyncConfig<
4751
4760
  return
4752
4761
  }
4753
4762
  const currentTransaction = getOpenTransaction()
4754
- // An immediate source transaction reserves its FIFO turn before
4763
+ // A source transaction that reserved its FIFO turn commits before
4755
4764
  // subset hydration advances the generation it was built against.
4756
- if (
4757
- currentTransaction &&
4758
- !currentTransaction.internal &&
4759
- currentTransaction.beginOptions?.immediate
4760
- ) {
4765
+ if (currentTransaction?.reservesCommitTurn) {
4761
4766
  await waitForPublicationAdmission(
4762
4767
  currentTransaction,
4763
4768
  options,
@@ -2,12 +2,7 @@ import { IR } from '@tanstack/db'
2
2
  import type { LoadSubsetOptions } from '@tanstack/db'
3
3
 
4
4
  export type RemoteSubsetWirePrimitive =
5
- | undefined
6
- | null
7
- | boolean
8
- | string
9
- | number
10
- | bigint
5
+ undefined | null | boolean | string | number | bigint
11
6
 
12
7
  export type RemoteSubsetWireTypedArray =
13
8
  | Int8Array