@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.
@@ -2278,30 +2278,44 @@ function throttleTrailing(func, wait) {
2278
2278
  }
2279
2279
  };
2280
2280
  }
2281
- /**
2282
- * Throttle a function to be called at most once every "wait" milliseconds,
2283
- * on the leading and trailing edge.
2284
- *
2285
- * Roughly equivalent to lodash/throttle with {leading: true, trailing: true}
2286
- */
2287
- function throttleLeadingTrailing(func, wait) {
2288
- let timeoutId = null;
2289
- let lastCallTime = 0;
2290
- const invokeFunction = () => {
2291
- func();
2292
- lastCallTime = Date.now();
2293
- timeoutId = null;
2294
- };
2295
- return function () {
2296
- const now = Date.now();
2297
- const timeToWait = wait - (now - lastCallTime);
2298
- if (timeToWait <= 0) {
2299
- // Leading edge: Call the function immediately if enough time has passed
2300
- invokeFunction();
2301
- }
2302
- else if (!timeoutId) {
2303
- // Set a timeout for the trailing edge if not already set
2304
- timeoutId = setTimeout(invokeFunction, timeToWait);
2281
+ function asyncNotifier() {
2282
+ let waitingConsumer = null;
2283
+ let hasPendingNotification = false;
2284
+ return {
2285
+ notify() {
2286
+ if (waitingConsumer != null) {
2287
+ waitingConsumer();
2288
+ waitingConsumer = null;
2289
+ }
2290
+ else {
2291
+ hasPendingNotification = true;
2292
+ }
2293
+ },
2294
+ waitForNotification(signal) {
2295
+ return new Promise((resolve) => {
2296
+ if (waitingConsumer != null) {
2297
+ throw new Error('Illegal call to waitForNotification, already has a waiter.');
2298
+ }
2299
+ if (signal.aborted) {
2300
+ resolve();
2301
+ }
2302
+ else if (hasPendingNotification) {
2303
+ resolve();
2304
+ hasPendingNotification = false;
2305
+ }
2306
+ else {
2307
+ function complete() {
2308
+ signal.removeEventListener('abort', onAbort);
2309
+ resolve();
2310
+ }
2311
+ function onAbort() {
2312
+ waitingConsumer = null;
2313
+ resolve();
2314
+ }
2315
+ waitingConsumer = complete;
2316
+ signal.addEventListener('abort', onAbort);
2317
+ }
2318
+ });
2305
2319
  }
2306
2320
  };
2307
2321
  }
@@ -8132,7 +8146,7 @@ function requireDist () {
8132
8146
 
8133
8147
  var distExports = requireDist();
8134
8148
 
8135
- var version = "1.53.1";
8149
+ var version = "1.53.2";
8136
8150
  var PACKAGE = {
8137
8151
  version: version};
8138
8152
 
@@ -9040,19 +9054,15 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
9040
9054
  class AbstractStreamingSyncImplementation extends BaseObserver {
9041
9055
  options;
9042
9056
  abortController;
9043
- // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
9044
- // This allows ensuring that all upload processes can be aborted.
9045
- uploadAbortController;
9046
9057
  crudUpdateListener;
9047
9058
  streamingSyncPromise;
9048
9059
  logger;
9049
9060
  activeStreams;
9050
9061
  connectionMayHaveChanged = false;
9051
- isUploadingCrud = false;
9062
+ crudUploadNotifier = asyncNotifier();
9052
9063
  notifyCompletedUploads;
9053
9064
  handleActiveStreamsChange;
9054
9065
  syncStatus;
9055
- triggerCrudUpload;
9056
9066
  constructor(options) {
9057
9067
  super();
9058
9068
  this.options = options;
@@ -9068,16 +9078,9 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9068
9078
  }
9069
9079
  });
9070
9080
  this.abortController = null;
9071
- this.triggerCrudUpload = throttleLeadingTrailing(() => {
9072
- if (!this.syncStatus.connected || this.isUploadingCrud) {
9073
- return;
9074
- }
9075
- this.isUploadingCrud = true;
9076
- this._uploadAllCrud().finally(() => {
9077
- this.notifyCompletedUploads?.();
9078
- this.isUploadingCrud = false;
9079
- });
9080
- }, this.options.crudUploadThrottleMs);
9081
+ }
9082
+ triggerCrudUpload() {
9083
+ this.crudUploadNotifier.notify();
9081
9084
  }
9082
9085
  async waitForReady() { }
9083
9086
  waitForStatus(status) {
@@ -9125,7 +9128,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9125
9128
  super.dispose();
9126
9129
  this.crudUpdateListener?.();
9127
9130
  this.crudUpdateListener = undefined;
9128
- this.uploadAbortController?.abort();
9129
9131
  }
9130
9132
  async getWriteCheckpoint() {
9131
9133
  const clientId = await this.options.adapter.getClientId();
@@ -9135,7 +9137,17 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9135
9137
  this.logger.debug(`Created write checkpoint: ${checkpoint}`);
9136
9138
  return checkpoint;
9137
9139
  }
9138
- async _uploadAllCrud() {
9140
+ async crudUploadLoop(signal) {
9141
+ while (!signal.aborted) {
9142
+ await Promise.all([
9143
+ // Start the initial CRUD upload on connect. Then, keep polling until we're done.
9144
+ this._uploadAllCrud(signal),
9145
+ this.delayRetry(signal, this.options.crudUploadThrottleMs)
9146
+ ]);
9147
+ await this.crudUploadNotifier.waitForNotification(signal);
9148
+ }
9149
+ }
9150
+ async _uploadAllCrud(signal) {
9139
9151
  return this.obtainLock({
9140
9152
  type: exports.LockType.CRUD,
9141
9153
  callback: async () => {
@@ -9143,12 +9155,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9143
9155
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
9144
9156
  */
9145
9157
  let checkedCrudItem;
9146
- const controller = new AbortController();
9147
- this.uploadAbortController = controller;
9148
- this.abortController?.signal.addEventListener('abort', () => {
9149
- controller.abort();
9150
- }, { once: true });
9151
- while (!controller.signal.aborted) {
9158
+ while (!signal.aborted) {
9152
9159
  try {
9153
9160
  /**
9154
9161
  * This is the first item in the FIFO CRUD queue.
@@ -9178,7 +9185,10 @@ The next upload iteration will be delayed.`);
9178
9185
  else {
9179
9186
  // Uploading is completed
9180
9187
  const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
9181
- if (neededUpdate == false && checkedCrudItem != null) {
9188
+ if (neededUpdate) {
9189
+ this.notifyCompletedUploads?.();
9190
+ }
9191
+ else if (checkedCrudItem != null) {
9182
9192
  // Only log this if there was something to upload
9183
9193
  this.logger.debug('Upload complete, no write checkpoint needed.');
9184
9194
  }
@@ -9193,7 +9203,7 @@ The next upload iteration will be delayed.`);
9193
9203
  uploadError: ex
9194
9204
  }
9195
9205
  });
9196
- await this.delayRetry(controller.signal);
9206
+ await this.delayRetry(signal);
9197
9207
  if (!this.isConnected) {
9198
9208
  // Exit the upload loop if the sync stream is no longer connected
9199
9209
  break;
@@ -9208,7 +9218,6 @@ The next upload iteration will be delayed.`);
9208
9218
  });
9209
9219
  }
9210
9220
  }
9211
- this.uploadAbortController = undefined;
9212
9221
  }
9213
9222
  });
9214
9223
  }
@@ -9218,7 +9227,10 @@ The next upload iteration will be delayed.`);
9218
9227
  }
9219
9228
  const controller = new AbortController();
9220
9229
  this.abortController = controller;
9221
- this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
9230
+ this.streamingSyncPromise = Promise.all([
9231
+ this.crudUploadLoop(controller.signal).catch((ex) => this.logger.error('Error in crud upload loop', ex)),
9232
+ this.streamingSync(controller.signal, options)
9233
+ ]);
9222
9234
  // Return a promise that resolves when the connection status is updated to indicate that we're connected.
9223
9235
  return new Promise((resolve) => {
9224
9236
  const disposer = this.registerListener({
@@ -9256,14 +9268,7 @@ The next upload iteration will be delayed.`);
9256
9268
  this.abortController = null;
9257
9269
  this.updateSyncStatus({ connected: false, connecting: false });
9258
9270
  }
9259
- /**
9260
- * @deprecated use [connect instead]
9261
- */
9262
9271
  async streamingSync(signal, options) {
9263
- if (!signal) {
9264
- this.abortController = new AbortController();
9265
- signal = this.abortController.signal;
9266
- }
9267
9272
  /**
9268
9273
  * Listen for CRUD updates and trigger upstream uploads
9269
9274
  */
@@ -9637,14 +9642,13 @@ The next upload iteration will be delayed.`);
9637
9642
  // trigger this for all updates
9638
9643
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
9639
9644
  }
9640
- async delayRetry(signal) {
9645
+ async delayRetry(signal, delay = this.options.retryDelayMs) {
9641
9646
  return new Promise((resolve) => {
9642
9647
  if (signal?.aborted) {
9643
9648
  // If the signal is already aborted, resolve immediately
9644
9649
  resolve();
9645
9650
  return;
9646
9651
  }
9647
- const { retryDelayMs } = this.options;
9648
9652
  let timeoutId;
9649
9653
  const endDelay = () => {
9650
9654
  resolve();
@@ -9655,7 +9659,7 @@ The next upload iteration will be delayed.`);
9655
9659
  signal?.removeEventListener('abort', endDelay);
9656
9660
  };
9657
9661
  signal?.addEventListener('abort', endDelay, { once: true });
9658
- timeoutId = setTimeout(endDelay, retryDelayMs);
9662
+ timeoutId = setTimeout(endDelay, delay);
9659
9663
  });
9660
9664
  }
9661
9665
  updateSubscriptions(subscriptions) {