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

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/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 {
@@ -22,10 +24,11 @@ import {
22
24
  PersistedCollectionDurabilityError,
23
25
  toPersistedCollectionDurabilityError,
24
26
  } from './errors'
25
- import { serializeSQLiteBigInt } from './sqlite-value'
27
+ import { serializeSQLiteBigInt, serializeSQLiteTemporal } from './sqlite-value'
26
28
  import {
27
29
  toProcessLocalLoadSubsetOptions,
28
30
  toTransportedLoadSubsetOptions,
31
+ validateRemoteSubsetOptions,
29
32
  } from './remote-subset-wire'
30
33
  import {
31
34
  reportRemoteSubsetOwnerError,
@@ -289,8 +292,7 @@ export type PersistedRowMetadataMutation<
289
292
  > = { type: `set`; key: TKey; value: unknown } | { type: `delete`; key: TKey }
290
293
 
291
294
  export type PersistedCollectionMetadataMutation =
292
- | { type: `set`; key: string; value: unknown }
293
- | { type: `delete`; key: string }
295
+ { type: `set`; key: string; value: unknown } | { type: `delete`; key: string }
294
296
 
295
297
  export type ReplayableTxDelta<
296
298
  T extends Record<string, unknown> = Record<string, unknown>,
@@ -542,6 +544,12 @@ export interface PersistedCollectionCoordinator {
542
544
  collectionId: string,
543
545
  mutations: Array<PersistedMutationEnvelope>,
544
546
  ) => Promise<ApplyLocalMutationsResponse>
547
+ /**
548
+ * Applies a committed transaction through the supplied adapter when present.
549
+ * Wrappers must forward this leader-local loan: scheduling it again can wait
550
+ * on the hydration that is awaiting this call. Never serialize or retain the
551
+ * loan after the call; ordinary work uses the registered collection adapter.
552
+ */
545
553
  requestApplyCommittedTx: (
546
554
  collectionId: string,
547
555
  tx: PersistedTx,
@@ -738,7 +746,7 @@ const DEFAULT_DB_NAME = `tanstack-db`
738
746
  const REMOTE_ENSURE_RETRY_DELAY_MS = 50
739
747
 
740
748
  type SyncControlFns<T extends object, TKey extends string | number> = {
741
- begin: ((options?: { immediate?: boolean }) => void) | null
749
+ begin: (() => void) | null
742
750
  write:
743
751
  | ((
744
752
  message:
@@ -886,8 +894,9 @@ export class SingleProcessCoordinator implements PersistedCollectionCoordinator
886
894
  public async requestApplyCommittedTx(
887
895
  collectionId: string,
888
896
  tx: PersistedTx,
897
+ scopedAdapter?: HydrationPersistenceAdapter,
889
898
  ): Promise<ApplyCommittedTxResponse> {
890
- const adapter = this.collectionAdapters.get(collectionId)
899
+ const adapter = scopedAdapter ?? this.collectionAdapters.get(collectionId)
891
900
  if (!adapter) {
892
901
  throw new InvalidPersistedCollectionConfigError(
893
902
  `SingleProcessCoordinator has no persistence adapter configured for collection "${collectionId}"`,
@@ -1046,7 +1055,6 @@ type BufferedSyncTransaction<T extends object, TKey extends string | number> = {
1046
1055
  truncate: boolean
1047
1056
  internal: boolean
1048
1057
  lifecycleGeneration: number
1049
- beginOptions?: { immediate?: boolean }
1050
1058
  expectedResumeGenerationOwner?: symbol
1051
1059
  signal?: AbortSignal
1052
1060
  prependHydrationRows: (
@@ -1066,6 +1074,8 @@ type OpenSyncTransaction<T extends object, TKey extends string | number> = Omit<
1066
1074
  applicationReceipt?: SyncAppliedReceipt
1067
1075
  operationKeys: Set<TKey>
1068
1076
  queuedBecauseHydrating: boolean
1077
+ // Set by `reserveCommitTurn`: subset hydration waits for this commit.
1078
+ reservesCommitTurn?: boolean
1069
1079
  hasDependentSuccessor: boolean
1070
1080
  publicationAdmissionWaiters?: Set<{
1071
1081
  resolve: () => void
@@ -1150,6 +1160,9 @@ function toStableSerializable(value: unknown): unknown {
1150
1160
  return undefined
1151
1161
  }
1152
1162
 
1163
+ const temporal = serializeSQLiteTemporal(value)
1164
+ if (temporal) return temporal
1165
+
1153
1166
  if (value instanceof Date) {
1154
1167
  return value.toISOString()
1155
1168
  }
@@ -1326,15 +1339,13 @@ class PersistedCollectionRuntime<
1326
1339
  private resumeGenerationOwner = Symbol(`persisted resume generation owner`)
1327
1340
  private lifecycleGeneration = 0
1328
1341
  private internalApplyDepth = 0
1329
- private sourcePublicationWaitDepth = 0
1330
1342
  private appliedReceiptSequence = 0
1331
1343
  private syncErrorReported = false
1332
1344
  private reportedSyncError: unknown
1333
1345
  private readonly pendingAppliedReceipts = new Map<number, Promise<void>>()
1334
1346
  private hydratingGeneration: number | null = null
1335
1347
  private terminalFailure:
1336
- | { lifecycleGeneration: number; error: unknown }
1337
- | undefined
1348
+ { lifecycleGeneration: number; error: unknown } | undefined
1338
1349
  private coordinatorUnsubscribe: (() => void) | null = null
1339
1350
  private remoteSubsetOwnerUnsubscribe: (() => void) | null = null
1340
1351
  private indexAddedUnsubscribe: (() => void) | null = null
@@ -1409,10 +1420,20 @@ class PersistedCollectionRuntime<
1409
1420
  return receipt
1410
1421
  }
1411
1422
 
1412
- private async waitForAppliedReceiptsAfter(cursor: number): Promise<void> {
1423
+ // Startup waits for acceptance: a local mutation's handler can wait for
1424
+ // startup, and the rows publish when that mutation settles. Readiness waits
1425
+ // for them in core. A subset load waits for visibility.
1426
+ private async waitForAppliedReceiptsAfter(
1427
+ cursor: number,
1428
+ until: `accepted` | `visible` = `visible`,
1429
+ ): Promise<void> {
1413
1430
  await Promise.all(
1414
1431
  Array.from(this.pendingAppliedReceipts, ([sequence, receipt]) =>
1415
- sequence > cursor ? receipt : undefined,
1432
+ sequence <= cursor
1433
+ ? undefined
1434
+ : until === `accepted`
1435
+ ? whenSyncAccepted(receipt)
1436
+ : receipt,
1416
1437
  ),
1417
1438
  )
1418
1439
  }
@@ -1623,7 +1644,10 @@ class PersistedCollectionRuntime<
1623
1644
  startup.appliedCursor !== undefined &&
1624
1645
  lifecycleGeneration === this.lifecycleGeneration
1625
1646
  ) {
1626
- await this.waitForAppliedReceiptsAfter(startup.appliedCursor)
1647
+ await this.waitForAppliedReceiptsAfter(
1648
+ startup.appliedCursor,
1649
+ `accepted`,
1650
+ )
1627
1651
  }
1628
1652
  }
1629
1653
  if (lifecycleGeneration === this.lifecycleGeneration) {
@@ -1664,7 +1688,7 @@ class PersistedCollectionRuntime<
1664
1688
  appliedCursor !== undefined &&
1665
1689
  lifecycleGeneration === this.lifecycleGeneration
1666
1690
  ) {
1667
- await this.waitForAppliedReceiptsAfter(appliedCursor)
1691
+ await this.waitForAppliedReceiptsAfter(appliedCursor, `accepted`)
1668
1692
  }
1669
1693
  })().catch((error) => {
1670
1694
  throw this.markTerminalFailure(error, lifecycleGeneration)
@@ -1717,7 +1741,6 @@ class PersistedCollectionRuntime<
1717
1741
  await this.hydrateSubsetUnsafe(
1718
1742
  baseline,
1719
1743
  {
1720
- requestRemoteEnsure: false,
1721
1744
  lifecycleGeneration,
1722
1745
  bindKeySetEvidence: true,
1723
1746
  },
@@ -1795,7 +1818,7 @@ class PersistedCollectionRuntime<
1795
1818
  const applied = this.replaceCollectionMetadataSnapshot(
1796
1819
  snapshot.collectionMetadata,
1797
1820
  )
1798
- if (applied !== true) await applied
1821
+ await whenSyncAccepted(applied)
1799
1822
  }
1800
1823
 
1801
1824
  private async loadCollectionMetadataSnapshot(
@@ -1827,7 +1850,7 @@ class PersistedCollectionRuntime<
1827
1850
  .map(({ key }) => key)
1828
1851
 
1829
1852
  return this.withInternalApply(() => {
1830
- this.syncControls.begin?.({ immediate: true })
1853
+ this.syncControls.begin?.()
1831
1854
 
1832
1855
  currentKeys.forEach((key) => {
1833
1856
  if (!nextMetadata.has(key)) {
@@ -1853,6 +1876,8 @@ class PersistedCollectionRuntime<
1853
1876
  const truncateGeneration = this.sourceTruncateGeneration
1854
1877
  const routeRemoteDemandDuringHydration =
1855
1878
  this.canRouteRemoteDemandThroughCoordinator()
1879
+ // Wire admission failures are permanent input errors, not transport retries.
1880
+ if (routeRemoteDemandDuringHydration) validateRemoteSubsetOptions(options)
1856
1881
  this.activeSubsets.set(subsetKey, options)
1857
1882
  const appliedCursor = this.appliedReceiptSequence
1858
1883
  try {
@@ -1862,9 +1887,6 @@ class PersistedCollectionRuntime<
1862
1887
  this.hydrateSubsetUnsafe(
1863
1888
  options,
1864
1889
  {
1865
- requestRemoteEnsure:
1866
- this.mode === `sync-present` &&
1867
- !routeRemoteDemandDuringHydration,
1868
1890
  lifecycleGeneration,
1869
1891
  requestLocalLoadFailure: true,
1870
1892
  rejectBufferedReplayFailure: true,
@@ -1883,6 +1905,14 @@ class PersistedCollectionRuntime<
1883
1905
  })
1884
1906
  if (lifecycleGeneration !== this.lifecycleGeneration) return
1885
1907
  await this.waitForAppliedReceiptsAfter(appliedCursor)
1908
+ // Leadership can change while hydration waits. Validate newly remote
1909
+ // demand inside the admission cleanup boundary, before retry can own it.
1910
+ if (
1911
+ !routeRemoteDemandDuringHydration &&
1912
+ this.canRouteRemoteDemandThroughCoordinator()
1913
+ ) {
1914
+ validateRemoteSubsetOptions(options)
1915
+ }
1886
1916
  } catch (error) {
1887
1917
  if (this.activeSubsets.get(subsetKey) === options) {
1888
1918
  this.activeSubsets.delete(subsetKey)
@@ -2043,7 +2073,6 @@ class PersistedCollectionRuntime<
2043
2073
  this.hydrateSubsetUnsafe(
2044
2074
  options,
2045
2075
  {
2046
- requestRemoteEnsure: false,
2047
2076
  lifecycleGeneration,
2048
2077
  rejectBufferedReplayFailure: true,
2049
2078
  },
@@ -2072,16 +2101,6 @@ class PersistedCollectionRuntime<
2072
2101
  ): Promise<void> {
2073
2102
  const failure = this.getCurrentTerminalFailure()
2074
2103
  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
2104
  return this.applyMutex.run(async () => {
2086
2105
  await this.applyBufferedSyncTransactionUnsafe(transaction)
2087
2106
  })
@@ -2326,7 +2345,6 @@ class PersistedCollectionRuntime<
2326
2345
  private async hydrateSubsetUnsafe(
2327
2346
  options: LoadSubsetOptions,
2328
2347
  config: {
2329
- requestRemoteEnsure: boolean
2330
2348
  lifecycleGeneration: number
2331
2349
  bindKeySetEvidence?: boolean
2332
2350
  requestLocalLoadFailure?: boolean
@@ -2373,7 +2391,7 @@ class PersistedCollectionRuntime<
2373
2391
  hydrationContext.suppliedRowKeys.add(row.key)
2374
2392
  }
2375
2393
  const applied = this.applyRowsToCollection(rows)
2376
- if (applied !== true) await applied
2394
+ await whenSyncAccepted(applied)
2377
2395
  }
2378
2396
  } finally {
2379
2397
  if (
@@ -2387,10 +2405,6 @@ class PersistedCollectionRuntime<
2387
2405
  if (config.lifecycleGeneration !== this.lifecycleGeneration) return
2388
2406
  replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter)
2389
2407
  if (config.lifecycleGeneration !== this.lifecycleGeneration) return
2390
-
2391
- if (config.requestRemoteEnsure && !replayFailure) {
2392
- this.queueRemoteSubsetEnsure(options)
2393
- }
2394
2408
  } catch (error) {
2395
2409
  if (config.requestLocalLoadFailure && !rowsLoaded) {
2396
2410
  // Keep admitting source work to the hydration queue while recovery
@@ -2433,8 +2447,7 @@ class PersistedCollectionRuntime<
2433
2447
  adapter: HydrationPersistenceAdapter,
2434
2448
  ): Promise<void> {
2435
2449
  let snapshotRows:
2436
- | Map<TKey, { key: TKey; value: T; metadata?: unknown }>
2437
- | undefined
2450
+ Map<TKey, { key: TKey; value: T; metadata?: unknown }> | undefined
2438
2451
  type RecoveryPresence = `present` | `absent` | `unknown`
2439
2452
  const recoveredPresence = new Map<TKey, RecoveryPresence>()
2440
2453
  let snapshotInvalidatedByTruncate = false
@@ -2570,7 +2583,7 @@ class PersistedCollectionRuntime<
2570
2583
  }
2571
2584
 
2572
2585
  return this.withInternalApply(() => {
2573
- this.syncControls.begin?.({ immediate: true })
2586
+ this.syncControls.begin?.()
2574
2587
 
2575
2588
  for (const row of rows) {
2576
2589
  if (this.collection?._hasHydratedKey(row.key)) {
@@ -2668,7 +2681,7 @@ class PersistedCollectionRuntime<
2668
2681
  .map(({ key }) => key)
2669
2682
 
2670
2683
  return this.withInternalApply(() => {
2671
- this.syncControls.begin?.({ immediate: true })
2684
+ this.syncControls.begin?.()
2672
2685
  this.syncControls.truncate?.()
2673
2686
 
2674
2687
  for (const row of rows) {
@@ -2739,14 +2752,17 @@ class PersistedCollectionRuntime<
2739
2752
  : transaction.applyToCollection()
2740
2753
  applicationReturned = true
2741
2754
  abortedDuringApplication = transaction.signal?.aborted === true
2742
- if (applied !== true) {
2743
- this.sourcePublicationWaitDepth++
2744
- try {
2745
- await applied
2746
- } finally {
2747
- this.sourcePublicationWaitDepth--
2748
- }
2749
- }
2755
+ // Inside the apply mutex, internal work waits only for acceptance: a
2756
+ // local mutation's handler may need this mutex, and the rows publish
2757
+ // when that mutation settles. A source transaction is stored once it is
2758
+ // visible, except one held by a persisting optimistic transaction,
2759
+ // which is stored now so a handler can await its acceptance.
2760
+ if (transaction.internal) await whenSyncAccepted(applied)
2761
+ else if (
2762
+ applied !== true &&
2763
+ !this.collection?._state.hasPersistingTransaction()
2764
+ )
2765
+ await applied
2750
2766
  this.throwIfLifecycleReplaced(transaction.lifecycleGeneration)
2751
2767
 
2752
2768
  if (!transaction.internal) {
@@ -2972,7 +2988,7 @@ class PersistedCollectionRuntime<
2972
2988
  }
2973
2989
 
2974
2990
  const applied = this.withInternalApply(() => {
2975
- this.syncControls.begin?.({ immediate: true })
2991
+ this.syncControls.begin?.()
2976
2992
 
2977
2993
  for (const mutation of mutations) {
2978
2994
  if (mutation.type === `delete`) {
@@ -2991,9 +3007,10 @@ class PersistedCollectionRuntime<
2991
3007
  return this.syncControls.commit?.() ?? true
2992
3008
  })
2993
3009
 
2994
- if (applied !== true) {
2995
- await applied
2996
- }
3010
+ // The mutation handler awaits this confirmation, so wait only for
3011
+ // acceptance: the rows become visible when the mutation settles.
3012
+ const accepted = whenSyncAccepted(applied)
3013
+ if (accepted !== true) await accepted
2997
3014
  }
2998
3015
 
2999
3016
  private filterMutationsForCollection(
@@ -3256,6 +3273,9 @@ class PersistedCollectionRuntime<
3256
3273
  const subsetKey = this.getSubsetKey(options)
3257
3274
  if (this.activeSubsets.get(subsetKey) !== options) return
3258
3275
 
3276
+ // A previously local acquisition can become remote after an ownership
3277
+ // change. Recovery must reject permanent wire errors before retry owns it.
3278
+ validateRemoteSubsetOptions(options)
3259
3279
  this.pendingRemoteSubsetEnsures.set(subsetKey, options)
3260
3280
  void this.flushPendingRemoteSubsetEnsures()
3261
3281
  }
@@ -3545,11 +3565,11 @@ class PersistedCollectionRuntime<
3545
3565
  this.resetSequence++
3546
3566
  if (this.syncControls.begin && this.syncControls.commit) {
3547
3567
  const applied = this.withInternalApply(() => {
3548
- this.syncControls.begin?.({ immediate: true })
3568
+ this.syncControls.begin?.()
3549
3569
  this.syncControls.truncate?.()
3550
3570
  return this.syncControls.commit?.() ?? true
3551
3571
  })
3552
- if (applied !== true) await applied
3572
+ await whenSyncAccepted(applied)
3553
3573
  }
3554
3574
 
3555
3575
  if (lifecycleGeneration !== this.lifecycleGeneration) return
@@ -3600,7 +3620,7 @@ class PersistedCollectionRuntime<
3600
3620
  )
3601
3621
 
3602
3622
  const applied = this.withInternalApply(() => {
3603
- this.syncControls.begin?.({ immediate: true })
3623
+ this.syncControls.begin?.()
3604
3624
 
3605
3625
  for (const {
3606
3626
  key: changedKey,
@@ -3646,7 +3666,7 @@ class PersistedCollectionRuntime<
3646
3666
 
3647
3667
  return this.syncControls.commit?.() ?? true
3648
3668
  })
3649
- if (applied !== true) await applied
3669
+ await whenSyncAccepted(applied)
3650
3670
  }
3651
3671
 
3652
3672
  private async reloadActiveSubsetsUnsafe(
@@ -3692,7 +3712,7 @@ class PersistedCollectionRuntime<
3692
3712
  })),
3693
3713
  collectionMetadata,
3694
3714
  )
3695
- if (applied !== true) await applied
3715
+ await whenSyncAccepted(applied)
3696
3716
  } finally {
3697
3717
  if (
3698
3718
  this.activeHydrationContext === hydrationContext &&
@@ -3802,9 +3822,8 @@ class PersistedCollectionRuntime<
3802
3822
  indexMetadata: CollectionIndexMetadata,
3803
3823
  adapter: HydrationPersistenceAdapter,
3804
3824
  ): Promise<boolean> {
3805
- const spec = this.buildPersistedIndexSpec(indexMetadata)
3806
-
3807
3825
  try {
3826
+ const spec = this.buildPersistedIndexSpec(indexMetadata)
3808
3827
  await adapter.ensureIndex(
3809
3828
  this.collectionId,
3810
3829
  indexMetadata.signature,
@@ -3821,9 +3840,8 @@ class PersistedCollectionRuntime<
3821
3840
  indexMetadata: CollectionIndexMetadata,
3822
3841
  completedLocally: boolean,
3823
3842
  ): Promise<void> {
3824
- const spec = this.buildPersistedIndexSpec(indexMetadata)
3825
-
3826
3843
  try {
3844
+ const spec = this.buildPersistedIndexSpec(indexMetadata)
3827
3845
  await this.persistence.coordinator.requestEnsurePersistedIndex(
3828
3846
  this.collectionId,
3829
3847
  indexMetadata.signature,
@@ -3934,6 +3952,19 @@ function createWrappedSyncConfig<
3934
3952
  ) => {
3935
3953
  removePendingPublicationTransaction(transaction)
3936
3954
  }
3955
+ // A source receipt is accepted once its rows are durable, and resolves
3956
+ // when core makes them visible.
3957
+ const visibleAfterDurable = (
3958
+ transaction: OpenSyncTransaction<T, TKey>,
3959
+ durable: Promise<void>,
3960
+ ): Promise<void> => {
3961
+ const visible = durable.then(async () => {
3962
+ const receipt = transaction.applicationReceipt
3963
+ if (receipt !== undefined && receipt !== true) await receipt
3964
+ })
3965
+ void visible.catch(() => undefined)
3966
+ return withAcceptedReceipt(visible, durable)
3967
+ }
3937
3968
  const settleRuntimeTransaction = (
3938
3969
  transaction: OpenSyncTransaction<T, TKey>,
3939
3970
  applied: Promise<void>,
@@ -4072,14 +4103,7 @@ function createWrappedSyncConfig<
4072
4103
  }
4073
4104
  transaction.deferredHydrationMetadataDeleteKeys.clear()
4074
4105
 
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
- )
4106
+ params.begin()
4083
4107
  if (transaction.truncate) params.truncate()
4084
4108
  for (const operation of transaction.operations) {
4085
4109
  if (operation.type === `delete`) {
@@ -4106,14 +4130,10 @@ function createWrappedSyncConfig<
4106
4130
 
4107
4131
  const applied = params.commit(signal)
4108
4132
  transaction.applicationReceipt = applied
4109
- if (applied === true) {
4110
- removePendingPublicationTransaction(transaction)
4111
- } else {
4112
- void applied.then(
4113
- () => removePendingPublicationTransaction(transaction),
4114
- () => removePendingPublicationTransaction(transaction),
4115
- )
4116
- }
4133
+ // Core accepted the transaction, so it is no longer staged here;
4134
+ // the receipt resolves when it is visible.
4135
+ removePendingPublicationTransaction(transaction)
4136
+ if (applied !== true) void applied.catch(() => undefined)
4117
4137
  return applied
4118
4138
  } catch (error) {
4119
4139
  removePendingPublicationTransaction(transaction)
@@ -4135,6 +4155,11 @@ function createWrappedSyncConfig<
4135
4155
  const persistenceCapability: SyncPersistenceCapabilityV1<TKey> = {
4136
4156
  protocol: SYNC_PERSISTENCE_PROTOCOL,
4137
4157
  version: SYNC_PERSISTENCE_VERSION,
4158
+ reserveCommitTurn: () => {
4159
+ const openTransaction = getOpenTransaction()
4160
+ if (openTransaction && !openTransaction.internal)
4161
+ openTransaction.reservesCommitTurn = true
4162
+ },
4138
4163
  hydrateBaseline: async () => {
4139
4164
  if (startupState.cleanedUp) return
4140
4165
  try {
@@ -4192,7 +4217,7 @@ function createWrappedSyncConfig<
4192
4217
  })
4193
4218
  .catch(() => undefined)
4194
4219
  },
4195
- begin: (options?: { immediate?: boolean }) => {
4220
+ begin: () => {
4196
4221
  if (startupState.cleanedUp) return undefined
4197
4222
  const terminalFailure = getTerminalFailure()
4198
4223
  const internal = runtime.isApplyingInternally()
@@ -4205,7 +4230,6 @@ function createWrappedSyncConfig<
4205
4230
  truncate: false,
4206
4231
  internal,
4207
4232
  lifecycleGeneration: runtime.getLifecycleGeneration(),
4208
- beginOptions: options,
4209
4233
  operationKeys: new Set(),
4210
4234
  hasDependentSuccessor: false,
4211
4235
  queuedBecauseHydrating:
@@ -4522,7 +4546,6 @@ function createWrappedSyncConfig<
4522
4546
  truncate: openTransaction.truncate,
4523
4547
  internal: false,
4524
4548
  lifecycleGeneration: openTransaction.lifecycleGeneration,
4525
- beginOptions: openTransaction.beginOptions,
4526
4549
  expectedResumeGenerationOwner:
4527
4550
  openTransaction.expectedResumeGenerationOwner,
4528
4551
  signal,
@@ -4576,9 +4599,12 @@ function createWrappedSyncConfig<
4576
4599
  })
4577
4600
  settlePublicationAdmissionWaiters(openTransaction)
4578
4601
  settleRuntimeTransaction(openTransaction, applied)
4579
- return applied
4602
+ return visibleAfterDurable(openTransaction, applied)
4580
4603
  }
4581
4604
 
4605
+ // Admission is complete. Only queued hydration replay remains tied
4606
+ // to its sequence; retain hydrationContext for row metadata ownership.
4607
+ openTransaction.hydrationSequence = undefined
4582
4608
  let applied: Promise<void>
4583
4609
  try {
4584
4610
  applied = runtime.applyHydrationBufferedTransaction(transaction)
@@ -4588,7 +4614,7 @@ function createWrappedSyncConfig<
4588
4614
  }
4589
4615
  settlePublicationAdmissionWaiters(openTransaction)
4590
4616
  settleRuntimeTransaction(openTransaction, applied)
4591
- return applied
4617
+ return visibleAfterDurable(openTransaction, applied)
4592
4618
  },
4593
4619
  }
4594
4620
 
@@ -4728,9 +4754,7 @@ function createWrappedSyncConfig<
4728
4754
 
4729
4755
  const openTransaction = getOpenTransaction()
4730
4756
  const needsPublicationAdmission =
4731
- openTransaction &&
4732
- !openTransaction.internal &&
4733
- openTransaction.beginOptions?.immediate
4757
+ openTransaction?.reservesCommitTurn === true
4734
4758
  const hydrated =
4735
4759
  sourceResultSettled && !needsPublicationAdmission
4736
4760
  ? runtime.loadHydratedSubset(options, (loadOptions) =>
@@ -4751,13 +4775,9 @@ function createWrappedSyncConfig<
4751
4775
  return
4752
4776
  }
4753
4777
  const currentTransaction = getOpenTransaction()
4754
- // An immediate source transaction reserves its FIFO turn before
4778
+ // A source transaction that reserved its FIFO turn commits before
4755
4779
  // subset hydration advances the generation it was built against.
4756
- if (
4757
- currentTransaction &&
4758
- !currentTransaction.internal &&
4759
- currentTransaction.beginOptions?.immediate
4760
- ) {
4780
+ if (currentTransaction?.reservesCommitTurn) {
4761
4781
  await waitForPublicationAdmission(
4762
4782
  currentTransaction,
4763
4783
  options,