@powersync/web 0.0.0-dev-20251127205344 → 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,40 +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
- /**
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));
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();
19076
19149
  }
19077
- return {
19078
- shouldReconnect,
19079
- trackedPort
19080
- };
19150
+ this.collectActiveSubscriptions();
19151
+ return () => trackedPort.clientProvider[comlink__WEBPACK_IMPORTED_MODULE_7__.releaseProxy]();
19081
19152
  });
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
19153
  }
19097
19154
  triggerCrudUpload() {
19098
19155
  this.withSyncImplementation(async (sync) => {
@@ -19129,10 +19186,13 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19129
19186
  const syncParams = this.syncParams;
19130
19187
  // Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
19131
19188
  return new _db_sync_WebStreamingSyncImplementation__WEBPACK_IMPORTED_MODULE_3__.WebStreamingSyncImplementation({
19132
- 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),
19133
19190
  remote: new _db_sync_WebRemote__WEBPACK_IMPORTED_MODULE_2__.WebRemote({
19134
19191
  invalidateCredentials: async () => {
19135
- 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
+ }
19136
19196
  try {
19137
19197
  this.logger.log('calling the last port client provider to invalidate credentials');
19138
19198
  lastPort.clientProvider.invalidateCredentials();
@@ -19142,7 +19202,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19142
19202
  }
19143
19203
  },
19144
19204
  fetchCredentials: async () => {
19145
- 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
+ }
19146
19209
  return new Promise(async (resolve, reject) => {
19147
19210
  const abortController = new AbortController();
19148
19211
  this.fetchCredentialsController = {
@@ -19164,7 +19227,10 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19164
19227
  }
19165
19228
  }, this.logger),
19166
19229
  uploadCrud: async () => {
19167
- 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
+ }
19168
19234
  return new Promise(async (resolve, reject) => {
19169
19235
  const abortController = new AbortController();
19170
19236
  this.uploadDataController = {
@@ -19190,19 +19256,47 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19190
19256
  logger: this.logger
19191
19257
  });
19192
19258
  }
19259
+ /**
19260
+ * Opens a worker wrapped database connection. Using the last connected client port.
19261
+ */
19193
19262
  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 () => {
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
+ });
19206
19300
  const wrapped = new _db_adapters_WorkerWrappedAsyncDatabaseConnection__WEBPACK_IMPORTED_MODULE_5__.WorkerWrappedAsyncDatabaseConnection({
19207
19301
  remote,
19208
19302
  baseConnection: db,
@@ -19222,11 +19316,12 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19222
19316
  wrapped.markRemoteClosed();
19223
19317
  });
19224
19318
  return wrapped;
19225
- },
19226
- logger: this.logger
19227
- });
19228
- await locked.init();
19229
- 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
+ }
19230
19325
  }
19231
19326
  /**
19232
19327
  * A method to update the all shared statuses for each
@@ -19249,6 +19344,29 @@ class SharedSyncImplementation extends _powersync_common__WEBPACK_IMPORTED_MODUL
19249
19344
  this.updateAllStatuses(status);
19250
19345
  }
19251
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
+ }
19252
19370
 
19253
19371
 
19254
19372
  /***/ }),
@@ -19279,6 +19397,7 @@ class WorkerClient {
19279
19397
  sync;
19280
19398
  port;
19281
19399
  resolvedPort = null;
19400
+ resolvedPortPromise = null;
19282
19401
  constructor(sync, port) {
19283
19402
  this.sync = sync;
19284
19403
  this.port = port;
@@ -19295,7 +19414,15 @@ class WorkerClient {
19295
19414
  await this.removePort();
19296
19415
  }
19297
19416
  });
19298
- this.resolvedPort = await this.sync.addPort(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;
19299
19426
  }
19300
19427
  async removePort() {
19301
19428
  if (this.resolvedPort) {
@@ -19316,7 +19443,20 @@ class WorkerClient {
19316
19443
  * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
19317
19444
  * it can consider the connection to be closed.
19318
19445
  */
19319
- addLockBasedCloseSignal(name) {
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
+ }
19320
19460
  (0,_shared_navigator__WEBPACK_IMPORTED_MODULE_0__.getNavigatorLocks)().request(name, async () => {
19321
19461
  await this.removePort();
19322
19462
  });