@powersync/common 1.53.0 → 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.0";
10670
+ var version = "1.53.2";
10657
10671
  var PACKAGE = {
10658
10672
  version: version};
10659
10673
 
@@ -10837,6 +10851,7 @@ function injectable(source) {
10837
10851
  let waiter = undefined; // An active, waiting next() call.
10838
10852
  // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
10839
10853
  let pendingSourceEvent = null;
10854
+ let sourceFetchInFlight = false;
10840
10855
  let pendingInjectedEvents = [];
10841
10856
  const consumeWaiter = () => {
10842
10857
  const pending = waiter;
@@ -10845,6 +10860,7 @@ function injectable(source) {
10845
10860
  };
10846
10861
  const fetchFromSource = () => {
10847
10862
  const resolveWaiter = (propagate) => {
10863
+ sourceFetchInFlight = false;
10848
10864
  const active = consumeWaiter();
10849
10865
  if (active) {
10850
10866
  propagate(active);
@@ -10853,6 +10869,7 @@ function injectable(source) {
10853
10869
  pendingSourceEvent = propagate;
10854
10870
  }
10855
10871
  };
10872
+ sourceFetchInFlight = true;
10856
10873
  const nextFromSource = source.next();
10857
10874
  nextFromSource.then((value) => {
10858
10875
  sourceIsDone = value.done == true;
@@ -10879,7 +10896,9 @@ function injectable(source) {
10879
10896
  }
10880
10897
  // Nothing pending? Fetch from source
10881
10898
  waiter = { resolve, reject };
10882
- return fetchFromSource();
10899
+ if (!sourceFetchInFlight) {
10900
+ fetchFromSource();
10901
+ }
10883
10902
  });
10884
10903
  },
10885
10904
  inject: (event) => {
@@ -11556,19 +11575,15 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
11556
11575
  class AbstractStreamingSyncImplementation extends BaseObserver {
11557
11576
  options;
11558
11577
  abortController;
11559
- // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
11560
- // This allows ensuring that all upload processes can be aborted.
11561
- uploadAbortController;
11562
11578
  crudUpdateListener;
11563
11579
  streamingSyncPromise;
11564
11580
  logger;
11565
11581
  activeStreams;
11566
11582
  connectionMayHaveChanged = false;
11567
- isUploadingCrud = false;
11583
+ crudUploadNotifier = asyncNotifier();
11568
11584
  notifyCompletedUploads;
11569
11585
  handleActiveStreamsChange;
11570
11586
  syncStatus;
11571
- triggerCrudUpload;
11572
11587
  constructor(options) {
11573
11588
  super();
11574
11589
  this.options = options;
@@ -11584,16 +11599,9 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11584
11599
  }
11585
11600
  });
11586
11601
  this.abortController = null;
11587
- this.triggerCrudUpload = throttleLeadingTrailing(() => {
11588
- if (!this.syncStatus.connected || this.isUploadingCrud) {
11589
- return;
11590
- }
11591
- this.isUploadingCrud = true;
11592
- this._uploadAllCrud().finally(() => {
11593
- this.notifyCompletedUploads?.();
11594
- this.isUploadingCrud = false;
11595
- });
11596
- }, this.options.crudUploadThrottleMs);
11602
+ }
11603
+ triggerCrudUpload() {
11604
+ this.crudUploadNotifier.notify();
11597
11605
  }
11598
11606
  async waitForReady() { }
11599
11607
  waitForStatus(status) {
@@ -11641,7 +11649,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11641
11649
  super.dispose();
11642
11650
  this.crudUpdateListener?.();
11643
11651
  this.crudUpdateListener = undefined;
11644
- this.uploadAbortController?.abort();
11645
11652
  }
11646
11653
  async getWriteCheckpoint() {
11647
11654
  const clientId = await this.options.adapter.getClientId();
@@ -11651,7 +11658,17 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11651
11658
  this.logger.debug(`Created write checkpoint: ${checkpoint}`);
11652
11659
  return checkpoint;
11653
11660
  }
11654
- 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) {
11655
11672
  return this.obtainLock({
11656
11673
  type: LockType.CRUD,
11657
11674
  callback: async () => {
@@ -11659,12 +11676,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11659
11676
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
11660
11677
  */
11661
11678
  let checkedCrudItem;
11662
- const controller = new AbortController();
11663
- this.uploadAbortController = controller;
11664
- this.abortController?.signal.addEventListener('abort', () => {
11665
- controller.abort();
11666
- }, { once: true });
11667
- while (!controller.signal.aborted) {
11679
+ while (!signal.aborted) {
11668
11680
  try {
11669
11681
  /**
11670
11682
  * This is the first item in the FIFO CRUD queue.
@@ -11694,7 +11706,10 @@ The next upload iteration will be delayed.`);
11694
11706
  else {
11695
11707
  // Uploading is completed
11696
11708
  const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
11697
- if (neededUpdate == false && checkedCrudItem != null) {
11709
+ if (neededUpdate) {
11710
+ this.notifyCompletedUploads?.();
11711
+ }
11712
+ else if (checkedCrudItem != null) {
11698
11713
  // Only log this if there was something to upload
11699
11714
  this.logger.debug('Upload complete, no write checkpoint needed.');
11700
11715
  }
@@ -11709,7 +11724,7 @@ The next upload iteration will be delayed.`);
11709
11724
  uploadError: ex
11710
11725
  }
11711
11726
  });
11712
- await this.delayRetry(controller.signal);
11727
+ await this.delayRetry(signal);
11713
11728
  if (!this.isConnected) {
11714
11729
  // Exit the upload loop if the sync stream is no longer connected
11715
11730
  break;
@@ -11724,7 +11739,6 @@ The next upload iteration will be delayed.`);
11724
11739
  });
11725
11740
  }
11726
11741
  }
11727
- this.uploadAbortController = undefined;
11728
11742
  }
11729
11743
  });
11730
11744
  }
@@ -11734,7 +11748,10 @@ The next upload iteration will be delayed.`);
11734
11748
  }
11735
11749
  const controller = new AbortController();
11736
11750
  this.abortController = controller;
11737
- 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
+ ]);
11738
11755
  // Return a promise that resolves when the connection status is updated to indicate that we're connected.
11739
11756
  return new Promise((resolve) => {
11740
11757
  const disposer = this.registerListener({
@@ -11772,14 +11789,7 @@ The next upload iteration will be delayed.`);
11772
11789
  this.abortController = null;
11773
11790
  this.updateSyncStatus({ connected: false, connecting: false });
11774
11791
  }
11775
- /**
11776
- * @deprecated use [connect instead]
11777
- */
11778
11792
  async streamingSync(signal, options) {
11779
- if (!signal) {
11780
- this.abortController = new AbortController();
11781
- signal = this.abortController.signal;
11782
- }
11783
11793
  /**
11784
11794
  * Listen for CRUD updates and trigger upstream uploads
11785
11795
  */
@@ -12153,14 +12163,13 @@ The next upload iteration will be delayed.`);
12153
12163
  // trigger this for all updates
12154
12164
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
12155
12165
  }
12156
- async delayRetry(signal) {
12166
+ async delayRetry(signal, delay = this.options.retryDelayMs) {
12157
12167
  return new Promise((resolve) => {
12158
12168
  if (signal?.aborted) {
12159
12169
  // If the signal is already aborted, resolve immediately
12160
12170
  resolve();
12161
12171
  return;
12162
12172
  }
12163
- const { retryDelayMs } = this.options;
12164
12173
  let timeoutId;
12165
12174
  const endDelay = () => {
12166
12175
  resolve();
@@ -12171,7 +12180,7 @@ The next upload iteration will be delayed.`);
12171
12180
  signal?.removeEventListener('abort', endDelay);
12172
12181
  };
12173
12182
  signal?.addEventListener('abort', endDelay, { once: true });
12174
- timeoutId = setTimeout(endDelay, retryDelayMs);
12183
+ timeoutId = setTimeout(endDelay, delay);
12175
12184
  });
12176
12185
  }
12177
12186
  updateSubscriptions(subscriptions) {