@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.
@@ -4163,30 +4163,44 @@ function throttleTrailing(func, wait) {
4163
4163
  }
4164
4164
  };
4165
4165
  }
4166
- /**
4167
- * Throttle a function to be called at most once every "wait" milliseconds,
4168
- * on the leading and trailing edge.
4169
- *
4170
- * Roughly equivalent to lodash/throttle with {leading: true, trailing: true}
4171
- */
4172
- function throttleLeadingTrailing(func, wait) {
4173
- let timeoutId = null;
4174
- let lastCallTime = 0;
4175
- const invokeFunction = () => {
4176
- func();
4177
- lastCallTime = Date.now();
4178
- timeoutId = null;
4179
- };
4180
- return function () {
4181
- const now = Date.now();
4182
- const timeToWait = wait - (now - lastCallTime);
4183
- if (timeToWait <= 0) {
4184
- // Leading edge: Call the function immediately if enough time has passed
4185
- invokeFunction();
4186
- }
4187
- else if (!timeoutId) {
4188
- // Set a timeout for the trailing edge if not already set
4189
- timeoutId = setTimeout(invokeFunction, timeToWait);
4166
+ function asyncNotifier() {
4167
+ let waitingConsumer = null;
4168
+ let hasPendingNotification = false;
4169
+ return {
4170
+ notify() {
4171
+ if (waitingConsumer != null) {
4172
+ waitingConsumer();
4173
+ waitingConsumer = null;
4174
+ }
4175
+ else {
4176
+ hasPendingNotification = true;
4177
+ }
4178
+ },
4179
+ waitForNotification(signal) {
4180
+ return new Promise((resolve) => {
4181
+ if (waitingConsumer != null) {
4182
+ throw new Error('Illegal call to waitForNotification, already has a waiter.');
4183
+ }
4184
+ if (signal.aborted) {
4185
+ resolve();
4186
+ }
4187
+ else if (hasPendingNotification) {
4188
+ resolve();
4189
+ hasPendingNotification = false;
4190
+ }
4191
+ else {
4192
+ function complete() {
4193
+ signal.removeEventListener('abort', onAbort);
4194
+ resolve();
4195
+ }
4196
+ function onAbort() {
4197
+ waitingConsumer = null;
4198
+ resolve();
4199
+ }
4200
+ waitingConsumer = complete;
4201
+ signal.addEventListener('abort', onAbort);
4202
+ }
4203
+ });
4190
4204
  }
4191
4205
  };
4192
4206
  }
@@ -12388,7 +12402,7 @@ function requireDist () {
12388
12402
 
12389
12403
  var distExports = requireDist();
12390
12404
 
12391
- var version = "1.53.1";
12405
+ var version = "1.53.2";
12392
12406
  var PACKAGE = {
12393
12407
  version: version};
12394
12408
 
@@ -13296,19 +13310,15 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
13296
13310
  class AbstractStreamingSyncImplementation extends BaseObserver {
13297
13311
  options;
13298
13312
  abortController;
13299
- // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
13300
- // This allows ensuring that all upload processes can be aborted.
13301
- uploadAbortController;
13302
13313
  crudUpdateListener;
13303
13314
  streamingSyncPromise;
13304
13315
  logger;
13305
13316
  activeStreams;
13306
13317
  connectionMayHaveChanged = false;
13307
- isUploadingCrud = false;
13318
+ crudUploadNotifier = asyncNotifier();
13308
13319
  notifyCompletedUploads;
13309
13320
  handleActiveStreamsChange;
13310
13321
  syncStatus;
13311
- triggerCrudUpload;
13312
13322
  constructor(options) {
13313
13323
  super();
13314
13324
  this.options = options;
@@ -13324,16 +13334,9 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13324
13334
  }
13325
13335
  });
13326
13336
  this.abortController = null;
13327
- this.triggerCrudUpload = throttleLeadingTrailing(() => {
13328
- if (!this.syncStatus.connected || this.isUploadingCrud) {
13329
- return;
13330
- }
13331
- this.isUploadingCrud = true;
13332
- this._uploadAllCrud().finally(() => {
13333
- this.notifyCompletedUploads?.();
13334
- this.isUploadingCrud = false;
13335
- });
13336
- }, this.options.crudUploadThrottleMs);
13337
+ }
13338
+ triggerCrudUpload() {
13339
+ this.crudUploadNotifier.notify();
13337
13340
  }
13338
13341
  async waitForReady() { }
13339
13342
  waitForStatus(status) {
@@ -13381,7 +13384,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13381
13384
  super.dispose();
13382
13385
  this.crudUpdateListener?.();
13383
13386
  this.crudUpdateListener = undefined;
13384
- this.uploadAbortController?.abort();
13385
13387
  }
13386
13388
  async getWriteCheckpoint() {
13387
13389
  const clientId = await this.options.adapter.getClientId();
@@ -13391,7 +13393,17 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13391
13393
  this.logger.debug(`Created write checkpoint: ${checkpoint}`);
13392
13394
  return checkpoint;
13393
13395
  }
13394
- async _uploadAllCrud() {
13396
+ async crudUploadLoop(signal) {
13397
+ while (!signal.aborted) {
13398
+ await Promise.all([
13399
+ // Start the initial CRUD upload on connect. Then, keep polling until we're done.
13400
+ this._uploadAllCrud(signal),
13401
+ this.delayRetry(signal, this.options.crudUploadThrottleMs)
13402
+ ]);
13403
+ await this.crudUploadNotifier.waitForNotification(signal);
13404
+ }
13405
+ }
13406
+ async _uploadAllCrud(signal) {
13395
13407
  return this.obtainLock({
13396
13408
  type: LockType.CRUD,
13397
13409
  callback: async () => {
@@ -13399,12 +13411,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13399
13411
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
13400
13412
  */
13401
13413
  let checkedCrudItem;
13402
- const controller = new AbortController();
13403
- this.uploadAbortController = controller;
13404
- this.abortController?.signal.addEventListener('abort', () => {
13405
- controller.abort();
13406
- }, { once: true });
13407
- while (!controller.signal.aborted) {
13414
+ while (!signal.aborted) {
13408
13415
  try {
13409
13416
  /**
13410
13417
  * This is the first item in the FIFO CRUD queue.
@@ -13434,7 +13441,10 @@ The next upload iteration will be delayed.`);
13434
13441
  else {
13435
13442
  // Uploading is completed
13436
13443
  const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
13437
- if (neededUpdate == false && checkedCrudItem != null) {
13444
+ if (neededUpdate) {
13445
+ this.notifyCompletedUploads?.();
13446
+ }
13447
+ else if (checkedCrudItem != null) {
13438
13448
  // Only log this if there was something to upload
13439
13449
  this.logger.debug('Upload complete, no write checkpoint needed.');
13440
13450
  }
@@ -13449,7 +13459,7 @@ The next upload iteration will be delayed.`);
13449
13459
  uploadError: ex
13450
13460
  }
13451
13461
  });
13452
- await this.delayRetry(controller.signal);
13462
+ await this.delayRetry(signal);
13453
13463
  if (!this.isConnected) {
13454
13464
  // Exit the upload loop if the sync stream is no longer connected
13455
13465
  break;
@@ -13464,7 +13474,6 @@ The next upload iteration will be delayed.`);
13464
13474
  });
13465
13475
  }
13466
13476
  }
13467
- this.uploadAbortController = undefined;
13468
13477
  }
13469
13478
  });
13470
13479
  }
@@ -13474,7 +13483,10 @@ The next upload iteration will be delayed.`);
13474
13483
  }
13475
13484
  const controller = new AbortController();
13476
13485
  this.abortController = controller;
13477
- this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
13486
+ this.streamingSyncPromise = Promise.all([
13487
+ this.crudUploadLoop(controller.signal).catch((ex) => this.logger.error('Error in crud upload loop', ex)),
13488
+ this.streamingSync(controller.signal, options)
13489
+ ]);
13478
13490
  // Return a promise that resolves when the connection status is updated to indicate that we're connected.
13479
13491
  return new Promise((resolve) => {
13480
13492
  const disposer = this.registerListener({
@@ -13512,14 +13524,7 @@ The next upload iteration will be delayed.`);
13512
13524
  this.abortController = null;
13513
13525
  this.updateSyncStatus({ connected: false, connecting: false });
13514
13526
  }
13515
- /**
13516
- * @deprecated use [connect instead]
13517
- */
13518
13527
  async streamingSync(signal, options) {
13519
- if (!signal) {
13520
- this.abortController = new AbortController();
13521
- signal = this.abortController.signal;
13522
- }
13523
13528
  /**
13524
13529
  * Listen for CRUD updates and trigger upstream uploads
13525
13530
  */
@@ -13893,14 +13898,13 @@ The next upload iteration will be delayed.`);
13893
13898
  // trigger this for all updates
13894
13899
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
13895
13900
  }
13896
- async delayRetry(signal) {
13901
+ async delayRetry(signal, delay = this.options.retryDelayMs) {
13897
13902
  return new Promise((resolve) => {
13898
13903
  if (signal?.aborted) {
13899
13904
  // If the signal is already aborted, resolve immediately
13900
13905
  resolve();
13901
13906
  return;
13902
13907
  }
13903
- const { retryDelayMs } = this.options;
13904
13908
  let timeoutId;
13905
13909
  const endDelay = () => {
13906
13910
  resolve();
@@ -13911,7 +13915,7 @@ The next upload iteration will be delayed.`);
13911
13915
  signal?.removeEventListener('abort', endDelay);
13912
13916
  };
13913
13917
  signal?.addEventListener('abort', endDelay, { once: true });
13914
- timeoutId = setTimeout(endDelay, retryDelayMs);
13918
+ timeoutId = setTimeout(endDelay, delay);
13915
13919
  });
13916
13920
  }
13917
13921
  updateSubscriptions(subscriptions) {