@powersync/web 0.0.0-dev-20251127205344 → 0.0.0-dev-20251201150812

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.
Files changed (56) hide show
  1. package/dist/1807036ae51c10ee4d23.wasm +0 -0
  2. package/dist/{10072fe45f0a8fab0a0e.wasm → 307d8ce2280e3bae09d5.wasm} +0 -0
  3. package/dist/{6e435e51534839845554.wasm → cd8b9e8f4c87bf81c169.wasm} +0 -0
  4. package/dist/e797080f5ed0b5324166.wasm +0 -0
  5. package/dist/index.umd.js +336 -177
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +310 -143
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +19 -5
  10. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  11. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +2 -2
  12. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
  13. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +2 -2
  14. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
  15. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +2 -2
  16. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
  17. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +2 -2
  18. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
  19. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +20 -23
  20. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  21. package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
  22. package/lib/src/db/PowerSyncDatabase.js +4 -4
  23. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +8 -0
  24. package/lib/src/db/adapters/AsyncDatabaseConnection.js +11 -1
  25. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +14 -0
  26. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +61 -8
  27. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
  28. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +15 -7
  29. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +1 -4
  30. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +45 -31
  31. package/lib/src/worker/sync/MockSyncService.d.ts +2 -0
  32. package/lib/src/worker/sync/MockSyncService.js +3 -0
  33. package/lib/src/worker/sync/MockSyncServiceTypes.d.ts +101 -0
  34. package/lib/src/worker/sync/MockSyncServiceTypes.js +1 -0
  35. package/lib/src/worker/sync/MockSyncServiceWorker.d.ts +56 -0
  36. package/lib/src/worker/sync/MockSyncServiceWorker.js +369 -0
  37. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +18 -8
  38. package/lib/src/worker/sync/SharedSyncImplementation.js +174 -108
  39. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
  40. package/lib/src/worker/sync/WorkerClient.d.ts +4 -5
  41. package/lib/src/worker/sync/WorkerClient.js +5 -7
  42. package/lib/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +3 -3
  44. package/src/db/PowerSyncDatabase.ts +13 -15
  45. package/src/db/adapters/AsyncDatabaseConnection.ts +12 -0
  46. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +71 -9
  47. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +19 -6
  48. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +56 -40
  49. package/src/worker/sync/MockSyncService.ts +3 -0
  50. package/src/worker/sync/MockSyncServiceTypes.ts +71 -0
  51. package/src/worker/sync/MockSyncServiceWorker.ts +406 -0
  52. package/src/worker/sync/SharedSyncImplementation.ts +201 -120
  53. package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
  54. package/src/worker/sync/WorkerClient.ts +6 -11
  55. package/dist/a730f7ca717b02234beb.wasm +0 -0
  56. package/dist/aa2f408d64445fed090e.wasm +0 -0
@@ -3228,12 +3228,26 @@ class SyncStatus {
3228
3228
  return {
3229
3229
  connected: this.connected,
3230
3230
  connecting: this.connecting,
3231
- dataFlow: this.dataFlowStatus,
3231
+ dataFlow: {
3232
+ ...this.dataFlowStatus,
3233
+ uploadError: this.serializeError(this.dataFlowStatus.uploadError),
3234
+ downloadError: this.serializeError(this.dataFlowStatus.downloadError)
3235
+ },
3232
3236
  lastSyncedAt: this.lastSyncedAt,
3233
3237
  hasSynced: this.hasSynced,
3234
3238
  priorityStatusEntries: this.priorityStatusEntries
3235
3239
  };
3236
3240
  }
3241
+ serializeError(error) {
3242
+ if (typeof error == 'undefined') {
3243
+ return undefined;
3244
+ }
3245
+ return {
3246
+ name: error.name,
3247
+ message: error.message,
3248
+ stack: error.stack
3249
+ };
3250
+ }
3237
3251
  static comparePriorities(a, b) {
3238
3252
  return b.priority - a.priority; // Reverse because higher priorities have lower numbers
3239
3253
  }
@@ -14659,7 +14673,7 @@ The next upload iteration will be delayed.`);
14659
14673
  uploadError: ex
14660
14674
  }
14661
14675
  });
14662
- await this.delayRetry(controller.signal);
14676
+ await this.delayRetry(controller.signal, this.options.crudUploadThrottleMs);
14663
14677
  if (!this.isConnected) {
14664
14678
  // Exit the upload loop if the sync stream is no longer connected
14665
14679
  break;
@@ -15370,14 +15384,14 @@ The next upload iteration will be delayed.`);
15370
15384
  // trigger this for all updates
15371
15385
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
15372
15386
  }
15373
- async delayRetry(signal) {
15387
+ async delayRetry(signal, delayMs) {
15374
15388
  return new Promise((resolve) => {
15375
15389
  if (signal?.aborted) {
15376
15390
  // If the signal is already aborted, resolve immediately
15377
15391
  resolve();
15378
15392
  return;
15379
15393
  }
15380
- const { retryDelayMs } = this.options;
15394
+ const delay = delayMs ?? this.options.retryDelayMs;
15381
15395
  let timeoutId;
15382
15396
  const endDelay = () => {
15383
15397
  resolve();
@@ -15388,7 +15402,7 @@ The next upload iteration will be delayed.`);
15388
15402
  signal?.removeEventListener('abort', endDelay);
15389
15403
  };
15390
15404
  signal?.addEventListener('abort', endDelay, { once: true });
15391
- timeoutId = setTimeout(endDelay, retryDelayMs);
15405
+ timeoutId = setTimeout(endDelay, delay);
15392
15406
  });
15393
15407
  }
15394
15408
  updateSubscriptions(subscriptions) {
@@ -17600,6 +17614,31 @@ const parseQuery = (query, parameters) => {
17600
17614
  //# sourceMappingURL=bundle.mjs.map
17601
17615
 
17602
17616
 
17617
+ /***/ }),
17618
+
17619
+ /***/ "./lib/src/db/adapters/AsyncDatabaseConnection.js":
17620
+ /*!********************************************************!*\
17621
+ !*** ./lib/src/db/adapters/AsyncDatabaseConnection.js ***!
17622
+ \********************************************************/
17623
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
17624
+
17625
+ __webpack_require__.r(__webpack_exports__);
17626
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
17627
+ /* harmony export */ ConnectionClosedError: () => (/* binding */ ConnectionClosedError)
17628
+ /* harmony export */ });
17629
+ /**
17630
+ * Thrown when an underlying database connection is closed.
17631
+ * This is particularly relevant when worker connections are marked as closed while
17632
+ * operations are still in progress.
17633
+ */
17634
+ class ConnectionClosedError extends Error {
17635
+ constructor(message) {
17636
+ super(message);
17637
+ this.name = 'ConnectionClosedError';
17638
+ }
17639
+ }
17640
+
17641
+
17603
17642
  /***/ }),
17604
17643
 
17605
17644
  /***/ "./lib/src/db/adapters/LockedAsyncDatabaseAdapter.js":
@@ -17614,8 +17653,10 @@ __webpack_require__.r(__webpack_exports__);
17614
17653
  /* harmony export */ });
17615
17654
  /* harmony import */ var _powersync_common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @powersync/common */ "../common/dist/bundle.mjs");
17616
17655
  /* harmony import */ var _shared_navigator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../..//shared/navigator */ "./lib/src/shared/navigator.js");
17617
- /* harmony import */ var _WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./WorkerWrappedAsyncDatabaseConnection */ "./lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js");
17618
- /* harmony import */ var _wa_sqlite_WASQLiteConnection__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./wa-sqlite/WASQLiteConnection */ "./lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js");
17656
+ /* harmony import */ var _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./AsyncDatabaseConnection */ "./lib/src/db/adapters/AsyncDatabaseConnection.js");
17657
+ /* harmony import */ var _WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./WorkerWrappedAsyncDatabaseConnection */ "./lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js");
17658
+ /* harmony import */ var _wa_sqlite_WASQLiteConnection__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./wa-sqlite/WASQLiteConnection */ "./lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js");
17659
+
17619
17660
 
17620
17661
 
17621
17662
 
@@ -17637,6 +17678,7 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17637
17678
  _config = null;
17638
17679
  pendingAbortControllers;
17639
17680
  requiresHolds;
17681
+ databaseOpenPromise = null;
17640
17682
  closing;
17641
17683
  closed;
17642
17684
  constructor(options) {
@@ -17686,16 +17728,45 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17686
17728
  async init() {
17687
17729
  return this.initPromise;
17688
17730
  }
17689
- async _init() {
17731
+ async openInternalDB() {
17732
+ // Dispose any previous table change listener.
17733
+ this._disposeTableChangeListener?.();
17734
+ this._disposeTableChangeListener = null;
17735
+ const isReOpen = !!this._db;
17690
17736
  this._db = await this.options.openConnection();
17691
17737
  await this._db.init();
17692
17738
  this._config = await this._db.getConfig();
17693
17739
  await this.registerOnChangeListener(this._db);
17694
- this.iterateListeners((cb) => cb.initialized?.());
17740
+ if (isReOpen) {
17741
+ this.iterateListeners((cb) => cb.databaseReOpened?.());
17742
+ }
17695
17743
  /**
17696
17744
  * This is only required for the long-lived shared IndexedDB connections.
17697
17745
  */
17698
- this.requiresHolds = this._config.vfs == _wa_sqlite_WASQLiteConnection__WEBPACK_IMPORTED_MODULE_3__.WASQLiteVFS.IDBBatchAtomicVFS;
17746
+ this.requiresHolds = this._config.vfs == _wa_sqlite_WASQLiteConnection__WEBPACK_IMPORTED_MODULE_4__.WASQLiteVFS.IDBBatchAtomicVFS;
17747
+ }
17748
+ _reOpen() {
17749
+ this.databaseOpenPromise = this.openInternalDB().finally(() => {
17750
+ this.databaseOpenPromise = null;
17751
+ });
17752
+ return this.databaseOpenPromise;
17753
+ }
17754
+ /**
17755
+ * Re-opens the underlying database.
17756
+ * Returns a pending operation if one is already in progress.
17757
+ */
17758
+ async reOpenInternalDB() {
17759
+ if (!this.options.reOpenOnConnectionClosed) {
17760
+ throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
17761
+ }
17762
+ if (this.databaseOpenPromise) {
17763
+ return this.databaseOpenPromise;
17764
+ }
17765
+ return this._reOpen();
17766
+ }
17767
+ async _init() {
17768
+ await this.openInternalDB();
17769
+ this.iterateListeners((cb) => cb.initialized?.());
17699
17770
  }
17700
17771
  getConfiguration() {
17701
17772
  if (!this._config) {
@@ -17708,7 +17779,7 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17708
17779
  await this.initPromise;
17709
17780
  }
17710
17781
  async shareConnection() {
17711
- if (false == this._db instanceof _WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__.WorkerWrappedAsyncDatabaseConnection) {
17782
+ if (false == this._db instanceof _WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_3__.WorkerWrappedAsyncDatabaseConnection) {
17712
17783
  throw new Error(`Only worker connections can be shared`);
17713
17784
  }
17714
17785
  return this._db.shareConnection();
@@ -17762,13 +17833,13 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17762
17833
  async readLock(fn, options) {
17763
17834
  await this.waitForInitialized();
17764
17835
  return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), {
17765
- timeoutMs: options?.timeoutMs
17836
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
17766
17837
  });
17767
17838
  }
17768
17839
  async writeLock(fn, options) {
17769
17840
  await this.waitForInitialized();
17770
17841
  return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), {
17771
- timeoutMs: options?.timeoutMs
17842
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
17772
17843
  });
17773
17844
  }
17774
17845
  async acquireLock(callback, options) {
@@ -17779,7 +17850,7 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17779
17850
  const abortController = new AbortController();
17780
17851
  this.pendingAbortControllers.add(abortController);
17781
17852
  const { timeoutMs } = options ?? {};
17782
- const timoutId = timeoutMs
17853
+ const timeoutId = timeoutMs
17783
17854
  ? setTimeout(() => {
17784
17855
  abortController.abort(`Timeout after ${timeoutMs}ms`);
17785
17856
  this.pendingAbortControllers.delete(abortController);
@@ -17787,13 +17858,35 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17787
17858
  : null;
17788
17859
  return (0,_shared_navigator__WEBPACK_IMPORTED_MODULE_1__.getNavigatorLocks)().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, async () => {
17789
17860
  this.pendingAbortControllers.delete(abortController);
17790
- if (timoutId) {
17791
- clearTimeout(timoutId);
17861
+ if (timeoutId) {
17862
+ clearTimeout(timeoutId);
17792
17863
  }
17793
- const holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
17864
+ let holdId = null;
17794
17865
  try {
17866
+ // The database is being opened in the background. Wait for it here.
17867
+ if (this.databaseOpenPromise) {
17868
+ try {
17869
+ await this.databaseOpenPromise;
17870
+ }
17871
+ catch (ex) {
17872
+ // This will cause a retry of opening the database.
17873
+ const wrappedError = new _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__.ConnectionClosedError('Could not open database');
17874
+ wrappedError.cause = ex;
17875
+ throw wrappedError;
17876
+ }
17877
+ }
17878
+ holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
17795
17879
  return await callback();
17796
17880
  }
17881
+ catch (ex) {
17882
+ if (ex instanceof _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__.ConnectionClosedError) {
17883
+ if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
17884
+ // Immediately re-open the database. We need to miss as little table updates as possible.
17885
+ this.reOpenInternalDB();
17886
+ }
17887
+ }
17888
+ throw ex;
17889
+ }
17797
17890
  finally {
17798
17891
  if (holdId) {
17799
17892
  await this.baseDB.releaseHold(holdId);
@@ -17926,17 +18019,22 @@ __webpack_require__.r(__webpack_exports__);
17926
18019
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
17927
18020
  /* harmony export */ WorkerWrappedAsyncDatabaseConnection: () => (/* binding */ WorkerWrappedAsyncDatabaseConnection)
17928
18021
  /* harmony export */ });
17929
- /* harmony import */ var comlink__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! comlink */ "../../node_modules/comlink/dist/esm/comlink.mjs");
18022
+ /* harmony import */ var _powersync_common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @powersync/common */ "../common/dist/bundle.mjs");
18023
+ /* harmony import */ var comlink__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! comlink */ "../../node_modules/comlink/dist/esm/comlink.mjs");
18024
+ /* harmony import */ var _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./AsyncDatabaseConnection */ "./lib/src/db/adapters/AsyncDatabaseConnection.js");
18025
+
18026
+
17930
18027
 
17931
18028
  /**
17932
18029
  * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
17933
18030
  * functions for worker listeners.
17934
18031
  */
17935
- class WorkerWrappedAsyncDatabaseConnection {
18032
+ class WorkerWrappedAsyncDatabaseConnection extends _powersync_common__WEBPACK_IMPORTED_MODULE_0__.BaseObserver {
17936
18033
  options;
17937
18034
  lockAbortController = new AbortController();
17938
18035
  notifyRemoteClosed;
17939
18036
  constructor(options) {
18037
+ super();
17940
18038
  this.options = options;
17941
18039
  if (options.remoteCanCloseUnexpectedly) {
17942
18040
  this.notifyRemoteClosed = new AbortController();
@@ -17969,17 +18067,20 @@ class WorkerWrappedAsyncDatabaseConnection {
17969
18067
  isAutoCommit() {
17970
18068
  return this.withRemote(() => this.baseConnection.isAutoCommit());
17971
18069
  }
17972
- withRemote(workerPromise) {
18070
+ withRemote(workerPromise, fireActionOnAbort = false) {
17973
18071
  const controller = this.notifyRemoteClosed;
17974
18072
  if (controller) {
17975
18073
  return new Promise((resolve, reject) => {
17976
18074
  if (controller.signal.aborted) {
17977
- reject(new Error('Called operation on closed remote'));
17978
- // Don't run the operation if we're going to reject
17979
- return;
18075
+ reject(new _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_1__.ConnectionClosedError('Called operation on closed remote'));
18076
+ if (!fireActionOnAbort) {
18077
+ // Don't run the operation if we're going to reject
18078
+ // We might want to fire-and-forget the operation in some cases (like a close operation)
18079
+ return;
18080
+ }
17980
18081
  }
17981
18082
  function handleAbort() {
17982
- reject(new Error('Remote peer closed with request in flight'));
18083
+ reject(new _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_1__.ConnectionClosedError('Remote peer closed with request in flight'));
17983
18084
  }
17984
18085
  function completePromise(action) {
17985
18086
  controller.signal.removeEventListener('abort', handleAbort);
@@ -18033,7 +18134,7 @@ class WorkerWrappedAsyncDatabaseConnection {
18033
18134
  reject(ex);
18034
18135
  }
18035
18136
  }));
18036
- const newPort = await remote[comlink__WEBPACK_IMPORTED_MODULE_0__.createEndpoint]();
18137
+ const newPort = await remote[comlink__WEBPACK_IMPORTED_MODULE_2__.createEndpoint]();
18037
18138
  return { port: newPort, identifier };
18038
18139
  }
18039
18140
  /**
@@ -18041,17 +18142,19 @@ class WorkerWrappedAsyncDatabaseConnection {
18041
18142
  * This can be extended by custom implementations in order to handle proxy events.
18042
18143
  */
18043
18144
  async registerOnTableChange(callback) {
18044
- return this.baseConnection.registerOnTableChange(comlink__WEBPACK_IMPORTED_MODULE_0__.proxy(callback));
18145
+ return this.baseConnection.registerOnTableChange(comlink__WEBPACK_IMPORTED_MODULE_2__.proxy(callback));
18045
18146
  }
18046
18147
  async close() {
18047
18148
  // Abort any pending lock requests.
18048
18149
  this.lockAbortController.abort();
18049
18150
  try {
18050
- await this.withRemote(() => this.baseConnection.close());
18151
+ // fire and forget the close operation
18152
+ await this.withRemote(() => this.baseConnection.close(), true);
18051
18153
  }
18052
18154
  finally {
18053
- this.options.remote[comlink__WEBPACK_IMPORTED_MODULE_0__.releaseProxy]();
18155
+ this.options.remote[comlink__WEBPACK_IMPORTED_MODULE_2__.releaseProxy]();
18054
18156
  this.options.onClose?.();
18157
+ this.iterateListeners((l) => l.closing?.());
18055
18158
  }
18056
18159
  }
18057
18160
  execute(sql, params) {
@@ -18877,7 +18980,6 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18877
18980
  statusListener;
18878
18981
  fetchCredentialsController;
18879
18982
  uploadDataController;
18880
- dbAdapter;
18881
18983
  syncParams;
18882
18984
  logger;
18883
18985
  lastConnectOptions;
@@ -18886,10 +18988,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18886
18988
  connectionManager;
18887
18989
  syncStatus;
18888
18990
  broadCastLogger;
18991
+ distributedDB;
18889
18992
  constructor() {
18890
18993
  super();
18891
18994
  this.ports = [];
18892
- this.dbAdapter = null;
18893
18995
  this.syncParams = null;
18894
18996
  this.logger = (0,_powersync_common__WEBPACK_IMPORTED_MODULE_0__.createLogger)('shared-sync');
18895
18997
  this.lastConnectOptions = undefined;
@@ -18902,26 +19004,23 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18902
19004
  }
18903
19005
  });
18904
19006
  });
19007
+ // Should be configured once we get params
19008
+ this.distributedDB = null;
18905
19009
  this.syncStatus = new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SyncStatus({});
18906
19010
  this.broadCastLogger = new _BroadcastLogger__WEBPACK_IMPORTED_MODULE_6__.BroadcastLogger(this.ports);
18907
19011
  this.connectionManager = new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.ConnectionManager({
18908
19012
  createSyncImplementation: async () => {
18909
- return this.portMutex.runExclusive(async () => {
18910
- await this.waitForReady();
18911
- if (!this.dbAdapter) {
18912
- await this.openInternalDB();
19013
+ await this.waitForReady();
19014
+ const sync = this.generateStreamingImplementation();
19015
+ const onDispose = sync.registerListener({
19016
+ statusChanged: (status) => {
19017
+ this.updateAllStatuses(status.toJSON());
18913
19018
  }
18914
- const sync = this.generateStreamingImplementation();
18915
- const onDispose = sync.registerListener({
18916
- statusChanged: (status) => {
18917
- this.updateAllStatuses(status.toJSON());
18918
- }
18919
- });
18920
- return {
18921
- sync,
18922
- onDispose
18923
- };
18924
19019
  });
19020
+ return {
19021
+ sync,
19022
+ onDispose
19023
+ };
18925
19024
  },
18926
19025
  logger: this.logger
18927
19026
  });
@@ -18932,6 +19031,29 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18932
19031
  get isConnected() {
18933
19032
  return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
18934
19033
  }
19034
+ /**
19035
+ * Gets the last client port which we know is safe from unexpected closes.
19036
+ */
19037
+ async getLastWrappedPort() {
19038
+ // Find the last port which is not closing
19039
+ return await this.portMutex.runExclusive(() => {
19040
+ for (let i = this.ports.length - 1; i >= 0; i--) {
19041
+ if (!this.ports[i].isClosing) {
19042
+ return this.ports[i];
19043
+ }
19044
+ }
19045
+ return;
19046
+ });
19047
+ }
19048
+ /**
19049
+ * In some very rare cases a specific tab might not respond to requests.
19050
+ * This returns a random port which is not closing.
19051
+ */
19052
+ async getRandomWrappedPort() {
19053
+ return await this.portMutex.runExclusive(() => {
19054
+ return this.ports[Math.floor(Math.random() * this.ports.length)];
19055
+ });
19056
+ }
18935
19057
  async waitForStatus(status) {
18936
19058
  return this.withSyncImplementation(async (sync) => {
18937
19059
  return sync.waitForStatus(status);
@@ -18972,28 +19094,45 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18972
19094
  async setParams(params) {
18973
19095
  await this.portMutex.runExclusive(async () => {
18974
19096
  this.collectActiveSubscriptions();
18975
- if (this.syncParams) {
18976
- // Cannot modify already existing sync implementation params
18977
- // But we can ask for a DB adapter, if required, at this point.
18978
- if (!this.dbAdapter) {
18979
- await this.openInternalDB();
18980
- }
18981
- return;
18982
- }
18983
- // First time setting params
18984
- this.syncParams = params;
18985
- if (params.streamOptions?.flags?.broadcastLogs) {
18986
- this.logger = this.broadCastLogger;
18987
- }
18988
- self.onerror = (event) => {
18989
- // Share any uncaught events on the broadcast logger
18990
- this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
18991
- };
18992
- if (!this.dbAdapter) {
18993
- await this.openInternalDB();
19097
+ });
19098
+ if (this.syncParams) {
19099
+ // Cannot modify already existing sync implementation params
19100
+ return;
19101
+ }
19102
+ // First time setting params
19103
+ this.syncParams = params;
19104
+ if (params.streamOptions?.flags?.broadcastLogs) {
19105
+ this.logger = this.broadCastLogger;
19106
+ }
19107
+ const lockedAdapter = new _db_adapters_LockedAsyncDatabaseAdapter__WEBPACK_IMPORTED_MODULE_4__.LockedAsyncDatabaseAdapter({
19108
+ name: params.dbParams.dbFilename,
19109
+ openConnection: async () => {
19110
+ // Gets a connection from the clients when a new connection is requested.
19111
+ const db = await this.openInternalDB();
19112
+ db.registerListener({
19113
+ closing: () => {
19114
+ lockedAdapter.reOpenInternalDB();
19115
+ }
19116
+ });
19117
+ return db;
19118
+ },
19119
+ logger: this.logger,
19120
+ reOpenOnConnectionClosed: true
19121
+ });
19122
+ this.distributedDB = lockedAdapter;
19123
+ await lockedAdapter.init();
19124
+ lockedAdapter.registerListener({
19125
+ databaseReOpened: () => {
19126
+ // We may have missed some table updates while the database was closed.
19127
+ // We can poke the crud in case we missed any updates.
19128
+ this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
18994
19129
  }
18995
- this.iterateListeners((l) => l.initialized?.());
18996
19130
  });
19131
+ self.onerror = (event) => {
19132
+ // Share any uncaught events on the broadcast logger
19133
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
19134
+ };
19135
+ this.iterateListeners((l) => l.initialized?.());
18997
19136
  }
18998
19137
  async dispose() {
18999
19138
  await this.waitForReady();
@@ -19022,7 +19161,8 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19022
19161
  port,
19023
19162
  clientProvider: comlink__WEBPACK_IMPORTED_MODULE_7__.wrap(port),
19024
19163
  currentSubscriptions: [],
19025
- closeListeners: []
19164
+ closeListeners: [],
19165
+ isClosing: false
19026
19166
  };
19027
19167
  this.ports.push(portProvider);
19028
19168
  // Give the newly connected client the latest status
@@ -19038,14 +19178,16 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19038
19178
  * clients.
19039
19179
  */
19040
19180
  async removePort(port) {
19181
+ // Ports might be removed faster than we can process them.
19182
+ port.isClosing = true;
19041
19183
  // Remove the port within a mutex context.
19042
19184
  // Warns if the port is not found. This should not happen in practice.
19043
19185
  // We return early if the port is not found.
19044
- const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
19186
+ return await this.portMutex.runExclusive(async () => {
19045
19187
  const index = this.ports.findIndex((p) => p == port);
19046
19188
  if (index < 0) {
19047
19189
  this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
19048
- return {};
19190
+ return () => { };
19049
19191
  }
19050
19192
  const trackedPort = this.ports[index];
19051
19193
  // Remove from the list of active ports
@@ -19059,40 +19201,13 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19059
19201
  abortController.controller.abort(new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.AbortOperation('Closing pending requests after client port is removed'));
19060
19202
  }
19061
19203
  });
19062
- const shouldReconnect = !!this.connectionManager.syncStreamImplementation && this.ports.length > 0;
19063
- /**
19064
- * If the current database adapter is the one that is being closed, we need to disconnect from the backend.
19065
- * We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other
19066
- * connect operations coming from other tabs.
19067
- */
19068
- if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
19069
- this.logger.debug(`Disconnecting due to closed database: should reconnect: ${shouldReconnect}`);
19070
- this.dbAdapter = null;
19071
- // Unconditionally close the connection because the database it's writing to has just been closed.
19072
- // The connection has been closed previously, this might throw. We should be able to ignore it.
19073
- await this.connectionManager
19074
- .disconnect()
19075
- .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
19204
+ // Close the worker wrapped database connection, we can't accurately rely on this connection
19205
+ for (const closeListener of trackedPort.closeListeners) {
19206
+ await closeListener();
19076
19207
  }
19077
- return {
19078
- shouldReconnect,
19079
- trackedPort
19080
- };
19208
+ this.collectActiveSubscriptions();
19209
+ return () => trackedPort.clientProvider[comlink__WEBPACK_IMPORTED_MODULE_7__.releaseProxy]();
19081
19210
  });
19082
- if (!trackedPort) {
19083
- // We could not find the port to remove
19084
- return () => { };
19085
- }
19086
- for (const closeListener of trackedPort.closeListeners) {
19087
- await closeListener();
19088
- }
19089
- if (shouldReconnect) {
19090
- await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
19091
- }
19092
- // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
19093
- this.collectActiveSubscriptions();
19094
- // Release proxy
19095
- return () => trackedPort.clientProvider[comlink__WEBPACK_IMPORTED_MODULE_7__.releaseProxy]();
19096
19211
  }
19097
19212
  triggerCrudUpload() {
19098
19213
  this.withSyncImplementation(async (sync) => {
@@ -19129,10 +19244,13 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19129
19244
  const syncParams = this.syncParams;
19130
19245
  // Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
19131
19246
  return new _db_sync_WebStreamingSyncImplementation__WEBPACK_IMPORTED_MODULE_3__.WebStreamingSyncImplementation({
19132
- adapter: new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SqliteBucketStorage(this.dbAdapter, this.logger),
19247
+ adapter: new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SqliteBucketStorage(this.distributedDB, this.logger),
19133
19248
  remote: new _db_sync_WebRemote__WEBPACK_IMPORTED_MODULE_2__.WebRemote({
19134
19249
  invalidateCredentials: async () => {
19135
- const lastPort = this.ports[this.ports.length - 1];
19250
+ const lastPort = await this.getLastWrappedPort();
19251
+ if (!lastPort) {
19252
+ throw new Error('No client port found to invalidate credentials');
19253
+ }
19136
19254
  try {
19137
19255
  this.logger.log('calling the last port client provider to invalidate credentials');
19138
19256
  lastPort.clientProvider.invalidateCredentials();
@@ -19142,7 +19260,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19142
19260
  }
19143
19261
  },
19144
19262
  fetchCredentials: async () => {
19145
- const lastPort = this.ports[this.ports.length - 1];
19263
+ const lastPort = await this.getLastWrappedPort();
19264
+ if (!lastPort) {
19265
+ throw new Error('No client port found to fetch credentials');
19266
+ }
19146
19267
  return new Promise(async (resolve, reject) => {
19147
19268
  const abortController = new AbortController();
19148
19269
  this.fetchCredentialsController = {
@@ -19164,7 +19285,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19164
19285
  }
19165
19286
  }, this.logger),
19166
19287
  uploadCrud: async () => {
19167
- const lastPort = this.ports[this.ports.length - 1];
19288
+ const lastPort = await this.getLastWrappedPort();
19289
+ if (!lastPort) {
19290
+ throw new Error('No client port found to upload crud');
19291
+ }
19168
19292
  return new Promise(async (resolve, reject) => {
19169
19293
  const abortController = new AbortController();
19170
19294
  this.uploadDataController = {
@@ -19190,19 +19314,52 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19190
19314
  logger: this.logger
19191
19315
  });
19192
19316
  }
19317
+ /**
19318
+ * Opens a worker wrapped database connection. Using the last connected client port.
19319
+ */
19193
19320
  async openInternalDB() {
19194
- const lastClient = this.ports[this.ports.length - 1];
19195
- if (!lastClient) {
19196
- // Should not really happen in practice
19197
- throw new Error(`Could not open DB connection since no client is connected.`);
19198
- }
19199
- const workerPort = await lastClient.clientProvider.getDBWorkerPort();
19200
- const remote = comlink__WEBPACK_IMPORTED_MODULE_7__.wrap(workerPort);
19201
- const identifier = this.syncParams.dbParams.dbFilename;
19202
- const db = await remote(this.syncParams.dbParams);
19203
- const locked = new _db_adapters_LockedAsyncDatabaseAdapter__WEBPACK_IMPORTED_MODULE_4__.LockedAsyncDatabaseAdapter({
19204
- name: identifier,
19205
- openConnection: async () => {
19321
+ while (true) {
19322
+ try {
19323
+ const client = await this.getRandomWrappedPort();
19324
+ if (!client) {
19325
+ // Should not really happen in practice
19326
+ throw new Error(`Could not open DB connection since no client is connected.`);
19327
+ }
19328
+ // Fail-safe timeout for opening a database connection.
19329
+ const timeout = setTimeout(() => {
19330
+ abortController.abort();
19331
+ }, 10_000);
19332
+ /**
19333
+ * Handle cases where the client might close while opening a connection.
19334
+ */
19335
+ const abortController = new AbortController();
19336
+ const closeListener = () => {
19337
+ abortController.abort();
19338
+ };
19339
+ const removeCloseListener = () => {
19340
+ const index = client.closeListeners.indexOf(closeListener);
19341
+ if (index >= 0) {
19342
+ client.closeListeners.splice(index, 1);
19343
+ }
19344
+ };
19345
+ client.closeListeners.push(closeListener);
19346
+ const workerPort = await withAbort(() => client.clientProvider.getDBWorkerPort(), abortController.signal).catch((ex) => {
19347
+ removeCloseListener();
19348
+ throw ex;
19349
+ });
19350
+ const remote = comlink__WEBPACK_IMPORTED_MODULE_7__.wrap(workerPort);
19351
+ const identifier = this.syncParams.dbParams.dbFilename;
19352
+ /**
19353
+ * The open could fail if the tab is closed while we're busy opening the database.
19354
+ * This operation is typically executed inside an exclusive portMutex lock.
19355
+ * We typically execute the closeListeners using the portMutex in a different context.
19356
+ * We can't rely on the closeListeners to abort the operation if the tab is closed.
19357
+ */
19358
+ const db = await withAbort(() => remote(this.syncParams.dbParams), abortController.signal).finally(() => {
19359
+ // We can remove the close listener here since we no longer need it past this point.
19360
+ removeCloseListener();
19361
+ });
19362
+ clearTimeout(timeout);
19206
19363
  const wrapped = new _db_adapters_WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_5__.WorkerWrappedAsyncDatabaseConnection({
19207
19364
  remote,
19208
19365
  baseConnection: db,
@@ -19211,22 +19368,23 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19211
19368
  // that and ensure pending requests are aborted when the tab is closed.
19212
19369
  remoteCanCloseUnexpectedly: true
19213
19370
  });
19214
- lastClient.closeListeners.push(async () => {
19371
+ client.closeListeners.push(async () => {
19215
19372
  this.logger.info('Aborting open connection because associated tab closed.');
19216
19373
  /**
19217
19374
  * Don't await this close operation. It might never resolve if the tab is closed.
19218
- * We run the close operation first, before marking the remote as closed. This gives the database some chance
19219
- * to close the connection.
19375
+ * We mark the remote as closed first, this will reject any pending requests.
19376
+ * We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
19220
19377
  */
19221
- wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
19222
19378
  wrapped.markRemoteClosed();
19379
+ wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
19223
19380
  });
19224
19381
  return wrapped;
19225
- },
19226
- logger: this.logger
19227
- });
19228
- await locked.init();
19229
- this.dbAdapter = lastClient.db = locked;
19382
+ }
19383
+ catch (ex) {
19384
+ this.logger.warn('Error opening internal DB', ex);
19385
+ await new Promise((resolve) => setTimeout(resolve, 1000));
19386
+ }
19387
+ }
19230
19388
  }
19231
19389
  /**
19232
19390
  * A method to update the all shared statuses for each
@@ -19236,18 +19394,29 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19236
19394
  this.syncStatus = new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SyncStatus(status);
19237
19395
  this.ports.forEach((p) => p.clientProvider.statusChanged(status));
19238
19396
  }
19239
- /**
19240
- * A function only used for unit tests which updates the internal
19241
- * sync stream client and all tab client's sync status
19242
- */
19243
- async _testUpdateAllStatuses(status) {
19244
- if (!this.connectionManager.syncStreamImplementation) {
19245
- throw new Error('Cannot update status without a sync stream implementation');
19397
+ }
19398
+ /**
19399
+ * Runs the action with an abort controller.
19400
+ */
19401
+ function withAbort(action, signal) {
19402
+ return new Promise((resolve, reject) => {
19403
+ if (signal.aborted) {
19404
+ reject(new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.AbortOperation('Operation aborted by abort controller'));
19405
+ return;
19246
19406
  }
19247
- // Only assigning, don't call listeners for this test
19248
- this.connectionManager.syncStreamImplementation.syncStatus = new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SyncStatus(status);
19249
- this.updateAllStatuses(status);
19250
- }
19407
+ function handleAbort() {
19408
+ signal.removeEventListener('abort', handleAbort);
19409
+ reject(new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.AbortOperation('Operation aborted by abort controller'));
19410
+ }
19411
+ signal.addEventListener('abort', handleAbort, { once: true });
19412
+ function completePromise(action) {
19413
+ signal.removeEventListener('abort', handleAbort);
19414
+ action();
19415
+ }
19416
+ action()
19417
+ .then((data) => completePromise(() => resolve(data)))
19418
+ .catch((e) => completePromise(() => reject(e)));
19419
+ });
19251
19420
  }
19252
19421
 
19253
19422
 
@@ -19279,12 +19448,11 @@ class WorkerClient {
19279
19448
  sync;
19280
19449
  port;
19281
19450
  resolvedPort = null;
19451
+ resolvedPortPromise = null;
19282
19452
  constructor(sync, port) {
19283
19453
  this.sync = sync;
19284
19454
  this.port = port;
19285
19455
  comlink__WEBPACK_IMPORTED_MODULE_2__.expose(this, this.port);
19286
- }
19287
- async initialize() {
19288
19456
  /**
19289
19457
  * Adds an extra listener which can remove this port
19290
19458
  * from the list of monitored ports.
@@ -19295,7 +19463,6 @@ class WorkerClient {
19295
19463
  await this.removePort();
19296
19464
  }
19297
19465
  });
19298
- this.resolvedPort = await this.sync.addPort(this.port);
19299
19466
  }
19300
19467
  async removePort() {
19301
19468
  if (this.resolvedPort) {
@@ -19316,7 +19483,10 @@ class WorkerClient {
19316
19483
  * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
19317
19484
  * it can consider the connection to be closed.
19318
19485
  */
19319
- addLockBasedCloseSignal(name) {
19486
+ async addLockBasedCloseSignal(name) {
19487
+ // Only add the port once the lock has been obtained on the client.
19488
+ this.resolvedPort = await this.sync.addPort(this.port);
19489
+ // Don't await this lock request
19320
19490
  (0,_shared_navigator__WEBPACK_IMPORTED_MODULE_0__.getNavigatorLocks)().request(name, async () => {
19321
19491
  await this.removePort();
19322
19492
  });
@@ -19348,9 +19518,6 @@ class WorkerClient {
19348
19518
  disconnect() {
19349
19519
  return this.sync.disconnect();
19350
19520
  }
19351
- async _testUpdateAllStatuses(status) {
19352
- return this.sync._testUpdateAllStatuses(status);
19353
- }
19354
19521
  }
19355
19522
 
19356
19523
 
@@ -19532,7 +19699,7 @@ logger.useDefaults();
19532
19699
  const sharedSyncImplementation = new _SharedSyncImplementation__WEBPACK_IMPORTED_MODULE_1__.SharedSyncImplementation();
19533
19700
  _self.onconnect = async function (event) {
19534
19701
  const port = event.ports[0];
19535
- await new _WorkerClient__WEBPACK_IMPORTED_MODULE_2__.WorkerClient(sharedSyncImplementation, port).initialize();
19702
+ new _WorkerClient__WEBPACK_IMPORTED_MODULE_2__.WorkerClient(sharedSyncImplementation, port);
19536
19703
  };
19537
19704
 
19538
19705
  })();