@powersync/common 1.53.1 → 1.53.2

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.
package/dist/bundle.cjs CHANGED
@@ -2430,30 +2430,44 @@ function throttleTrailing(func, wait) {
2430
2430
  }
2431
2431
  };
2432
2432
  }
2433
- /**
2434
- * Throttle a function to be called at most once every "wait" milliseconds,
2435
- * on the leading and trailing edge.
2436
- *
2437
- * Roughly equivalent to lodash/throttle with {leading: true, trailing: true}
2438
- */
2439
- function throttleLeadingTrailing(func, wait) {
2440
- let timeoutId = null;
2441
- let lastCallTime = 0;
2442
- const invokeFunction = () => {
2443
- func();
2444
- lastCallTime = Date.now();
2445
- timeoutId = null;
2446
- };
2447
- return function () {
2448
- const now = Date.now();
2449
- const timeToWait = wait - (now - lastCallTime);
2450
- if (timeToWait <= 0) {
2451
- // Leading edge: Call the function immediately if enough time has passed
2452
- invokeFunction();
2453
- }
2454
- else if (!timeoutId) {
2455
- // Set a timeout for the trailing edge if not already set
2456
- timeoutId = setTimeout(invokeFunction, timeToWait);
2433
+ function asyncNotifier() {
2434
+ let waitingConsumer = null;
2435
+ let hasPendingNotification = false;
2436
+ return {
2437
+ notify() {
2438
+ if (waitingConsumer != null) {
2439
+ waitingConsumer();
2440
+ waitingConsumer = null;
2441
+ }
2442
+ else {
2443
+ hasPendingNotification = true;
2444
+ }
2445
+ },
2446
+ waitForNotification(signal) {
2447
+ return new Promise((resolve) => {
2448
+ if (waitingConsumer != null) {
2449
+ throw new Error('Illegal call to waitForNotification, already has a waiter.');
2450
+ }
2451
+ if (signal.aborted) {
2452
+ resolve();
2453
+ }
2454
+ else if (hasPendingNotification) {
2455
+ resolve();
2456
+ hasPendingNotification = false;
2457
+ }
2458
+ else {
2459
+ function complete() {
2460
+ signal.removeEventListener('abort', onAbort);
2461
+ resolve();
2462
+ }
2463
+ function onAbort() {
2464
+ waitingConsumer = null;
2465
+ resolve();
2466
+ }
2467
+ waitingConsumer = complete;
2468
+ signal.addEventListener('abort', onAbort);
2469
+ }
2470
+ });
2457
2471
  }
2458
2472
  };
2459
2473
  }
@@ -10655,7 +10669,7 @@ function requireDist () {
10655
10669
 
10656
10670
  var distExports = requireDist();
10657
10671
 
10658
- var version = "1.53.1";
10672
+ var version = "1.53.2";
10659
10673
  var PACKAGE = {
10660
10674
  version: version};
10661
10675
 
@@ -11563,19 +11577,15 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
11563
11577
  class AbstractStreamingSyncImplementation extends BaseObserver {
11564
11578
  options;
11565
11579
  abortController;
11566
- // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
11567
- // This allows ensuring that all upload processes can be aborted.
11568
- uploadAbortController;
11569
11580
  crudUpdateListener;
11570
11581
  streamingSyncPromise;
11571
11582
  logger;
11572
11583
  activeStreams;
11573
11584
  connectionMayHaveChanged = false;
11574
- isUploadingCrud = false;
11585
+ crudUploadNotifier = asyncNotifier();
11575
11586
  notifyCompletedUploads;
11576
11587
  handleActiveStreamsChange;
11577
11588
  syncStatus;
11578
- triggerCrudUpload;
11579
11589
  constructor(options) {
11580
11590
  super();
11581
11591
  this.options = options;
@@ -11591,16 +11601,9 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11591
11601
  }
11592
11602
  });
11593
11603
  this.abortController = null;
11594
- this.triggerCrudUpload = throttleLeadingTrailing(() => {
11595
- if (!this.syncStatus.connected || this.isUploadingCrud) {
11596
- return;
11597
- }
11598
- this.isUploadingCrud = true;
11599
- this._uploadAllCrud().finally(() => {
11600
- this.notifyCompletedUploads?.();
11601
- this.isUploadingCrud = false;
11602
- });
11603
- }, this.options.crudUploadThrottleMs);
11604
+ }
11605
+ triggerCrudUpload() {
11606
+ this.crudUploadNotifier.notify();
11604
11607
  }
11605
11608
  async waitForReady() { }
11606
11609
  waitForStatus(status) {
@@ -11648,7 +11651,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11648
11651
  super.dispose();
11649
11652
  this.crudUpdateListener?.();
11650
11653
  this.crudUpdateListener = undefined;
11651
- this.uploadAbortController?.abort();
11652
11654
  }
11653
11655
  async getWriteCheckpoint() {
11654
11656
  const clientId = await this.options.adapter.getClientId();
@@ -11658,7 +11660,17 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11658
11660
  this.logger.debug(`Created write checkpoint: ${checkpoint}`);
11659
11661
  return checkpoint;
11660
11662
  }
11661
- async _uploadAllCrud() {
11663
+ async crudUploadLoop(signal) {
11664
+ while (!signal.aborted) {
11665
+ await Promise.all([
11666
+ // Start the initial CRUD upload on connect. Then, keep polling until we're done.
11667
+ this._uploadAllCrud(signal),
11668
+ this.delayRetry(signal, this.options.crudUploadThrottleMs)
11669
+ ]);
11670
+ await this.crudUploadNotifier.waitForNotification(signal);
11671
+ }
11672
+ }
11673
+ async _uploadAllCrud(signal) {
11662
11674
  return this.obtainLock({
11663
11675
  type: exports.LockType.CRUD,
11664
11676
  callback: async () => {
@@ -11666,12 +11678,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11666
11678
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
11667
11679
  */
11668
11680
  let checkedCrudItem;
11669
- const controller = new AbortController();
11670
- this.uploadAbortController = controller;
11671
- this.abortController?.signal.addEventListener('abort', () => {
11672
- controller.abort();
11673
- }, { once: true });
11674
- while (!controller.signal.aborted) {
11681
+ while (!signal.aborted) {
11675
11682
  try {
11676
11683
  /**
11677
11684
  * This is the first item in the FIFO CRUD queue.
@@ -11701,7 +11708,10 @@ The next upload iteration will be delayed.`);
11701
11708
  else {
11702
11709
  // Uploading is completed
11703
11710
  const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
11704
- if (neededUpdate == false && checkedCrudItem != null) {
11711
+ if (neededUpdate) {
11712
+ this.notifyCompletedUploads?.();
11713
+ }
11714
+ else if (checkedCrudItem != null) {
11705
11715
  // Only log this if there was something to upload
11706
11716
  this.logger.debug('Upload complete, no write checkpoint needed.');
11707
11717
  }
@@ -11716,7 +11726,7 @@ The next upload iteration will be delayed.`);
11716
11726
  uploadError: ex
11717
11727
  }
11718
11728
  });
11719
- await this.delayRetry(controller.signal);
11729
+ await this.delayRetry(signal);
11720
11730
  if (!this.isConnected) {
11721
11731
  // Exit the upload loop if the sync stream is no longer connected
11722
11732
  break;
@@ -11731,7 +11741,6 @@ The next upload iteration will be delayed.`);
11731
11741
  });
11732
11742
  }
11733
11743
  }
11734
- this.uploadAbortController = undefined;
11735
11744
  }
11736
11745
  });
11737
11746
  }
@@ -11741,7 +11750,10 @@ The next upload iteration will be delayed.`);
11741
11750
  }
11742
11751
  const controller = new AbortController();
11743
11752
  this.abortController = controller;
11744
- this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
11753
+ this.streamingSyncPromise = Promise.all([
11754
+ this.crudUploadLoop(controller.signal).catch((ex) => this.logger.error('Error in crud upload loop', ex)),
11755
+ this.streamingSync(controller.signal, options)
11756
+ ]);
11745
11757
  // Return a promise that resolves when the connection status is updated to indicate that we're connected.
11746
11758
  return new Promise((resolve) => {
11747
11759
  const disposer = this.registerListener({
@@ -11779,14 +11791,7 @@ The next upload iteration will be delayed.`);
11779
11791
  this.abortController = null;
11780
11792
  this.updateSyncStatus({ connected: false, connecting: false });
11781
11793
  }
11782
- /**
11783
- * @deprecated use [connect instead]
11784
- */
11785
11794
  async streamingSync(signal, options) {
11786
- if (!signal) {
11787
- this.abortController = new AbortController();
11788
- signal = this.abortController.signal;
11789
- }
11790
11795
  /**
11791
11796
  * Listen for CRUD updates and trigger upstream uploads
11792
11797
  */
@@ -12160,14 +12165,13 @@ The next upload iteration will be delayed.`);
12160
12165
  // trigger this for all updates
12161
12166
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
12162
12167
  }
12163
- async delayRetry(signal) {
12168
+ async delayRetry(signal, delay = this.options.retryDelayMs) {
12164
12169
  return new Promise((resolve) => {
12165
12170
  if (signal?.aborted) {
12166
12171
  // If the signal is already aborted, resolve immediately
12167
12172
  resolve();
12168
12173
  return;
12169
12174
  }
12170
- const { retryDelayMs } = this.options;
12171
12175
  let timeoutId;
12172
12176
  const endDelay = () => {
12173
12177
  resolve();
@@ -12178,7 +12182,7 @@ The next upload iteration will be delayed.`);
12178
12182
  signal?.removeEventListener('abort', endDelay);
12179
12183
  };
12180
12184
  signal?.addEventListener('abort', endDelay, { once: true });
12181
- timeoutId = setTimeout(endDelay, retryDelayMs);
12185
+ timeoutId = setTimeout(endDelay, delay);
12182
12186
  });
12183
12187
  }
12184
12188
  updateSubscriptions(subscriptions) {