@tanstack/powersync-db-collection 0.1.67 → 0.2.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/dist/cjs/PowerSyncTransactor.cjs +31 -18
- package/dist/cjs/PowerSyncTransactor.cjs.map +1 -1
- package/dist/cjs/PowerSyncTransactor.d.cts +4 -4
- package/dist/cjs/definitions.cjs.map +1 -1
- package/dist/cjs/definitions.d.cts +4 -4
- package/dist/cjs/helpers.cjs.map +1 -1
- package/dist/cjs/helpers.d.cts +6 -9
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/powersync.cjs +254 -82
- package/dist/cjs/powersync.cjs.map +1 -1
- package/dist/cjs/serialization.cjs.map +1 -1
- package/dist/cjs/serialization.d.cts +2 -6
- package/dist/esm/PowerSyncTransactor.d.ts +4 -4
- package/dist/esm/PowerSyncTransactor.js +32 -19
- package/dist/esm/PowerSyncTransactor.js.map +1 -1
- package/dist/esm/definitions.d.ts +4 -4
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/helpers.d.ts +6 -9
- package/dist/esm/helpers.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/powersync.js +255 -83
- package/dist/esm/powersync.js.map +1 -1
- package/dist/esm/serialization.d.ts +2 -6
- package/dist/esm/serialization.js.map +1 -1
- package/package.json +7 -6
- package/src/PowerSyncTransactor.ts +46 -25
- package/src/definitions.ts +11 -5
- package/src/helpers.ts +10 -18
- package/src/index.ts +1 -0
- package/src/powersync.ts +316 -101
- package/src/serialization.ts +8 -17
package/src/powersync.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DiffTriggerOperation, sanitizeSQL } from '@powersync/common'
|
|
1
|
+
import { DiffTriggerOperation, LogLevels, sanitizeSQL } from '@powersync/common'
|
|
2
2
|
import { or, withCollectionConfigFactory } from '@tanstack/db'
|
|
3
3
|
import { compileSQLite } from './sqlite-compiler'
|
|
4
4
|
import { PendingOperationStore } from './PendingOperationStore'
|
|
@@ -17,8 +17,6 @@ import type {
|
|
|
17
17
|
import type {
|
|
18
18
|
AnyTableColumnType,
|
|
19
19
|
ExtractedTable,
|
|
20
|
-
ExtractedTableColumns,
|
|
21
|
-
MapBaseColumnType,
|
|
22
20
|
OptionalExtractedTable,
|
|
23
21
|
} from './helpers'
|
|
24
22
|
import type {
|
|
@@ -26,7 +24,6 @@ import type {
|
|
|
26
24
|
ConfigWithArbitraryCollectionTypes,
|
|
27
25
|
ConfigWithSQLiteInputType,
|
|
28
26
|
ConfigWithSQLiteTypes,
|
|
29
|
-
CustomSQLiteSerializer,
|
|
30
27
|
EnhancedPowerSyncCollectionConfig,
|
|
31
28
|
InferPowerSyncOutputType,
|
|
32
29
|
PowerSyncCollectionConfig,
|
|
@@ -36,6 +33,14 @@ import type { PendingOperation } from './PendingOperationStore'
|
|
|
36
33
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
37
34
|
import type { LockContext, Table, TriggerDiffRecord } from '@powersync/common'
|
|
38
35
|
|
|
36
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
37
|
+
return (
|
|
38
|
+
!!value &&
|
|
39
|
+
(typeof value === `object` || typeof value === `function`) &&
|
|
40
|
+
typeof (value as { then?: unknown }).then === `function`
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
/**
|
|
40
45
|
* Creates PowerSync collection options for use with a standard Collection.
|
|
41
46
|
*
|
|
@@ -275,12 +280,15 @@ function createPowerSyncCollectionConfig<
|
|
|
275
280
|
return validation.value
|
|
276
281
|
} else if (`issues` in validation) {
|
|
277
282
|
const issueMessage = `Failed to validate incoming data for ${viewName}. Issues: ${validation.issues.map((issue) => `${issue.path} - ${issue.message}`)}`
|
|
278
|
-
database.logger.error
|
|
283
|
+
database.logger.log({ level: LogLevels.error, message: issueMessage })
|
|
279
284
|
onDeserializationError!(validation)
|
|
280
285
|
throw new Error(issueMessage)
|
|
281
286
|
} else {
|
|
282
287
|
const unknownErrorMessage = `Unknown deserialization error for ${viewName}`
|
|
283
|
-
database.logger.
|
|
288
|
+
database.logger.log({
|
|
289
|
+
level: LogLevels.error,
|
|
290
|
+
message: unknownErrorMessage,
|
|
291
|
+
})
|
|
284
292
|
onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })
|
|
285
293
|
throw new Error(unknownErrorMessage)
|
|
286
294
|
}
|
|
@@ -324,6 +332,8 @@ function createPowerSyncCollectionConfig<
|
|
|
324
332
|
| ((options?: { context?: LockContext }) => Promise<void>)
|
|
325
333
|
| null = null
|
|
326
334
|
let trackingSetup: Promise<void> | null = null
|
|
335
|
+
let activeTrackingDisposal: Promise<void> | null = null
|
|
336
|
+
let abortTrackingDisposal: Promise<void> | null = null
|
|
327
337
|
|
|
328
338
|
if (syncMode === `eager`) {
|
|
329
339
|
return runEagerSync()
|
|
@@ -347,11 +357,35 @@ function createPowerSyncCollectionConfig<
|
|
|
347
357
|
|
|
348
358
|
const dispose = disposeTracking
|
|
349
359
|
if (!dispose) {
|
|
360
|
+
// Reconciliation may have claimed the disposer after this caller
|
|
361
|
+
// observed setup. Join that invocation instead of settling early.
|
|
362
|
+
await activeTrackingDisposal
|
|
350
363
|
return
|
|
351
364
|
}
|
|
352
365
|
|
|
353
366
|
disposeTracking = null
|
|
354
|
-
|
|
367
|
+
let disposal: Promise<void>
|
|
368
|
+
try {
|
|
369
|
+
disposal = Promise.resolve(dispose(context ? { context } : undefined))
|
|
370
|
+
} catch (error) {
|
|
371
|
+
disposal = Promise.reject(error)
|
|
372
|
+
}
|
|
373
|
+
activeTrackingDisposal = disposal
|
|
374
|
+
try {
|
|
375
|
+
await disposal
|
|
376
|
+
} finally {
|
|
377
|
+
if (activeTrackingDisposal === disposal) {
|
|
378
|
+
activeTrackingDisposal = null
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function disposeTrackingAfterAbort(): Promise<void> {
|
|
384
|
+
const disposal = (abortTrackingDisposal ??= safelyDisposeTracking())
|
|
385
|
+
// Abort event dispatch does not observe a listener's returned promise.
|
|
386
|
+
// The eager cleanup callback awaits this same promise below.
|
|
387
|
+
void disposal.catch(() => undefined)
|
|
388
|
+
return disposal
|
|
355
389
|
}
|
|
356
390
|
|
|
357
391
|
async function establishTracking(
|
|
@@ -392,6 +426,7 @@ function createPowerSyncCollectionConfig<
|
|
|
392
426
|
return await database.triggers.createDiffTrigger({
|
|
393
427
|
source: viewName,
|
|
394
428
|
destination: trackedTableName,
|
|
429
|
+
columns: table.columns.map((column) => column.name),
|
|
395
430
|
setupContext,
|
|
396
431
|
when,
|
|
397
432
|
hooks: {
|
|
@@ -416,9 +451,10 @@ function createPowerSyncCollectionConfig<
|
|
|
416
451
|
}
|
|
417
452
|
appliedReceipts.push(commit())
|
|
418
453
|
}
|
|
419
|
-
database.logger.
|
|
420
|
-
|
|
421
|
-
|
|
454
|
+
database.logger.log({
|
|
455
|
+
level: LogLevels.info,
|
|
456
|
+
message: `Sync is ready for ${viewName} into ${trackedTableName}`,
|
|
457
|
+
})
|
|
422
458
|
},
|
|
423
459
|
},
|
|
424
460
|
})
|
|
@@ -442,10 +478,11 @@ function createPowerSyncCollectionConfig<
|
|
|
442
478
|
await flushDiffRecordsWithContext(context, ignoredReceipts)
|
|
443
479
|
})
|
|
444
480
|
.catch((error) => {
|
|
445
|
-
database.logger.
|
|
446
|
-
|
|
481
|
+
database.logger.log({
|
|
482
|
+
level: LogLevels.error,
|
|
483
|
+
message: `An error has been detected in the sync handler`,
|
|
447
484
|
error,
|
|
448
|
-
)
|
|
485
|
+
})
|
|
449
486
|
})
|
|
450
487
|
}
|
|
451
488
|
|
|
@@ -503,18 +540,20 @@ function createPowerSyncCollectionConfig<
|
|
|
503
540
|
// transaction that currently parks it.
|
|
504
541
|
pendingOperationStore.resolvePendingFor(pendingOperations)
|
|
505
542
|
} catch (error) {
|
|
506
|
-
database.logger.
|
|
507
|
-
|
|
543
|
+
database.logger.log({
|
|
544
|
+
level: LogLevels.error,
|
|
545
|
+
message: `An error has been detected in the sync handler`,
|
|
508
546
|
error,
|
|
509
|
-
)
|
|
547
|
+
})
|
|
510
548
|
}
|
|
511
549
|
}
|
|
512
550
|
|
|
513
551
|
// The sync function needs to be synchronous.
|
|
514
552
|
async function start(afterOnChangeRegistered?: () => Promise<void>) {
|
|
515
|
-
database.logger.
|
|
516
|
-
|
|
517
|
-
|
|
553
|
+
database.logger.log({
|
|
554
|
+
level: LogLevels.info,
|
|
555
|
+
message: `Sync is starting for ${viewName} into ${trackedTableName}`,
|
|
556
|
+
})
|
|
518
557
|
database.onChangeWithCallback(
|
|
519
558
|
{
|
|
520
559
|
onChange: async () => {
|
|
@@ -532,12 +571,12 @@ function createPowerSyncCollectionConfig<
|
|
|
532
571
|
|
|
533
572
|
// If the abort controller was aborted while processing the request above
|
|
534
573
|
if (abortController.signal.aborted) {
|
|
535
|
-
await
|
|
574
|
+
await disposeTrackingAfterAbort()
|
|
536
575
|
} else {
|
|
537
576
|
abortController.signal.addEventListener(
|
|
538
577
|
`abort`,
|
|
539
|
-
|
|
540
|
-
|
|
578
|
+
() => {
|
|
579
|
+
void disposeTrackingAfterAbort()
|
|
541
580
|
},
|
|
542
581
|
{ once: true },
|
|
543
582
|
)
|
|
@@ -548,14 +587,14 @@ function createPowerSyncCollectionConfig<
|
|
|
548
587
|
// Registers a diff trigger for the entire table.
|
|
549
588
|
function runEagerSync() {
|
|
550
589
|
let onUnload: CleanupFn | void | null = null
|
|
590
|
+
let onUnloadStarted = false
|
|
551
591
|
|
|
552
|
-
start(async () => {
|
|
592
|
+
const startup = start(async () => {
|
|
553
593
|
const cleanup = await restConfig.onLoad?.()
|
|
594
|
+
onUnload = cleanup
|
|
554
595
|
if (abortController.signal.aborted) {
|
|
555
|
-
cleanup?.()
|
|
556
596
|
return
|
|
557
597
|
}
|
|
558
|
-
onUnload = cleanup
|
|
559
598
|
|
|
560
599
|
const appliedReceipts: Array<SyncAppliedReceipt> = []
|
|
561
600
|
await establishTracking(
|
|
@@ -584,21 +623,61 @@ function createPowerSyncCollectionConfig<
|
|
|
584
623
|
await Promise.all(appliedReceipts)
|
|
585
624
|
markReady()
|
|
586
625
|
}).catch((error) => {
|
|
587
|
-
database.logger.
|
|
588
|
-
|
|
626
|
+
database.logger.log({
|
|
627
|
+
level: LogLevels.error,
|
|
628
|
+
message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
|
|
589
629
|
error,
|
|
590
|
-
)
|
|
630
|
+
})
|
|
591
631
|
if (collection.status === `loading`) {
|
|
592
632
|
markError(error)
|
|
593
633
|
}
|
|
594
634
|
})
|
|
595
635
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
636
|
+
const invokeOnUnload = (): void | Promise<void> => {
|
|
637
|
+
if (onUnloadStarted || !onUnload) return
|
|
638
|
+
onUnloadStarted = true
|
|
639
|
+
const cleanup = onUnload
|
|
640
|
+
onUnload = null
|
|
641
|
+
return cleanup()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return async () => {
|
|
645
|
+
database.logger.log({
|
|
646
|
+
level: LogLevels.info,
|
|
647
|
+
message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,
|
|
648
|
+
})
|
|
600
649
|
abortController.abort()
|
|
601
|
-
|
|
650
|
+
const trackingDisposal = disposeTrackingAfterAbort()
|
|
651
|
+
|
|
652
|
+
let onUnloadFailure: { error: unknown } | undefined
|
|
653
|
+
const settleOnUnload = async () => {
|
|
654
|
+
try {
|
|
655
|
+
await invokeOnUnload()
|
|
656
|
+
} catch (error) {
|
|
657
|
+
onUnloadFailure ??= { error }
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Invoke an already-installed callback in this stack, but also wait
|
|
662
|
+
// for startup to publish a callback acquired concurrently with abort.
|
|
663
|
+
const settlements = await Promise.allSettled([
|
|
664
|
+
startup,
|
|
665
|
+
trackingDisposal,
|
|
666
|
+
settleOnUnload(),
|
|
667
|
+
])
|
|
668
|
+
const disposalSettlement = settlements[1]
|
|
669
|
+
await settleOnUnload()
|
|
670
|
+
if (disposalSettlement.status === `rejected` && onUnloadFailure) {
|
|
671
|
+
throw new AggregateError(
|
|
672
|
+
[disposalSettlement.reason, onUnloadFailure.error],
|
|
673
|
+
`PowerSync tracking disposal and load-hook cleanup both failed`,
|
|
674
|
+
{ cause: disposalSettlement.reason },
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
if (disposalSettlement.status === `rejected`) {
|
|
678
|
+
throw disposalSettlement.reason
|
|
679
|
+
}
|
|
680
|
+
if (onUnloadFailure) throw onUnloadFailure.error
|
|
602
681
|
}
|
|
603
682
|
}
|
|
604
683
|
|
|
@@ -614,22 +693,40 @@ function createPowerSyncCollectionConfig<
|
|
|
614
693
|
options: LoadSubsetOptions
|
|
615
694
|
failures: number
|
|
616
695
|
}
|
|
696
|
+
type DemandCleanupTask = {
|
|
697
|
+
order: number
|
|
698
|
+
promise: Promise<unknown>
|
|
699
|
+
}
|
|
700
|
+
type DemandCleanupFailure = {
|
|
701
|
+
order: number
|
|
702
|
+
error: unknown
|
|
703
|
+
}
|
|
704
|
+
type DemandCleanupCollector = {
|
|
705
|
+
tasks: Array<DemandCleanupTask>
|
|
706
|
+
ownedTasks: Set<Promise<unknown>>
|
|
707
|
+
failures: Array<DemandCleanupFailure>
|
|
708
|
+
nextOrder: number
|
|
709
|
+
}
|
|
617
710
|
|
|
618
711
|
const demands = new Map<LoadSubsetOptions, DemandRecord>()
|
|
619
712
|
const releasedSubsets = new WeakSet<LoadSubsetOptions>()
|
|
620
713
|
const pendingReleases: Array<PendingRelease> = []
|
|
714
|
+
const pendingDemandLoads = new Set<Promise<void>>()
|
|
715
|
+
const pendingDemandCleanups = new Set<Promise<unknown>>()
|
|
621
716
|
let stopped = false
|
|
622
717
|
let trackingRevision = 0
|
|
623
718
|
let reconciledTrackingRevision = 0
|
|
624
719
|
let rebuildPromise: Promise<void> | null = null
|
|
625
720
|
let drainingReleases = false
|
|
626
721
|
let releaseRetryTimer: ReturnType<typeof setTimeout> | undefined
|
|
722
|
+
let demandCleanupCollector: DemandCleanupCollector | null = null
|
|
627
723
|
const startup = start()
|
|
628
724
|
void startup.catch((error) =>
|
|
629
|
-
database.logger.
|
|
630
|
-
|
|
725
|
+
database.logger.log({
|
|
726
|
+
level: LogLevels.error,
|
|
727
|
+
message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
|
|
631
728
|
error,
|
|
632
|
-
),
|
|
729
|
+
}),
|
|
633
730
|
)
|
|
634
731
|
|
|
635
732
|
const activeWhereExpressions = () =>
|
|
@@ -653,51 +750,70 @@ function createPowerSyncCollectionConfig<
|
|
|
653
750
|
if (!isCurrent()) return
|
|
654
751
|
|
|
655
752
|
const active = activeWhereExpressions()
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
{
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
753
|
+
// Tracking was absent during an error. Positive baseline rows
|
|
754
|
+
// alone cannot reveal deletes or predicate exits from that gap.
|
|
755
|
+
const missing =
|
|
756
|
+
collection.status === `error`
|
|
757
|
+
? new Set(collection.keys())
|
|
758
|
+
: undefined
|
|
759
|
+
if (active.length > 0) {
|
|
760
|
+
const combinedWhere =
|
|
761
|
+
active.length === 1
|
|
762
|
+
? active[0]
|
|
763
|
+
: or(active[0], active[1], ...active.slice(2))
|
|
764
|
+
const compiledNewData = compileSQLite(
|
|
765
|
+
{ where: combinedWhere },
|
|
766
|
+
{ jsonColumn: 'NEW.data' },
|
|
767
|
+
)
|
|
768
|
+
const compiledOldData = compileSQLite(
|
|
769
|
+
{ where: combinedWhere },
|
|
770
|
+
{ jsonColumn: 'OLD.data' },
|
|
771
|
+
)
|
|
772
|
+
const compiledView = compileSQLite({ where: combinedWhere })
|
|
773
|
+
const newDataWhenClause = toInlinedWhereClause(compiledNewData)
|
|
774
|
+
const oldDataWhenClause = toInlinedWhereClause(compiledOldData)
|
|
775
|
+
const viewWhereClause = toInlinedWhereClause(compiledView)
|
|
776
|
+
await establishTracking(
|
|
777
|
+
{
|
|
778
|
+
setupContext: ctx,
|
|
779
|
+
when: {
|
|
780
|
+
[DiffTriggerOperation.INSERT]: newDataWhenClause,
|
|
781
|
+
[DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
|
|
782
|
+
[DiffTriggerOperation.DELETE]: oldDataWhenClause,
|
|
783
|
+
},
|
|
784
|
+
writeType: (rowId: string) =>
|
|
785
|
+
collection.has(rowId) ? `update` : `insert`,
|
|
786
|
+
batchQuery: (
|
|
787
|
+
lockContext: LockContext,
|
|
788
|
+
batchSize: number,
|
|
789
|
+
cursor: number,
|
|
790
|
+
) =>
|
|
791
|
+
lockContext
|
|
792
|
+
.getAll<TableType>(
|
|
793
|
+
`SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
|
|
794
|
+
[batchSize, cursor],
|
|
795
|
+
)
|
|
796
|
+
.then((rows) => {
|
|
797
|
+
for (const row of rows) missing?.delete(row.id)
|
|
798
|
+
return rows
|
|
799
|
+
}),
|
|
681
800
|
},
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
lockContext: LockContext,
|
|
686
|
-
batchSize: number,
|
|
687
|
-
cursor: number,
|
|
688
|
-
) =>
|
|
689
|
-
lockContext.getAll<TableType>(
|
|
690
|
-
`SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
|
|
691
|
-
[batchSize, cursor],
|
|
692
|
-
),
|
|
693
|
-
},
|
|
694
|
-
appliedReceipts,
|
|
695
|
-
)
|
|
801
|
+
appliedReceipts,
|
|
802
|
+
)
|
|
803
|
+
}
|
|
696
804
|
if (!isCurrent()) await safelyDisposeTracking(ctx)
|
|
805
|
+
else if (missing?.size) {
|
|
806
|
+
begin()
|
|
807
|
+
for (const key of missing) write({ type: `delete`, key })
|
|
808
|
+
appliedReceipts.push(commit())
|
|
809
|
+
}
|
|
697
810
|
})
|
|
698
811
|
await Promise.all(appliedReceipts)
|
|
699
812
|
if (isCurrent()) {
|
|
700
813
|
reconciledTrackingRevision = revision
|
|
814
|
+
// Replacing the trigger alone is not recovery: its baseline
|
|
815
|
+
// writes must also be applied before the source is ready again.
|
|
816
|
+
if (collection.status === `error`) markReady()
|
|
701
817
|
}
|
|
702
818
|
}
|
|
703
819
|
}
|
|
@@ -742,6 +858,13 @@ function createPowerSyncCollectionConfig<
|
|
|
742
858
|
if (cleanup) demand.cleanup = cleanup
|
|
743
859
|
} catch (error) {
|
|
744
860
|
demands.delete(options)
|
|
861
|
+
const collector = demandCleanupCollector
|
|
862
|
+
if (collector) {
|
|
863
|
+
collector.failures.push({
|
|
864
|
+
order: collector.nextOrder++,
|
|
865
|
+
error,
|
|
866
|
+
})
|
|
867
|
+
}
|
|
745
868
|
throw error
|
|
746
869
|
}
|
|
747
870
|
|
|
@@ -753,8 +876,7 @@ function createPowerSyncCollectionConfig<
|
|
|
753
876
|
options.signal?.aborted ||
|
|
754
877
|
demands.get(options) !== demand
|
|
755
878
|
) {
|
|
756
|
-
|
|
757
|
-
demand.cleanup?.()
|
|
879
|
+
cleanupDemand(demand)
|
|
758
880
|
return
|
|
759
881
|
}
|
|
760
882
|
|
|
@@ -763,6 +885,14 @@ function createPowerSyncCollectionConfig<
|
|
|
763
885
|
await rebuildTracking()
|
|
764
886
|
}
|
|
765
887
|
|
|
888
|
+
const trackLoadSubset = (options: LoadSubsetOptions): Promise<void> => {
|
|
889
|
+
const task = loadSubset(options)
|
|
890
|
+
pendingDemandLoads.add(task)
|
|
891
|
+
const finish = () => pendingDemandLoads.delete(task)
|
|
892
|
+
void task.then(finish, finish)
|
|
893
|
+
return task
|
|
894
|
+
}
|
|
895
|
+
|
|
766
896
|
const toInlinedWhereClause = (compiled: {
|
|
767
897
|
where?: string
|
|
768
898
|
params: Array<unknown>
|
|
@@ -775,15 +905,64 @@ function createPowerSyncCollectionConfig<
|
|
|
775
905
|
)
|
|
776
906
|
}
|
|
777
907
|
|
|
908
|
+
const reportDemandCleanupFailure = (error: unknown): void => {
|
|
909
|
+
database.logger.log({
|
|
910
|
+
level: LogLevels.error,
|
|
911
|
+
message: `Could not clean up subset hook for ${viewName}`,
|
|
912
|
+
error,
|
|
913
|
+
})
|
|
914
|
+
}
|
|
915
|
+
|
|
778
916
|
const cleanupDemand = (demand: DemandRecord): void => {
|
|
779
917
|
demands.delete(demand.options)
|
|
918
|
+
const collector = demandCleanupCollector
|
|
919
|
+
const order = collector ? collector.nextOrder++ : undefined
|
|
780
920
|
try {
|
|
781
|
-
demand.cleanup
|
|
921
|
+
const cleanup = demand.cleanup
|
|
922
|
+
demand.cleanup = undefined
|
|
923
|
+
const result: unknown = cleanup?.()
|
|
924
|
+
if (!isPromiseLike(result)) return
|
|
925
|
+
|
|
926
|
+
const task = Promise.resolve(result)
|
|
927
|
+
pendingDemandCleanups.add(task)
|
|
928
|
+
const finish = () => pendingDemandCleanups.delete(task)
|
|
929
|
+
void task.then(finish, finish)
|
|
930
|
+
if (collector) {
|
|
931
|
+
collector.tasks.push({ order: order!, promise: task })
|
|
932
|
+
collector.ownedTasks.add(task)
|
|
933
|
+
void task.catch(() => undefined)
|
|
934
|
+
} else {
|
|
935
|
+
void task.catch((error) => {
|
|
936
|
+
if (!demandCleanupCollector?.ownedTasks.has(task)) {
|
|
937
|
+
reportDemandCleanupFailure(error)
|
|
938
|
+
}
|
|
939
|
+
})
|
|
940
|
+
}
|
|
782
941
|
} catch (error) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
942
|
+
if (collector) {
|
|
943
|
+
collector.failures.push({ order: order!, error })
|
|
944
|
+
} else {
|
|
945
|
+
reportDemandCleanupFailure(error)
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const throwDemandCleanupFailures = (
|
|
951
|
+
collector: DemandCleanupCollector,
|
|
952
|
+
primaryFailures: ReadonlyArray<unknown> = [],
|
|
953
|
+
): void => {
|
|
954
|
+
const errors = [
|
|
955
|
+
...primaryFailures,
|
|
956
|
+
...collector.failures
|
|
957
|
+
.slice()
|
|
958
|
+
.sort((left, right) => left.order - right.order)
|
|
959
|
+
.map(({ error }) => error),
|
|
960
|
+
]
|
|
961
|
+
if (errors.length === 1) throw errors[0]
|
|
962
|
+
if (errors.length > 1) {
|
|
963
|
+
throw new AggregateError(errors, `PowerSync cleanup failed`, {
|
|
964
|
+
cause: errors[0],
|
|
965
|
+
})
|
|
787
966
|
}
|
|
788
967
|
}
|
|
789
968
|
|
|
@@ -859,10 +1038,11 @@ function createPowerSyncCollectionConfig<
|
|
|
859
1038
|
)
|
|
860
1039
|
retryDelay =
|
|
861
1040
|
retryDelay === 0 ? delay : Math.min(retryDelay, delay)
|
|
862
|
-
database.logger.
|
|
863
|
-
|
|
1041
|
+
database.logger.log({
|
|
1042
|
+
level: LogLevels.error,
|
|
1043
|
+
message: `Could not release subset tracking for ${viewName}; retrying`,
|
|
864
1044
|
error,
|
|
865
|
-
)
|
|
1045
|
+
})
|
|
866
1046
|
}
|
|
867
1047
|
}
|
|
868
1048
|
} finally {
|
|
@@ -896,16 +1076,62 @@ function createPowerSyncCollectionConfig<
|
|
|
896
1076
|
stopped = true
|
|
897
1077
|
clearTimeout(releaseRetryTimer)
|
|
898
1078
|
releaseRetryTimer = undefined
|
|
899
|
-
database.logger.
|
|
900
|
-
|
|
901
|
-
|
|
1079
|
+
database.logger.log({
|
|
1080
|
+
level: LogLevels.info,
|
|
1081
|
+
message: `Sync has been stopped for ${viewName} into ${trackedTableName}`,
|
|
1082
|
+
})
|
|
902
1083
|
abortController.abort()
|
|
1084
|
+
const trackingDisposal = disposeTrackingAfterAbort()
|
|
1085
|
+
const pendingLoads = [...pendingDemandLoads]
|
|
1086
|
+
const pendingTasks = [...pendingDemandCleanups]
|
|
1087
|
+
const collector: DemandCleanupCollector = {
|
|
1088
|
+
tasks: pendingTasks.map((promise, order) => ({ order, promise })),
|
|
1089
|
+
ownedTasks: new Set(pendingTasks),
|
|
1090
|
+
failures: [],
|
|
1091
|
+
nextOrder: pendingTasks.length,
|
|
1092
|
+
}
|
|
1093
|
+
demandCleanupCollector = collector
|
|
903
1094
|
for (const demand of demands.values()) {
|
|
904
1095
|
cleanupDemand(demand)
|
|
905
1096
|
}
|
|
906
1097
|
pendingReleases.length = 0
|
|
1098
|
+
|
|
1099
|
+
return (async () => {
|
|
1100
|
+
const ownershipSettlements = await Promise.allSettled([
|
|
1101
|
+
trackingDisposal,
|
|
1102
|
+
...pendingLoads,
|
|
1103
|
+
])
|
|
1104
|
+
const disposalSettlement = ownershipSettlements[0]
|
|
1105
|
+
|
|
1106
|
+
let settledTasks = 0
|
|
1107
|
+
while (settledTasks < collector.tasks.length) {
|
|
1108
|
+
const tasks = collector.tasks.slice(settledTasks)
|
|
1109
|
+
settledTasks = collector.tasks.length
|
|
1110
|
+
const outcomes = await Promise.allSettled(
|
|
1111
|
+
tasks.map(({ promise }) => promise),
|
|
1112
|
+
)
|
|
1113
|
+
for (const [index, outcome] of outcomes.entries()) {
|
|
1114
|
+
if (outcome.status === `rejected`) {
|
|
1115
|
+
collector.failures.push({
|
|
1116
|
+
order: tasks[index]!.order,
|
|
1117
|
+
error: outcome.reason,
|
|
1118
|
+
})
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
throwDemandCleanupFailures(
|
|
1123
|
+
collector,
|
|
1124
|
+
disposalSettlement.status === `rejected`
|
|
1125
|
+
? [disposalSettlement.reason]
|
|
1126
|
+
: [],
|
|
1127
|
+
)
|
|
1128
|
+
})().finally(() => {
|
|
1129
|
+
if (demandCleanupCollector === collector) {
|
|
1130
|
+
demandCleanupCollector = null
|
|
1131
|
+
}
|
|
1132
|
+
})
|
|
907
1133
|
},
|
|
908
|
-
loadSubset:
|
|
1134
|
+
loadSubset: trackLoadSubset,
|
|
909
1135
|
unloadSubset,
|
|
910
1136
|
}
|
|
911
1137
|
}
|
|
@@ -946,18 +1172,7 @@ function createPowerSyncCollectionConfig<
|
|
|
946
1172
|
trackedTableName,
|
|
947
1173
|
metadataIsTracked,
|
|
948
1174
|
serializeValue: (value) =>
|
|
949
|
-
serializeForSQLite(
|
|
950
|
-
value,
|
|
951
|
-
// This is required by the input generic
|
|
952
|
-
table as Table<
|
|
953
|
-
MapBaseColumnType<InferPowerSyncOutputType<TTable, TSchema>>
|
|
954
|
-
>,
|
|
955
|
-
// Coerce serializer to the shape that corresponds to the Table constructed from OutputType
|
|
956
|
-
serializer as CustomSQLiteSerializer<
|
|
957
|
-
OutputType,
|
|
958
|
-
ExtractedTableColumns<Table<MapBaseColumnType<OutputType>>>
|
|
959
|
-
>,
|
|
960
|
-
),
|
|
1175
|
+
serializeForSQLite<TTable>(value, table, serializer),
|
|
961
1176
|
}),
|
|
962
1177
|
},
|
|
963
1178
|
}
|
package/src/serialization.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { ColumnType } from '@powersync/common'
|
|
|
2
2
|
import type { Table } from '@powersync/common'
|
|
3
3
|
import type { CustomSQLiteSerializer } from './definitions'
|
|
4
4
|
import type {
|
|
5
|
+
AnyTableColumnType,
|
|
5
6
|
ExtractedTable,
|
|
6
7
|
ExtractedTableColumns,
|
|
7
|
-
MapBaseColumnType,
|
|
8
8
|
} from './helpers'
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -13,10 +13,6 @@ import type {
|
|
|
13
13
|
* This function takes an object representing a row, a table schema, and an optional custom serializer map.
|
|
14
14
|
* It returns a new object with values transformed to be compatible with SQLite column types.
|
|
15
15
|
*
|
|
16
|
-
* ## Generics
|
|
17
|
-
* - `TOutput`: The shape of the input object, typically matching the row data.
|
|
18
|
-
* - `TTable`: The table schema, which must match the keys of `TOutput`.
|
|
19
|
-
*
|
|
20
16
|
* ## Parameters
|
|
21
17
|
* - `value`: The object to serialize (row data).
|
|
22
18
|
* - `tableSchema`: The schema describing the SQLite table columns and types.
|
|
@@ -37,17 +33,12 @@ import type {
|
|
|
37
33
|
* - Throws if a key in `value` does not exist in the schema.
|
|
38
34
|
* - Throws if a value cannot be converted to the required SQLite type.
|
|
39
35
|
*/
|
|
40
|
-
export function serializeForSQLite<
|
|
41
|
-
|
|
42
|
-
// The keys should match
|
|
43
|
-
TTable extends Table<MapBaseColumnType<TOutput>> = Table<
|
|
44
|
-
MapBaseColumnType<TOutput>
|
|
45
|
-
>,
|
|
46
|
-
>(
|
|
47
|
-
value: TOutput,
|
|
36
|
+
export function serializeForSQLite<TTable extends Table>(
|
|
37
|
+
value: AnyTableColumnType<TTable>,
|
|
48
38
|
tableSchema: TTable,
|
|
49
|
-
customSerializer:
|
|
50
|
-
|
|
39
|
+
customSerializer: CustomSQLiteSerializer<
|
|
40
|
+
AnyTableColumnType<TTable>,
|
|
41
|
+
ExtractedTableColumns<TTable>
|
|
51
42
|
> = {},
|
|
52
43
|
): ExtractedTable<TTable> {
|
|
53
44
|
return Object.fromEntries(
|
|
@@ -68,7 +59,7 @@ export function serializeForSQLite<
|
|
|
68
59
|
|
|
69
60
|
const customTransform = customSerializer[key]
|
|
70
61
|
if (customTransform) {
|
|
71
|
-
return [key, customTransform(value
|
|
62
|
+
return [key, customTransform(value)]
|
|
72
63
|
}
|
|
73
64
|
|
|
74
65
|
// Map to the output
|
|
@@ -98,5 +89,5 @@ export function serializeForSQLite<
|
|
|
98
89
|
}
|
|
99
90
|
}
|
|
100
91
|
}),
|
|
101
|
-
)
|
|
92
|
+
) as ExtractedTable<TTable>
|
|
102
93
|
}
|