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