@tanstack/db-sqlite-persistence-core 0.2.17 → 0.2.19

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
@@ -1,4 +1,5 @@
1
1
  import {
2
+ SyncTransactionAbortedError,
2
3
  compileSingleRowExpression,
3
4
  safeRandomUUID,
4
5
  toBooleanPredicate,
@@ -22,6 +23,7 @@ import type {
22
23
  InsertMutationFnParams,
23
24
  LoadSubsetOptions,
24
25
  PendingMutation,
26
+ SyncAppliedReceipt,
25
27
  SyncConfig,
26
28
  SyncConfigRes,
27
29
  SyncMetadataApi,
@@ -433,7 +435,7 @@ type SyncControlFns<T extends object, TKey extends string | number> = {
433
435
  | { type: `delete`; key: TKey },
434
436
  ) => void)
435
437
  | null
436
- commit: (() => void) | null
438
+ commit: ((signal?: AbortSignal) => SyncAppliedReceipt) | null
437
439
  truncate: (() => void) | null
438
440
  metadata: SyncMetadataApi<TKey> | null
439
441
  }
@@ -586,6 +588,9 @@ type BufferedSyncTransaction<T extends object, TKey extends string | number> = {
586
588
  >
587
589
  truncate: boolean
588
590
  internal: boolean
591
+ signal?: AbortSignal
592
+ resolveApplied?: () => void
593
+ rejectApplied?: (error: unknown) => void
589
594
  }
590
595
 
591
596
  type OpenSyncTransaction<
@@ -811,6 +816,8 @@ class PersistedCollectionRuntime<
811
816
  private startupMetadataPromise: Promise<void> | null = null
812
817
  private startPromise: Promise<void> | null = null
813
818
  private internalApplyDepth = 0
819
+ private appliedReceiptSequence = 0
820
+ private readonly pendingAppliedReceipts = new Map<number, Promise<void>>()
814
821
  private isHydrating = false
815
822
  private coordinatorUnsubscribe: (() => void) | null = null
816
823
  private indexAddedUnsubscribe: (() => void) | null = null
@@ -834,7 +841,32 @@ class PersistedCollectionRuntime<
834
841
  ) {}
835
842
 
836
843
  setSyncControls(syncControls: SyncControlFns<T, TKey>): void {
837
- this.syncControls = syncControls
844
+ const commit = syncControls.commit
845
+ this.syncControls = {
846
+ ...syncControls,
847
+ commit: commit
848
+ ? (signal) => this.trackAppliedReceipt(commit(signal))
849
+ : null,
850
+ }
851
+ }
852
+
853
+ private trackAppliedReceipt(receipt: SyncAppliedReceipt): SyncAppliedReceipt {
854
+ const sequence = ++this.appliedReceiptSequence
855
+ if (receipt === true) {
856
+ return true
857
+ }
858
+ this.pendingAppliedReceipts.set(sequence, receipt)
859
+ const removeReceipt = () => this.pendingAppliedReceipts.delete(sequence)
860
+ void receipt.then(removeReceipt, removeReceipt)
861
+ return receipt
862
+ }
863
+
864
+ private async waitForAppliedReceiptsAfter(cursor: number): Promise<void> {
865
+ await Promise.all(
866
+ Array.from(this.pendingAppliedReceipts, ([sequence, receipt]) =>
867
+ sequence > cursor ? receipt : undefined,
868
+ ),
869
+ )
838
870
  }
839
871
 
840
872
  clearSyncControls(): void {
@@ -906,9 +938,11 @@ class PersistedCollectionRuntime<
906
938
 
907
939
  if (this.syncMode !== `on-demand`) {
908
940
  this.activeSubsets.set(this.getSubsetKey({}), {})
941
+ const appliedCursor = this.appliedReceiptSequence
909
942
  await this.applyMutex.run(() =>
910
943
  this.hydrateSubsetUnsafe({}, { requestRemoteEnsure: false }),
911
944
  )
945
+ await this.waitForAppliedReceiptsAfter(appliedCursor)
912
946
  }
913
947
  }
914
948
 
@@ -985,17 +1019,19 @@ class PersistedCollectionRuntime<
985
1019
  ): Promise<void> {
986
1020
  this.activeSubsets.set(this.getSubsetKey(options), options)
987
1021
 
1022
+ const appliedCursor = this.appliedReceiptSequence
988
1023
  await this.applyMutex.run(() =>
989
1024
  this.hydrateSubsetUnsafe(options, {
990
1025
  requestRemoteEnsure: this.mode === `sync-present`,
991
1026
  }),
992
1027
  )
1028
+ await this.waitForAppliedReceiptsAfter(appliedCursor)
993
1029
 
994
1030
  if (upstreamLoadSubset) {
995
1031
  try {
996
1032
  const maybePromise = upstreamLoadSubset(options)
997
1033
  if (maybePromise instanceof Promise) {
998
- maybePromise.catch((error) => {
1034
+ await maybePromise.catch((error) => {
999
1035
  console.warn(
1000
1036
  `Failed to load remote subset in persisted wrapper:`,
1001
1037
  error,
@@ -1156,15 +1192,18 @@ class PersistedCollectionRuntime<
1156
1192
 
1157
1193
  this.pendingRemoteSubsetEnsures.clear()
1158
1194
  this.activeSubsets.clear()
1195
+ for (const transaction of this.queuedHydrationTransactions) {
1196
+ transaction.rejectApplied?.(new SyncTransactionAbortedError())
1197
+ }
1159
1198
  this.queuedHydrationTransactions.length = 0
1160
1199
  this.queuedTxCommitted.length = 0
1161
1200
  this.clearSyncControls()
1162
1201
  }
1163
1202
 
1164
- private withInternalApply(task: () => void): void {
1203
+ private withInternalApply<TResult>(task: () => TResult): TResult {
1165
1204
  this.internalApplyDepth++
1166
1205
  try {
1167
- task()
1206
+ return task()
1168
1207
  } finally {
1169
1208
  this.internalApplyDepth--
1170
1209
  }
@@ -1311,36 +1350,48 @@ class PersistedCollectionRuntime<
1311
1350
  if (!transaction) {
1312
1351
  continue
1313
1352
  }
1314
- await this.applyBufferedSyncTransactionUnsafe(transaction)
1353
+ try {
1354
+ await this.applyBufferedSyncTransactionUnsafe(transaction)
1355
+ } catch (error) {
1356
+ transaction.rejectApplied?.(error)
1357
+ for (const abandoned of this.queuedHydrationTransactions) {
1358
+ abandoned.rejectApplied?.(error)
1359
+ }
1360
+ this.queuedHydrationTransactions.length = 0
1361
+ throw error
1362
+ }
1315
1363
  }
1316
1364
  }
1317
1365
 
1318
1366
  private async applyBufferedSyncTransactionUnsafe(
1319
1367
  transaction: BufferedSyncTransaction<T, TKey>,
1320
1368
  ): Promise<void> {
1321
- if (
1322
- !this.syncControls.begin ||
1323
- !this.syncControls.write ||
1324
- !this.syncControls.commit
1325
- ) {
1369
+ if (transaction.signal?.aborted) {
1370
+ transaction.rejectApplied?.(new SyncTransactionAbortedError())
1326
1371
  return
1327
1372
  }
1328
1373
 
1329
- const applyToCollection = () => {
1330
- this.syncControls.begin?.()
1374
+ const { begin, write, commit, truncate, metadata } = this.syncControls
1375
+ if (!begin || !write || !commit) {
1376
+ transaction.rejectApplied?.(new SyncTransactionAbortedError())
1377
+ return
1378
+ }
1379
+
1380
+ const applyToCollection = (): SyncAppliedReceipt => {
1381
+ begin()
1331
1382
 
1332
1383
  if (transaction.truncate) {
1333
- this.syncControls.truncate?.()
1384
+ truncate?.()
1334
1385
  }
1335
1386
 
1336
1387
  for (const operation of transaction.operations) {
1337
1388
  if (operation.type === `delete`) {
1338
- this.syncControls.write?.({
1389
+ write({
1339
1390
  type: `delete`,
1340
1391
  key: operation.key,
1341
1392
  })
1342
1393
  } else {
1343
- this.syncControls.write?.({
1394
+ write({
1344
1395
  type: `update`,
1345
1396
  value: operation.value,
1346
1397
  metadata: operation.metadata,
@@ -1350,30 +1401,39 @@ class PersistedCollectionRuntime<
1350
1401
 
1351
1402
  for (const [key, metadataWrite] of transaction.rowMetadataWrites) {
1352
1403
  if (metadataWrite.type === `delete`) {
1353
- this.syncControls.metadata?.row.delete(key)
1404
+ metadata?.row.delete(key)
1354
1405
  } else {
1355
- this.syncControls.metadata?.row.set(key, metadataWrite.value)
1406
+ metadata?.row.set(key, metadataWrite.value)
1356
1407
  }
1357
1408
  }
1358
1409
 
1359
1410
  for (const [key, metadataWrite] of transaction.collectionMetadataWrites) {
1360
1411
  if (metadataWrite.type === `delete`) {
1361
- this.syncControls.metadata?.collection.delete(key)
1412
+ metadata?.collection.delete(key)
1362
1413
  } else {
1363
- this.syncControls.metadata?.collection.set(key, metadataWrite.value)
1414
+ metadata?.collection.set(key, metadataWrite.value)
1364
1415
  }
1365
1416
  }
1366
1417
 
1367
- this.syncControls.commit?.()
1418
+ return commit(transaction.signal)
1368
1419
  }
1369
1420
 
1370
- if (transaction.internal) {
1371
- this.withInternalApply(applyToCollection)
1372
- return
1373
- }
1421
+ try {
1422
+ const applied = transaction.internal
1423
+ ? this.withInternalApply(applyToCollection)
1424
+ : applyToCollection()
1425
+ if (applied !== true) {
1426
+ await applied
1427
+ }
1374
1428
 
1375
- applyToCollection()
1376
- await this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction)
1429
+ if (!transaction.internal) {
1430
+ await this.persistAndBroadcastExternalSyncTransactionUnsafe(transaction)
1431
+ }
1432
+ transaction.resolveApplied?.()
1433
+ } catch (error) {
1434
+ transaction.rejectApplied?.(error)
1435
+ throw error
1436
+ }
1377
1437
  }
1378
1438
 
1379
1439
  private async persistAndBroadcastExternalSyncTransactionUnsafe(
@@ -2457,14 +2517,25 @@ function createWrappedSyncConfig<
2457
2517
  params.truncate()
2458
2518
  }
2459
2519
  },
2460
- commit: () => {
2520
+ commit: (signal?: AbortSignal) => {
2461
2521
  const openTransaction = transactionStack.pop()
2462
2522
  if (!openTransaction) {
2463
- params.commit()
2464
- return
2523
+ return params.commit(signal)
2465
2524
  }
2466
2525
 
2467
2526
  if (openTransaction.queuedBecauseHydrating) {
2527
+ if (signal?.aborted) {
2528
+ const aborted = Promise.reject(new SyncTransactionAbortedError())
2529
+ void aborted.catch(() => undefined)
2530
+ return aborted
2531
+ }
2532
+ let resolveApplied!: () => void
2533
+ let rejectApplied!: (error: unknown) => void
2534
+ const applied = new Promise<void>((resolve, reject) => {
2535
+ resolveApplied = resolve
2536
+ rejectApplied = reject
2537
+ })
2538
+ void applied.catch(() => undefined)
2468
2539
  runtime.queueHydrationBufferedTransaction({
2469
2540
  operations: openTransaction.operations,
2470
2541
  rowMetadataWrites: openTransaction.rowMetadataWrites,
@@ -2472,14 +2543,18 @@ function createWrappedSyncConfig<
2472
2543
  openTransaction.collectionMetadataWrites,
2473
2544
  truncate: openTransaction.truncate,
2474
2545
  internal: openTransaction.internal,
2546
+ signal,
2547
+ resolveApplied,
2548
+ rejectApplied,
2475
2549
  })
2476
- return
2550
+ return applied
2477
2551
  }
2478
2552
 
2479
- params.commit()
2553
+ const applied = params.commit(signal)
2480
2554
  if (!openTransaction.internal) {
2481
- void runtime
2482
- .persistAndBroadcastExternalSyncTransaction({
2555
+ const persistAfterApplication = async () => {
2556
+ if (applied !== true) await applied
2557
+ await runtime.persistAndBroadcastExternalSyncTransaction({
2483
2558
  operations: openTransaction.operations,
2484
2559
  rowMetadataWrites: openTransaction.rowMetadataWrites,
2485
2560
  collectionMetadataWrites:
@@ -2487,13 +2562,12 @@ function createWrappedSyncConfig<
2487
2562
  truncate: openTransaction.truncate,
2488
2563
  internal: false,
2489
2564
  })
2490
- .catch((error) => {
2491
- console.warn(
2492
- `Failed to persist wrapped sync transaction:`,
2493
- error,
2494
- )
2495
- })
2565
+ }
2566
+ const persisted = persistAfterApplication()
2567
+ void persisted.catch(() => undefined)
2568
+ return persisted
2496
2569
  }
2570
+ return applied
2497
2571
  },
2498
2572
  }
2499
2573