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