@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.
@@ -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.0";
8149
+ var version = "1.53.2";
8136
8150
  var PACKAGE = {
8137
8151
  version: version};
8138
8152
 
@@ -8316,6 +8330,7 @@ function injectable(source) {
8316
8330
  let waiter = undefined; // An active, waiting next() call.
8317
8331
  // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
8318
8332
  let pendingSourceEvent = null;
8333
+ let sourceFetchInFlight = false;
8319
8334
  let pendingInjectedEvents = [];
8320
8335
  const consumeWaiter = () => {
8321
8336
  const pending = waiter;
@@ -8324,6 +8339,7 @@ function injectable(source) {
8324
8339
  };
8325
8340
  const fetchFromSource = () => {
8326
8341
  const resolveWaiter = (propagate) => {
8342
+ sourceFetchInFlight = false;
8327
8343
  const active = consumeWaiter();
8328
8344
  if (active) {
8329
8345
  propagate(active);
@@ -8332,6 +8348,7 @@ function injectable(source) {
8332
8348
  pendingSourceEvent = propagate;
8333
8349
  }
8334
8350
  };
8351
+ sourceFetchInFlight = true;
8335
8352
  const nextFromSource = source.next();
8336
8353
  nextFromSource.then((value) => {
8337
8354
  sourceIsDone = value.done == true;
@@ -8358,7 +8375,9 @@ function injectable(source) {
8358
8375
  }
8359
8376
  // Nothing pending? Fetch from source
8360
8377
  waiter = { resolve, reject };
8361
- return fetchFromSource();
8378
+ if (!sourceFetchInFlight) {
8379
+ fetchFromSource();
8380
+ }
8362
8381
  });
8363
8382
  },
8364
8383
  inject: (event) => {
@@ -9035,19 +9054,15 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
9035
9054
  class AbstractStreamingSyncImplementation extends BaseObserver {
9036
9055
  options;
9037
9056
  abortController;
9038
- // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
9039
- // This allows ensuring that all upload processes can be aborted.
9040
- uploadAbortController;
9041
9057
  crudUpdateListener;
9042
9058
  streamingSyncPromise;
9043
9059
  logger;
9044
9060
  activeStreams;
9045
9061
  connectionMayHaveChanged = false;
9046
- isUploadingCrud = false;
9062
+ crudUploadNotifier = asyncNotifier();
9047
9063
  notifyCompletedUploads;
9048
9064
  handleActiveStreamsChange;
9049
9065
  syncStatus;
9050
- triggerCrudUpload;
9051
9066
  constructor(options) {
9052
9067
  super();
9053
9068
  this.options = options;
@@ -9063,16 +9078,9 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9063
9078
  }
9064
9079
  });
9065
9080
  this.abortController = null;
9066
- this.triggerCrudUpload = throttleLeadingTrailing(() => {
9067
- if (!this.syncStatus.connected || this.isUploadingCrud) {
9068
- return;
9069
- }
9070
- this.isUploadingCrud = true;
9071
- this._uploadAllCrud().finally(() => {
9072
- this.notifyCompletedUploads?.();
9073
- this.isUploadingCrud = false;
9074
- });
9075
- }, this.options.crudUploadThrottleMs);
9081
+ }
9082
+ triggerCrudUpload() {
9083
+ this.crudUploadNotifier.notify();
9076
9084
  }
9077
9085
  async waitForReady() { }
9078
9086
  waitForStatus(status) {
@@ -9120,7 +9128,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9120
9128
  super.dispose();
9121
9129
  this.crudUpdateListener?.();
9122
9130
  this.crudUpdateListener = undefined;
9123
- this.uploadAbortController?.abort();
9124
9131
  }
9125
9132
  async getWriteCheckpoint() {
9126
9133
  const clientId = await this.options.adapter.getClientId();
@@ -9130,7 +9137,17 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9130
9137
  this.logger.debug(`Created write checkpoint: ${checkpoint}`);
9131
9138
  return checkpoint;
9132
9139
  }
9133
- 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) {
9134
9151
  return this.obtainLock({
9135
9152
  type: exports.LockType.CRUD,
9136
9153
  callback: async () => {
@@ -9138,12 +9155,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
9138
9155
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
9139
9156
  */
9140
9157
  let checkedCrudItem;
9141
- const controller = new AbortController();
9142
- this.uploadAbortController = controller;
9143
- this.abortController?.signal.addEventListener('abort', () => {
9144
- controller.abort();
9145
- }, { once: true });
9146
- while (!controller.signal.aborted) {
9158
+ while (!signal.aborted) {
9147
9159
  try {
9148
9160
  /**
9149
9161
  * This is the first item in the FIFO CRUD queue.
@@ -9173,7 +9185,10 @@ The next upload iteration will be delayed.`);
9173
9185
  else {
9174
9186
  // Uploading is completed
9175
9187
  const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
9176
- if (neededUpdate == false && checkedCrudItem != null) {
9188
+ if (neededUpdate) {
9189
+ this.notifyCompletedUploads?.();
9190
+ }
9191
+ else if (checkedCrudItem != null) {
9177
9192
  // Only log this if there was something to upload
9178
9193
  this.logger.debug('Upload complete, no write checkpoint needed.');
9179
9194
  }
@@ -9188,7 +9203,7 @@ The next upload iteration will be delayed.`);
9188
9203
  uploadError: ex
9189
9204
  }
9190
9205
  });
9191
- await this.delayRetry(controller.signal);
9206
+ await this.delayRetry(signal);
9192
9207
  if (!this.isConnected) {
9193
9208
  // Exit the upload loop if the sync stream is no longer connected
9194
9209
  break;
@@ -9203,7 +9218,6 @@ The next upload iteration will be delayed.`);
9203
9218
  });
9204
9219
  }
9205
9220
  }
9206
- this.uploadAbortController = undefined;
9207
9221
  }
9208
9222
  });
9209
9223
  }
@@ -9213,7 +9227,10 @@ The next upload iteration will be delayed.`);
9213
9227
  }
9214
9228
  const controller = new AbortController();
9215
9229
  this.abortController = controller;
9216
- 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
+ ]);
9217
9234
  // Return a promise that resolves when the connection status is updated to indicate that we're connected.
9218
9235
  return new Promise((resolve) => {
9219
9236
  const disposer = this.registerListener({
@@ -9251,14 +9268,7 @@ The next upload iteration will be delayed.`);
9251
9268
  this.abortController = null;
9252
9269
  this.updateSyncStatus({ connected: false, connecting: false });
9253
9270
  }
9254
- /**
9255
- * @deprecated use [connect instead]
9256
- */
9257
9271
  async streamingSync(signal, options) {
9258
- if (!signal) {
9259
- this.abortController = new AbortController();
9260
- signal = this.abortController.signal;
9261
- }
9262
9272
  /**
9263
9273
  * Listen for CRUD updates and trigger upstream uploads
9264
9274
  */
@@ -9632,14 +9642,13 @@ The next upload iteration will be delayed.`);
9632
9642
  // trigger this for all updates
9633
9643
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
9634
9644
  }
9635
- async delayRetry(signal) {
9645
+ async delayRetry(signal, delay = this.options.retryDelayMs) {
9636
9646
  return new Promise((resolve) => {
9637
9647
  if (signal?.aborted) {
9638
9648
  // If the signal is already aborted, resolve immediately
9639
9649
  resolve();
9640
9650
  return;
9641
9651
  }
9642
- const { retryDelayMs } = this.options;
9643
9652
  let timeoutId;
9644
9653
  const endDelay = () => {
9645
9654
  resolve();
@@ -9650,7 +9659,7 @@ The next upload iteration will be delayed.`);
9650
9659
  signal?.removeEventListener('abort', endDelay);
9651
9660
  };
9652
9661
  signal?.addEventListener('abort', endDelay, { once: true });
9653
- timeoutId = setTimeout(endDelay, retryDelayMs);
9662
+ timeoutId = setTimeout(endDelay, delay);
9654
9663
  });
9655
9664
  }
9656
9665
  updateSubscriptions(subscriptions) {