@powersync/common 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,10 @@
1
1
  import Logger from 'js-logger';
2
2
  import { SyncStatus } from '../../../db/crud/SyncStatus.js';
3
+ import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
3
4
  import { AbortOperation } from '../../../utils/AbortOperation.js';
4
5
  import { BaseObserver } from '../../../utils/BaseObserver.js';
5
6
  import { onAbortPromise, throttleLeadingTrailing } from '../../../utils/async.js';
7
+ import { PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js';
6
8
  import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
7
9
  import { FetchStrategy } from './AbstractRemote.js';
8
10
  import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData } from './streaming-sync-types.js';
@@ -16,6 +18,49 @@ export var SyncStreamConnectionMethod;
16
18
  SyncStreamConnectionMethod["HTTP"] = "http";
17
19
  SyncStreamConnectionMethod["WEB_SOCKET"] = "web-socket";
18
20
  })(SyncStreamConnectionMethod || (SyncStreamConnectionMethod = {}));
21
+ export var SyncClientImplementation;
22
+ (function (SyncClientImplementation) {
23
+ /**
24
+ * Decodes and handles sync lines received from the sync service in JavaScript.
25
+ *
26
+ * This is the default option.
27
+ *
28
+ * @deprecated Don't use {@link SyncClientImplementation.JAVASCRIPT} directly. Instead, use
29
+ * {@link DEFAULT_SYNC_CLIENT_IMPLEMENTATION} or omit the option. The explicit choice to use
30
+ * the JavaScript-based sync implementation will be removed from a future version of the SDK.
31
+ */
32
+ SyncClientImplementation["JAVASCRIPT"] = "js";
33
+ /**
34
+ * This implementation offloads the sync line decoding and handling into the PowerSync
35
+ * core extension.
36
+ *
37
+ * @experimental
38
+ * While this implementation is more performant than {@link SyncClientImplementation.JAVASCRIPT},
39
+ * it has seen less real-world testing and is marked as __experimental__ at the moment.
40
+ *
41
+ * ## Compatibility warning
42
+ *
43
+ * The Rust sync client stores sync data in a format that is slightly different than the one used
44
+ * by the old {@link JAVASCRIPT} implementation. When adopting the {@link RUST} client on existing
45
+ * databases, the PowerSync SDK will migrate the format automatically.
46
+ * Further, the {@link JAVASCRIPT} client in recent versions of the PowerSync JS SDK (starting from
47
+ * the version introducing {@link RUST} as an option) also supports the new format, so you can switch
48
+ * back to {@link JAVASCRIPT} later.
49
+ *
50
+ * __However__: Upgrading the SDK version, then adopting {@link RUST} as a sync client and later
51
+ * downgrading the SDK to an older version (necessarily using the JavaScript-based implementation then)
52
+ * can lead to sync issues.
53
+ */
54
+ SyncClientImplementation["RUST"] = "rust";
55
+ })(SyncClientImplementation || (SyncClientImplementation = {}));
56
+ /**
57
+ * The default {@link SyncClientImplementation} to use.
58
+ *
59
+ * Please use this field instead of {@link SyncClientImplementation.JAVASCRIPT} directly. A future version
60
+ * of the PowerSync SDK will enable {@link SyncClientImplementation.RUST} by default and remove the JavaScript
61
+ * option.
62
+ */
63
+ export const DEFAULT_SYNC_CLIENT_IMPLEMENTATION = SyncClientImplementation.JAVASCRIPT;
19
64
  export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
20
65
  export const DEFAULT_RETRY_DELAY_MS = 5000;
21
66
  export const DEFAULT_STREAMING_SYNC_OPTIONS = {
@@ -25,6 +70,7 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
25
70
  };
26
71
  export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
27
72
  connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
73
+ clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
28
74
  fetchStrategy: FetchStrategy.Buffered,
29
75
  params: {}
30
76
  };
@@ -40,6 +86,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
40
86
  crudUpdateListener;
41
87
  streamingSyncPromise;
42
88
  pendingCrudUpload;
89
+ notifyCompletedUploads;
43
90
  syncStatus;
44
91
  triggerCrudUpload;
45
92
  constructor(options) {
@@ -61,6 +108,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
61
108
  }
62
109
  this.pendingCrudUpload = new Promise((resolve) => {
63
110
  this._uploadAllCrud().finally(() => {
111
+ this.notifyCompletedUploads?.();
64
112
  this.pendingCrudUpload = undefined;
65
113
  resolve();
66
114
  });
@@ -348,6 +396,31 @@ The next upload iteration will be delayed.`);
348
396
  }
349
397
  return [req, localDescriptions];
350
398
  }
399
+ /**
400
+ * Older versions of the JS SDK used to encode subkeys as JSON in {@link OplogEntry.toJSON}.
401
+ * Because subkeys are always strings, this leads to quotes being added around them in `ps_oplog`.
402
+ * While this is not a problem as long as it's done consistently, it causes issues when a database
403
+ * created by the JS SDK is used with other SDKs, or (more likely) when the new Rust sync client
404
+ * is enabled.
405
+ *
406
+ * So, we add a migration from the old key format (with quotes) to the new one (no quotes). The
407
+ * migration is only triggered when necessary (for now). The function returns whether the new format
408
+ * should be used, so that the JS SDK is able to write to updated databases.
409
+ *
410
+ * @param requireFixedKeyFormat Whether we require the new format or also support the old one.
411
+ * The Rust client requires the new subkey format.
412
+ * @returns Whether the database is now using the new, fixed subkey format.
413
+ */
414
+ async requireKeyFormat(requireFixedKeyFormat) {
415
+ const hasMigrated = await this.options.adapter.hasMigratedSubkeys();
416
+ if (requireFixedKeyFormat && !hasMigrated) {
417
+ await this.options.adapter.migrateToFixedSubkeys();
418
+ return true;
419
+ }
420
+ else {
421
+ return hasMigrated;
422
+ }
423
+ }
351
424
  async streamingSyncIteration(signal, options) {
352
425
  await this.obtainLock({
353
426
  type: LockType.SYNC,
@@ -357,219 +430,373 @@ The next upload iteration will be delayed.`);
357
430
  ...DEFAULT_STREAM_CONNECTION_OPTIONS,
358
431
  ...(options ?? {})
359
432
  };
360
- this.logger.debug('Streaming sync iteration started');
361
- this.options.adapter.startSession();
362
- let [req, bucketMap] = await this.collectLocalBucketState();
363
- // These are compared by reference
364
- let targetCheckpoint = null;
365
- let validatedCheckpoint = null;
366
- let appliedCheckpoint = null;
367
- const clientId = await this.options.adapter.getClientId();
368
- if (signal.aborted) {
433
+ if (resolvedOptions.clientImplementation == SyncClientImplementation.JAVASCRIPT) {
434
+ await this.legacyStreamingSyncIteration(signal, resolvedOptions);
435
+ }
436
+ else {
437
+ await this.requireKeyFormat(true);
438
+ await this.rustSyncIteration(signal, resolvedOptions);
439
+ }
440
+ }
441
+ });
442
+ }
443
+ async legacyStreamingSyncIteration(signal, resolvedOptions) {
444
+ this.logger.debug('Streaming sync iteration started');
445
+ this.options.adapter.startSession();
446
+ let [req, bucketMap] = await this.collectLocalBucketState();
447
+ // These are compared by reference
448
+ let targetCheckpoint = null;
449
+ let validatedCheckpoint = null;
450
+ let appliedCheckpoint = null;
451
+ const clientId = await this.options.adapter.getClientId();
452
+ const usingFixedKeyFormat = await this.requireKeyFormat(false);
453
+ this.logger.debug('Requesting stream from server');
454
+ const syncOptions = {
455
+ path: '/sync/stream',
456
+ abortSignal: signal,
457
+ data: {
458
+ buckets: req,
459
+ include_checksum: true,
460
+ raw_data: true,
461
+ parameters: resolvedOptions.params,
462
+ client_id: clientId
463
+ }
464
+ };
465
+ let stream;
466
+ if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
467
+ stream = await this.options.remote.postStream(syncOptions);
468
+ }
469
+ else {
470
+ stream = await this.options.remote.socketStream({
471
+ ...syncOptions,
472
+ ...{ fetchStrategy: resolvedOptions.fetchStrategy }
473
+ });
474
+ }
475
+ this.logger.debug('Stream established. Processing events');
476
+ while (!stream.closed) {
477
+ const line = await stream.read();
478
+ if (!line) {
479
+ // The stream has closed while waiting
480
+ return;
481
+ }
482
+ // A connection is active and messages are being received
483
+ if (!this.syncStatus.connected) {
484
+ // There is a connection now
485
+ Promise.resolve().then(() => this.triggerCrudUpload());
486
+ this.updateSyncStatus({
487
+ connected: true
488
+ });
489
+ }
490
+ if (isStreamingSyncCheckpoint(line)) {
491
+ targetCheckpoint = line.checkpoint;
492
+ const bucketsToDelete = new Set(bucketMap.keys());
493
+ const newBuckets = new Map();
494
+ for (const checksum of line.checkpoint.buckets) {
495
+ newBuckets.set(checksum.bucket, {
496
+ name: checksum.bucket,
497
+ priority: checksum.priority ?? FALLBACK_PRIORITY
498
+ });
499
+ bucketsToDelete.delete(checksum.bucket);
500
+ }
501
+ if (bucketsToDelete.size > 0) {
502
+ this.logger.debug('Removing buckets', [...bucketsToDelete]);
503
+ }
504
+ bucketMap = newBuckets;
505
+ await this.options.adapter.removeBuckets([...bucketsToDelete]);
506
+ await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
507
+ await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
508
+ }
509
+ else if (isStreamingSyncCheckpointComplete(line)) {
510
+ const result = await this.applyCheckpoint(targetCheckpoint, signal);
511
+ if (result.endIteration) {
369
512
  return;
370
513
  }
371
- this.logger.debug('Requesting stream from server');
372
- const syncOptions = {
373
- path: '/sync/stream',
374
- abortSignal: signal,
375
- data: {
376
- buckets: req,
377
- include_checksum: true,
378
- raw_data: true,
379
- parameters: resolvedOptions.params,
380
- client_id: clientId
381
- }
382
- };
383
- let stream;
384
- if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
385
- stream = await this.options.remote.postStream(syncOptions);
514
+ else if (result.applied) {
515
+ appliedCheckpoint = targetCheckpoint;
516
+ }
517
+ validatedCheckpoint = targetCheckpoint;
518
+ }
519
+ else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
520
+ const priority = line.partial_checkpoint_complete.priority;
521
+ this.logger.debug('Partial checkpoint complete', priority);
522
+ const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
523
+ if (!result.checkpointValid) {
524
+ // This means checksums failed. Start again with a new checkpoint.
525
+ // TODO: better back-off
526
+ await new Promise((resolve) => setTimeout(resolve, 50));
527
+ return;
528
+ }
529
+ else if (!result.ready) {
530
+ // If we have pending uploads, we can't complete new checkpoints outside of priority 0.
531
+ // We'll resolve this for a complete checkpoint.
386
532
  }
387
533
  else {
388
- stream = await this.options.remote.socketStream({
389
- ...syncOptions,
390
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
534
+ // We'll keep on downloading, but can report that this priority is synced now.
535
+ this.logger.debug('partial checkpoint validation succeeded');
536
+ // All states with a higher priority can be deleted since this partial sync includes them.
537
+ const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
538
+ priorityStates.push({
539
+ priority,
540
+ lastSyncedAt: new Date(),
541
+ hasSynced: true
542
+ });
543
+ this.updateSyncStatus({
544
+ connected: true,
545
+ priorityStatusEntries: priorityStates
391
546
  });
392
547
  }
393
- this.logger.debug('Stream established. Processing events');
394
- while (!stream.closed) {
395
- const line = await stream.read();
396
- if (!line) {
397
- // The stream has closed while waiting
398
- return;
399
- }
400
- // A connection is active and messages are being received
401
- if (!this.syncStatus.connected) {
402
- // There is a connection now
403
- Promise.resolve().then(() => this.triggerCrudUpload());
404
- this.updateSyncStatus({
405
- connected: true
406
- });
407
- }
408
- if (isStreamingSyncCheckpoint(line)) {
409
- targetCheckpoint = line.checkpoint;
410
- const bucketsToDelete = new Set(bucketMap.keys());
411
- const newBuckets = new Map();
412
- for (const checksum of line.checkpoint.buckets) {
413
- newBuckets.set(checksum.bucket, {
414
- name: checksum.bucket,
415
- priority: checksum.priority ?? FALLBACK_PRIORITY
416
- });
417
- bucketsToDelete.delete(checksum.bucket);
418
- }
419
- if (bucketsToDelete.size > 0) {
420
- this.logger.debug('Removing buckets', [...bucketsToDelete]);
421
- }
422
- bucketMap = newBuckets;
423
- await this.options.adapter.removeBuckets([...bucketsToDelete]);
424
- await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
425
- await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
426
- }
427
- else if (isStreamingSyncCheckpointComplete(line)) {
428
- const result = await this.applyCheckpoint(targetCheckpoint, signal);
429
- if (result.endIteration) {
430
- return;
431
- }
432
- else if (result.applied) {
433
- appliedCheckpoint = targetCheckpoint;
434
- }
435
- validatedCheckpoint = targetCheckpoint;
436
- }
437
- else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
438
- const priority = line.partial_checkpoint_complete.priority;
439
- this.logger.debug('Partial checkpoint complete', priority);
440
- const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
441
- if (!result.checkpointValid) {
442
- // This means checksums failed. Start again with a new checkpoint.
443
- // TODO: better back-off
444
- await new Promise((resolve) => setTimeout(resolve, 50));
445
- return;
446
- }
447
- else if (!result.ready) {
448
- // If we have pending uploads, we can't complete new checkpoints outside of priority 0.
449
- // We'll resolve this for a complete checkpoint.
450
- }
451
- else {
452
- // We'll keep on downloading, but can report that this priority is synced now.
453
- this.logger.debug('partial checkpoint validation succeeded');
454
- // All states with a higher priority can be deleted since this partial sync includes them.
455
- const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
456
- priorityStates.push({
457
- priority,
458
- lastSyncedAt: new Date(),
459
- hasSynced: true
460
- });
461
- this.updateSyncStatus({
462
- connected: true,
463
- priorityStatusEntries: priorityStates
464
- });
465
- }
466
- }
467
- else if (isStreamingSyncCheckpointDiff(line)) {
468
- // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
469
- if (targetCheckpoint == null) {
470
- throw new Error('Checkpoint diff without previous checkpoint');
471
- }
472
- const diff = line.checkpoint_diff;
473
- const newBuckets = new Map();
474
- for (const checksum of targetCheckpoint.buckets) {
475
- newBuckets.set(checksum.bucket, checksum);
476
- }
477
- for (const checksum of diff.updated_buckets) {
478
- newBuckets.set(checksum.bucket, checksum);
479
- }
480
- for (const bucket of diff.removed_buckets) {
481
- newBuckets.delete(bucket);
482
- }
483
- const newCheckpoint = {
484
- last_op_id: diff.last_op_id,
485
- buckets: [...newBuckets.values()],
486
- write_checkpoint: diff.write_checkpoint
548
+ }
549
+ else if (isStreamingSyncCheckpointDiff(line)) {
550
+ // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
551
+ if (targetCheckpoint == null) {
552
+ throw new Error('Checkpoint diff without previous checkpoint');
553
+ }
554
+ const diff = line.checkpoint_diff;
555
+ const newBuckets = new Map();
556
+ for (const checksum of targetCheckpoint.buckets) {
557
+ newBuckets.set(checksum.bucket, checksum);
558
+ }
559
+ for (const checksum of diff.updated_buckets) {
560
+ newBuckets.set(checksum.bucket, checksum);
561
+ }
562
+ for (const bucket of diff.removed_buckets) {
563
+ newBuckets.delete(bucket);
564
+ }
565
+ const newCheckpoint = {
566
+ last_op_id: diff.last_op_id,
567
+ buckets: [...newBuckets.values()],
568
+ write_checkpoint: diff.write_checkpoint
569
+ };
570
+ targetCheckpoint = newCheckpoint;
571
+ await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
572
+ bucketMap = new Map();
573
+ newBuckets.forEach((checksum, name) => bucketMap.set(name, {
574
+ name: checksum.bucket,
575
+ priority: checksum.priority ?? FALLBACK_PRIORITY
576
+ }));
577
+ const bucketsToDelete = diff.removed_buckets;
578
+ if (bucketsToDelete.length > 0) {
579
+ this.logger.debug('Remove buckets', bucketsToDelete);
580
+ }
581
+ await this.options.adapter.removeBuckets(bucketsToDelete);
582
+ await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
583
+ }
584
+ else if (isStreamingSyncData(line)) {
585
+ const { data } = line;
586
+ const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
587
+ let updatedProgress = null;
588
+ if (previousProgress) {
589
+ updatedProgress = { ...previousProgress };
590
+ const progressForBucket = updatedProgress[data.bucket];
591
+ if (progressForBucket) {
592
+ updatedProgress[data.bucket] = {
593
+ ...progressForBucket,
594
+ since_last: progressForBucket.since_last + data.data.length
487
595
  };
488
- targetCheckpoint = newCheckpoint;
489
- await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
490
- bucketMap = new Map();
491
- newBuckets.forEach((checksum, name) => bucketMap.set(name, {
492
- name: checksum.bucket,
493
- priority: checksum.priority ?? FALLBACK_PRIORITY
494
- }));
495
- const bucketsToDelete = diff.removed_buckets;
496
- if (bucketsToDelete.length > 0) {
497
- this.logger.debug('Remove buckets', bucketsToDelete);
498
- }
499
- await this.options.adapter.removeBuckets(bucketsToDelete);
500
- await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
501
596
  }
502
- else if (isStreamingSyncData(line)) {
503
- const { data } = line;
504
- const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
505
- let updatedProgress = null;
506
- if (previousProgress) {
507
- updatedProgress = { ...previousProgress };
508
- const progressForBucket = updatedProgress[data.bucket];
509
- if (progressForBucket) {
510
- updatedProgress[data.bucket] = {
511
- ...progressForBucket,
512
- sinceLast: Math.min(progressForBucket.sinceLast + data.data.length, progressForBucket.targetCount - progressForBucket.atLast)
513
- };
514
- }
515
- }
516
- this.updateSyncStatus({
517
- dataFlow: {
518
- downloading: true,
519
- downloadProgress: updatedProgress
520
- }
521
- });
522
- await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] });
597
+ }
598
+ this.updateSyncStatus({
599
+ dataFlow: {
600
+ downloading: true,
601
+ downloadProgress: updatedProgress
523
602
  }
524
- else if (isStreamingKeepalive(line)) {
525
- const remaining_seconds = line.token_expires_in;
526
- if (remaining_seconds == 0) {
527
- // Connection would be closed automatically right after this
528
- this.logger.debug('Token expiring; reconnect');
529
- this.options.remote.invalidateCredentials();
530
- /**
531
- * For a rare case where the backend connector does not update the token
532
- * (uses the same one), this should have some delay.
533
- */
534
- await this.delayRetry();
535
- return;
536
- }
537
- else if (remaining_seconds < 30) {
538
- this.logger.debug('Token will expire soon; reconnect');
539
- // Pre-emptively refresh the token
540
- this.options.remote.invalidateCredentials();
541
- return;
603
+ });
604
+ await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] }, usingFixedKeyFormat);
605
+ }
606
+ else if (isStreamingKeepalive(line)) {
607
+ const remaining_seconds = line.token_expires_in;
608
+ if (remaining_seconds == 0) {
609
+ // Connection would be closed automatically right after this
610
+ this.logger.debug('Token expiring; reconnect');
611
+ /**
612
+ * For a rare case where the backend connector does not update the token
613
+ * (uses the same one), this should have some delay.
614
+ */
615
+ await this.delayRetry();
616
+ return;
617
+ }
618
+ else if (remaining_seconds < 30) {
619
+ this.logger.debug('Token will expire soon; reconnect');
620
+ // Pre-emptively refresh the token
621
+ this.options.remote.invalidateCredentials();
622
+ return;
623
+ }
624
+ this.triggerCrudUpload();
625
+ }
626
+ else {
627
+ this.logger.debug('Sync complete');
628
+ if (targetCheckpoint === appliedCheckpoint) {
629
+ this.updateSyncStatus({
630
+ connected: true,
631
+ lastSyncedAt: new Date(),
632
+ priorityStatusEntries: [],
633
+ dataFlow: {
634
+ downloadError: undefined
542
635
  }
543
- this.triggerCrudUpload();
636
+ });
637
+ }
638
+ else if (validatedCheckpoint === targetCheckpoint) {
639
+ const result = await this.applyCheckpoint(targetCheckpoint, signal);
640
+ if (result.endIteration) {
641
+ return;
544
642
  }
545
- else {
546
- this.logger.debug('Sync complete');
547
- if (targetCheckpoint === appliedCheckpoint) {
548
- this.updateSyncStatus({
549
- connected: true,
550
- lastSyncedAt: new Date(),
551
- priorityStatusEntries: [],
552
- dataFlow: {
553
- downloadError: undefined
554
- }
555
- });
556
- }
557
- else if (validatedCheckpoint === targetCheckpoint) {
558
- const result = await this.applyCheckpoint(targetCheckpoint, signal);
559
- if (result.endIteration) {
560
- return;
561
- }
562
- else if (result.applied) {
563
- appliedCheckpoint = targetCheckpoint;
564
- }
565
- }
643
+ else if (result.applied) {
644
+ appliedCheckpoint = targetCheckpoint;
566
645
  }
567
646
  }
568
- this.logger.debug('Stream input empty');
569
- // Connection closed. Likely due to auth issue.
570
- return;
571
647
  }
572
- });
648
+ }
649
+ this.logger.debug('Stream input empty');
650
+ // Connection closed. Likely due to auth issue.
651
+ return;
652
+ }
653
+ async rustSyncIteration(signal, resolvedOptions) {
654
+ const syncImplementation = this;
655
+ const adapter = this.options.adapter;
656
+ const remote = this.options.remote;
657
+ let receivingLines = null;
658
+ const abortController = new AbortController();
659
+ signal.addEventListener('abort', () => abortController.abort());
660
+ // Pending sync lines received from the service, as well as local events that trigger a powersync_control
661
+ // invocation (local events include refreshed tokens and completed uploads).
662
+ // This is a single data stream so that we can handle all control calls from a single place.
663
+ let controlInvocations = null;
664
+ async function connect(instr) {
665
+ const syncOptions = {
666
+ path: '/sync/stream',
667
+ abortSignal: abortController.signal,
668
+ data: instr.request
669
+ };
670
+ if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
671
+ controlInvocations = await remote.postStreamRaw(syncOptions, (line) => ({
672
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
673
+ payload: line
674
+ }));
675
+ }
676
+ else {
677
+ controlInvocations = await remote.socketStreamRaw({
678
+ ...syncOptions,
679
+ fetchStrategy: resolvedOptions.fetchStrategy
680
+ }, (buffer) => ({
681
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
682
+ payload: buffer
683
+ }));
684
+ }
685
+ try {
686
+ while (!controlInvocations.closed) {
687
+ const line = await controlInvocations.read();
688
+ if (line == null) {
689
+ return;
690
+ }
691
+ await control(line.command, line.payload);
692
+ }
693
+ }
694
+ finally {
695
+ const activeInstructions = controlInvocations;
696
+ // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
697
+ // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
698
+ // to null.
699
+ controlInvocations = null;
700
+ await activeInstructions.close();
701
+ }
702
+ }
703
+ async function stop() {
704
+ await control(PowerSyncControlCommand.STOP);
705
+ }
706
+ async function control(op, payload) {
707
+ const rawResponse = await adapter.control(op, payload ?? null);
708
+ await handleInstructions(JSON.parse(rawResponse));
709
+ }
710
+ async function handleInstruction(instruction) {
711
+ if ('LogLine' in instruction) {
712
+ switch (instruction.LogLine.severity) {
713
+ case 'DEBUG':
714
+ syncImplementation.logger.debug(instruction.LogLine.line);
715
+ break;
716
+ case 'INFO':
717
+ syncImplementation.logger.info(instruction.LogLine.line);
718
+ break;
719
+ case 'WARNING':
720
+ syncImplementation.logger.warn(instruction.LogLine.line);
721
+ break;
722
+ }
723
+ }
724
+ else if ('UpdateSyncStatus' in instruction) {
725
+ function coreStatusToJs(status) {
726
+ return {
727
+ priority: status.priority,
728
+ hasSynced: status.has_synced ?? undefined,
729
+ lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at) : undefined
730
+ };
731
+ }
732
+ const info = instruction.UpdateSyncStatus.status;
733
+ const coreCompleteSync = info.priority_status.find((s) => s.priority == FULL_SYNC_PRIORITY);
734
+ const completeSync = coreCompleteSync != null ? coreStatusToJs(coreCompleteSync) : null;
735
+ syncImplementation.updateSyncStatus({
736
+ connected: info.connected,
737
+ connecting: info.connecting,
738
+ dataFlow: {
739
+ downloading: info.downloading != null,
740
+ downloadProgress: info.downloading?.buckets
741
+ },
742
+ lastSyncedAt: completeSync?.lastSyncedAt,
743
+ hasSynced: completeSync?.hasSynced,
744
+ priorityStatusEntries: info.priority_status.map(coreStatusToJs)
745
+ });
746
+ }
747
+ else if ('EstablishSyncStream' in instruction) {
748
+ if (receivingLines != null) {
749
+ // Already connected, this shouldn't happen during a single iteration.
750
+ throw 'Unexpected request to establish sync stream, already connected';
751
+ }
752
+ receivingLines = connect(instruction.EstablishSyncStream);
753
+ }
754
+ else if ('FetchCredentials' in instruction) {
755
+ if (instruction.FetchCredentials.did_expire) {
756
+ remote.invalidateCredentials();
757
+ }
758
+ else {
759
+ remote.invalidateCredentials();
760
+ // Restart iteration after the credentials have been refreshed.
761
+ remote.fetchCredentials().then((_) => {
762
+ controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
763
+ }, (err) => {
764
+ syncImplementation.logger.warn('Could not prefetch credentials', err);
765
+ });
766
+ }
767
+ }
768
+ else if ('CloseSyncStream' in instruction) {
769
+ abortController.abort();
770
+ }
771
+ else if ('FlushFileSystem' in instruction) {
772
+ // Not necessary on JS platforms.
773
+ }
774
+ else if ('DidCompleteSync' in instruction) {
775
+ syncImplementation.updateSyncStatus({
776
+ dataFlow: {
777
+ downloadError: undefined
778
+ }
779
+ });
780
+ }
781
+ }
782
+ async function handleInstructions(instructions) {
783
+ for (const instr of instructions) {
784
+ await handleInstruction(instr);
785
+ }
786
+ }
787
+ try {
788
+ await control(PowerSyncControlCommand.START, JSON.stringify({
789
+ parameters: resolvedOptions.params
790
+ }));
791
+ this.notifyCompletedUploads = () => {
792
+ controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
793
+ };
794
+ await receivingLines;
795
+ }
796
+ finally {
797
+ this.notifyCompletedUploads = undefined;
798
+ await stop();
799
+ }
573
800
  }
574
801
  async updateSyncStatusForStartingCheckpoint(checkpoint) {
575
802
  const localProgress = await this.options.adapter.getBucketOperationProgress();
@@ -583,9 +810,9 @@ The next upload iteration will be delayed.`);
583
810
  // The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
584
811
  // will use by default.
585
812
  priority: bucket.priority ?? 3,
586
- atLast: atLast,
587
- sinceLast: sinceLast,
588
- targetCount: bucket.count ?? 0
813
+ at_last: atLast,
814
+ since_last: sinceLast,
815
+ target_count: bucket.count ?? 0
589
816
  };
590
817
  if (bucket.count != null && bucket.count < atLast + sinceLast) {
591
818
  // Either due to a defrag / sync rule deploy or a compaction operation, the size
@@ -597,8 +824,8 @@ The next upload iteration will be delayed.`);
597
824
  if (invalidated) {
598
825
  for (const bucket in progress) {
599
826
  const bucketProgress = progress[bucket];
600
- bucketProgress.atLast = 0;
601
- bucketProgress.sinceLast = 0;
827
+ bucketProgress.at_last = 0;
828
+ bucketProgress.since_last = 0;
602
829
  }
603
830
  }
604
831
  this.updateSyncStatus({