@@ -20,9 +20,12 @@ use crate::{
20
20
error:: { PowerSyncError , PowerSyncErrorCause } ,
21
21
kv:: client_id,
22
22
state:: DatabaseState ,
23
- sync:: { checkpoint:: OwnedBucketChecksum , interface:: StartSyncStream } ,
23
+ sync:: {
24
+ checkpoint:: OwnedBucketChecksum , interface:: StartSyncStream , line:: DataLine ,
25
+ sync_status:: Timestamp , BucketPriority ,
26
+ } ,
24
27
} ;
25
- use sqlite_nostd:: { self as sqlite, ResultCode } ;
28
+ use sqlite_nostd:: { self as sqlite} ;
26
29
27
30
use super :: {
28
31
interface:: { Instruction , LogSeverity , StreamingSyncRequest , SyncControlRequest , SyncEvent } ,
@@ -245,50 +248,50 @@ impl StreamingSyncIteration {
245
248
Wait { a : PhantomData }
246
249
}
247
250
248
- /// Handles a single sync line.
251
+ /// Starts handling a single sync line without altering any in-memory state of the state
252
+ /// machine.
249
253
///
250
- /// When it returns `Ok(true)` , the sync iteration should be stopped. For errors, the type of
251
- /// error determines whether the iteration can continue .
252
- fn handle_line (
253
- & mut self ,
254
- target : & mut SyncTarget ,
254
+ /// After this call succeeds , the returned value can be used to update the state. For a
255
+ /// discussion on why this split is necessary, see [SyncStateMachineTransition] .
256
+ fn prepare_handling_sync_line < ' a > (
257
+ & self ,
258
+ target : & SyncTarget ,
255
259
event : & mut ActiveEvent ,
256
- line : & SyncLine ,
257
- ) -> Result < bool , PowerSyncError > {
258
- match line {
260
+ line : & ' a SyncLine < ' a > ,
261
+ ) -> Result < SyncStateMachineTransition < ' a > , PowerSyncError > {
262
+ Ok ( match line {
259
263
SyncLine :: Checkpoint ( checkpoint) => {
260
- self . validated_but_not_applied = None ;
261
- let to_delete = target. track_checkpoint ( & checkpoint) ;
264
+ let ( to_delete, updated_target) = target. track_checkpoint ( & checkpoint) ;
262
265
263
266
self . adapter
264
267
. delete_buckets ( to_delete. iter ( ) . map ( |b| b. as_str ( ) ) ) ?;
265
- let progress = self . load_progress ( target . target_checkpoint ( ) . unwrap ( ) ) ?;
266
- self . status . update (
267
- |s| s . start_tracking_checkpoint ( progress) ,
268
- & mut event . instructions ,
269
- ) ;
268
+ let progress = self . load_progress ( updated_target . target_checkpoint ( ) . unwrap ( ) ) ?;
269
+ SyncStateMachineTransition :: StartTrackingCheckpoint {
270
+ progress,
271
+ updated_target ,
272
+ }
270
273
}
271
274
SyncLine :: CheckpointDiff ( diff) => {
272
- let Some ( target) = target. target_checkpoint_mut ( ) else {
275
+ let Some ( target) = target. target_checkpoint ( ) else {
273
276
return Err ( PowerSyncError :: sync_protocol_error (
274
277
"Received checkpoint_diff without previous checkpoint" ,
275
278
PowerSyncErrorCause :: Unknown ,
276
279
) ) ;
277
280
} ;
278
281
282
+ let mut target = target. clone ( ) ;
279
283
target. apply_diff ( & diff) ;
280
- self . validated_but_not_applied = None ;
281
284
self . adapter
282
285
. delete_buckets ( diff. removed_buckets . iter ( ) . map ( |i| & * * i) ) ?;
283
286
284
- let progress = self . load_progress ( target) ?;
285
- self . status . update (
286
- |s| s . start_tracking_checkpoint ( progress) ,
287
- & mut event . instructions ,
288
- ) ;
287
+ let progress = self . load_progress ( & target) ?;
288
+ SyncStateMachineTransition :: StartTrackingCheckpoint {
289
+ progress,
290
+ updated_target : SyncTarget :: Tracking ( target ) ,
291
+ }
289
292
}
290
293
SyncLine :: CheckpointComplete ( _) => {
291
- let Some ( target) = target. target_checkpoint_mut ( ) else {
294
+ let Some ( target) = target. target_checkpoint ( ) else {
292
295
return Err ( PowerSyncError :: sync_protocol_error (
293
296
"Received checkpoint complete without previous checkpoint" ,
294
297
PowerSyncErrorCause :: Unknown ,
@@ -307,29 +310,34 @@ impl StreamingSyncIteration {
307
310
severity : LogSeverity :: WARNING ,
308
311
line : format ! ( "Could not apply checkpoint, {checkpoint_result}" ) . into ( ) ,
309
312
} ) ;
310
- return Ok ( true ) ;
313
+ SyncStateMachineTransition :: CloseIteration
311
314
}
312
315
SyncLocalResult :: PendingLocalChanges => {
313
316
event. instructions . push ( Instruction :: LogLine {
314
317
severity : LogSeverity :: INFO ,
315
318
line : "Could not apply checkpoint due to local data. Will retry at completed upload or next checkpoint." . into ( ) ,
316
319
} ) ;
317
320
318
- self . validated_but_not_applied = Some ( target. clone ( ) ) ;
321
+ SyncStateMachineTransition :: SyncLocalFailedDueToPendingCrud {
322
+ validated_but_not_applied : target. clone ( ) ,
323
+ }
319
324
}
320
325
SyncLocalResult :: ChangesApplied => {
321
326
event. instructions . push ( Instruction :: LogLine {
322
327
severity : LogSeverity :: DEBUG ,
323
328
line : "Validated and applied checkpoint" . into ( ) ,
324
329
} ) ;
325
330
event. instructions . push ( Instruction :: FlushFileSystem { } ) ;
326
- self . handle_checkpoint_applied ( event) ?;
331
+ SyncStateMachineTransition :: SyncLocalChangesApplied {
332
+ partial : None ,
333
+ timestamp : self . adapter . now ( ) ?,
334
+ }
327
335
}
328
336
}
329
337
}
330
338
SyncLine :: CheckpointPartiallyComplete ( complete) => {
331
339
let priority = complete. priority ;
332
- let Some ( target) = target. target_checkpoint_mut ( ) else {
340
+ let Some ( target) = target. target_checkpoint ( ) else {
333
341
return Err ( PowerSyncError :: state_error (
334
342
"Received checkpoint complete without previous checkpoint" ,
335
343
) ) ;
@@ -353,45 +361,105 @@ impl StreamingSyncIteration {
353
361
)
354
362
. into ( ) ,
355
363
} ) ;
356
- return Ok ( true ) ;
364
+ SyncStateMachineTransition :: CloseIteration
357
365
}
358
366
SyncLocalResult :: PendingLocalChanges => {
359
367
// If we have pending uploads, we can't complete new checkpoints outside
360
368
// of priority 0. We'll resolve this for a complete checkpoint later.
369
+ SyncStateMachineTransition :: Empty
361
370
}
362
371
SyncLocalResult :: ChangesApplied => {
363
372
let now = self . adapter . now ( ) ?;
364
- event. instructions . push ( Instruction :: FlushFileSystem { } ) ;
365
- self . status . update (
366
- |status| {
367
- status. partial_checkpoint_complete ( priority, now) ;
368
- } ,
369
- & mut event. instructions ,
370
- ) ;
373
+ SyncStateMachineTransition :: SyncLocalChangesApplied {
374
+ partial : Some ( priority) ,
375
+ timestamp : now,
376
+ }
371
377
}
372
378
}
373
379
}
374
380
SyncLine :: Data ( data_line) => {
375
- self . status
376
- . update ( |s| s. track_line ( & data_line) , & mut event. instructions ) ;
377
381
insert_bucket_operations ( & self . adapter , & data_line) ?;
382
+ SyncStateMachineTransition :: DataLineSaved { line : data_line }
378
383
}
379
384
SyncLine :: KeepAlive ( token) => {
380
385
if token. is_expired ( ) {
381
386
// Token expired already - stop the connection immediately.
382
387
event
383
388
. instructions
384
389
. push ( Instruction :: FetchCredentials { did_expire : true } ) ;
385
- return Ok ( true ) ;
390
+
391
+ SyncStateMachineTransition :: CloseIteration
386
392
} else if token. should_prefetch ( ) {
387
393
event
388
394
. instructions
389
395
. push ( Instruction :: FetchCredentials { did_expire : false } ) ;
396
+ SyncStateMachineTransition :: Empty
397
+ } else {
398
+ SyncStateMachineTransition :: Empty
390
399
}
391
400
}
392
- }
401
+ } )
402
+ }
393
403
394
- Ok ( false )
404
+ /// Applies a sync state transition, returning whether the iteration should be stopped.
405
+ fn apply_transition (
406
+ & mut self ,
407
+ target : & mut SyncTarget ,
408
+ event : & mut ActiveEvent ,
409
+ transition : SyncStateMachineTransition ,
410
+ ) -> bool {
411
+ match transition {
412
+ SyncStateMachineTransition :: StartTrackingCheckpoint {
413
+ progress,
414
+ updated_target,
415
+ } => {
416
+ self . status . update (
417
+ |s| s. start_tracking_checkpoint ( progress) ,
418
+ & mut event. instructions ,
419
+ ) ;
420
+ self . validated_but_not_applied = None ;
421
+ * target = updated_target;
422
+ }
423
+ SyncStateMachineTransition :: DataLineSaved { line } => {
424
+ self . status
425
+ . update ( |s| s. track_line ( & line) , & mut event. instructions ) ;
426
+ }
427
+ SyncStateMachineTransition :: CloseIteration => return true ,
428
+ SyncStateMachineTransition :: SyncLocalFailedDueToPendingCrud {
429
+ validated_but_not_applied,
430
+ } => {
431
+ self . validated_but_not_applied = Some ( validated_but_not_applied) ;
432
+ }
433
+ SyncStateMachineTransition :: SyncLocalChangesApplied { partial, timestamp } => {
434
+ if let Some ( priority) = partial {
435
+ self . status . update (
436
+ |status| {
437
+ status. partial_checkpoint_complete ( priority, timestamp) ;
438
+ } ,
439
+ & mut event. instructions ,
440
+ ) ;
441
+ } else {
442
+ self . handle_checkpoint_applied ( event, timestamp) ;
443
+ }
444
+ }
445
+ SyncStateMachineTransition :: Empty => { }
446
+ } ;
447
+
448
+ false
449
+ }
450
+
451
+ /// Handles a single sync line.
452
+ ///
453
+ /// When it returns `Ok(true)`, the sync iteration should be stopped. For errors, the type of
454
+ /// error determines whether the iteration can continue.
455
+ fn handle_line (
456
+ & mut self ,
457
+ target : & mut SyncTarget ,
458
+ event : & mut ActiveEvent ,
459
+ line : & SyncLine ,
460
+ ) -> Result < bool , PowerSyncError > {
461
+ let transition = self . prepare_handling_sync_line ( target, event, line) ?;
462
+ Ok ( self . apply_transition ( target, event, transition) )
395
463
}
396
464
397
465
/// Runs a full sync iteration, returning nothing when it completes regularly or an error when
@@ -432,7 +500,7 @@ impl StreamingSyncIteration {
432
500
. into ( ) ,
433
501
} ) ;
434
502
435
- self . handle_checkpoint_applied ( event) ? ;
503
+ self . handle_checkpoint_applied ( event, self . adapter . now ( ) ? ) ;
436
504
}
437
505
_ => {
438
506
event. instructions . push ( Instruction :: LogLine {
@@ -522,16 +590,13 @@ impl StreamingSyncIteration {
522
590
Ok ( local_bucket_names)
523
591
}
524
592
525
- fn handle_checkpoint_applied ( & mut self , event : & mut ActiveEvent ) -> Result < ( ) , ResultCode > {
593
+ fn handle_checkpoint_applied ( & mut self , event : & mut ActiveEvent , timestamp : Timestamp ) {
526
594
event. instructions . push ( Instruction :: DidCompleteSync { } ) ;
527
595
528
- let now = self . adapter . now ( ) ?;
529
596
self . status . update (
530
- |status| status. applied_checkpoint ( now ) ,
597
+ |status| status. applied_checkpoint ( timestamp ) ,
531
598
& mut event. instructions ,
532
599
) ;
533
-
534
- Ok ( ( ) )
535
600
}
536
601
}
537
602
@@ -553,18 +618,16 @@ impl SyncTarget {
553
618
}
554
619
}
555
620
556
- fn target_checkpoint_mut ( & mut self ) -> Option < & mut OwnedCheckpoint > {
557
- match self {
558
- Self :: Tracking ( cp) => Some ( cp) ,
559
- _ => None ,
560
- }
561
- }
562
-
563
621
/// Starts tracking the received `Checkpoint`.
564
622
///
565
- /// This updates the internal state and returns a set of buckets to delete because they've been
566
- /// tracked locally but not in the new checkpoint.
567
- fn track_checkpoint < ' a > ( & mut self , checkpoint : & Checkpoint < ' a > ) -> BTreeSet < String > {
623
+ /// This returns a set of buckets to delete because they've been tracked locally but not in the
624
+ /// checkpoint, as well as the updated state of the [SyncTarget] to apply after deleting those
625
+ /// buckets.
626
+ ///
627
+ /// The new state is not applied automatically - the old state should be kept in-memory until
628
+ /// the buckets have actually been deleted so that the operation can be retried if deleting
629
+ /// buckets fails.
630
+ fn track_checkpoint < ' a > ( & self , checkpoint : & Checkpoint < ' a > ) -> ( BTreeSet < String > , Self ) {
568
631
let mut to_delete: BTreeSet < String > = match & self {
569
632
SyncTarget :: Tracking ( checkpoint) => checkpoint. buckets . keys ( ) . cloned ( ) . collect ( ) ,
570
633
SyncTarget :: BeforeCheckpoint ( buckets) => buckets. iter ( ) . cloned ( ) . collect ( ) ,
@@ -576,8 +639,10 @@ impl SyncTarget {
576
639
to_delete. remove ( & * bucket. bucket ) ;
577
640
}
578
641
579
- * self = SyncTarget :: Tracking ( OwnedCheckpoint :: from_checkpoint ( checkpoint, buckets) ) ;
580
- to_delete
642
+ (
643
+ to_delete,
644
+ SyncTarget :: Tracking ( OwnedCheckpoint :: from_checkpoint ( checkpoint, buckets) ) ,
645
+ )
581
646
}
582
647
}
583
648
@@ -614,3 +679,32 @@ impl OwnedCheckpoint {
614
679
self . write_checkpoint = diff. write_checkpoint ;
615
680
}
616
681
}
682
+
683
+ /// A transition representing pending changes between [StreamingSyncIteration::prepare_handling_sync_line]
684
+ /// and [StreamingSyncIteration::apply_transition].
685
+ ///
686
+ /// This split allows the main logic handling sync lines to take a non-mutable reference to internal
687
+ /// client state, guaranteeing that it does not mutate state until changes have been written to the
688
+ /// database. Only after those writes have succeeded are the internal state changes applied.
689
+ ///
690
+ /// This split ensures that `powersync_control` calls are idempotent when running into temporary
691
+ /// SQLite errors, a property we need for compatibility with e.g. WA-sqlite, where the VFS can
692
+ /// return `BUSY` errors and the SQLite library automatically retries running statements.
693
+ enum SyncStateMachineTransition < ' a > {
694
+ StartTrackingCheckpoint {
695
+ progress : SyncDownloadProgress ,
696
+ updated_target : SyncTarget ,
697
+ } ,
698
+ DataLineSaved {
699
+ line : & ' a DataLine < ' a > ,
700
+ } ,
701
+ SyncLocalFailedDueToPendingCrud {
702
+ validated_but_not_applied : OwnedCheckpoint ,
703
+ } ,
704
+ SyncLocalChangesApplied {
705
+ partial : Option < BucketPriority > ,
706
+ timestamp : Timestamp ,
707
+ } ,
708
+ CloseIteration ,
709
+ Empty ,
710
+ }
0 commit comments