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