@tanstack/powersync-db-collection 0.2.12 → 1.0.0-rc.1

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/powersync.ts CHANGED
@@ -325,7 +325,22 @@ function createPowerSyncCollectionConfig<
325
325
  */
326
326
  const sync: SyncConfig<OutputType, string> = {
327
327
  sync: (params) => {
328
- const { begin, write, collection, commit, markReady, markError } = params
328
+ const {
329
+ begin,
330
+ write,
331
+ collection,
332
+ commit,
333
+ fail,
334
+ reportError,
335
+ reportHealthy,
336
+ } = params
337
+ // Readiness of this sync run: a reload runs `sync` again while the
338
+ // Collection still shows the previous run's status.
339
+ let ready = false
340
+ const markReady = () => {
341
+ ready = true
342
+ params.markReady()
343
+ }
329
344
  const abortController = new AbortController()
330
345
 
331
346
  let disposeTracking:
@@ -409,6 +424,9 @@ function createPowerSyncCollectionConfig<
409
424
  async function createDiffTrigger(
410
425
  options: {
411
426
  setupContext?: LockContext
427
+ // The load reads every row and replaces the rows the Collection
428
+ // holds.
429
+ snapshot?: boolean
412
430
  when: Record<DiffTriggerOperation, string>
413
431
  writeType: (rowId: string) => OperationType
414
432
  batchQuery: (
@@ -419,7 +437,7 @@ function createPowerSyncCollectionConfig<
419
437
  },
420
438
  appliedReceipts: Array<SyncAppliedReceipt>,
421
439
  ) {
422
- const { setupContext, when, writeType, batchQuery } = options
440
+ const { setupContext, snapshot, when, writeType, batchQuery } = options
423
441
 
424
442
  return await database.triggers.createDiffTrigger({
425
443
  source: viewName,
@@ -429,11 +447,25 @@ function createPowerSyncCollectionConfig<
429
447
  when,
430
448
  hooks: {
431
449
  beforeCreate: async (context) => {
450
+ // A load of every row is one snapshot transaction: during a
451
+ // reload it replaces the kept rows, so a row the table no
452
+ // longer has goes. A subset load commits each batch. A
453
+ // transaction begins only once its rows are read, so a batch
454
+ // that fails leaves none open.
455
+ const batches: Array<Array<TableType>> = []
456
+ const apply = () => {
457
+ begin({ snapshot })
458
+ for (const row of batches.splice(0).flat()) {
459
+ write({
460
+ type: writeType(row.id),
461
+ value: deserializeSyncRow(row),
462
+ })
463
+ }
464
+ appliedReceipts.push(commit())
465
+ }
432
466
  let currentBatchCount = syncBatchSize
433
467
  let cursor = 0
434
468
  while (currentBatchCount == syncBatchSize) {
435
- begin()
436
-
437
469
  const batchItems = await batchQuery(
438
470
  context,
439
471
  syncBatchSize,
@@ -441,14 +473,10 @@ function createPowerSyncCollectionConfig<
441
473
  )
442
474
  currentBatchCount = batchItems.length
443
475
  cursor += currentBatchCount
444
- for (const row of batchItems) {
445
- write({
446
- type: writeType(row.id),
447
- value: deserializeSyncRow(row),
448
- })
449
- }
450
- appliedReceipts.push(commit())
476
+ batches.push(batchItems)
477
+ if (!snapshot) apply()
451
478
  }
479
+ if (snapshot) apply()
452
480
  database.logger.log({
453
481
  level: LogLevels.info,
454
482
  message: `Sync is ready for ${viewName} into ${trackedTableName}`,
@@ -597,6 +625,9 @@ function createPowerSyncCollectionConfig<
597
625
  const appliedReceipts: Array<SyncAppliedReceipt> = []
598
626
  await establishTracking(
599
627
  {
628
+ // When the Collection holds rows, such as a reload's kept rows,
629
+ // the load of every row replaces them.
630
+ snapshot: !collection._state.acceptedSyncedEntries().next().done,
600
631
  when: {
601
632
  [DiffTriggerOperation.INSERT]: `TRUE`,
602
633
  [DiffTriggerOperation.UPDATE]: `TRUE`,
@@ -625,9 +656,7 @@ function createPowerSyncCollectionConfig<
625
656
  message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
626
657
  error,
627
658
  })
628
- if (collection.status === `loading`) {
629
- markError(error)
630
- }
659
+ if (!ready) fail(error)
631
660
  })
632
661
 
633
662
  const invokeOnUnload = (): void | Promise<void> => {
@@ -733,6 +762,8 @@ function createPowerSyncCollectionConfig<
733
762
 
734
763
  // One reconciliation owns every queued revision so callers cannot
735
764
  // settle against a stale trigger configuration.
765
+ // Set while a failed rebuild may have left the source untracked.
766
+ let trackingOutage = false
736
767
  const reconcileTracking = async (): Promise<void> => {
737
768
  while (!stopped && reconciledTrackingRevision !== trackingRevision) {
738
769
  const revision = trackingRevision
@@ -747,12 +778,11 @@ function createPowerSyncCollectionConfig<
747
778
  if (!isCurrent()) return
748
779
 
749
780
  const active = activeWhereExpressions()
750
- // Tracking was absent during an error. Positive baseline rows
781
+ // Tracking was absent during an outage. Positive baseline rows
751
782
  // alone cannot reveal deletes or predicate exits from that gap.
752
- const missing =
753
- collection.status === `error`
754
- ? new Set(collection.keys())
755
- : undefined
783
+ const missing = trackingOutage
784
+ ? new Set(collection.keys())
785
+ : undefined
756
786
  if (active.length > 0) {
757
787
  const combinedWhere =
758
788
  active.length === 1
@@ -808,9 +838,12 @@ function createPowerSyncCollectionConfig<
808
838
  await Promise.all(appliedReceipts)
809
839
  if (isCurrent()) {
810
840
  reconciledTrackingRevision = revision
811
- // Replacing the trigger alone is not recovery: its baseline
812
- // writes must also be applied before the source is ready again.
813
- if (collection.status === `error`) markReady()
841
+ // Replacing the trigger alone does not restore change tracking:
842
+ // its baseline writes must also apply before sync works again.
843
+ if (trackingOutage) {
844
+ trackingOutage = false
845
+ reportHealthy()
846
+ }
814
847
  }
815
848
  }
816
849
  }
@@ -821,9 +854,15 @@ function createPowerSyncCollectionConfig<
821
854
  while (!stopped && reconciledTrackingRevision !== trackingRevision) {
822
855
  await (rebuildPromise ??= reconcileTracking()
823
856
  .catch((error) => {
824
- // A rebuild may already have removed every active diff trigger.
825
- // Do not leave healthy consumers ready against a stale source.
826
- if (!stopped) markError(error)
857
+ // A rebuild may already have removed every active diff trigger,
858
+ // so rows can grow stale. Later demand rebuilds again, so after
859
+ // Collection readiness this is a failureReason, not fail().
860
+ if (stopped) {
861
+ // Cleanup owns the outcome.
862
+ } else if (ready) {
863
+ trackingOutage = true
864
+ reportError(error)
865
+ } else fail(error)
827
866
  throw error
828
867
  })
829
868
  .finally(() => {