@@ -37,29 +37,32 @@ import {
37
37
UpdateKeyNotFoundError ,
38
38
} from "./errors"
39
39
import { CollectionEvents } from "./collection-events.js"
40
+ import { currentStateAsChanges } from "./change-events"
41
+ import { CollectionSubscription } from "./collection-subscription.js"
40
42
import type {
41
43
AllCollectionEvents ,
42
44
CollectionEventHandler ,
43
45
} from "./collection-events.js"
44
- import { currentStateAsChanges } from "./change-events"
45
- import { CollectionSubscription } from "./collection-subscription.js"
46
46
import type { Transaction } from "./transactions"
47
47
import type { StandardSchemaV1 } from "@standard-schema/spec"
48
48
import type { SingleRowRefProxy } from "./query/builder/ref-proxy"
49
49
import type {
50
50
ChangeMessage ,
51
+ CleanupFn ,
51
52
CollectionConfig ,
52
53
CollectionStatus ,
53
54
CurrentStateAsChangesOptions ,
54
55
Fn ,
55
56
InferSchemaInput ,
56
57
InferSchemaOutput ,
57
58
InsertConfig ,
59
+ OnLoadMoreOptions ,
58
60
OperationConfig ,
59
61
OptimisticChangeMessage ,
60
62
PendingMutation ,
61
63
StandardSchema ,
62
64
SubscribeChangesOptions ,
65
+ SyncConfigRes ,
63
66
Transaction as TransactionType ,
64
67
TransactionWithMutations ,
65
68
UtilsRecord ,
@@ -266,6 +269,9 @@ export class CollectionImpl<
266
269
private gcTimeoutId : ReturnType < typeof setTimeout > | null = null
267
270
private preloadPromise : Promise < void > | null = null
268
271
private syncCleanupFn : ( ( ) => void ) | null = null
272
+ private syncOnLoadMoreFn :
273
+ | ( ( options : OnLoadMoreOptions ) => void | Promise < void > )
274
+ | null = null
269
275
270
276
// Event system
271
277
private events : CollectionEvents
@@ -488,106 +494,111 @@ export class CollectionImpl<
488
494
this . setStatus ( `loading` )
489
495
490
496
try {
491
- const cleanupFn = this . config . sync . sync ( {
492
- collection : this ,
493
- begin : ( ) => {
494
- this . pendingSyncedTransactions . push ( {
495
- committed : false ,
496
- operations : [ ] ,
497
- deletedKeys : new Set ( ) ,
498
- } )
499
- } ,
500
- write : ( messageWithoutKey : Omit < ChangeMessage < TOutput > , `key`> ) => {
501
- const pendingTransaction =
502
- this . pendingSyncedTransactions [
503
- this . pendingSyncedTransactions . length - 1
504
- ]
505
- if ( ! pendingTransaction ) {
506
- throw new NoPendingSyncTransactionWriteError ( )
507
- }
508
- if ( pendingTransaction . committed ) {
509
- throw new SyncTransactionAlreadyCommittedWriteError ( )
510
- }
511
- const key = this . getKeyFromItem ( messageWithoutKey . value )
512
-
513
- // Check if an item with this key already exists when inserting
514
- if ( messageWithoutKey . type === `insert` ) {
515
- const insertingIntoExistingSynced = this . syncedData . has ( key )
516
- const hasPendingDeleteForKey =
517
- pendingTransaction . deletedKeys . has ( key )
518
- const isTruncateTransaction = pendingTransaction . truncate === true
519
- // Allow insert after truncate in the same transaction even if it existed in syncedData
520
- if (
521
- insertingIntoExistingSynced &&
522
- ! hasPendingDeleteForKey &&
523
- ! isTruncateTransaction
524
- ) {
525
- throw new DuplicateKeySyncError ( key , this . id )
497
+ const syncRes = normalizeSyncFnResult (
498
+ this . config . sync . sync ( {
499
+ collection : this ,
500
+ begin : ( ) => {
501
+ this . pendingSyncedTransactions . push ( {
502
+ committed : false ,
503
+ operations : [ ] ,
504
+ deletedKeys : new Set ( ) ,
505
+ } )
506
+ } ,
507
+ write : ( messageWithoutKey : Omit < ChangeMessage < TOutput > , `key`> ) => {
508
+ const pendingTransaction =
509
+ this . pendingSyncedTransactions [
510
+ this . pendingSyncedTransactions . length - 1
511
+ ]
512
+ if ( ! pendingTransaction ) {
513
+ throw new NoPendingSyncTransactionWriteError ( )
514
+ }
515
+ if ( pendingTransaction . committed ) {
516
+ throw new SyncTransactionAlreadyCommittedWriteError ( )
517
+ }
518
+ const key = this . getKeyFromItem ( messageWithoutKey . value )
519
+
520
+ // Check if an item with this key already exists when inserting
521
+ if ( messageWithoutKey . type === `insert` ) {
522
+ const insertingIntoExistingSynced = this . syncedData . has ( key )
523
+ const hasPendingDeleteForKey =
524
+ pendingTransaction . deletedKeys . has ( key )
525
+ const isTruncateTransaction = pendingTransaction . truncate === true
526
+ // Allow insert after truncate in the same transaction even if it existed in syncedData
527
+ if (
528
+ insertingIntoExistingSynced &&
529
+ ! hasPendingDeleteForKey &&
530
+ ! isTruncateTransaction
531
+ ) {
532
+ throw new DuplicateKeySyncError ( key , this . id )
533
+ }
526
534
}
527
- }
528
535
529
- const message : ChangeMessage < TOutput > = {
530
- ...messageWithoutKey ,
531
- key,
532
- }
533
- pendingTransaction . operations . push ( message )
536
+ const message : ChangeMessage < TOutput > = {
537
+ ...messageWithoutKey ,
538
+ key,
539
+ }
540
+ pendingTransaction . operations . push ( message )
534
541
535
- if ( messageWithoutKey . type === `delete` ) {
536
- pendingTransaction . deletedKeys . add ( key )
537
- }
538
- } ,
539
- commit : ( ) => {
540
- const pendingTransaction =
541
- this . pendingSyncedTransactions [
542
- this . pendingSyncedTransactions . length - 1
543
- ]
544
- if ( ! pendingTransaction ) {
545
- throw new NoPendingSyncTransactionCommitError ( )
546
- }
547
- if ( pendingTransaction . committed ) {
548
- throw new SyncTransactionAlreadyCommittedError ( )
549
- }
542
+ if ( messageWithoutKey . type === `delete` ) {
543
+ pendingTransaction . deletedKeys . add ( key )
544
+ }
545
+ } ,
546
+ commit : ( ) => {
547
+ const pendingTransaction =
548
+ this . pendingSyncedTransactions [
549
+ this . pendingSyncedTransactions . length - 1
550
+ ]
551
+ if ( ! pendingTransaction ) {
552
+ throw new NoPendingSyncTransactionCommitError ( )
553
+ }
554
+ if ( pendingTransaction . committed ) {
555
+ throw new SyncTransactionAlreadyCommittedError ( )
556
+ }
550
557
551
- pendingTransaction . committed = true
558
+ pendingTransaction . committed = true
552
559
553
- // Update status to initialCommit when transitioning from loading
554
- // This indicates we're in the process of committing the first transaction
555
- if ( this . _status === `loading` ) {
556
- this . setStatus ( `initialCommit` )
557
- }
558
-
559
- this . commitPendingTransactions ( )
560
- } ,
561
- markReady : ( ) => {
562
- this . markReady ( )
563
- } ,
564
- truncate : ( ) => {
565
- const pendingTransaction =
566
- this . pendingSyncedTransactions [
567
- this . pendingSyncedTransactions . length - 1
568
- ]
569
- if ( ! pendingTransaction ) {
570
- throw new NoPendingSyncTransactionWriteError ( )
571
- }
572
- if ( pendingTransaction . committed ) {
573
- throw new SyncTransactionAlreadyCommittedWriteError ( )
574
- }
560
+ // Update status to initialCommit when transitioning from loading
561
+ // This indicates we're in the process of committing the first transaction
562
+ if ( this . _status === `loading` ) {
563
+ this . setStatus ( `initialCommit` )
564
+ }
575
565
576
- // Clear all operations from the current transaction
577
- pendingTransaction . operations = [ ]
578
- pendingTransaction . deletedKeys . clear ( )
566
+ this . commitPendingTransactions ( )
567
+ } ,
568
+ markReady : ( ) => {
569
+ this . markReady ( )
570
+ } ,
571
+ truncate : ( ) => {
572
+ const pendingTransaction =
573
+ this . pendingSyncedTransactions [
574
+ this . pendingSyncedTransactions . length - 1
575
+ ]
576
+ if ( ! pendingTransaction ) {
577
+ throw new NoPendingSyncTransactionWriteError ( )
578
+ }
579
+ if ( pendingTransaction . committed ) {
580
+ throw new SyncTransactionAlreadyCommittedWriteError ( )
581
+ }
579
582
580
- // Mark the transaction as a truncate operation. During commit, this triggers:
581
- // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
582
- // - Clearing of syncedData/syncedMetadata
583
- // - Subsequent synced ops applied on the fresh base
584
- // - Finally, optimistic mutations re-applied on top (single batch)
585
- pendingTransaction . truncate = true
586
- } ,
587
- } )
583
+ // Clear all operations from the current transaction
584
+ pendingTransaction . operations = [ ]
585
+ pendingTransaction . deletedKeys . clear ( )
586
+
587
+ // Mark the transaction as a truncate operation. During commit, this triggers:
588
+ // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
589
+ // - Clearing of syncedData/syncedMetadata
590
+ // - Subsequent synced ops applied on the fresh base
591
+ // - Finally, optimistic mutations re-applied on top (single batch)
592
+ pendingTransaction . truncate = true
593
+ } ,
594
+ } )
595
+ )
588
596
589
597
// Store cleanup function if provided
590
- this . syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null
598
+ this . syncCleanupFn = syncRes ?. cleanup ?? null
599
+
600
+ // Store onLoadMore function if provided
601
+ this . syncOnLoadMoreFn = syncRes ?. onLoadMore ?? null
591
602
} catch ( error ) {
592
603
this . setStatus ( `error` )
593
604
throw error
@@ -633,6 +644,18 @@ export class CollectionImpl<
633
644
return this . preloadPromise
634
645
}
635
646
647
+ /**
648
+ * Requests the sync layer to load more data.
649
+ * @param options Options to control what data is being loaded
650
+ * @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
651
+ * If data loading is synchronous, the data is loaded when the method returns.
652
+ */
653
+ public syncMore ( options : OnLoadMoreOptions ) : void | Promise < void > {
654
+ if ( this . syncOnLoadMoreFn ) {
655
+ return this . syncOnLoadMoreFn ( options )
656
+ }
657
+ }
658
+
636
659
/**
637
660
* Clean up the collection by stopping sync and clearing data
638
661
* This can be called manually or automatically by garbage collection
@@ -2478,3 +2501,15 @@ export class CollectionImpl<
2478
2501
return this . events . waitFor ( event , timeout )
2479
2502
}
2480
2503
}
2504
+
2505
+ function normalizeSyncFnResult ( result : void | CleanupFn | SyncConfigRes ) {
2506
+ if ( typeof result === `function` ) {
2507
+ return { cleanup : result }
2508
+ }
2509
+
2510
+ if ( typeof result === `object` ) {
2511
+ return result
2512
+ }
2513
+
2514
+ return undefined
2515
+ }
0 commit comments