@powersync/web 1.38.1 → 1.38.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.
@@ -5321,30 +5321,44 @@ function throttleTrailing(func, wait) {
5321
5321
  }
5322
5322
  };
5323
5323
  }
5324
- /**
5325
- * Throttle a function to be called at most once every "wait" milliseconds,
5326
- * on the leading and trailing edge.
5327
- *
5328
- * Roughly equivalent to lodash/throttle with {leading: true, trailing: true}
5329
- */
5330
- function throttleLeadingTrailing(func, wait) {
5331
- let timeoutId = null;
5332
- let lastCallTime = 0;
5333
- const invokeFunction = () => {
5334
- func();
5335
- lastCallTime = Date.now();
5336
- timeoutId = null;
5337
- };
5338
- return function () {
5339
- const now = Date.now();
5340
- const timeToWait = wait - (now - lastCallTime);
5341
- if (timeToWait <= 0) {
5342
- // Leading edge: Call the function immediately if enough time has passed
5343
- invokeFunction();
5344
- }
5345
- else if (!timeoutId) {
5346
- // Set a timeout for the trailing edge if not already set
5347
- timeoutId = setTimeout(invokeFunction, timeToWait);
5324
+ function asyncNotifier() {
5325
+ let waitingConsumer = null;
5326
+ let hasPendingNotification = false;
5327
+ return {
5328
+ notify() {
5329
+ if (waitingConsumer != null) {
5330
+ waitingConsumer();
5331
+ waitingConsumer = null;
5332
+ }
5333
+ else {
5334
+ hasPendingNotification = true;
5335
+ }
5336
+ },
5337
+ waitForNotification(signal) {
5338
+ return new Promise((resolve) => {
5339
+ if (waitingConsumer != null) {
5340
+ throw new Error('Illegal call to waitForNotification, already has a waiter.');
5341
+ }
5342
+ if (signal.aborted) {
5343
+ resolve();
5344
+ }
5345
+ else if (hasPendingNotification) {
5346
+ resolve();
5347
+ hasPendingNotification = false;
5348
+ }
5349
+ else {
5350
+ function complete() {
5351
+ signal.removeEventListener('abort', onAbort);
5352
+ resolve();
5353
+ }
5354
+ function onAbort() {
5355
+ waitingConsumer = null;
5356
+ resolve();
5357
+ }
5358
+ waitingConsumer = complete;
5359
+ signal.addEventListener('abort', onAbort);
5360
+ }
5361
+ });
5348
5362
  }
5349
5363
  };
5350
5364
  }
@@ -13546,7 +13560,7 @@ function requireDist () {
13546
13560
 
13547
13561
  var distExports = requireDist();
13548
13562
 
13549
- var version = "1.53.1";
13563
+ var version = "1.53.2";
13550
13564
  var PACKAGE = {
13551
13565
  version: version};
13552
13566
 
@@ -14454,19 +14468,15 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
14454
14468
  class AbstractStreamingSyncImplementation extends BaseObserver {
14455
14469
  options;
14456
14470
  abortController;
14457
- // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
14458
- // This allows ensuring that all upload processes can be aborted.
14459
- uploadAbortController;
14460
14471
  crudUpdateListener;
14461
14472
  streamingSyncPromise;
14462
14473
  logger;
14463
14474
  activeStreams;
14464
14475
  connectionMayHaveChanged = false;
14465
- isUploadingCrud = false;
14476
+ crudUploadNotifier = asyncNotifier();
14466
14477
  notifyCompletedUploads;
14467
14478
  handleActiveStreamsChange;
14468
14479
  syncStatus;
14469
- triggerCrudUpload;
14470
14480
  constructor(options) {
14471
14481
  super();
14472
14482
  this.options = options;
@@ -14482,16 +14492,9 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
14482
14492
  }
14483
14493
  });
14484
14494
  this.abortController = null;
14485
- this.triggerCrudUpload = throttleLeadingTrailing(() => {
14486
- if (!this.syncStatus.connected || this.isUploadingCrud) {
14487
- return;
14488
- }
14489
- this.isUploadingCrud = true;
14490
- this._uploadAllCrud().finally(() => {
14491
- this.notifyCompletedUploads?.();
14492
- this.isUploadingCrud = false;
14493
- });
14494
- }, this.options.crudUploadThrottleMs);
14495
+ }
14496
+ triggerCrudUpload() {
14497
+ this.crudUploadNotifier.notify();
14495
14498
  }
14496
14499
  async waitForReady() { }
14497
14500
  waitForStatus(status) {
@@ -14539,7 +14542,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
14539
14542
  super.dispose();
14540
14543
  this.crudUpdateListener?.();
14541
14544
  this.crudUpdateListener = undefined;
14542
- this.uploadAbortController?.abort();
14543
14545
  }
14544
14546
  async getWriteCheckpoint() {
14545
14547
  const clientId = await this.options.adapter.getClientId();
@@ -14549,7 +14551,17 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
14549
14551
  this.logger.debug(`Created write checkpoint: ${checkpoint}`);
14550
14552
  return checkpoint;
14551
14553
  }
14552
- async _uploadAllCrud() {
14554
+ async crudUploadLoop(signal) {
14555
+ while (!signal.aborted) {
14556
+ await Promise.all([
14557
+ // Start the initial CRUD upload on connect. Then, keep polling until we're done.
14558
+ this._uploadAllCrud(signal),
14559
+ this.delayRetry(signal, this.options.crudUploadThrottleMs)
14560
+ ]);
14561
+ await this.crudUploadNotifier.waitForNotification(signal);
14562
+ }
14563
+ }
14564
+ async _uploadAllCrud(signal) {
14553
14565
  return this.obtainLock({
14554
14566
  type: LockType.CRUD,
14555
14567
  callback: async () => {
@@ -14557,12 +14569,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
14557
14569
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
14558
14570
  */
14559
14571
  let checkedCrudItem;
14560
- const controller = new AbortController();
14561
- this.uploadAbortController = controller;
14562
- this.abortController?.signal.addEventListener('abort', () => {
14563
- controller.abort();
14564
- }, { once: true });
14565
- while (!controller.signal.aborted) {
14572
+ while (!signal.aborted) {
14566
14573
  try {
14567
14574
  /**
14568
14575
  * This is the first item in the FIFO CRUD queue.
@@ -14592,7 +14599,10 @@ The next upload iteration will be delayed.`);
14592
14599
  else {
14593
14600
  // Uploading is completed
14594
14601
  const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
14595
- if (neededUpdate == false && checkedCrudItem != null) {
14602
+ if (neededUpdate) {
14603
+ this.notifyCompletedUploads?.();
14604
+ }
14605
+ else if (checkedCrudItem != null) {
14596
14606
  // Only log this if there was something to upload
14597
14607
  this.logger.debug('Upload complete, no write checkpoint needed.');
14598
14608
  }
@@ -14607,7 +14617,7 @@ The next upload iteration will be delayed.`);
14607
14617
  uploadError: ex
14608
14618
  }
14609
14619
  });
14610
- await this.delayRetry(controller.signal);
14620
+ await this.delayRetry(signal);
14611
14621
  if (!this.isConnected) {
14612
14622
  // Exit the upload loop if the sync stream is no longer connected
14613
14623
  break;
@@ -14622,7 +14632,6 @@ The next upload iteration will be delayed.`);
14622
14632
  });
14623
14633
  }
14624
14634
  }
14625
- this.uploadAbortController = undefined;
14626
14635
  }
14627
14636
  });
14628
14637
  }
@@ -14632,7 +14641,10 @@ The next upload iteration will be delayed.`);
14632
14641
  }
14633
14642
  const controller = new AbortController();
14634
14643
  this.abortController = controller;
14635
- this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
14644
+ this.streamingSyncPromise = Promise.all([
14645
+ this.crudUploadLoop(controller.signal).catch((ex) => this.logger.error('Error in crud upload loop', ex)),
14646
+ this.streamingSync(controller.signal, options)
14647
+ ]);
14636
14648
  // Return a promise that resolves when the connection status is updated to indicate that we're connected.
14637
14649
  return new Promise((resolve) => {
14638
14650
  const disposer = this.registerListener({
@@ -14670,14 +14682,7 @@ The next upload iteration will be delayed.`);
14670
14682
  this.abortController = null;
14671
14683
  this.updateSyncStatus({ connected: false, connecting: false });
14672
14684
  }
14673
- /**
14674
- * @deprecated use [connect instead]
14675
- */
14676
14685
  async streamingSync(signal, options) {
14677
- if (!signal) {
14678
- this.abortController = new AbortController();
14679
- signal = this.abortController.signal;
14680
- }
14681
14686
  /**
14682
14687
  * Listen for CRUD updates and trigger upstream uploads
14683
14688
  */
@@ -15051,14 +15056,13 @@ The next upload iteration will be delayed.`);
15051
15056
  // trigger this for all updates
15052
15057
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
15053
15058
  }
15054
- async delayRetry(signal) {
15059
+ async delayRetry(signal, delay = this.options.retryDelayMs) {
15055
15060
  return new Promise((resolve) => {
15056
15061
  if (signal?.aborted) {
15057
15062
  // If the signal is already aborted, resolve immediately
15058
15063
  resolve();
15059
15064
  return;
15060
15065
  }
15061
- const { retryDelayMs } = this.options;
15062
15066
  let timeoutId;
15063
15067
  const endDelay = () => {
15064
15068
  resolve();
@@ -15069,7 +15073,7 @@ The next upload iteration will be delayed.`);
15069
15073
  signal?.removeEventListener('abort', endDelay);
15070
15074
  };
15071
15075
  signal?.addEventListener('abort', endDelay, { once: true });
15072
- timeoutId = setTimeout(endDelay, retryDelayMs);
15076
+ timeoutId = setTimeout(endDelay, delay);
15073
15077
  });
15074
15078
  }
15075
15079
  updateSubscriptions(subscriptions) {