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