1
1
import Logger , { ILogger } from 'js-logger' ;
2
2
3
- import { SyncPriorityStatus , SyncStatus , SyncStatusOptions } from '../../../db/crud/SyncStatus.js' ;
3
+ import { SyncStatus , SyncStatusOptions } from '../../../db/crud/SyncStatus.js' ;
4
4
import { AbortOperation } from '../../../utils/AbortOperation.js' ;
5
5
import { BaseListener , BaseObserver , Disposable } from '../../../utils/BaseObserver.js' ;
6
- import { throttleLeadingTrailing } from '../../../utils/throttle .js' ;
6
+ import { onAbortPromise , throttleLeadingTrailing } from '../../../utils/async .js' ;
7
7
import { BucketChecksum , BucketDescription , BucketStorageAdapter , Checkpoint } from '../bucket/BucketStorageAdapter.js' ;
8
8
import { CrudEntry } from '../bucket/CrudEntry.js' ;
9
9
import { SyncDataBucket } from '../bucket/SyncDataBucket.js' ;
@@ -161,6 +161,7 @@ export abstract class AbstractStreamingSyncImplementation
161
161
protected abortController : AbortController | null ;
162
162
protected crudUpdateListener ?: ( ) => void ;
163
163
protected streamingSyncPromise ?: Promise < void > ;
164
+ private pendingCrudUpload ?: Promise < void > ;
164
165
165
166
syncStatus : SyncStatus ;
166
167
triggerCrudUpload : ( ) => void ;
@@ -181,10 +182,16 @@ export abstract class AbstractStreamingSyncImplementation
181
182
this . abortController = null ;
182
183
183
184
this . triggerCrudUpload = throttleLeadingTrailing ( ( ) => {
184
- if ( ! this . syncStatus . connected || this . syncStatus . dataFlowStatus . uploading ) {
185
+ if ( ! this . syncStatus . connected || this . pendingCrudUpload != null ) {
185
186
return ;
186
187
}
187
- this . _uploadAllCrud ( ) ;
188
+
189
+ this . pendingCrudUpload = new Promise ( ( resolve ) => {
190
+ this . _uploadAllCrud ( ) . finally ( ( ) => {
191
+ this . pendingCrudUpload = undefined ;
192
+ resolve ( ) ;
193
+ } ) ;
194
+ } ) ;
188
195
} , this . options . crudUploadThrottleMs ! ) ;
189
196
}
190
197
@@ -434,16 +441,8 @@ The next upload iteration will be delayed.`);
434
441
if ( signal ?. aborted ) {
435
442
break ;
436
443
}
437
- const { retry } = await this . streamingSyncIteration ( nestedAbortController . signal , options ) ;
438
- if ( ! retry ) {
439
- /**
440
- * A sync error ocurred that we cannot recover from here.
441
- * This loop must terminate.
442
- * The nestedAbortController will close any open network requests and streams below.
443
- */
444
- break ;
445
- }
446
- // Continue immediately
444
+ await this . streamingSyncIteration ( nestedAbortController . signal , options ) ;
445
+ // Continue immediately, streamingSyncIteration will wait before completing if necessary.
447
446
} catch ( ex ) {
448
447
/**
449
448
* Either:
@@ -501,8 +500,8 @@ The next upload iteration will be delayed.`);
501
500
protected async streamingSyncIteration (
502
501
signal : AbortSignal ,
503
502
options ?: PowerSyncConnectionOptions
504
- ) : Promise < { retry ?: boolean } > {
505
- return await this . obtainLock ( {
503
+ ) : Promise < void > {
504
+ await this . obtainLock ( {
506
505
type : LockType . SYNC ,
507
506
signal,
508
507
callback : async ( ) => {
@@ -552,7 +551,7 @@ The next upload iteration will be delayed.`);
552
551
const line = await stream . read ( ) ;
553
552
if ( ! line ) {
554
553
// The stream has closed while waiting
555
- return { retry : true } ;
554
+ return ;
556
555
}
557
556
558
557
// A connection is active and messages are being received
@@ -582,30 +581,12 @@ The next upload iteration will be delayed.`);
582
581
await this . options . adapter . removeBuckets ( [ ...bucketsToDelete ] ) ;
583
582
await this . options . adapter . setTargetCheckpoint ( targetCheckpoint ) ;
584
583
} else if ( isStreamingSyncCheckpointComplete ( line ) ) {
585
- this . logger . debug ( 'Checkpoint complete' , targetCheckpoint ) ;
586
- const result = await this . options . adapter . syncLocalDatabase ( targetCheckpoint ! ) ;
587
- if ( ! result . checkpointValid ) {
588
- // This means checksums failed. Start again with a new checkpoint.
589
- // TODO: better back-off
590
- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
591
- return { retry : true } ;
592
- } else if ( ! result . ready ) {
593
- // Checksums valid, but need more data for a consistent checkpoint.
594
- // Continue waiting.
595
- // landing here the whole time
596
- } else {
584
+ const result = await this . applyCheckpoint ( targetCheckpoint ! , signal ) ;
585
+ if ( result . endIteration ) {
586
+ return ;
587
+ } else if ( result . applied ) {
597
588
appliedCheckpoint = targetCheckpoint ;
598
- this . logger . debug ( 'validated checkpoint' , appliedCheckpoint ) ;
599
- this . updateSyncStatus ( {
600
- connected : true ,
601
- lastSyncedAt : new Date ( ) ,
602
- dataFlow : {
603
- downloading : false ,
604
- downloadError : undefined
605
- }
606
- } ) ;
607
589
}
608
-
609
590
validatedCheckpoint = targetCheckpoint ;
610
591
} else if ( isStreamingSyncCheckpointPartiallyComplete ( line ) ) {
611
592
const priority = line . partial_checkpoint_complete . priority ;
@@ -615,9 +596,10 @@ The next upload iteration will be delayed.`);
615
596
// This means checksums failed. Start again with a new checkpoint.
616
597
// TODO: better back-off
617
598
await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
618
- return { retry : true } ;
599
+ return ;
619
600
} else if ( ! result . ready ) {
620
- // Need more data for a consistent partial sync within a priority - continue waiting.
601
+ // If we have pending uploads, we can't complete new checkpoints outside of priority 0.
602
+ // We'll resolve this for a complete checkpoint.
621
603
} else {
622
604
// We'll keep on downloading, but can report that this priority is synced now.
623
605
this . logger . debug ( 'partial checkpoint validation succeeded' ) ;
@@ -691,7 +673,7 @@ The next upload iteration will be delayed.`);
691
673
* (uses the same one), this should have some delay.
692
674
*/
693
675
await this . delayRetry ( ) ;
694
- return { retry : true } ;
676
+ return ;
695
677
}
696
678
this . triggerCrudUpload ( ) ;
697
679
} else {
@@ -707,37 +689,67 @@ The next upload iteration will be delayed.`);
707
689
}
708
690
} ) ;
709
691
} else if ( validatedCheckpoint === targetCheckpoint ) {
710
- const result = await this . options . adapter . syncLocalDatabase ( targetCheckpoint ! ) ;
711
- if ( ! result . checkpointValid ) {
712
- // This means checksums failed. Start again with a new checkpoint.
713
- // TODO: better back-off
714
- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
715
- return { retry : false } ;
716
- } else if ( ! result . ready ) {
717
- // Checksums valid, but need more data for a consistent checkpoint.
718
- // Continue waiting.
719
- } else {
692
+ const result = await this . applyCheckpoint ( targetCheckpoint ! , signal ) ;
693
+ if ( result . endIteration ) {
694
+ return ;
695
+ } else if ( result . applied ) {
720
696
appliedCheckpoint = targetCheckpoint ;
721
- this . updateSyncStatus ( {
722
- connected : true ,
723
- lastSyncedAt : new Date ( ) ,
724
- priorityStatusEntries : [ ] ,
725
- dataFlow : {
726
- downloading : false ,
727
- downloadError : undefined
728
- }
729
- } ) ;
730
697
}
731
698
}
732
699
}
733
700
}
734
701
this . logger . debug ( 'Stream input empty' ) ;
735
702
// Connection closed. Likely due to auth issue.
736
- return { retry : true } ;
703
+ return ;
737
704
}
738
705
} ) ;
739
706
}
740
707
708
+ private async applyCheckpoint ( checkpoint : Checkpoint , abort : AbortSignal ) {
709
+ let result = await this . options . adapter . syncLocalDatabase ( checkpoint ) ;
710
+ const pending = this . pendingCrudUpload ;
711
+
712
+ if ( ! result . checkpointValid ) {
713
+ this . logger . debug ( 'Checksum mismatch in checkpoint, will reconnect' ) ;
714
+ // This means checksums failed. Start again with a new checkpoint.
715
+ // TODO: better back-off
716
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
717
+ return { applied : false , endIteration : true } ;
718
+ } else if ( ! result . ready && pending != null ) {
719
+ // We have pending entries in the local upload queue or are waiting to confirm a write
720
+ // checkpoint, which prevented this checkpoint from applying. Wait for that to complete and
721
+ // try again.
722
+ this . logger . debug (
723
+ 'Could not apply checkpoint due to local data. Waiting for in-progress upload before retrying.'
724
+ ) ;
725
+ await Promise . race ( [ pending , onAbortPromise ( abort ) ] ) ;
726
+
727
+ if ( abort . aborted ) {
728
+ return { applied : false , endIteration : true } ;
729
+ }
730
+
731
+ // Try again now that uploads have completed.
732
+ result = await this . options . adapter . syncLocalDatabase ( checkpoint ) ;
733
+ }
734
+
735
+ if ( result . checkpointValid && result . ready ) {
736
+ this . logger . debug ( 'validated checkpoint' , checkpoint ) ;
737
+ this . updateSyncStatus ( {
738
+ connected : true ,
739
+ lastSyncedAt : new Date ( ) ,
740
+ dataFlow : {
741
+ downloading : false ,
742
+ downloadError : undefined
743
+ }
744
+ } ) ;
745
+
746
+ return { applied : true , endIteration : false } ;
747
+ } else {
748
+ this . logger . debug ( 'Could not apply checkpoint. Waiting for next sync complete line.' ) ;
749
+ return { applied : false , endIteration : false } ;
750
+ }
751
+ }
752
+
741
753
protected updateSyncStatus ( options : SyncStatusOptions ) {
742
754
const updatedStatus = new SyncStatus ( {
743
755
connected : options . connected ?? this . syncStatus . connected ,
0 commit comments