@tanstack/electric-db-collection 0.4.4 → 0.4.6

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/electric.ts CHANGED
@@ -47,6 +47,7 @@ import type {
47
47
  DeleteMutationFnParams,
48
48
  InsertMutationFnParams,
49
49
  LoadSubsetOptions,
50
+ SyncAppliedReceipt,
50
51
  SyncConfig,
51
52
  SyncMode,
52
53
  UpdateMutationFnParams,
@@ -526,6 +527,8 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
526
527
  begin,
527
528
  write,
528
529
  commit,
530
+ getCommitCursor,
531
+ waitForCommitsAfter,
529
532
  collectionId,
530
533
  encodeColumnName,
531
534
  signal,
@@ -539,7 +542,9 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
539
542
  value: T
540
543
  metadata: Record<string, unknown>
541
544
  }) => void
542
- commit: () => void
545
+ commit: (signal?: AbortSignal) => SyncAppliedReceipt
546
+ getCommitCursor: () => number
547
+ waitForCommitsAfter: (cursor: number) => Promise<void>
543
548
  collectionId?: string
544
549
  /**
545
550
  * Optional function to encode column names (e.g., camelCase to snake_case).
@@ -573,6 +578,7 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
573
578
  }
574
579
 
575
580
  const loadSubset = async (opts: LoadSubsetOptions) => {
581
+ const commitCursor = getCommitCursor()
576
582
  if (opts.signal?.aborted) return
577
583
 
578
584
  if (isBufferingInitialSync()) {
@@ -593,7 +599,7 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
593
599
  metadata: { ...row.headers },
594
600
  })
595
601
  }
596
- commit()
602
+ await commit(opts.signal)
597
603
  debug(`${logPrefix}Applied snapshot with ${rows.length} rows`)
598
604
  }
599
605
  } catch (error) {
@@ -691,6 +697,7 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
691
697
  }
692
698
  throw error
693
699
  }
700
+ await waitForCommitsAfter(commitCursor)
694
701
  }
695
702
 
696
703
  return new DeduplicatedLoadSubset({ loadSubset })
@@ -1487,13 +1494,33 @@ function createElectricSync<T extends Row<unknown>>(
1487
1494
  const {
1488
1495
  begin,
1489
1496
  write,
1490
- commit,
1497
+ commit: commitSyncTransaction,
1491
1498
  markReady,
1492
1499
  markError,
1493
1500
  truncate,
1494
1501
  collection,
1495
1502
  metadata,
1496
1503
  } = params
1504
+ let commitSequence = 0
1505
+ const pendingAppliedReceipts = new Map<number, Promise<void>>()
1506
+ const commit = (signal?: AbortSignal): SyncAppliedReceipt => {
1507
+ const sequence = ++commitSequence
1508
+ const applied = commitSyncTransaction(signal)
1509
+ if (applied === true) {
1510
+ return true
1511
+ }
1512
+ pendingAppliedReceipts.set(sequence, applied)
1513
+ const removeReceipt = () => pendingAppliedReceipts.delete(sequence)
1514
+ void applied.then(removeReceipt, removeReceipt)
1515
+ return applied
1516
+ }
1517
+ const waitForCommitsAfter = async (cursor: number): Promise<void> => {
1518
+ await Promise.all(
1519
+ Array.from(pendingAppliedReceipts, ([sequence, applied]) =>
1520
+ sequence > cursor ? applied : undefined,
1521
+ ),
1522
+ )
1523
+ }
1497
1524
  const readPersistedResumeState = (): ElectricResumeState | undefined => {
1498
1525
  const persistedResumeState = metadata?.collection.get(`electric:resume`)
1499
1526
  return parseElectricResumeState(persistedResumeState)
@@ -1518,7 +1545,13 @@ function createElectricSync<T extends Row<unknown>>(
1518
1545
 
1519
1546
  // Wrap markReady to wait for test hook in progressive mode
1520
1547
  let progressiveReadyGate: Promise<void> | null = null
1521
- const wrappedMarkReady = (isBuffering: boolean) => {
1548
+ let streamErrorVersion = 0
1549
+ const wrappedMarkReady = (
1550
+ isBuffering: boolean,
1551
+ expectedErrorVersion = streamErrorVersion,
1552
+ ) => {
1553
+ if (streamErrorVersion !== expectedErrorVersion) return
1554
+
1522
1555
  // Only create gate if we're in buffering phase (first up-to-date)
1523
1556
  if (
1524
1557
  isBuffering &&
@@ -1528,7 +1561,9 @@ function createElectricSync<T extends Row<unknown>>(
1528
1561
  // Create a new gate promise for this sync cycle
1529
1562
  progressiveReadyGate = testHooks.beforeMarkingReady()
1530
1563
  progressiveReadyGate.then(() => {
1531
- markReady()
1564
+ if (streamErrorVersion === expectedErrorVersion) {
1565
+ markReady()
1566
+ }
1532
1567
  })
1533
1568
  } else {
1534
1569
  // No hook, not buffering, or already past first up-to-date
@@ -1583,6 +1618,7 @@ function createElectricSync<T extends Row<unknown>>(
1583
1618
  (canUsePersistedResume ? persistedResumeState.handle : undefined),
1584
1619
  signal: abortController.signal,
1585
1620
  onError: (errorParams) => {
1621
+ streamErrorVersion++
1586
1622
  // Note that Electric sends a 409 error on a `must-refetch` message, but the
1587
1623
  // ShapeStream handled this and it will not reach this handler, therefor
1588
1624
  // this handler will not run for a `must-refetch`.
@@ -1730,6 +1766,8 @@ function createElectricSync<T extends Row<unknown>>(
1730
1766
  begin,
1731
1767
  write,
1732
1768
  commit,
1769
+ getCommitCursor: () => commitSequence,
1770
+ waitForCommitsAfter,
1733
1771
  collectionId,
1734
1772
  // Pass the columnMapper's encode function to transform column names
1735
1773
  // (e.g., camelCase to snake_case) when compiling SQL for subset queries
@@ -1892,6 +1930,8 @@ function createElectricSync<T extends Row<unknown>>(
1892
1930
  }
1893
1931
 
1894
1932
  if (commitPoint !== null) {
1933
+ let applied: SyncAppliedReceipt = true
1934
+ const wasBufferingInitialSync = isBufferingInitialSync()
1895
1935
  // PROGRESSIVE MODE: Atomic swap on first up-to-date (not subset-end)
1896
1936
  // EXCEPTION: Skip atomic swap if a transaction is already started (e.g., from must-refetch).
1897
1937
  // In that case, do a normal commit to properly close the existing transaction.
@@ -1946,7 +1986,7 @@ function createElectricSync<T extends Row<unknown>>(
1946
1986
 
1947
1987
  // Commit the atomic swap
1948
1988
  stageResumeMetadata()
1949
- commit()
1989
+ applied = commit()
1950
1990
 
1951
1991
  // Exit buffering phase by marking that we've received up-to-date
1952
1992
  // isBufferingInitialSync() will now return false
@@ -1960,15 +2000,24 @@ function createElectricSync<T extends Row<unknown>>(
1960
2000
  // Both up-to-date and subset-end trigger a commit
1961
2001
  if (transactionStarted) {
1962
2002
  stageResumeMetadata()
1963
- commit()
2003
+ applied = commit()
1964
2004
  transactionStarted = false
1965
2005
  } else if (commitPoint === `up-to-date` && metadata) {
1966
2006
  begin()
1967
2007
  stageResumeMetadata()
1968
- commit()
2008
+ applied = commit()
1969
2009
  }
1970
2010
  }
1971
- wrappedMarkReady(isBufferingInitialSync())
2011
+ const readyErrorVersion = streamErrorVersion
2012
+ if (applied === true) {
2013
+ wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion)
2014
+ } else {
2015
+ void applied.then(
2016
+ () =>
2017
+ wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion),
2018
+ () => undefined,
2019
+ )
2020
+ }
1972
2021
 
1973
2022
  // Track that we've received the first up-to-date for progressive mode
1974
2023
  if (commitPoint === `up-to-date`) {