11import Logger , { ILogger } from 'js-logger' ;
22
3- import { SyncPriorityStatus , SyncStatus , SyncStatusOptions } from '../../../db/crud/SyncStatus.js' ;
3+ import { SyncStatus , SyncStatusOptions } from '../../../db/crud/SyncStatus.js' ;
44import { AbortOperation } from '../../../utils/AbortOperation.js' ;
55import { BaseListener , BaseObserver , Disposable } from '../../../utils/BaseObserver.js' ;
6- import { throttleLeadingTrailing } from '../../../utils/throttle .js' ;
6+ import { onAbortPromise , throttleLeadingTrailing } from '../../../utils/async .js' ;
77import { BucketChecksum , BucketDescription , BucketStorageAdapter , Checkpoint } from '../bucket/BucketStorageAdapter.js' ;
88import { CrudEntry } from '../bucket/CrudEntry.js' ;
99import { SyncDataBucket } from '../bucket/SyncDataBucket.js' ;
@@ -161,6 +161,7 @@ export abstract class AbstractStreamingSyncImplementation
161161 protected abortController : AbortController | null ;
162162 protected crudUpdateListener ?: ( ) => void ;
163163 protected streamingSyncPromise ?: Promise < void > ;
164+ private pendingCrudUpload ?: Promise < void > ;
164165
165166 syncStatus : SyncStatus ;
166167 triggerCrudUpload : ( ) => void ;
@@ -181,10 +182,16 @@ export abstract class AbstractStreamingSyncImplementation
181182 this . abortController = null ;
182183
183184 this . triggerCrudUpload = throttleLeadingTrailing ( ( ) => {
184- if ( ! this . syncStatus . connected || this . syncStatus . dataFlowStatus . uploading ) {
185+ if ( ! this . syncStatus . connected || this . pendingCrudUpload != null ) {
185186 return ;
186187 }
187- this . _uploadAllCrud ( ) ;
188+
189+ this . pendingCrudUpload = new Promise ( ( resolve ) => {
190+ this . _uploadAllCrud ( ) . finally ( ( ) => {
191+ this . pendingCrudUpload = undefined ;
192+ resolve ( ) ;
193+ } ) ;
194+ } ) ;
188195 } , this . options . crudUploadThrottleMs ! ) ;
189196 }
190197
@@ -582,30 +589,12 @@ The next upload iteration will be delayed.`);
582589 await this . options . adapter . removeBuckets ( [ ...bucketsToDelete ] ) ;
583590 await this . options . adapter . setTargetCheckpoint ( targetCheckpoint ) ;
584591 } 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 ) ) ;
592+ const result = await this . applyCheckpoint ( targetCheckpoint ! , signal ) ;
593+ if ( result . endIteration ) {
591594 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 {
595+ } else if ( result . applied ) {
597596 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- } ) ;
607597 }
608-
609598 validatedCheckpoint = targetCheckpoint ;
610599 } else if ( isStreamingSyncCheckpointPartiallyComplete ( line ) ) {
611600 const priority = line . partial_checkpoint_complete . priority ;
@@ -617,7 +606,8 @@ The next upload iteration will be delayed.`);
617606 await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
618607 return { retry : true } ;
619608 } else if ( ! result . ready ) {
620- // Need more data for a consistent partial sync within a priority - continue waiting.
609+ // If we have pending uploads, we can't complete new checkpoints outside of priority 0.
610+ // We'll resolve this for a complete checkpoint.
621611 } else {
622612 // We'll keep on downloading, but can report that this priority is synced now.
623613 this . logger . debug ( 'partial checkpoint validation succeeded' ) ;
@@ -707,26 +697,13 @@ The next upload iteration will be delayed.`);
707697 }
708698 } ) ;
709699 } 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 ) ) ;
700+ const result = await this . applyCheckpoint ( targetCheckpoint ! , signal ) ;
701+ if ( result . endIteration ) {
702+ // TODO: Why is this one retry: false? That's the only change from when we receive
703+ // the line above?
715704 return { retry : false } ;
716- } else if ( ! result . ready ) {
717- // Checksums valid, but need more data for a consistent checkpoint.
718- // Continue waiting.
719- } else {
705+ } else if ( result . applied ) {
720706 appliedCheckpoint = targetCheckpoint ;
721- this . updateSyncStatus ( {
722- connected : true ,
723- lastSyncedAt : new Date ( ) ,
724- priorityStatusEntries : [ ] ,
725- dataFlow : {
726- downloading : false ,
727- downloadError : undefined
728- }
729- } ) ;
730707 }
731708 }
732709 }
@@ -738,6 +715,48 @@ The next upload iteration will be delayed.`);
738715 } ) ;
739716 }
740717
718+ private async applyCheckpoint ( checkpoint : Checkpoint , abort : AbortSignal ) {
719+ let result = await this . options . adapter . syncLocalDatabase ( checkpoint ) ;
720+ if ( ! result . checkpointValid ) {
721+ this . logger . debug ( 'Checksum mismatch in checkpoint, will reconnect' ) ;
722+ // This means checksums failed. Start again with a new checkpoint.
723+ // TODO: better back-off
724+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
725+ return { applied : false , endIteration : true } ;
726+ } else if ( ! result . ready ) {
727+ // We have pending entries in the local upload queue or are waiting to confirm a write
728+ // checkpoint. See if that is happening right now.
729+ const pending = this . pendingCrudUpload ;
730+ if ( pending != null ) {
731+ await Promise . race ( [ pending , onAbortPromise ( abort ) ] ) ;
732+ }
733+
734+ if ( abort . aborted || pending == null ) {
735+ return { applied : false , endIteration : true } ;
736+ }
737+
738+ // Try again now that uploads have completed.
739+ result = await this . options . adapter . syncLocalDatabase ( checkpoint ) ;
740+ }
741+
742+ if ( result . checkpointValid && result . ready ) {
743+ this . logger . debug ( 'validated checkpoint' , checkpoint ) ;
744+ this . updateSyncStatus ( {
745+ connected : true ,
746+ lastSyncedAt : new Date ( ) ,
747+ dataFlow : {
748+ downloading : false ,
749+ downloadError : undefined
750+ }
751+ } ) ;
752+
753+ return { applied : true , endIteration : false } ;
754+ } else {
755+ this . logger . debug ( 'Could not apply checkpoint even after waiting for uploads. Waiting for next sync complete line.' ) ;
756+ return { applied : false , endIteration : false } ;
757+ }
758+ }
759+
741760 protected updateSyncStatus ( options : SyncStatusOptions ) {
742761 const updatedStatus = new SyncStatus ( {
743762 connected : options . connected ?? this . syncStatus . connected ,
0 commit comments