@powersync/web 1.38.0 → 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.0";
12405
+ var version = "1.53.2";
12392
12406
  var PACKAGE = {
12393
12407
  version: version};
12394
12408
 
@@ -12572,6 +12586,7 @@ function injectable(source) {
12572
12586
  let waiter = undefined; // An active, waiting next() call.
12573
12587
  // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
12574
12588
  let pendingSourceEvent = null;
12589
+ let sourceFetchInFlight = false;
12575
12590
  let pendingInjectedEvents = [];
12576
12591
  const consumeWaiter = () => {
12577
12592
  const pending = waiter;
@@ -12580,6 +12595,7 @@ function injectable(source) {
12580
12595
  };
12581
12596
  const fetchFromSource = () => {
12582
12597
  const resolveWaiter = (propagate) => {
12598
+ sourceFetchInFlight = false;
12583
12599
  const active = consumeWaiter();
12584
12600
  if (active) {
12585
12601
  propagate(active);
@@ -12588,6 +12604,7 @@ function injectable(source) {
12588
12604
  pendingSourceEvent = propagate;
12589
12605
  }
12590
12606
  };
12607
+ sourceFetchInFlight = true;
12591
12608
  const nextFromSource = source.next();
12592
12609
  nextFromSource.then((value) => {
12593
12610
  sourceIsDone = value.done == true;
@@ -12614,7 +12631,9 @@ function injectable(source) {
12614
12631
  }
12615
12632
  // Nothing pending? Fetch from source
12616
12633
  waiter = { resolve, reject };
12617
- return fetchFromSource();
12634
+ if (!sourceFetchInFlight) {
12635
+ fetchFromSource();
12636
+ }
12618
12637
  });
12619
12638
  },
12620
12639
  inject: (event) => {
@@ -13291,19 +13310,15 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
13291
13310
  class AbstractStreamingSyncImplementation extends BaseObserver {
13292
13311
  options;
13293
13312
  abortController;
13294
- // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
13295
- // This allows ensuring that all upload processes can be aborted.
13296
- uploadAbortController;
13297
13313
  crudUpdateListener;
13298
13314
  streamingSyncPromise;
13299
13315
  logger;
13300
13316
  activeStreams;
13301
13317
  connectionMayHaveChanged = false;
13302
- isUploadingCrud = false;
13318
+ crudUploadNotifier = asyncNotifier();
13303
13319
  notifyCompletedUploads;
13304
13320
  handleActiveStreamsChange;
13305
13321
  syncStatus;
13306
- triggerCrudUpload;
13307
13322
  constructor(options) {
13308
13323
  super();
13309
13324
  this.options = options;
@@ -13319,16 +13334,9 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13319
13334
  }
13320
13335
  });
13321
13336
  this.abortController = null;
13322
- this.triggerCrudUpload = throttleLeadingTrailing(() => {
13323
- if (!this.syncStatus.connected || this.isUploadingCrud) {
13324
- return;
13325
- }
13326
- this.isUploadingCrud = true;
13327
- this._uploadAllCrud().finally(() => {
13328
- this.notifyCompletedUploads?.();
13329
- this.isUploadingCrud = false;
13330
- });
13331
- }, this.options.crudUploadThrottleMs);
13337
+ }
13338
+ triggerCrudUpload() {
13339
+ this.crudUploadNotifier.notify();
13332
13340
  }
13333
13341
  async waitForReady() { }
13334
13342
  waitForStatus(status) {
@@ -13376,7 +13384,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13376
13384
  super.dispose();
13377
13385
  this.crudUpdateListener?.();
13378
13386
  this.crudUpdateListener = undefined;
13379
- this.uploadAbortController?.abort();
13380
13387
  }
13381
13388
  async getWriteCheckpoint() {
13382
13389
  const clientId = await this.options.adapter.getClientId();
@@ -13386,7 +13393,17 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13386
13393
  this.logger.debug(`Created write checkpoint: ${checkpoint}`);
13387
13394
  return checkpoint;
13388
13395
  }
13389
- 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) {
13390
13407
  return this.obtainLock({
13391
13408
  type: LockType.CRUD,
13392
13409
  callback: async () => {
@@ -13394,12 +13411,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
13394
13411
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
13395
13412
  */
13396
13413
  let checkedCrudItem;
13397
- const controller = new AbortController();
13398
- this.uploadAbortController = controller;
13399
- this.abortController?.signal.addEventListener('abort', () => {
13400
- controller.abort();
13401
- }, { once: true });
13402
- while (!controller.signal.aborted) {
13414
+ while (!signal.aborted) {
13403
13415
  try {
13404
13416
  /**
13405
13417
  * This is the first item in the FIFO CRUD queue.
@@ -13429,7 +13441,10 @@ The next upload iteration will be delayed.`);
13429
13441
  else {
13430
13442
  // Uploading is completed
13431
13443
  const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
13432
- if (neededUpdate == false && checkedCrudItem != null) {
13444
+ if (neededUpdate) {
13445
+ this.notifyCompletedUploads?.();
13446
+ }
13447
+ else if (checkedCrudItem != null) {
13433
13448
  // Only log this if there was something to upload
13434
13449
  this.logger.debug('Upload complete, no write checkpoint needed.');
13435
13450
  }
@@ -13444,7 +13459,7 @@ The next upload iteration will be delayed.`);
13444
13459
  uploadError: ex
13445
13460
  }
13446
13461
  });
13447
- await this.delayRetry(controller.signal);
13462
+ await this.delayRetry(signal);
13448
13463
  if (!this.isConnected) {
13449
13464
  // Exit the upload loop if the sync stream is no longer connected
13450
13465
  break;
@@ -13459,7 +13474,6 @@ The next upload iteration will be delayed.`);
13459
13474
  });
13460
13475
  }
13461
13476
  }
13462
- this.uploadAbortController = undefined;
13463
13477
  }
13464
13478
  });
13465
13479
  }
@@ -13469,7 +13483,10 @@ The next upload iteration will be delayed.`);
13469
13483
  }
13470
13484
  const controller = new AbortController();
13471
13485
  this.abortController = controller;
13472
- 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
+ ]);
13473
13490
  // Return a promise that resolves when the connection status is updated to indicate that we're connected.
13474
13491
  return new Promise((resolve) => {
13475
13492
  const disposer = this.registerListener({
@@ -13507,14 +13524,7 @@ The next upload iteration will be delayed.`);
13507
13524
  this.abortController = null;
13508
13525
  this.updateSyncStatus({ connected: false, connecting: false });
13509
13526
  }
13510
- /**
13511
- * @deprecated use [connect instead]
13512
- */
13513
13527
  async streamingSync(signal, options) {
13514
- if (!signal) {
13515
- this.abortController = new AbortController();
13516
- signal = this.abortController.signal;
13517
- }
13518
13528
  /**
13519
13529
  * Listen for CRUD updates and trigger upstream uploads
13520
13530
  */
@@ -13888,14 +13898,13 @@ The next upload iteration will be delayed.`);
13888
13898
  // trigger this for all updates
13889
13899
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
13890
13900
  }
13891
- async delayRetry(signal) {
13901
+ async delayRetry(signal, delay = this.options.retryDelayMs) {
13892
13902
  return new Promise((resolve) => {
13893
13903
  if (signal?.aborted) {
13894
13904
  // If the signal is already aborted, resolve immediately
13895
13905
  resolve();
13896
13906
  return;
13897
13907
  }
13898
- const { retryDelayMs } = this.options;
13899
13908
  let timeoutId;
13900
13909
  const endDelay = () => {
13901
13910
  resolve();
@@ -13906,7 +13915,7 @@ The next upload iteration will be delayed.`);
13906
13915
  signal?.removeEventListener('abort', endDelay);
13907
13916
  };
13908
13917
  signal?.addEventListener('abort', endDelay, { once: true });
13909
- timeoutId = setTimeout(endDelay, retryDelayMs);
13918
+ timeoutId = setTimeout(endDelay, delay);
13910
13919
  });
13911
13920
  }
13912
13921
  updateSubscriptions(subscriptions) {