@tanstack/electric-db-collection 0.4.10 → 0.5.2
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/dist/cjs/electric.cjs +146 -103
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +94 -25
- package/dist/cjs/sql-compiler.cjs +43 -92
- package/dist/cjs/sql-compiler.cjs.map +1 -1
- package/dist/esm/electric.d.ts +94 -25
- package/dist/esm/electric.js +147 -104
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/sql-compiler.js +43 -92
- package/dist/esm/sql-compiler.js.map +1 -1
- package/package.json +3 -3
- package/src/electric.ts +295 -166
- package/src/sql-compiler.ts +57 -134
package/src/electric.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
DeduplicatedLoadSubset,
|
|
10
10
|
LoadSubsetOperationAbortedError,
|
|
11
11
|
and,
|
|
12
|
+
validateSyncPersistenceCapability,
|
|
13
|
+
warnOnce,
|
|
12
14
|
withCollectionConfigFactory,
|
|
13
15
|
withCollectionSyncConfigCleanup,
|
|
14
16
|
withCollectionSyncConfigFactory,
|
|
@@ -51,10 +53,8 @@ import type {
|
|
|
51
53
|
LoadSubsetOptions,
|
|
52
54
|
SyncAppliedReceipt,
|
|
53
55
|
SyncConfig,
|
|
54
|
-
SyncMetadataApi,
|
|
55
56
|
SyncMode,
|
|
56
57
|
UpdateMutationFnParams,
|
|
57
|
-
UtilsRecord,
|
|
58
58
|
} from '@tanstack/db'
|
|
59
59
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
60
60
|
import type {
|
|
@@ -67,21 +67,11 @@ import type {
|
|
|
67
67
|
ShapeStreamOptions,
|
|
68
68
|
} from '@electric-sql/client'
|
|
69
69
|
|
|
70
|
-
type ElectricSyncMetadataWithHydration = SyncMetadataApi<string | number> & {
|
|
71
|
-
row: SyncMetadataApi<string | number>[`row`] & {
|
|
72
|
-
whenHydrated?: () => Promise<void>
|
|
73
|
-
// Capability marker for wrappers predating the hydration barrier.
|
|
74
|
-
scanPersisted?: unknown
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
70
|
// Re-export for user convenience in custom match functions
|
|
79
71
|
export { isChangeMessage, isControlMessage } from '@electric-sql/client'
|
|
80
72
|
|
|
81
73
|
const debug = DebugModule.debug(`ts/db:electric`)
|
|
82
74
|
|
|
83
|
-
const FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS = 250
|
|
84
|
-
|
|
85
75
|
/**
|
|
86
76
|
* Symbol for internal test hooks (hidden from public API)
|
|
87
77
|
*/
|
|
@@ -299,11 +289,11 @@ export type MatchFunction<T extends Row<unknown>> = (
|
|
|
299
289
|
/**
|
|
300
290
|
* Matching strategies for Electric synchronization
|
|
301
291
|
* Handlers can return:
|
|
302
|
-
* - Txid strategy: { txid: number | number[], timeout?: number } (
|
|
292
|
+
* - Txid strategy: { txid: number | number[], timeout?: number } (deprecated compatibility behavior)
|
|
303
293
|
* - Void (no return value) - mutation completes without waiting
|
|
304
294
|
*
|
|
305
295
|
* The optional timeout property specifies how long to wait for the txid(s) in milliseconds.
|
|
306
|
-
* If not specified, defaults to
|
|
296
|
+
* If not specified, defaults to 15000ms.
|
|
307
297
|
*/
|
|
308
298
|
export type MatchingStrategy = {
|
|
309
299
|
txid: Txid | Array<Txid>
|
|
@@ -352,13 +342,7 @@ export interface ElectricCollectionConfig<
|
|
|
352
342
|
T extends Row<unknown> = Row<unknown>,
|
|
353
343
|
TSchema extends StandardSchemaV1 = never,
|
|
354
344
|
> extends Omit<
|
|
355
|
-
BaseCollectionConfig<
|
|
356
|
-
T,
|
|
357
|
-
string | number,
|
|
358
|
-
TSchema,
|
|
359
|
-
ElectricCollectionUtils<T>,
|
|
360
|
-
any
|
|
361
|
-
>,
|
|
345
|
+
BaseCollectionConfig<T, string | number, TSchema, ElectricCollectionUtils<T>>,
|
|
362
346
|
`onInsert` | `onUpdate` | `onDelete` | `syncMode`
|
|
363
347
|
> {
|
|
364
348
|
/**
|
|
@@ -375,43 +359,82 @@ export interface ElectricCollectionConfig<
|
|
|
375
359
|
|
|
376
360
|
/**
|
|
377
361
|
* Optional asynchronous handler function called before an insert operation
|
|
362
|
+
*
|
|
363
|
+
* **IMPORTANT - Electric Synchronization:**
|
|
364
|
+
* This handler **must not resolve** until synchronization is confirmed.
|
|
365
|
+
* Await one of these synchronization utilities before the handler completes:
|
|
366
|
+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
|
|
367
|
+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
|
|
368
|
+
*
|
|
369
|
+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
|
|
370
|
+
*
|
|
378
371
|
* @param params Object containing transaction and collection information
|
|
379
|
-
* @returns Promise
|
|
372
|
+
* @returns Promise that should resolve after synchronization is complete
|
|
373
|
+
*
|
|
374
|
+
* **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
|
|
375
|
+
* Use `await collection.utils.awaitTxId(txid)` within the handler instead.
|
|
376
|
+
*
|
|
380
377
|
* @example
|
|
381
|
-
* //
|
|
382
|
-
* onInsert: async ({ transaction }) => {
|
|
378
|
+
* // Recommended: Wait for txid to sync
|
|
379
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
383
380
|
* const newItem = transaction.mutations[0].modified
|
|
384
381
|
* const result = await api.todos.create({
|
|
385
382
|
* data: newItem
|
|
386
383
|
* })
|
|
387
|
-
*
|
|
384
|
+
* // Wait for txid to sync before handler completes
|
|
385
|
+
* await collection.utils.awaitTxId(result.txid)
|
|
388
386
|
* }
|
|
389
387
|
*
|
|
390
388
|
* @example
|
|
391
389
|
* // Insert handler with custom timeout
|
|
392
|
-
* onInsert: async ({ transaction }) => {
|
|
390
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
393
391
|
* const newItem = transaction.mutations[0].modified
|
|
394
392
|
* const result = await api.todos.create({
|
|
395
393
|
* data: newItem
|
|
396
394
|
* })
|
|
397
|
-
*
|
|
395
|
+
* // Wait up to 10 seconds for txid
|
|
396
|
+
* await collection.utils.awaitTxId(result.txid, 10000)
|
|
398
397
|
* }
|
|
399
398
|
*
|
|
400
399
|
* @example
|
|
401
|
-
* // Insert handler with
|
|
402
|
-
* onInsert: async ({ transaction }) => {
|
|
400
|
+
* // Insert handler with timeout error handling
|
|
401
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
402
|
+
* const newItem = transaction.mutations[0].modified
|
|
403
|
+
* const result = await api.todos.create({
|
|
404
|
+
* data: newItem
|
|
405
|
+
* })
|
|
406
|
+
*
|
|
407
|
+
* try {
|
|
408
|
+
* await collection.utils.awaitTxId(result.txid, 5000)
|
|
409
|
+
* } catch (error) {
|
|
410
|
+
* // Decide sync timeout policy:
|
|
411
|
+
* // - Throw to rollback optimistic state
|
|
412
|
+
* // - Catch to keep optimistic state (eventual consistency)
|
|
413
|
+
* // - Schedule background retry
|
|
414
|
+
* console.warn('Sync timeout, keeping optimistic state:', error)
|
|
415
|
+
* // Don't throw - allow optimistic state to persist
|
|
416
|
+
* }
|
|
417
|
+
* }
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* // Insert handler with multiple items
|
|
421
|
+
* onInsert: async ({ transaction, collection }) => {
|
|
403
422
|
* const items = transaction.mutations.map(m => m.modified)
|
|
404
423
|
* const results = await Promise.all(
|
|
405
424
|
* items.map(item => api.todos.create({ data: item }))
|
|
406
425
|
* )
|
|
407
|
-
*
|
|
426
|
+
* // Wait for all txids to sync
|
|
427
|
+
* await Promise.all(
|
|
428
|
+
* results.map(r => collection.utils.awaitTxId(r.txid))
|
|
429
|
+
* )
|
|
408
430
|
* }
|
|
409
431
|
*
|
|
410
432
|
* @example
|
|
411
|
-
* // Use awaitMatch utility for custom matching
|
|
433
|
+
* // Alternative: Use awaitMatch utility for custom matching logic
|
|
412
434
|
* onInsert: async ({ transaction, collection }) => {
|
|
413
435
|
* const newItem = transaction.mutations[0].modified
|
|
414
436
|
* await api.todos.create({ data: newItem })
|
|
437
|
+
* // Wait for specific change to appear in sync stream
|
|
415
438
|
* await collection.utils.awaitMatch(
|
|
416
439
|
* (message) => isChangeMessage(message) &&
|
|
417
440
|
* message.headers.operation === 'insert' &&
|
|
@@ -429,24 +452,39 @@ export interface ElectricCollectionConfig<
|
|
|
429
452
|
|
|
430
453
|
/**
|
|
431
454
|
* Optional asynchronous handler function called before an update operation
|
|
455
|
+
*
|
|
456
|
+
* **IMPORTANT - Electric Synchronization:**
|
|
457
|
+
* This handler **must not resolve** until synchronization is confirmed.
|
|
458
|
+
* Await one of these synchronization utilities before the handler completes:
|
|
459
|
+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
|
|
460
|
+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
|
|
461
|
+
*
|
|
462
|
+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
|
|
463
|
+
*
|
|
432
464
|
* @param params Object containing transaction and collection information
|
|
433
|
-
* @returns Promise
|
|
465
|
+
* @returns Promise that should resolve after synchronization is complete
|
|
466
|
+
*
|
|
467
|
+
* **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
|
|
468
|
+
* Use `await collection.utils.awaitTxId(txid)` within the handler instead.
|
|
469
|
+
*
|
|
434
470
|
* @example
|
|
435
|
-
* //
|
|
436
|
-
* onUpdate: async ({ transaction }) => {
|
|
471
|
+
* // Recommended: Wait for txid to sync
|
|
472
|
+
* onUpdate: async ({ transaction, collection }) => {
|
|
437
473
|
* const { original, changes } = transaction.mutations[0]
|
|
438
474
|
* const result = await api.todos.update({
|
|
439
475
|
* where: { id: original.id },
|
|
440
476
|
* data: changes
|
|
441
477
|
* })
|
|
442
|
-
*
|
|
478
|
+
* // Wait for txid to sync before handler completes
|
|
479
|
+
* await collection.utils.awaitTxId(result.txid)
|
|
443
480
|
* }
|
|
444
481
|
*
|
|
445
482
|
* @example
|
|
446
|
-
* // Use awaitMatch utility for custom matching
|
|
483
|
+
* // Alternative: Use awaitMatch utility for custom matching logic
|
|
447
484
|
* onUpdate: async ({ transaction, collection }) => {
|
|
448
485
|
* const { original, changes } = transaction.mutations[0]
|
|
449
486
|
* await api.todos.update({ where: { id: original.id }, data: changes })
|
|
487
|
+
* // Wait for specific change to appear in sync stream
|
|
450
488
|
* await collection.utils.awaitMatch(
|
|
451
489
|
* (message) => isChangeMessage(message) &&
|
|
452
490
|
* message.headers.operation === 'update' &&
|
|
@@ -464,23 +502,38 @@ export interface ElectricCollectionConfig<
|
|
|
464
502
|
|
|
465
503
|
/**
|
|
466
504
|
* Optional asynchronous handler function called before a delete operation
|
|
505
|
+
*
|
|
506
|
+
* **IMPORTANT - Electric Synchronization:**
|
|
507
|
+
* This handler **must not resolve** until synchronization is confirmed.
|
|
508
|
+
* Await one of these synchronization utilities before the handler completes:
|
|
509
|
+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
|
|
510
|
+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
|
|
511
|
+
*
|
|
512
|
+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
|
|
513
|
+
*
|
|
467
514
|
* @param params Object containing transaction and collection information
|
|
468
|
-
* @returns Promise
|
|
515
|
+
* @returns Promise that should resolve after synchronization is complete
|
|
516
|
+
*
|
|
517
|
+
* **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0.
|
|
518
|
+
* Use `await collection.utils.awaitTxId(txid)` within the handler instead.
|
|
519
|
+
*
|
|
469
520
|
* @example
|
|
470
|
-
* //
|
|
471
|
-
* onDelete: async ({ transaction }) => {
|
|
521
|
+
* // Recommended: Wait for txid to sync
|
|
522
|
+
* onDelete: async ({ transaction, collection }) => {
|
|
472
523
|
* const mutation = transaction.mutations[0]
|
|
473
524
|
* const result = await api.todos.delete({
|
|
474
525
|
* id: mutation.original.id
|
|
475
526
|
* })
|
|
476
|
-
*
|
|
527
|
+
* // Wait for txid to sync before handler completes
|
|
528
|
+
* await collection.utils.awaitTxId(result.txid)
|
|
477
529
|
* }
|
|
478
530
|
*
|
|
479
531
|
* @example
|
|
480
|
-
* // Use awaitMatch utility for custom matching
|
|
532
|
+
* // Alternative: Use awaitMatch utility for custom matching logic
|
|
481
533
|
* onDelete: async ({ transaction, collection }) => {
|
|
482
534
|
* const mutation = transaction.mutations[0]
|
|
483
535
|
* await api.todos.delete({ id: mutation.original.id })
|
|
536
|
+
* // Wait for specific change to appear in sync stream
|
|
484
537
|
* await collection.utils.awaitMatch(
|
|
485
538
|
* (message) => isChangeMessage(message) &&
|
|
486
539
|
* message.headers.operation === 'delete' &&
|
|
@@ -599,6 +652,7 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
|
|
|
599
652
|
commit,
|
|
600
653
|
getCommitCursor,
|
|
601
654
|
waitForCommitsAfter,
|
|
655
|
+
recordSnapshotRow,
|
|
602
656
|
collectionId,
|
|
603
657
|
encodeColumnName,
|
|
604
658
|
signal,
|
|
@@ -615,6 +669,7 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
|
|
|
615
669
|
commit: (signal?: AbortSignal) => SyncAppliedReceipt
|
|
616
670
|
getCommitCursor: () => number
|
|
617
671
|
waitForCommitsAfter: (cursor: number) => Promise<void>
|
|
672
|
+
recordSnapshotRow?: (row: T, message: Message<T>) => void
|
|
618
673
|
collectionId?: string
|
|
619
674
|
/**
|
|
620
675
|
* Optional function to encode column names (e.g., camelCase to snake_case).
|
|
@@ -676,7 +731,9 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
|
|
|
676
731
|
metadata: { ...row.headers },
|
|
677
732
|
})
|
|
678
733
|
}
|
|
679
|
-
|
|
734
|
+
const applied = commit(opts.signal)
|
|
735
|
+
for (const row of rows) recordSnapshotRow?.(row.value, row)
|
|
736
|
+
if (applied !== true) await applied
|
|
680
737
|
debug(`${logPrefix}Applied snapshot with ${rows.length} rows`)
|
|
681
738
|
}
|
|
682
739
|
} catch (error) {
|
|
@@ -695,56 +752,6 @@ function createLoadSubsetDedupe<T extends Row<unknown>>({
|
|
|
695
752
|
|
|
696
753
|
const { cursor, where, orderBy, limit } = opts
|
|
697
754
|
|
|
698
|
-
// When the stream is already up-to-date, it may be in a long-poll wait.
|
|
699
|
-
// Forcing a disconnect-and-refresh ensures requestSnapshot gets a response
|
|
700
|
-
// from a fresh server round-trip rather than waiting for the current poll to end.
|
|
701
|
-
// Some native fetch implementations (notably React Native/Expo) may not abort
|
|
702
|
-
// long-poll requests promptly. Bound the wait so on-demand live queries don't
|
|
703
|
-
// remain loading until the long-poll naturally times out.
|
|
704
|
-
// If the refresh fails or times out, we fall through to requestSnapshot which
|
|
705
|
-
// still works.
|
|
706
|
-
if (stream.isUpToDate) {
|
|
707
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
708
|
-
const abortSignals = [signal, opts.signal].filter(
|
|
709
|
-
(candidate): candidate is AbortSignal => candidate !== undefined,
|
|
710
|
-
)
|
|
711
|
-
let rejectAbort: (reason: unknown) => void = () => {}
|
|
712
|
-
const aborted = new Promise<never>((_resolve, reject) => {
|
|
713
|
-
rejectAbort = reject
|
|
714
|
-
})
|
|
715
|
-
const abort = (event: Event) =>
|
|
716
|
-
rejectAbort(abortReason(event.currentTarget as AbortSignal))
|
|
717
|
-
for (const abortSignal of abortSignals) {
|
|
718
|
-
abortSignal.addEventListener(`abort`, abort, { once: true })
|
|
719
|
-
}
|
|
720
|
-
try {
|
|
721
|
-
await Promise.race([
|
|
722
|
-
stream.forceDisconnectAndRefresh(),
|
|
723
|
-
new Promise<void>((resolve) => {
|
|
724
|
-
timeoutId = setTimeout(
|
|
725
|
-
resolve,
|
|
726
|
-
FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS,
|
|
727
|
-
)
|
|
728
|
-
}),
|
|
729
|
-
aborted,
|
|
730
|
-
])
|
|
731
|
-
} catch (error) {
|
|
732
|
-
if (signal.aborted || opts.signal?.aborted) throw error
|
|
733
|
-
if (handleSnapshotError(error, `forceDisconnectAndRefresh`)) {
|
|
734
|
-
return
|
|
735
|
-
}
|
|
736
|
-
debug(
|
|
737
|
-
`${logPrefix}forceDisconnectAndRefresh failed, proceeding to requestSnapshot: %o`,
|
|
738
|
-
error,
|
|
739
|
-
)
|
|
740
|
-
} finally {
|
|
741
|
-
clearTimeout(timeoutId)
|
|
742
|
-
for (const abortSignal of abortSignals) {
|
|
743
|
-
abortSignal.removeEventListener(`abort`, abort)
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
755
|
throwIfAborted()
|
|
749
756
|
|
|
750
757
|
// Upstream limitation: ShapeStream.requestSnapshot() publishes its rows
|
|
@@ -815,7 +822,9 @@ export type AwaitMatchFn<T extends Row<unknown>> = (
|
|
|
815
822
|
*/
|
|
816
823
|
export interface ElectricCollectionUtils<
|
|
817
824
|
T extends Row<unknown> = Row<unknown>,
|
|
818
|
-
>
|
|
825
|
+
> {
|
|
826
|
+
// Keep this interface closed: extending UtilsRecord would make every
|
|
827
|
+
// nonexistent adapter utility appear as `any`.
|
|
819
828
|
awaitTxId: AwaitTxIdFn
|
|
820
829
|
awaitMatch: AwaitMatchFn<T>
|
|
821
830
|
}
|
|
@@ -930,7 +939,7 @@ class ElectricLifecycle<T extends Row<unknown>> {
|
|
|
930
939
|
|
|
931
940
|
private async awaitTxId(
|
|
932
941
|
txId: Txid,
|
|
933
|
-
timeout: number =
|
|
942
|
+
timeout: number = 15000,
|
|
934
943
|
): Promise<boolean> {
|
|
935
944
|
debug(
|
|
936
945
|
`${this.collectionId ? `[${this.collectionId}] ` : ``}awaitTxId called with txid %d`,
|
|
@@ -958,7 +967,7 @@ class ElectricLifecycle<T extends Row<unknown>> {
|
|
|
958
967
|
|
|
959
968
|
private async awaitMatch(
|
|
960
969
|
matchFn: MatchFunction<T>,
|
|
961
|
-
timeout: number =
|
|
970
|
+
timeout: number = 15000,
|
|
962
971
|
): Promise<boolean> {
|
|
963
972
|
debug(
|
|
964
973
|
`${this.collectionId ? `[${this.collectionId}] ` : ``}awaitMatch called with custom function`,
|
|
@@ -1137,17 +1146,32 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
1137
1146
|
* Process matching strategy and wait for synchronization
|
|
1138
1147
|
*/
|
|
1139
1148
|
const processMatchingStrategy = async (
|
|
1140
|
-
result:
|
|
1149
|
+
result: unknown,
|
|
1141
1150
|
waitForTxId: AwaitTxIdFn,
|
|
1142
1151
|
): Promise<void> => {
|
|
1152
|
+
const canHaveProperties =
|
|
1153
|
+
(typeof result === `object` && result !== null) ||
|
|
1154
|
+
typeof result === `function`
|
|
1155
|
+
|
|
1143
1156
|
// Only wait if result contains txid
|
|
1144
|
-
if (
|
|
1145
|
-
const
|
|
1157
|
+
if (canHaveProperties && `txid` in result) {
|
|
1158
|
+
const strategy = result as Exclude<MatchingStrategy, void>
|
|
1159
|
+
// Warn about deprecated return value pattern
|
|
1160
|
+
warnOnce(
|
|
1161
|
+
'electric-collection-txid-return',
|
|
1162
|
+
'[TanStack DB] DEPRECATED: Returning { txid } from mutation handlers is deprecated and will be removed in v1.0. ' +
|
|
1163
|
+
'Use `await collection.utils.awaitTxId(txid)` instead of returning { txid }. ' +
|
|
1164
|
+
'See migration guide: https://tanstack.com/db/latest/docs/collections/electric-collection#persistence-handlers--synchronization',
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
const timeout = strategy.timeout
|
|
1146
1168
|
// Handle both single txid and array of txids
|
|
1147
|
-
if (Array.isArray(
|
|
1148
|
-
await Promise.all(
|
|
1169
|
+
if (Array.isArray(strategy.txid)) {
|
|
1170
|
+
await Promise.all(
|
|
1171
|
+
strategy.txid.map((txid) => waitForTxId(txid, timeout)),
|
|
1172
|
+
)
|
|
1149
1173
|
} else {
|
|
1150
|
-
await waitForTxId(
|
|
1174
|
+
await waitForTxId(strategy.txid, timeout)
|
|
1151
1175
|
}
|
|
1152
1176
|
}
|
|
1153
1177
|
// If result is void/undefined, don't wait - mutation completes immediately
|
|
@@ -1162,6 +1186,7 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
1162
1186
|
}
|
|
1163
1187
|
|
|
1164
1188
|
// Create wrapper handlers for direct persistence operations that handle different matching strategies
|
|
1189
|
+
// These wrappers process deprecated return values but don't pass them through
|
|
1165
1190
|
const wrappedOnInsert = config.onInsert
|
|
1166
1191
|
? async (
|
|
1167
1192
|
params: InsertMutationFnParams<
|
|
@@ -1169,13 +1194,12 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
1169
1194
|
string | number,
|
|
1170
1195
|
ElectricCollectionUtils<T>
|
|
1171
1196
|
>,
|
|
1172
|
-
) => {
|
|
1197
|
+
): Promise<void> => {
|
|
1173
1198
|
const handlerResult = await config.onInsert!(params)
|
|
1174
1199
|
await processMatchingStrategy(
|
|
1175
1200
|
handlerResult,
|
|
1176
1201
|
getMutationAwaitTxId(params),
|
|
1177
1202
|
)
|
|
1178
|
-
return handlerResult
|
|
1179
1203
|
}
|
|
1180
1204
|
: undefined
|
|
1181
1205
|
|
|
@@ -1186,13 +1210,12 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
1186
1210
|
string | number,
|
|
1187
1211
|
ElectricCollectionUtils<T>
|
|
1188
1212
|
>,
|
|
1189
|
-
) => {
|
|
1213
|
+
): Promise<void> => {
|
|
1190
1214
|
const handlerResult = await config.onUpdate!(params)
|
|
1191
1215
|
await processMatchingStrategy(
|
|
1192
1216
|
handlerResult,
|
|
1193
1217
|
getMutationAwaitTxId(params),
|
|
1194
1218
|
)
|
|
1195
|
-
return handlerResult
|
|
1196
1219
|
}
|
|
1197
1220
|
: undefined
|
|
1198
1221
|
|
|
@@ -1203,13 +1226,12 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
1203
1226
|
string | number,
|
|
1204
1227
|
ElectricCollectionUtils<T>
|
|
1205
1228
|
>,
|
|
1206
|
-
) => {
|
|
1229
|
+
): Promise<void> => {
|
|
1207
1230
|
const handlerResult = await config.onDelete!(params)
|
|
1208
1231
|
await processMatchingStrategy(
|
|
1209
1232
|
handlerResult,
|
|
1210
1233
|
getMutationAwaitTxId(params),
|
|
1211
1234
|
)
|
|
1212
|
-
return handlerResult
|
|
1213
1235
|
}
|
|
1214
1236
|
: undefined
|
|
1215
1237
|
|
|
@@ -1308,7 +1330,6 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1308
1330
|
const { getLifecycle, syncMode, collectionId, testHooks } = options
|
|
1309
1331
|
|
|
1310
1332
|
let relationSchema: string | undefined
|
|
1311
|
-
let warnedUnverifiableResume = false
|
|
1312
1333
|
|
|
1313
1334
|
const createTagState = () => {
|
|
1314
1335
|
const tagCache = new Map<MoveTag, ParsedMoveTag>()
|
|
@@ -1679,11 +1700,16 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1679
1700
|
return parseElectricResumeState(persistedResumeState)
|
|
1680
1701
|
}
|
|
1681
1702
|
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const
|
|
1703
|
+
const persistence =
|
|
1704
|
+
metadata === undefined
|
|
1705
|
+
? null
|
|
1706
|
+
: validateSyncPersistenceCapability(metadata.persistence)
|
|
1707
|
+
const hydrateBaseline = persistence?.hydrateBaseline
|
|
1708
|
+
const resumeSnapshot = persistence?.resumeSnapshot
|
|
1709
|
+
const certifyResumeSnapshot = resumeSnapshot?.certify
|
|
1710
|
+
const getKeySetEvidence = resumeSnapshot?.getKeySetEvidence
|
|
1711
|
+
const expectCurrentCommit = resumeSnapshot?.expectCurrentCommit
|
|
1712
|
+
const persistedKeySetEvidence = getKeySetEvidence?.()
|
|
1687
1713
|
|
|
1688
1714
|
const persistedResumeState = getNewestElectricResumeState(
|
|
1689
1715
|
readPersistedResumeState(),
|
|
@@ -1696,32 +1722,26 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1696
1722
|
const hasIncompatiblePersistedResume =
|
|
1697
1723
|
persistedResumeState?.kind === `resume` &&
|
|
1698
1724
|
persistedResumeState.shapeId !== shapeIdentity
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1725
|
+
// A pre-ledger `unknown` baseline cannot justify a non-initial cursor.
|
|
1726
|
+
// One fresh replacement establishes consistent evidence for later resumes.
|
|
1727
|
+
const lacksCompletePersistedKeySet =
|
|
1702
1728
|
persistedResumeState?.kind === `resume` &&
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
if (hasUnverifiablePersistedResume && !warnedUnverifiableResume) {
|
|
1706
|
-
warnedUnverifiableResume = true
|
|
1707
|
-
console.warn(
|
|
1708
|
-
`Electric persistence cannot verify hydration for saved resume state. Update the persistence adapter alongside Electric to enable safe resume.`,
|
|
1709
|
-
)
|
|
1710
|
-
}
|
|
1729
|
+
getKeySetEvidence !== undefined &&
|
|
1730
|
+
persistedKeySetEvidence?.status !== `consistent`
|
|
1711
1731
|
const needsFullSnapshot =
|
|
1712
1732
|
shapeOptions.offset === undefined &&
|
|
1713
1733
|
shapeOptions.handle === undefined &&
|
|
1714
1734
|
persistedResumeState !== undefined &&
|
|
1715
1735
|
(persistedResumeState.kind === `reset` ||
|
|
1736
|
+
lacksCompletePersistedKeySet ||
|
|
1716
1737
|
(!retainsTagState && persistedResumeState.requiresTagState !== false))
|
|
1717
1738
|
const canUsePersistedResume =
|
|
1718
1739
|
shapeOptions.offset === undefined &&
|
|
1719
1740
|
shapeOptions.handle === undefined &&
|
|
1720
1741
|
persistedResumeState?.kind === `resume` &&
|
|
1721
1742
|
!hasIncompatiblePersistedResume &&
|
|
1722
|
-
!hasUnverifiablePersistedResume &&
|
|
1723
1743
|
// Cached rows do not contain authoritative tag/active-condition state.
|
|
1724
|
-
//
|
|
1744
|
+
// Only a complete adapter ledger can justify a persisted cursor.
|
|
1725
1745
|
!needsFullSnapshot
|
|
1726
1746
|
const hasExplicitResumeOffset =
|
|
1727
1747
|
shapeOptions.offset !== undefined && shapeOptions.offset !== `-1`
|
|
@@ -1729,6 +1749,8 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1729
1749
|
clearTagTrackingState()
|
|
1730
1750
|
}
|
|
1731
1751
|
const receivesCompleteRows = shapeOptions.params?.replica === `full`
|
|
1752
|
+
const requiresKeySetCertification =
|
|
1753
|
+
canUsePersistedResume && certifyResumeSnapshot !== undefined
|
|
1732
1754
|
// Eager and progressive streams that start after the initial offset can
|
|
1733
1755
|
// only apply partial updates when the local materialization is complete.
|
|
1734
1756
|
const requiresCompleteResume =
|
|
@@ -1741,7 +1763,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1741
1763
|
(syncMode === `eager` || needsFullSnapshot) &&
|
|
1742
1764
|
!canUsePersistedResume &&
|
|
1743
1765
|
!hasExplicitResumeOffset &&
|
|
1744
|
-
|
|
1766
|
+
hydrateBaseline !== undefined
|
|
1745
1767
|
|
|
1746
1768
|
// Wrap markReady to wait for test hook in progressive mode
|
|
1747
1769
|
let progressiveReadyGate: Promise<void> | null = null
|
|
@@ -1856,6 +1878,54 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1856
1878
|
!hasReceivedUpToDate &&
|
|
1857
1879
|
!isResettingSnapshot
|
|
1858
1880
|
const bufferedMessages: Array<Message<T>> = [] // Buffer change messages during initial sync
|
|
1881
|
+
// Progressive subset rows are provisional: the initial atomic swap
|
|
1882
|
+
// normally discards them. Retain one only when a later stream update
|
|
1883
|
+
// needs that full row as its baseline.
|
|
1884
|
+
const progressiveSnapshotRows = new Map<string | number, Message<T>>()
|
|
1885
|
+
|
|
1886
|
+
// Presence spans the open source transaction and persisted callbacks
|
|
1887
|
+
// whose FIFO receipts have not applied yet. Later callbacks must see
|
|
1888
|
+
// those staged writes even though persistence has not replayed them into
|
|
1889
|
+
// core. Once the newest receipt applies, syncedData is authoritative.
|
|
1890
|
+
const pendingPresence = new Map<string | number, boolean>()
|
|
1891
|
+
let usesBaseline = true
|
|
1892
|
+
let pendingPresenceRevision = 0
|
|
1893
|
+
|
|
1894
|
+
const recordPendingPresence = (
|
|
1895
|
+
rowId: string | number,
|
|
1896
|
+
present: boolean,
|
|
1897
|
+
) => {
|
|
1898
|
+
pendingPresenceRevision++
|
|
1899
|
+
pendingPresence.set(rowId, present)
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
const recordPendingTruncate = () => {
|
|
1903
|
+
pendingPresenceRevision++
|
|
1904
|
+
pendingPresence.clear()
|
|
1905
|
+
progressiveSnapshotRows.clear()
|
|
1906
|
+
usesBaseline = false
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
const commitSourceTransaction = (): SyncAppliedReceipt => {
|
|
1910
|
+
const committedPresenceRevision = pendingPresenceRevision
|
|
1911
|
+
const applied = commit()
|
|
1912
|
+
if (metadata?.persistence) {
|
|
1913
|
+
const retireAppliedPresence = () => {
|
|
1914
|
+
if (pendingPresenceRevision === committedPresenceRevision) {
|
|
1915
|
+
pendingPresence.clear()
|
|
1916
|
+
usesBaseline = true
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (applied === true) retireAppliedPresence()
|
|
1920
|
+
else void applied.then(retireAppliedPresence, () => undefined)
|
|
1921
|
+
}
|
|
1922
|
+
return applied
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const beginSourceTransaction = () => {
|
|
1926
|
+
if (metadata?.persistence) begin({ immediate: true })
|
|
1927
|
+
else begin()
|
|
1928
|
+
}
|
|
1859
1929
|
|
|
1860
1930
|
// Track keys that have been synced to handle overlapping subset queries.
|
|
1861
1931
|
// When multiple subset queries return the same row, the server sends `insert`
|
|
@@ -1886,7 +1956,9 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1886
1956
|
metadata?.collection.set(`electric:resume`, resumeState)
|
|
1887
1957
|
}
|
|
1888
1958
|
|
|
1889
|
-
const commitResetResumeMetadataImmediately = (
|
|
1959
|
+
const commitResetResumeMetadataImmediately = (
|
|
1960
|
+
expectInResumeSnapshot = false,
|
|
1961
|
+
) => {
|
|
1890
1962
|
const resetState: ElectricResumeState = {
|
|
1891
1963
|
kind: `reset`,
|
|
1892
1964
|
updatedAt: Date.now(),
|
|
@@ -1896,16 +1968,21 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1896
1968
|
if (metadata) {
|
|
1897
1969
|
begin({ immediate: true })
|
|
1898
1970
|
metadata.collection.set(`electric:resume`, resetState)
|
|
1971
|
+
if (expectInResumeSnapshot) {
|
|
1972
|
+
expectCurrentCommit?.()
|
|
1973
|
+
}
|
|
1899
1974
|
commit()
|
|
1900
1975
|
}
|
|
1901
1976
|
}
|
|
1902
1977
|
|
|
1903
1978
|
if (
|
|
1904
1979
|
hasIncompatiblePersistedResume ||
|
|
1905
|
-
hasUnverifiablePersistedResume ||
|
|
1906
1980
|
(needsFullSnapshot && persistedResumeState.kind === `resume`)
|
|
1907
1981
|
) {
|
|
1908
|
-
|
|
1982
|
+
// This reset is part of the current runtime's startup decision. The
|
|
1983
|
+
// persisted wrapper may commit it before loading the atomic baseline,
|
|
1984
|
+
// so carry ownership of exactly this generation into certification.
|
|
1985
|
+
commitResetResumeMetadataImmediately(true)
|
|
1909
1986
|
}
|
|
1910
1987
|
|
|
1911
1988
|
/**
|
|
@@ -1976,6 +2053,10 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1976
2053
|
commit,
|
|
1977
2054
|
getCommitCursor: () => commitSequence,
|
|
1978
2055
|
waitForCommitsAfter,
|
|
2056
|
+
recordSnapshotRow: (row, message) => {
|
|
2057
|
+
recordPendingPresence(collection.getKeyFromItem(row), true)
|
|
2058
|
+
progressiveSnapshotRows.set(collection.getKeyFromItem(row), message)
|
|
2059
|
+
},
|
|
1979
2060
|
collectionId,
|
|
1980
2061
|
// Pass the columnMapper's encode function to transform column names
|
|
1981
2062
|
// (e.g., camelCase to snake_case) when compiling SQL for subset queries
|
|
@@ -1986,8 +2067,31 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1986
2067
|
|
|
1987
2068
|
const resumeKeysPromise =
|
|
1988
2069
|
requiresCompleteResume || freshSnapshotPending
|
|
1989
|
-
?
|
|
1990
|
-
|
|
2070
|
+
? hydrateBaseline
|
|
2071
|
+
? (async () => {
|
|
2072
|
+
await hydrateBaseline()
|
|
2073
|
+
const currentKeySetEvidence = getKeySetEvidence?.()
|
|
2074
|
+
if (
|
|
2075
|
+
canUsePersistedResume &&
|
|
2076
|
+
currentKeySetEvidence?.status !== `consistent`
|
|
2077
|
+
) {
|
|
2078
|
+
throw new Error(
|
|
2079
|
+
`Electric persisted resume baseline could not be certified during hydration`,
|
|
2080
|
+
)
|
|
2081
|
+
}
|
|
2082
|
+
})()
|
|
2083
|
+
: undefined
|
|
2084
|
+
: requiresKeySetCertification
|
|
2085
|
+
? (async () => {
|
|
2086
|
+
await certifyResumeSnapshot()
|
|
2087
|
+
const currentKeySetEvidence = getKeySetEvidence?.()
|
|
2088
|
+
if (currentKeySetEvidence?.status !== `consistent`) {
|
|
2089
|
+
throw new Error(
|
|
2090
|
+
`Electric persisted resume baseline could not be certified`,
|
|
2091
|
+
)
|
|
2092
|
+
}
|
|
2093
|
+
})()
|
|
2094
|
+
: undefined
|
|
1991
2095
|
let areResumeKeysReady = !resumeKeysPromise
|
|
1992
2096
|
const pendingResumeBatches: Array<Array<Message<T>>> = []
|
|
1993
2097
|
let unsubscribeStream: () => void = () => {}
|
|
@@ -2021,35 +2125,40 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2021
2125
|
|
|
2022
2126
|
if (freshSnapshotPending) {
|
|
2023
2127
|
freshSnapshotPending = false
|
|
2024
|
-
|
|
2128
|
+
beginSourceTransaction()
|
|
2025
2129
|
transactionStarted = true
|
|
2026
2130
|
truncate()
|
|
2131
|
+
recordPendingTruncate()
|
|
2027
2132
|
syncedKeys.clear()
|
|
2028
2133
|
clearTagTrackingState()
|
|
2029
2134
|
isResettingSnapshot = true
|
|
2030
2135
|
resetGeneration++
|
|
2031
2136
|
}
|
|
2032
2137
|
|
|
2033
|
-
//
|
|
2034
|
-
//
|
|
2035
|
-
//
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2138
|
+
// Without persistence, core owns pending source transactions and can
|
|
2139
|
+
// rebuild this callback's presence overlay from them. Persistence owns
|
|
2140
|
+
// queued source transactions until their FIFO turn, so retain the
|
|
2141
|
+
// overlay across callbacks instead. A queued truncate still fences off
|
|
2142
|
+
// the previous snapshot below.
|
|
2143
|
+
if (!metadata?.persistence && !transactionStarted) {
|
|
2144
|
+
pendingPresence.clear()
|
|
2145
|
+
usesBaseline = true
|
|
2146
|
+
for (const pending of collection._state.pendingSyncedTransactions) {
|
|
2147
|
+
if (pending.truncate) {
|
|
2148
|
+
pendingPresence.clear()
|
|
2149
|
+
usesBaseline = false
|
|
2150
|
+
}
|
|
2151
|
+
for (const operation of pending.operations) {
|
|
2152
|
+
pendingPresence.set(operation.key, operation.type !== `delete`)
|
|
2153
|
+
}
|
|
2045
2154
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2155
|
+
for (const message of bufferedMessages) {
|
|
2156
|
+
if (isChangeMessage(message)) {
|
|
2157
|
+
pendingPresence.set(
|
|
2158
|
+
collection.getKeyFromItem(message.value),
|
|
2159
|
+
message.headers.operation !== `delete`,
|
|
2160
|
+
)
|
|
2161
|
+
}
|
|
2053
2162
|
}
|
|
2054
2163
|
}
|
|
2055
2164
|
|
|
@@ -2091,7 +2200,10 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2091
2200
|
}
|
|
2092
2201
|
if (!receivesCompleteRows) continue
|
|
2093
2202
|
}
|
|
2094
|
-
|
|
2203
|
+
recordPendingPresence(rowId, operation !== `delete`)
|
|
2204
|
+
if (operation !== `update`) {
|
|
2205
|
+
progressiveSnapshotRows.delete(rowId)
|
|
2206
|
+
}
|
|
2095
2207
|
}
|
|
2096
2208
|
|
|
2097
2209
|
if (isChangeMessage(message)) {
|
|
@@ -2106,11 +2218,19 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2106
2218
|
// EXCEPTION: If a transaction is already started (e.g., from must-refetch), write
|
|
2107
2219
|
// directly to it instead of buffering. This prevents orphan transactions.
|
|
2108
2220
|
if (isBufferingInitialSync() && !transactionStarted) {
|
|
2221
|
+
if (message.headers.operation === `update`) {
|
|
2222
|
+
const rowId = collection.getKeyFromItem(message.value)
|
|
2223
|
+
const snapshotRow = progressiveSnapshotRows.get(rowId)
|
|
2224
|
+
if (snapshotRow) {
|
|
2225
|
+
bufferedMessages.push(snapshotRow)
|
|
2226
|
+
progressiveSnapshotRows.delete(rowId)
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2109
2229
|
bufferedMessages.push(message)
|
|
2110
2230
|
} else {
|
|
2111
2231
|
// Normal processing: write changes immediately
|
|
2112
2232
|
if (!transactionStarted) {
|
|
2113
|
-
|
|
2233
|
+
beginSourceTransaction()
|
|
2114
2234
|
transactionStarted = true
|
|
2115
2235
|
}
|
|
2116
2236
|
|
|
@@ -2142,11 +2262,11 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2142
2262
|
// Normal processing: process move-out immediately
|
|
2143
2263
|
transactionStarted = processMoveOutEvent(
|
|
2144
2264
|
message.headers.patterns,
|
|
2145
|
-
|
|
2265
|
+
beginSourceTransaction,
|
|
2146
2266
|
write,
|
|
2147
2267
|
transactionStarted,
|
|
2148
2268
|
(rowId) => {
|
|
2149
|
-
|
|
2269
|
+
recordPendingPresence(rowId, false)
|
|
2150
2270
|
syncedKeys.delete(rowId)
|
|
2151
2271
|
},
|
|
2152
2272
|
)
|
|
@@ -2168,7 +2288,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2168
2288
|
|
|
2169
2289
|
// Start a transaction and truncate the collection
|
|
2170
2290
|
if (!transactionStarted) {
|
|
2171
|
-
|
|
2291
|
+
beginSourceTransaction()
|
|
2172
2292
|
transactionStarted = true
|
|
2173
2293
|
}
|
|
2174
2294
|
|
|
@@ -2179,8 +2299,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2179
2299
|
|
|
2180
2300
|
// Clear synced keys tracking since we're starting fresh
|
|
2181
2301
|
syncedKeys.clear()
|
|
2182
|
-
|
|
2183
|
-
usesBaseline = false
|
|
2302
|
+
recordPendingTruncate()
|
|
2184
2303
|
isResettingSnapshot = true
|
|
2185
2304
|
resetGeneration++
|
|
2186
2305
|
|
|
@@ -2192,6 +2311,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2192
2311
|
commitPoint = null
|
|
2193
2312
|
hasReceivedUpToDate = false // Reset for progressive mode (isBufferingInitialSync will reflect this)
|
|
2194
2313
|
bufferedMessages.length = 0 // Clear buffered messages
|
|
2314
|
+
progressiveSnapshotRows.clear()
|
|
2195
2315
|
}
|
|
2196
2316
|
}
|
|
2197
2317
|
|
|
@@ -2222,7 +2342,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2222
2342
|
)
|
|
2223
2343
|
|
|
2224
2344
|
// Start atomic swap transaction
|
|
2225
|
-
|
|
2345
|
+
beginSourceTransaction()
|
|
2226
2346
|
|
|
2227
2347
|
// Truncate to clear all snapshot data
|
|
2228
2348
|
truncate()
|
|
@@ -2257,7 +2377,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2257
2377
|
// normal-stream transactionStarted flag is still false.
|
|
2258
2378
|
true,
|
|
2259
2379
|
(rowId) => {
|
|
2260
|
-
|
|
2380
|
+
recordPendingPresence(rowId, false)
|
|
2261
2381
|
syncedKeys.delete(rowId)
|
|
2262
2382
|
},
|
|
2263
2383
|
)
|
|
@@ -2269,11 +2389,12 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2269
2389
|
|
|
2270
2390
|
// Commit the atomic swap
|
|
2271
2391
|
stageResumeMetadata()
|
|
2272
|
-
applied =
|
|
2392
|
+
applied = commitSourceTransaction()
|
|
2273
2393
|
|
|
2274
2394
|
// Exit buffering phase by marking that we've received up-to-date
|
|
2275
2395
|
// isBufferingInitialSync() will now return false
|
|
2276
2396
|
bufferedMessages.length = 0
|
|
2397
|
+
progressiveSnapshotRows.clear()
|
|
2277
2398
|
|
|
2278
2399
|
debug(
|
|
2279
2400
|
`${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`,
|
|
@@ -2285,12 +2406,12 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2285
2406
|
if (!isResettingSnapshot || finishesReset) {
|
|
2286
2407
|
stageResumeMetadata()
|
|
2287
2408
|
}
|
|
2288
|
-
applied =
|
|
2409
|
+
applied = commitSourceTransaction()
|
|
2289
2410
|
transactionStarted = false
|
|
2290
2411
|
} else if (commitPoint === `up-to-date` && metadata) {
|
|
2291
|
-
|
|
2412
|
+
beginSourceTransaction()
|
|
2292
2413
|
stageResumeMetadata()
|
|
2293
|
-
applied =
|
|
2414
|
+
applied = commitSourceTransaction()
|
|
2294
2415
|
}
|
|
2295
2416
|
}
|
|
2296
2417
|
const readyErrorVersion = streamErrorVersion
|
|
@@ -2300,7 +2421,15 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
2300
2421
|
void applied.then(
|
|
2301
2422
|
() =>
|
|
2302
2423
|
wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion),
|
|
2303
|
-
() =>
|
|
2424
|
+
(error: unknown) => {
|
|
2425
|
+
if (!isActiveLifecycle() || abortController.signal.aborted) {
|
|
2426
|
+
return
|
|
2427
|
+
}
|
|
2428
|
+
streamErrorVersion++
|
|
2429
|
+
unsubscribeStream()
|
|
2430
|
+
abortController.abort()
|
|
2431
|
+
if (collection.status !== `error`) markError(error)
|
|
2432
|
+
},
|
|
2304
2433
|
)
|
|
2305
2434
|
}
|
|
2306
2435
|
|