@powersync/web 0.0.0-dev-20251126195153 → 0.0.0-dev-20251129133952

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.
@@ -14659,7 +14659,7 @@ The next upload iteration will be delayed.`);
14659
14659
  uploadError: ex
14660
14660
  }
14661
14661
  });
14662
- await this.delayRetry(controller.signal);
14662
+ await this.delayRetry(controller.signal, this.options.crudUploadThrottleMs);
14663
14663
  if (!this.isConnected) {
14664
14664
  // Exit the upload loop if the sync stream is no longer connected
14665
14665
  break;
@@ -15370,14 +15370,14 @@ The next upload iteration will be delayed.`);
15370
15370
  // trigger this for all updates
15371
15371
  this.iterateListeners((cb) => cb.statusUpdated?.(options));
15372
15372
  }
15373
- async delayRetry(signal) {
15373
+ async delayRetry(signal, delayMs) {
15374
15374
  return new Promise((resolve) => {
15375
15375
  if (signal?.aborted) {
15376
15376
  // If the signal is already aborted, resolve immediately
15377
15377
  resolve();
15378
15378
  return;
15379
15379
  }
15380
- const { retryDelayMs } = this.options;
15380
+ const delay = delayMs ?? this.options.retryDelayMs;
15381
15381
  let timeoutId;
15382
15382
  const endDelay = () => {
15383
15383
  resolve();
@@ -15388,7 +15388,7 @@ The next upload iteration will be delayed.`);
15388
15388
  signal?.removeEventListener('abort', endDelay);
15389
15389
  };
15390
15390
  signal?.addEventListener('abort', endDelay, { once: true });
15391
- timeoutId = setTimeout(endDelay, retryDelayMs);
15391
+ timeoutId = setTimeout(endDelay, delay);
15392
15392
  });
15393
15393
  }
15394
15394
  updateSubscriptions(subscriptions) {
@@ -17600,6 +17600,26 @@ const parseQuery = (query, parameters) => {
17600
17600
  //# sourceMappingURL=bundle.mjs.map
17601
17601
 
17602
17602
 
17603
+ /***/ }),
17604
+
17605
+ /***/ "./lib/src/db/adapters/AsyncDatabaseConnection.js":
17606
+ /*!********************************************************!*\
17607
+ !*** ./lib/src/db/adapters/AsyncDatabaseConnection.js ***!
17608
+ \********************************************************/
17609
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
17610
+
17611
+ __webpack_require__.r(__webpack_exports__);
17612
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
17613
+ /* harmony export */ ConnectionClosedError: () => (/* binding */ ConnectionClosedError)
17614
+ /* harmony export */ });
17615
+ class ConnectionClosedError extends Error {
17616
+ constructor(message) {
17617
+ super(message);
17618
+ this.name = 'ConnectionClosedError';
17619
+ }
17620
+ }
17621
+
17622
+
17603
17623
  /***/ }),
17604
17624
 
17605
17625
  /***/ "./lib/src/db/adapters/LockedAsyncDatabaseAdapter.js":
@@ -17614,8 +17634,10 @@ __webpack_require__.r(__webpack_exports__);
17614
17634
  /* harmony export */ });
17615
17635
  /* harmony import */ var _powersync_common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @powersync/common */ "../common/dist/bundle.mjs");
17616
17636
  /* 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");
17637
+ /* harmony import */ var _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./AsyncDatabaseConnection */ "./lib/src/db/adapters/AsyncDatabaseConnection.js");
17638
+ /* harmony import */ var _WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./WorkerWrappedAsyncDatabaseConnection */ "./lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js");
17639
+ /* harmony import */ var _wa_sqlite_WASQLiteConnection__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./wa-sqlite/WASQLiteConnection */ "./lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js");
17640
+
17619
17641
 
17620
17642
 
17621
17643
 
@@ -17637,6 +17659,8 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17637
17659
  _config = null;
17638
17660
  pendingAbortControllers;
17639
17661
  requiresHolds;
17662
+ requiresReOpen;
17663
+ databaseOpenPromise = null;
17640
17664
  closing;
17641
17665
  closed;
17642
17666
  constructor(options) {
@@ -17648,6 +17672,7 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17648
17672
  this.closed = false;
17649
17673
  this.closing = false;
17650
17674
  this.requiresHolds = null;
17675
+ this.requiresReOpen = false;
17651
17676
  // Set the name if provided. We can query for the name if not available yet
17652
17677
  this.debugMode = options.debugMode ?? false;
17653
17678
  if (this.debugMode) {
@@ -17686,16 +17711,26 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17686
17711
  async init() {
17687
17712
  return this.initPromise;
17688
17713
  }
17689
- async _init() {
17714
+ async openInternalDB() {
17715
+ // Dispose any previous table change listener.
17716
+ this._disposeTableChangeListener?.();
17717
+ this._disposeTableChangeListener = null;
17718
+ const isReOpen = !!this._db;
17690
17719
  this._db = await this.options.openConnection();
17691
17720
  await this._db.init();
17692
17721
  this._config = await this._db.getConfig();
17693
17722
  await this.registerOnChangeListener(this._db);
17694
- this.iterateListeners((cb) => cb.initialized?.());
17723
+ if (isReOpen) {
17724
+ this.iterateListeners((cb) => cb.databaseReOpened?.());
17725
+ }
17695
17726
  /**
17696
17727
  * This is only required for the long-lived shared IndexedDB connections.
17697
17728
  */
17698
- this.requiresHolds = this._config.vfs == _wa_sqlite_WASQLiteConnection__WEBPACK_IMPORTED_MODULE_3__.WASQLiteVFS.IDBBatchAtomicVFS;
17729
+ this.requiresHolds = this._config.vfs == _wa_sqlite_WASQLiteConnection__WEBPACK_IMPORTED_MODULE_4__.WASQLiteVFS.IDBBatchAtomicVFS;
17730
+ }
17731
+ async _init() {
17732
+ await this.openInternalDB();
17733
+ this.iterateListeners((cb) => cb.initialized?.());
17699
17734
  }
17700
17735
  getConfiguration() {
17701
17736
  if (!this._config) {
@@ -17708,7 +17743,7 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17708
17743
  await this.initPromise;
17709
17744
  }
17710
17745
  async shareConnection() {
17711
- if (false == this._db instanceof _WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__.WorkerWrappedAsyncDatabaseConnection) {
17746
+ if (false == this._db instanceof _WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_3__.WorkerWrappedAsyncDatabaseConnection) {
17712
17747
  throw new Error(`Only worker connections can be shared`);
17713
17748
  }
17714
17749
  return this._db.shareConnection();
@@ -17762,13 +17797,13 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17762
17797
  async readLock(fn, options) {
17763
17798
  await this.waitForInitialized();
17764
17799
  return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), {
17765
- timeoutMs: options?.timeoutMs
17800
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
17766
17801
  });
17767
17802
  }
17768
17803
  async writeLock(fn, options) {
17769
17804
  await this.waitForInitialized();
17770
17805
  return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), {
17771
- timeoutMs: options?.timeoutMs
17806
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
17772
17807
  });
17773
17808
  }
17774
17809
  async acquireLock(callback, options) {
@@ -17779,7 +17814,7 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17779
17814
  const abortController = new AbortController();
17780
17815
  this.pendingAbortControllers.add(abortController);
17781
17816
  const { timeoutMs } = options ?? {};
17782
- const timoutId = timeoutMs
17817
+ const timeoutId = timeoutMs
17783
17818
  ? setTimeout(() => {
17784
17819
  abortController.abort(`Timeout after ${timeoutMs}ms`);
17785
17820
  this.pendingAbortControllers.delete(abortController);
@@ -17787,13 +17822,37 @@ class LockedAsyncDatabaseAdapter extends _powersync_common__WEBPACK_IMPORTED_MOD
17787
17822
  : null;
17788
17823
  return (0,_shared_navigator__WEBPACK_IMPORTED_MODULE_1__.getNavigatorLocks)().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, async () => {
17789
17824
  this.pendingAbortControllers.delete(abortController);
17790
- if (timoutId) {
17791
- clearTimeout(timoutId);
17825
+ if (timeoutId) {
17826
+ clearTimeout(timeoutId);
17792
17827
  }
17793
- const holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
17828
+ let holdId = null;
17794
17829
  try {
17830
+ // The database is being opened in the background. Wait for it here.
17831
+ if (this.databaseOpenPromise) {
17832
+ try {
17833
+ await this.databaseOpenPromise;
17834
+ }
17835
+ catch (ex) {
17836
+ // This will cause a retry of opening the database.
17837
+ const wrappedError = new _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__.ConnectionClosedError('Could not open database');
17838
+ wrappedError.cause = ex;
17839
+ throw wrappedError;
17840
+ }
17841
+ }
17842
+ holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
17795
17843
  return await callback();
17796
17844
  }
17845
+ catch (ex) {
17846
+ if (ex instanceof _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_2__.ConnectionClosedError) {
17847
+ if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
17848
+ // Immediately re-open the database. We need to miss as little table updates as possible.
17849
+ this.databaseOpenPromise = this.openInternalDB().finally(() => {
17850
+ this.databaseOpenPromise = null;
17851
+ });
17852
+ }
17853
+ }
17854
+ throw ex;
17855
+ }
17797
17856
  finally {
17798
17857
  if (holdId) {
17799
17858
  await this.baseDB.releaseHold(holdId);
@@ -17926,7 +17985,9 @@ __webpack_require__.r(__webpack_exports__);
17926
17985
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
17927
17986
  /* harmony export */ WorkerWrappedAsyncDatabaseConnection: () => (/* binding */ WorkerWrappedAsyncDatabaseConnection)
17928
17987
  /* harmony export */ });
17929
- /* harmony import */ var comlink__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! comlink */ "../../node_modules/comlink/dist/esm/comlink.mjs");
17988
+ /* harmony import */ var comlink__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! comlink */ "../../node_modules/comlink/dist/esm/comlink.mjs");
17989
+ /* harmony import */ var _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./AsyncDatabaseConnection */ "./lib/src/db/adapters/AsyncDatabaseConnection.js");
17990
+
17930
17991
 
17931
17992
  /**
17932
17993
  * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
@@ -17974,12 +18035,12 @@ class WorkerWrappedAsyncDatabaseConnection {
17974
18035
  if (controller) {
17975
18036
  return new Promise((resolve, reject) => {
17976
18037
  if (controller.signal.aborted) {
17977
- reject(new Error('Called operation on closed remote'));
18038
+ reject(new _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_0__.ConnectionClosedError('Called operation on closed remote'));
17978
18039
  // Don't run the operation if we're going to reject
17979
18040
  return;
17980
18041
  }
17981
18042
  function handleAbort() {
17982
- reject(new Error('Remote peer closed with request in flight'));
18043
+ reject(new _AsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_0__.ConnectionClosedError('Remote peer closed with request in flight'));
17983
18044
  }
17984
18045
  function completePromise(action) {
17985
18046
  controller.signal.removeEventListener('abort', handleAbort);
@@ -18033,7 +18094,7 @@ class WorkerWrappedAsyncDatabaseConnection {
18033
18094
  reject(ex);
18034
18095
  }
18035
18096
  }));
18036
- const newPort = await remote[comlink__WEBPACK_IMPORTED_MODULE_0__.createEndpoint]();
18097
+ const newPort = await remote[comlink__WEBPACK_IMPORTED_MODULE_1__.createEndpoint]();
18037
18098
  return { port: newPort, identifier };
18038
18099
  }
18039
18100
  /**
@@ -18041,7 +18102,7 @@ class WorkerWrappedAsyncDatabaseConnection {
18041
18102
  * This can be extended by custom implementations in order to handle proxy events.
18042
18103
  */
18043
18104
  async registerOnTableChange(callback) {
18044
- return this.baseConnection.registerOnTableChange(comlink__WEBPACK_IMPORTED_MODULE_0__.proxy(callback));
18105
+ return this.baseConnection.registerOnTableChange(comlink__WEBPACK_IMPORTED_MODULE_1__.proxy(callback));
18045
18106
  }
18046
18107
  async close() {
18047
18108
  // Abort any pending lock requests.
@@ -18050,7 +18111,7 @@ class WorkerWrappedAsyncDatabaseConnection {
18050
18111
  await this.withRemote(() => this.baseConnection.close());
18051
18112
  }
18052
18113
  finally {
18053
- this.options.remote[comlink__WEBPACK_IMPORTED_MODULE_0__.releaseProxy]();
18114
+ this.options.remote[comlink__WEBPACK_IMPORTED_MODULE_1__.releaseProxy]();
18054
18115
  this.options.onClose?.();
18055
18116
  }
18056
18117
  }
@@ -18877,7 +18938,6 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18877
18938
  statusListener;
18878
18939
  fetchCredentialsController;
18879
18940
  uploadDataController;
18880
- dbAdapter;
18881
18941
  syncParams;
18882
18942
  logger;
18883
18943
  lastConnectOptions;
@@ -18886,10 +18946,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18886
18946
  connectionManager;
18887
18947
  syncStatus;
18888
18948
  broadCastLogger;
18949
+ distributedDB;
18889
18950
  constructor() {
18890
18951
  super();
18891
18952
  this.ports = [];
18892
- this.dbAdapter = null;
18893
18953
  this.syncParams = null;
18894
18954
  this.logger = (0,_powersync_common__WEBPACK_IMPORTED_MODULE_0__.createLogger)('shared-sync');
18895
18955
  this.lastConnectOptions = undefined;
@@ -18902,26 +18962,23 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18902
18962
  }
18903
18963
  });
18904
18964
  });
18965
+ // Should be configured once we get params
18966
+ this.distributedDB = null;
18905
18967
  this.syncStatus = new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SyncStatus({});
18906
18968
  this.broadCastLogger = new _BroadcastLogger__WEBPACK_IMPORTED_MODULE_6__.BroadcastLogger(this.ports);
18907
18969
  this.connectionManager = new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.ConnectionManager({
18908
18970
  createSyncImplementation: async () => {
18909
- return this.portMutex.runExclusive(async () => {
18910
- await this.waitForReady();
18911
- if (!this.dbAdapter) {
18912
- await this.openInternalDB();
18971
+ await this.waitForReady();
18972
+ const sync = this.generateStreamingImplementation();
18973
+ const onDispose = sync.registerListener({
18974
+ statusChanged: (status) => {
18975
+ this.updateAllStatuses(status.toJSON());
18913
18976
  }
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
18977
  });
18978
+ return {
18979
+ sync,
18980
+ onDispose
18981
+ };
18925
18982
  },
18926
18983
  logger: this.logger
18927
18984
  });
@@ -18932,6 +18989,18 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18932
18989
  get isConnected() {
18933
18990
  return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
18934
18991
  }
18992
+ /**
18993
+ * Gets the last client port which we know is safe from unexpected closes.
18994
+ */
18995
+ get lastWrappedPort() {
18996
+ // Find the last port which is protected from close
18997
+ for (let i = this.ports.length - 1; i >= 0; i--) {
18998
+ if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) {
18999
+ return this.ports[i];
19000
+ }
19001
+ }
19002
+ return;
19003
+ }
18935
19004
  async waitForStatus(status) {
18936
19005
  return this.withSyncImplementation(async (sync) => {
18937
19006
  return sync.waitForStatus(status);
@@ -18974,10 +19043,6 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18974
19043
  this.collectActiveSubscriptions();
18975
19044
  if (this.syncParams) {
18976
19045
  // 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
19046
  return;
18982
19047
  }
18983
19048
  // First time setting params
@@ -18985,13 +19050,28 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
18985
19050
  if (params.streamOptions?.flags?.broadcastLogs) {
18986
19051
  this.logger = this.broadCastLogger;
18987
19052
  }
19053
+ const lockedAdapter = new _db_adapters_LockedAsyncDatabaseAdapter__WEBPACK_IMPORTED_MODULE_4__.LockedAsyncDatabaseAdapter({
19054
+ name: params.dbParams.dbFilename,
19055
+ openConnection: async () => {
19056
+ // Gets a connection from the clients when a new connection is requested.
19057
+ return await this.openInternalDB();
19058
+ },
19059
+ logger: this.logger,
19060
+ reOpenOnConnectionClosed: true
19061
+ });
19062
+ this.distributedDB = lockedAdapter;
19063
+ await lockedAdapter.init();
19064
+ lockedAdapter.registerListener({
19065
+ databaseReOpened: () => {
19066
+ // We may have missed some table updates while the database was closed.
19067
+ // We can poke the crud in case we missed any updates.
19068
+ this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
19069
+ }
19070
+ });
18988
19071
  self.onerror = (event) => {
18989
19072
  // Share any uncaught events on the broadcast logger
18990
19073
  this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
18991
19074
  };
18992
- if (!this.dbAdapter) {
18993
- await this.openInternalDB();
18994
- }
18995
19075
  this.iterateListeners((l) => l.initialized?.());
18996
19076
  });
18997
19077
  }
@@ -19022,7 +19102,9 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19022
19102
  port,
19023
19103
  clientProvider: comlink__WEBPACK_IMPORTED_MODULE_7__.wrap(port),
19024
19104
  currentSubscriptions: [],
19025
- closeListeners: []
19105
+ closeListeners: [],
19106
+ isProtectedFromClose: false,
19107
+ isClosing: false
19026
19108
  };
19027
19109
  this.ports.push(portProvider);
19028
19110
  // Give the newly connected client the latest status
@@ -19038,14 +19120,16 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19038
19120
  * clients.
19039
19121
  */
19040
19122
  async removePort(port) {
19123
+ // Ports might be removed faster than we can process them.
19124
+ port.isClosing = true;
19041
19125
  // Remove the port within a mutex context.
19042
19126
  // Warns if the port is not found. This should not happen in practice.
19043
19127
  // We return early if the port is not found.
19044
- const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
19128
+ return await this.portMutex.runExclusive(async () => {
19045
19129
  const index = this.ports.findIndex((p) => p == port);
19046
19130
  if (index < 0) {
19047
19131
  this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
19048
- return {};
19132
+ return () => { };
19049
19133
  }
19050
19134
  const trackedPort = this.ports[index];
19051
19135
  // Remove from the list of active ports
@@ -19059,35 +19143,13 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19059
19143
  abortController.controller.abort(new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.AbortOperation('Closing pending requests after client port is removed'));
19060
19144
  }
19061
19145
  });
19062
- const shouldReconnect = !!this.connectionManager.syncStreamImplementation && this.ports.length > 0;
19063
- return {
19064
- shouldReconnect,
19065
- trackedPort
19066
- };
19067
- });
19068
- if (!trackedPort) {
19069
- // We could not find the port to remove
19070
- return () => { };
19071
- }
19072
- for (const closeListener of trackedPort.closeListeners) {
19073
- await closeListener();
19074
- }
19075
- if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
19076
- // Unconditionally close the connection because the database it's writing to has just been closed.
19077
- // The connection has been closed previously, this might throw. We should be able to ignore it.
19078
- await this.connectionManager
19079
- .disconnect()
19080
- .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
19081
- // Clearing the adapter will result in a new one being opened in connect
19082
- this.dbAdapter = null;
19083
- if (shouldReconnect) {
19084
- await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
19146
+ // Close the worker wrapped database connection, we can't accurately rely on this connection
19147
+ for (const closeListener of trackedPort.closeListeners) {
19148
+ await closeListener();
19085
19149
  }
19086
- }
19087
- // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
19088
- this.collectActiveSubscriptions();
19089
- // Release proxy
19090
- return () => trackedPort.clientProvider[comlink__WEBPACK_IMPORTED_MODULE_7__.releaseProxy]();
19150
+ this.collectActiveSubscriptions();
19151
+ return () => trackedPort.clientProvider[comlink__WEBPACK_IMPORTED_MODULE_7__.releaseProxy]();
19152
+ });
19091
19153
  }
19092
19154
  triggerCrudUpload() {
19093
19155
  this.withSyncImplementation(async (sync) => {
@@ -19124,10 +19186,13 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19124
19186
  const syncParams = this.syncParams;
19125
19187
  // Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
19126
19188
  return new _db_sync_WebStreamingSyncImplementation__WEBPACK_IMPORTED_MODULE_3__.WebStreamingSyncImplementation({
19127
- adapter: new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SqliteBucketStorage(this.dbAdapter, this.logger),
19189
+ adapter: new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.SqliteBucketStorage(this.distributedDB, this.logger),
19128
19190
  remote: new _db_sync_WebRemote__WEBPACK_IMPORTED_MODULE_2__.WebRemote({
19129
19191
  invalidateCredentials: async () => {
19130
- const lastPort = this.ports[this.ports.length - 1];
19192
+ const lastPort = this.lastWrappedPort;
19193
+ if (!lastPort) {
19194
+ throw new Error('No client port found to invalidate credentials');
19195
+ }
19131
19196
  try {
19132
19197
  this.logger.log('calling the last port client provider to invalidate credentials');
19133
19198
  lastPort.clientProvider.invalidateCredentials();
@@ -19137,7 +19202,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19137
19202
  }
19138
19203
  },
19139
19204
  fetchCredentials: async () => {
19140
- const lastPort = this.ports[this.ports.length - 1];
19205
+ const lastPort = this.lastWrappedPort;
19206
+ if (!lastPort) {
19207
+ throw new Error('No client port found to fetch credentials');
19208
+ }
19141
19209
  return new Promise(async (resolve, reject) => {
19142
19210
  const abortController = new AbortController();
19143
19211
  this.fetchCredentialsController = {
@@ -19159,7 +19227,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19159
19227
  }
19160
19228
  }, this.logger),
19161
19229
  uploadCrud: async () => {
19162
- const lastPort = this.ports[this.ports.length - 1];
19230
+ const lastPort = this.lastWrappedPort;
19231
+ if (!lastPort) {
19232
+ throw new Error('No client port found to upload crud');
19233
+ }
19163
19234
  return new Promise(async (resolve, reject) => {
19164
19235
  const abortController = new AbortController();
19165
19236
  this.uploadDataController = {
@@ -19185,19 +19256,47 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19185
19256
  logger: this.logger
19186
19257
  });
19187
19258
  }
19259
+ /**
19260
+ * Opens a worker wrapped database connection. Using the last connected client port.
19261
+ */
19188
19262
  async openInternalDB() {
19189
- const lastClient = this.ports[this.ports.length - 1];
19190
- if (!lastClient) {
19191
- // Should not really happen in practice
19192
- throw new Error(`Could not open DB connection since no client is connected.`);
19193
- }
19194
- const workerPort = await lastClient.clientProvider.getDBWorkerPort();
19195
- const remote = comlink__WEBPACK_IMPORTED_MODULE_7__.wrap(workerPort);
19196
- const identifier = this.syncParams.dbParams.dbFilename;
19197
- const db = await remote(this.syncParams.dbParams);
19198
- const locked = new _db_adapters_LockedAsyncDatabaseAdapter__WEBPACK_IMPORTED_MODULE_4__.LockedAsyncDatabaseAdapter({
19199
- name: identifier,
19200
- openConnection: async () => {
19263
+ while (true) {
19264
+ try {
19265
+ const lastClient = this.lastWrappedPort;
19266
+ if (!lastClient) {
19267
+ // Should not really happen in practice
19268
+ throw new Error(`Could not open DB connection since no client is connected.`);
19269
+ }
19270
+ /**
19271
+ * Handle cases where the client might close while opening a connection.
19272
+ */
19273
+ const abortController = new AbortController();
19274
+ const closeListener = () => {
19275
+ abortController.abort();
19276
+ };
19277
+ const removeCloseListener = () => {
19278
+ const index = lastClient.closeListeners.indexOf(closeListener);
19279
+ if (index >= 0) {
19280
+ lastClient.closeListeners.splice(index, 1);
19281
+ }
19282
+ };
19283
+ lastClient.closeListeners.push(closeListener);
19284
+ const workerPort = await withAbort(() => lastClient.clientProvider.getDBWorkerPort(), abortController.signal).catch((ex) => {
19285
+ removeCloseListener();
19286
+ throw ex;
19287
+ });
19288
+ const remote = comlink__WEBPACK_IMPORTED_MODULE_7__.wrap(workerPort);
19289
+ const identifier = this.syncParams.dbParams.dbFilename;
19290
+ /**
19291
+ * The open could fail if the tab is closed while we're busy opening the database.
19292
+ * This operation is typically executed inside an exclusive portMutex lock.
19293
+ * We typically execute the closeListeners using the portMutex in a different context.
19294
+ * We can't rely on the closeListeners to abort the operation if the tab is closed.
19295
+ */
19296
+ const db = await withAbort(() => remote(this.syncParams.dbParams), abortController.signal).finally(() => {
19297
+ // We can remove the close listener here since we no longer need it past this point.
19298
+ removeCloseListener();
19299
+ });
19201
19300
  const wrapped = new _db_adapters_WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_5__.WorkerWrappedAsyncDatabaseConnection({
19202
19301
  remote,
19203
19302
  baseConnection: db,
@@ -19208,15 +19307,21 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19208
19307
  });
19209
19308
  lastClient.closeListeners.push(async () => {
19210
19309
  this.logger.info('Aborting open connection because associated tab closed.');
19310
+ /**
19311
+ * Don't await this close operation. It might never resolve if the tab is closed.
19312
+ * We run the close operation first, before marking the remote as closed. This gives the database some chance
19313
+ * to close the connection.
19314
+ */
19211
19315
  wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
19212
19316
  wrapped.markRemoteClosed();
19213
19317
  });
19214
19318
  return wrapped;
19215
- },
19216
- logger: this.logger
19217
- });
19218
- await locked.init();
19219
- this.dbAdapter = lastClient.db = locked;
19319
+ }
19320
+ catch (ex) {
19321
+ this.logger.warn('Error opening internal DB', ex);
19322
+ await new Promise((resolve) => setTimeout(resolve, 1000));
19323
+ }
19324
+ }
19220
19325
  }
19221
19326
  /**
19222
19327
  * A method to update the all shared statuses for each
@@ -19239,6 +19344,29 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19239
19344
  this.updateAllStatuses(status);
19240
19345
  }
19241
19346
  }
19347
+ /**
19348
+ * Runs the action with an abort controller.
19349
+ */
19350
+ function withAbort(action, signal) {
19351
+ return new Promise((resolve, reject) => {
19352
+ if (signal.aborted) {
19353
+ reject(new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.AbortOperation('Operation aborted by abort controller'));
19354
+ return;
19355
+ }
19356
+ function handleAbort() {
19357
+ signal.removeEventListener('abort', handleAbort);
19358
+ reject(new _powersync_common__WEBPACK_IMPORTED_MODULE_0__.AbortOperation('Operation aborted by abort controller'));
19359
+ }
19360
+ signal.addEventListener('abort', handleAbort, { once: true });
19361
+ function completePromise(action) {
19362
+ signal.removeEventListener('abort', handleAbort);
19363
+ action();
19364
+ }
19365
+ action()
19366
+ .then((data) => completePromise(() => resolve(data)))
19367
+ .catch((e) => completePromise(() => reject(e)));
19368
+ });
19369
+ }
19242
19370
 
19243
19371
 
19244
19372
  /***/ }),
@@ -19254,8 +19382,8 @@ __webpack_require__.r(__webpack_exports__);
19254
19382
  /* harmony export */ WorkerClient: () => (/* binding */ WorkerClient)
19255
19383
  /* harmony export */ });
19256
19384
  /* harmony import */ var comlink__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! comlink */ "../../node_modules/comlink/dist/esm/comlink.mjs");
19257
- /* harmony import */ var _SharedSyncImplementation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./SharedSyncImplementation */ "./lib/src/worker/sync/SharedSyncImplementation.js");
19258
- /* harmony import */ var _shared_navigator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../shared/navigator */ "./lib/src/shared/navigator.js");
19385
+ /* harmony import */ var _shared_navigator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../shared/navigator */ "./lib/src/shared/navigator.js");
19386
+ /* harmony import */ var _SharedSyncImplementation__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./SharedSyncImplementation */ "./lib/src/worker/sync/SharedSyncImplementation.js");
19259
19387
 
19260
19388
 
19261
19389
 
@@ -19269,9 +19397,11 @@ class WorkerClient {
19269
19397
  sync;
19270
19398
  port;
19271
19399
  resolvedPort = null;
19400
+ resolvedPortPromise = null;
19272
19401
  constructor(sync, port) {
19273
19402
  this.sync = sync;
19274
19403
  this.port = port;
19404
+ comlink__WEBPACK_IMPORTED_MODULE_2__.expose(this, this.port);
19275
19405
  }
19276
19406
  async initialize() {
19277
19407
  /**
@@ -19280,12 +19410,19 @@ class WorkerClient {
19280
19410
  */
19281
19411
  this.port.addEventListener('message', async (event) => {
19282
19412
  const payload = event.data;
19283
- if (payload?.event == _SharedSyncImplementation__WEBPACK_IMPORTED_MODULE_0__.SharedSyncClientEvent.CLOSE_CLIENT) {
19413
+ if (payload?.event == _SharedSyncImplementation__WEBPACK_IMPORTED_MODULE_1__.SharedSyncClientEvent.CLOSE_CLIENT) {
19284
19414
  await this.removePort();
19285
19415
  }
19286
19416
  });
19287
- this.resolvedPort = await this.sync.addPort(this.port);
19288
- comlink__WEBPACK_IMPORTED_MODULE_2__.expose(this, this.port);
19417
+ /**
19418
+ * Keep a reference to the resolved port promise.
19419
+ * The init timing is difficult to predict due to the async message passing.
19420
+ * We only want to use a port if we are know it's been protected from being closed.
19421
+ * The lock based close signal will be added asynchronously. We need to use the
19422
+ * added port once the lock is configured.
19423
+ */
19424
+ this.resolvedPortPromise = this.sync.addPort(this.port);
19425
+ this.resolvedPort = await this.resolvedPortPromise;
19289
19426
  }
19290
19427
  async removePort() {
19291
19428
  if (this.resolvedPort) {
@@ -19294,7 +19431,7 @@ class WorkerClient {
19294
19431
  const release = await this.sync.removePort(resolved);
19295
19432
  this.resolvedPort = null;
19296
19433
  this.port.postMessage({
19297
- event: _SharedSyncImplementation__WEBPACK_IMPORTED_MODULE_0__.SharedSyncClientEvent.CLOSE_ACK,
19434
+ event: _SharedSyncImplementation__WEBPACK_IMPORTED_MODULE_1__.SharedSyncClientEvent.CLOSE_ACK,
19298
19435
  data: {}
19299
19436
  });
19300
19437
  release?.();
@@ -19306,8 +19443,21 @@ class WorkerClient {
19306
19443
  * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
19307
19444
  * it can consider the connection to be closed.
19308
19445
  */
19309
- addLockBasedCloseSignal(name) {
19310
- (0,_shared_navigator__WEBPACK_IMPORTED_MODULE_1__.getNavigatorLocks)().request(name, async () => {
19446
+ async addLockBasedCloseSignal(name) {
19447
+ if (!this.resolvedPortPromise) {
19448
+ // The init logic above is actually synchronous, so this should not happen.
19449
+ this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered');
19450
+ }
19451
+ else {
19452
+ const wrappedPort = await this.resolvedPortPromise;
19453
+ /**
19454
+ * The client registered a navigator lock. We now can guarantee detecting if the client has closed.
19455
+ * E.g. before this point: It's possible some ports might have been created and closed before the
19456
+ * lock based close signal is added. We should not trust those ports.
19457
+ */
19458
+ wrappedPort.isProtectedFromClose = true;
19459
+ }
19460
+ (0,_shared_navigator__WEBPACK_IMPORTED_MODULE_0__.getNavigatorLocks)().request(name, async () => {
19311
19461
  await this.removePort();
19312
19462
  });
19313
19463
  }