@@ -53,7 +53,7 @@ class StreamingSyncImplementation implements StreamingSync {
53
53
54
54
late final http.Client _client;
55
55
56
- final StreamController <String ? > _localPingController =
56
+ final StreamController <Null > _localPingController =
57
57
StreamController .broadcast ();
58
58
59
59
final Duration retryDelay;
@@ -340,96 +340,19 @@ class StreamingSyncImplementation implements StreamingSync {
340
340
}
341
341
342
342
_updateStatus (connected: true , connecting: false );
343
- if (line is Checkpoint ) {
344
- targetCheckpoint = line;
345
- final Set <String > bucketsToDelete = {...bucketSet};
346
- final Set <String > newBuckets = {};
347
- for (final checksum in line.checksums) {
348
- newBuckets.add (checksum.bucket);
349
- bucketsToDelete.remove (checksum.bucket);
350
- }
351
- bucketSet = newBuckets;
352
- await adapter.removeBuckets ([...bucketsToDelete]);
353
- _updateStatus (downloading: true );
354
- } else if (line is StreamingSyncCheckpointComplete ) {
355
- final result = await adapter.syncLocalDatabase (targetCheckpoint! );
356
- if (! result.checkpointValid) {
357
- // This means checksums failed. Start again with a new checkpoint.
358
- // TODO: better back-off
359
- // await new Promise((resolve) => setTimeout(resolve, 50));
360
- return false ;
361
- } else if (! result.ready) {
362
- // Checksums valid, but need more data for a consistent checkpoint.
363
- // Continue waiting.
364
- } else {
365
- appliedCheckpoint = targetCheckpoint;
366
-
367
- _updateStatus (
368
- downloading: false ,
369
- downloadError: _noError,
370
- lastSyncedAt: DateTime .now ());
371
- }
372
-
373
- validatedCheckpoint = targetCheckpoint;
374
- } else if (line is StreamingSyncCheckpointDiff ) {
375
- // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
376
- if (targetCheckpoint == null ) {
377
- throw PowerSyncProtocolException (
378
- 'Checkpoint diff without previous checkpoint' );
379
- }
380
- _updateStatus (downloading: true );
381
- final diff = line;
382
- final Map <String , BucketChecksum > newBuckets = {};
383
- for (var checksum in targetCheckpoint.checksums) {
384
- newBuckets[checksum.bucket] = checksum;
385
- }
386
- for (var checksum in diff.updatedBuckets) {
387
- newBuckets[checksum.bucket] = checksum;
388
- }
389
- for (var bucket in diff.removedBuckets) {
390
- newBuckets.remove (bucket);
391
- }
392
-
393
- final newCheckpoint = Checkpoint (
394
- lastOpId: diff.lastOpId,
395
- checksums: [...newBuckets.values],
396
- writeCheckpoint: diff.writeCheckpoint);
397
- targetCheckpoint = newCheckpoint;
398
-
399
- bucketSet = Set .from (newBuckets.keys);
400
- await adapter.removeBuckets (diff.removedBuckets);
401
- adapter.setTargetCheckpoint (targetCheckpoint);
402
- } else if (line is SyncBucketData ) {
403
- _updateStatus (downloading: true );
404
- await adapter.saveSyncData (SyncDataBatch ([line]));
405
- } else if (line is StreamingSyncKeepalive ) {
406
- if (line.tokenExpiresIn == 0 ) {
407
- // Token expired already - stop the connection immediately
408
- invalidCredentialsCallback? .call ().ignore ();
409
- break ;
410
- } else if (line.tokenExpiresIn <= 30 ) {
411
- // Token expires soon - refresh it in the background
412
- if (credentialsInvalidation == null &&
413
- invalidCredentialsCallback != null ) {
414
- credentialsInvalidation = invalidCredentialsCallback !().then ((_) {
415
- // Token has been refreshed - we should restart the connection.
416
- haveInvalidated = true ;
417
- // trigger next loop iteration ASAP, don't wait for another
418
- // message from the server.
419
- _localPingController.add (null );
420
- }, onError: (_) {
421
- // Token refresh failed - retry on next keepalive.
422
- credentialsInvalidation = null ;
423
- });
343
+ switch (line) {
344
+ case Checkpoint ():
345
+ targetCheckpoint = line;
346
+ final Set <String > bucketsToDelete = {...bucketSet};
347
+ final Set <String > newBuckets = {};
348
+ for (final checksum in line.checksums) {
349
+ newBuckets.add (checksum.bucket);
350
+ bucketsToDelete.remove (checksum.bucket);
424
351
}
425
- }
426
- } else {
427
- if (targetCheckpoint == appliedCheckpoint) {
428
- _updateStatus (
429
- downloading: false ,
430
- downloadError: _noError,
431
- lastSyncedAt: DateTime .now ());
432
- } else if (validatedCheckpoint == targetCheckpoint) {
352
+ bucketSet = newBuckets;
353
+ await adapter.removeBuckets ([...bucketsToDelete]);
354
+ _updateStatus (downloading: true );
355
+ case StreamingSyncCheckpointComplete ():
433
356
final result = await adapter.syncLocalDatabase (targetCheckpoint! );
434
357
if (! result.checkpointValid) {
435
358
// This means checksums failed. Start again with a new checkpoint.
@@ -447,7 +370,88 @@ class StreamingSyncImplementation implements StreamingSync {
447
370
downloadError: _noError,
448
371
lastSyncedAt: DateTime .now ());
449
372
}
450
- }
373
+
374
+ validatedCheckpoint = targetCheckpoint;
375
+ case StreamingSyncCheckpointDiff ():
376
+ // TODO: It may be faster to just keep track of the diff, instead of
377
+ // the entire checkpoint
378
+ if (targetCheckpoint == null ) {
379
+ throw PowerSyncProtocolException (
380
+ 'Checkpoint diff without previous checkpoint' );
381
+ }
382
+ _updateStatus (downloading: true );
383
+ final diff = line;
384
+ final Map <String , BucketChecksum > newBuckets = {};
385
+ for (var checksum in targetCheckpoint.checksums) {
386
+ newBuckets[checksum.bucket] = checksum;
387
+ }
388
+ for (var checksum in diff.updatedBuckets) {
389
+ newBuckets[checksum.bucket] = checksum;
390
+ }
391
+ for (var bucket in diff.removedBuckets) {
392
+ newBuckets.remove (bucket);
393
+ }
394
+
395
+ final newCheckpoint = Checkpoint (
396
+ lastOpId: diff.lastOpId,
397
+ checksums: [...newBuckets.values],
398
+ writeCheckpoint: diff.writeCheckpoint);
399
+ targetCheckpoint = newCheckpoint;
400
+
401
+ bucketSet = Set .from (newBuckets.keys);
402
+ await adapter.removeBuckets (diff.removedBuckets);
403
+ adapter.setTargetCheckpoint (targetCheckpoint);
404
+ case SyncDataBatch ():
405
+ _updateStatus (downloading: true );
406
+ await adapter.saveSyncData (line);
407
+ case StreamingSyncKeepalive (: final tokenExpiresIn):
408
+ if (tokenExpiresIn == 0 ) {
409
+ // Token expired already - stop the connection immediately
410
+ invalidCredentialsCallback? .call ().ignore ();
411
+ break ;
412
+ } else if (tokenExpiresIn <= 30 ) {
413
+ // Token expires soon - refresh it in the background
414
+ if (credentialsInvalidation == null &&
415
+ invalidCredentialsCallback != null ) {
416
+ credentialsInvalidation = invalidCredentialsCallback !().then ((_) {
417
+ // Token has been refreshed - we should restart the connection.
418
+ haveInvalidated = true ;
419
+ // trigger next loop iteration ASAP, don't wait for another
420
+ // message from the server.
421
+ _localPingController.add (null );
422
+ }, onError: (_) {
423
+ // Token refresh failed - retry on next keepalive.
424
+ credentialsInvalidation = null ;
425
+ });
426
+ }
427
+ }
428
+ case UnknownSyncLine (: final rawData):
429
+ isolateLogger.fine ('Unknown sync line: $rawData ' );
430
+ case null : // Local ping
431
+ if (targetCheckpoint == appliedCheckpoint) {
432
+ _updateStatus (
433
+ downloading: false ,
434
+ downloadError: _noError,
435
+ lastSyncedAt: DateTime .now ());
436
+ } else if (validatedCheckpoint == targetCheckpoint) {
437
+ final result = await adapter.syncLocalDatabase (targetCheckpoint! );
438
+ if (! result.checkpointValid) {
439
+ // This means checksums failed. Start again with a new checkpoint.
440
+ // TODO: better back-off
441
+ // await new Promise((resolve) => setTimeout(resolve, 50));
442
+ return false ;
443
+ } else if (! result.ready) {
444
+ // Checksums valid, but need more data for a consistent checkpoint.
445
+ // Continue waiting.
446
+ } else {
447
+ appliedCheckpoint = targetCheckpoint;
448
+
449
+ _updateStatus (
450
+ downloading: false ,
451
+ downloadError: _noError,
452
+ lastSyncedAt: DateTime .now ());
453
+ }
454
+ }
451
455
}
452
456
453
457
if (haveInvalidated) {
@@ -458,7 +462,8 @@ class StreamingSyncImplementation implements StreamingSync {
458
462
return true ;
459
463
}
460
464
461
- Stream <Object ?> streamingSyncRequest (StreamingSyncRequest data) async * {
465
+ Stream <StreamingSyncLine > streamingSyncRequest (
466
+ StreamingSyncRequest data) async * {
462
467
final credentials = await credentialsCallback ();
463
468
if (credentials == null ) {
464
469
throw CredentialsException ('Not logged in' );
@@ -494,12 +499,10 @@ class StreamingSyncImplementation implements StreamingSync {
494
499
}
495
500
496
501
// Note: The response stream is automatically closed when this loop errors
497
- await for (var line in ndjson (res.stream)) {
498
- if (aborted) {
499
- break ;
500
- }
501
- yield parseStreamingSyncLine (line as Map <String , dynamic >);
502
- }
502
+ yield * ndjson (res.stream)
503
+ .cast <Map <String , dynamic >>()
504
+ .transform (StreamingSyncLine .reader)
505
+ .takeWhile ((_) => ! aborted);
503
506
}
504
507
505
508
/// Delays the standard `retryDelay` Duration, but exits early if
0 commit comments