@powersync/web 1.30.0 → 1.32.0

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 (112) hide show
  1. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +1867 -0
  2. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +1 -0
  3. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530150.index.umd.js +555 -0
  4. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530150.index.umd.js.map +1 -0
  5. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530151.index.umd.js +555 -0
  6. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-2530151.index.umd.js.map +1 -0
  7. package/dist/index.umd.js +5022 -38504
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/worker/SharedSyncImplementation.umd.js +819 -2220
  10. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  11. package/dist/worker/WASQLiteDB.umd.js +524 -2121
  12. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  13. package/dist/worker/{node_modules_bson_lib_bson_mjs.umd.js → node_modules_pnpm_bson_6_10_4_node_modules_bson_lib_bson_mjs.umd.js} +8 -8
  14. package/dist/worker/node_modules_pnpm_bson_6_10_4_node_modules_bson_lib_bson_mjs.umd.js.map +1 -0
  15. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-3a94cf.umd.js +44 -0
  16. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-3a94cf.umd.js.map +1 -0
  17. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-868779.umd.js +44 -0
  18. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-868779.umd.js.map +1 -0
  19. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-f60d0d.umd.js +44 -0
  20. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-f60d0d.umd.js.map +1 -0
  21. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +44 -0
  22. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -0
  23. package/dist/worker/{node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-0d2437.umd.js} +32 -32
  24. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-0d2437.umd.js.map +1 -0
  25. package/dist/worker/{node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-1d4e74.umd.js} +24 -24
  26. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-1d4e74.umd.js.map +1 -0
  27. package/dist/worker/{node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-3622cf.umd.js} +24 -24
  28. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_4_1_node_modules_journeyapps_wa-sqlite_src_examples-3622cf.umd.js.map +1 -0
  29. package/lib/package.json +27 -23
  30. package/lib/src/db/NavigatorTriggerClaimManager.d.ts +6 -0
  31. package/lib/src/db/NavigatorTriggerClaimManager.js +20 -0
  32. package/lib/src/db/PowerSyncDatabase.d.ts +5 -2
  33. package/lib/src/db/PowerSyncDatabase.js +49 -11
  34. package/lib/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.d.ts +1 -1
  35. package/lib/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.js +1 -1
  36. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +2 -2
  37. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +2 -2
  38. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +1 -1
  39. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +21 -4
  40. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +116 -22
  41. package/lib/src/db/adapters/WebDBAdapter.d.ts +5 -2
  42. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +7 -3
  43. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +14 -7
  44. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +12 -0
  45. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +19 -0
  46. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +2 -2
  47. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +11 -2
  48. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +4 -4
  49. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +6 -6
  50. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +5 -5
  51. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +7 -7
  52. package/lib/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.d.ts +1 -1
  53. package/lib/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.js +3 -3
  54. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +6 -9
  55. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +54 -38
  56. package/lib/src/db/sync/WebRemote.js +1 -1
  57. package/lib/src/db/sync/WebStreamingSyncImplementation.d.ts +1 -1
  58. package/lib/src/db/sync/WebStreamingSyncImplementation.js +1 -1
  59. package/lib/src/index.d.ts +12 -12
  60. package/lib/src/index.js +12 -12
  61. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +2 -2
  62. package/lib/src/worker/db/WASQLiteDB.worker.js +3 -3
  63. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +2 -2
  64. package/lib/src/worker/db/WorkerWASQLiteConnection.js +1 -1
  65. package/lib/src/worker/db/open-worker-database.d.ts +2 -2
  66. package/lib/src/worker/db/open-worker-database.js +1 -1
  67. package/lib/src/worker/sync/BroadcastLogger.d.ts +1 -1
  68. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +21 -11
  69. package/lib/src/worker/sync/SharedSyncImplementation.js +209 -113
  70. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +3 -3
  71. package/lib/src/worker/sync/WorkerClient.d.ts +4 -5
  72. package/lib/src/worker/sync/WorkerClient.js +8 -10
  73. package/lib/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +23 -19
  75. package/src/db/NavigatorTriggerClaimManager.ts +23 -0
  76. package/src/db/PowerSyncDatabase.ts +64 -22
  77. package/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts +1 -1
  78. package/src/db/adapters/AbstractWebSQLOpenFactory.ts +3 -3
  79. package/src/db/adapters/AsyncDatabaseConnection.ts +1 -1
  80. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +138 -33
  81. package/src/db/adapters/WebDBAdapter.ts +6 -2
  82. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +20 -8
  83. package/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.ts +23 -0
  84. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +13 -5
  85. package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +8 -8
  86. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +9 -9
  87. package/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts +3 -3
  88. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +69 -51
  89. package/src/db/sync/WebRemote.ts +1 -1
  90. package/src/db/sync/WebStreamingSyncImplementation.ts +2 -2
  91. package/src/index.ts +12 -12
  92. package/src/worker/db/SharedWASQLiteConnection.ts +2 -2
  93. package/src/worker/db/WASQLiteDB.worker.ts +5 -6
  94. package/src/worker/db/WorkerWASQLiteConnection.ts +2 -2
  95. package/src/worker/db/open-worker-database.ts +2 -2
  96. package/src/worker/sync/BroadcastLogger.ts +1 -1
  97. package/src/worker/sync/SharedSyncImplementation.ts +241 -126
  98. package/src/worker/sync/SharedSyncImplementation.worker.ts +3 -3
  99. package/src/worker/sync/WorkerClient.ts +10 -14
  100. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +0 -1
  101. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +0 -44
  102. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +0 -1
  103. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +0 -44
  104. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +0 -1
  105. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +0 -44
  106. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +0 -1
  107. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +0 -44
  108. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +0 -1
  109. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js.map +0 -1
  110. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +0 -1
  111. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +0 -1
  112. /package/bin/{powersync.js → powersync.cjs} +0 -0
@@ -2,14 +2,14 @@ import {
2
2
  AbortOperation,
3
3
  BaseObserver,
4
4
  ConnectionManager,
5
- createLogger,
6
5
  DBAdapter,
7
6
  PowerSyncBackendConnector,
8
7
  SqliteBucketStorage,
9
8
  SubscribedStream,
10
9
  SyncStatus,
11
- type ILogger,
10
+ createLogger,
12
11
  type ILogLevel,
12
+ type ILogger,
13
13
  type PowerSyncConnectionOptions,
14
14
  type StreamingSyncImplementation,
15
15
  type StreamingSyncImplementationListener,
@@ -17,18 +17,18 @@ import {
17
17
  } from '@powersync/common';
18
18
  import { Mutex } from 'async-mutex';
19
19
  import * as Comlink from 'comlink';
20
- import { WebRemote } from '../../db/sync/WebRemote';
20
+ import { WebRemote } from '../../db/sync/WebRemote.js';
21
21
  import {
22
22
  WebStreamingSyncImplementation,
23
23
  WebStreamingSyncImplementationOptions
24
- } from '../../db/sync/WebStreamingSyncImplementation';
24
+ } from '../../db/sync/WebStreamingSyncImplementation.js';
25
25
 
26
- import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection';
27
- import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter';
28
- import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
29
- import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection';
30
- import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
31
- import { BroadcastLogger } from './BroadcastLogger';
26
+ import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection.js';
27
+ import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter.js';
28
+ import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection.js';
29
+ import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags.js';
30
+ import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider.js';
31
+ import { BroadcastLogger } from './BroadcastLogger.js';
32
32
 
33
33
  /**
34
34
  * @internal
@@ -76,6 +76,7 @@ export type WrappedSyncPort = {
76
76
  db?: DBAdapter;
77
77
  currentSubscriptions: SubscribedStream[];
78
78
  closeListeners: (() => void | Promise<void>)[];
79
+ isClosing: boolean;
79
80
  };
80
81
 
81
82
  /**
@@ -106,7 +107,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
106
107
  protected fetchCredentialsController?: RemoteOperationAbortController;
107
108
  protected uploadDataController?: RemoteOperationAbortController;
108
109
 
109
- protected dbAdapter: DBAdapter | null;
110
110
  protected syncParams: SharedSyncInitOptions | null;
111
111
  protected logger: ILogger;
112
112
  protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
@@ -116,11 +116,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
116
116
  protected connectionManager: ConnectionManager;
117
117
  syncStatus: SyncStatus;
118
118
  broadCastLogger: ILogger;
119
+ protected distributedDB: DBAdapter | null;
119
120
 
120
121
  constructor() {
121
122
  super();
122
123
  this.ports = [];
123
- this.dbAdapter = null;
124
124
  this.syncParams = null;
125
125
  this.logger = createLogger('shared-sync');
126
126
  this.lastConnectOptions = undefined;
@@ -135,29 +135,27 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
135
135
  });
136
136
  });
137
137
 
138
+ // Should be configured once we get params
139
+ this.distributedDB = null;
140
+
138
141
  this.syncStatus = new SyncStatus({});
139
142
  this.broadCastLogger = new BroadcastLogger(this.ports);
140
143
 
141
144
  this.connectionManager = new ConnectionManager({
142
145
  createSyncImplementation: async () => {
143
- return this.portMutex.runExclusive(async () => {
144
- await this.waitForReady();
145
- if (!this.dbAdapter) {
146
- await this.openInternalDB();
147
- }
146
+ await this.waitForReady();
148
147
 
149
- const sync = this.generateStreamingImplementation();
150
- const onDispose = sync.registerListener({
151
- statusChanged: (status) => {
152
- this.updateAllStatuses(status.toJSON());
153
- }
154
- });
155
-
156
- return {
157
- sync,
158
- onDispose
159
- };
148
+ const sync = this.generateStreamingImplementation();
149
+ const onDispose = sync.registerListener({
150
+ statusChanged: (status) => {
151
+ this.updateAllStatuses(status.toJSON());
152
+ }
160
153
  });
154
+
155
+ return {
156
+ sync,
157
+ onDispose
158
+ };
161
159
  },
162
160
  logger: this.logger
163
161
  });
@@ -171,6 +169,32 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
171
169
  return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
172
170
  }
173
171
 
172
+ /**
173
+ * Gets the last client port which we know is safe from unexpected closes.
174
+ */
175
+ protected async getLastWrappedPort(): Promise<WrappedSyncPort | undefined> {
176
+ // Find the last port which is not closing
177
+ return await this.portMutex.runExclusive(() => {
178
+ for (let i = this.ports.length - 1; i >= 0; i--) {
179
+ if (!this.ports[i].isClosing) {
180
+ return this.ports[i];
181
+ }
182
+ }
183
+ return;
184
+ });
185
+ }
186
+
187
+ /**
188
+ * In some very rare cases a specific tab might not respond to requests.
189
+ * This returns a random port which is not closing.
190
+ */
191
+ protected async getRandomWrappedPort(): Promise<WrappedSyncPort | undefined> {
192
+ return await this.portMutex.runExclusive(() => {
193
+ const nonClosingPorts = this.ports.filter((p) => !p.isClosing);
194
+ return nonClosingPorts[Math.floor(Math.random() * nonClosingPorts.length)];
195
+ });
196
+ }
197
+
174
198
  async waitForStatus(status: SyncStatusOptions): Promise<void> {
175
199
  return this.withSyncImplementation(async (sync) => {
176
200
  return sync.waitForStatus(status);
@@ -217,33 +241,60 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
217
241
  async setParams(params: SharedSyncInitOptions) {
218
242
  await this.portMutex.runExclusive(async () => {
219
243
  this.collectActiveSubscriptions();
220
- if (this.syncParams) {
221
- // Cannot modify already existing sync implementation params
222
- // But we can ask for a DB adapter, if required, at this point.
223
-
224
- if (!this.dbAdapter) {
225
- await this.openInternalDB();
226
- }
227
- return;
228
- }
244
+ });
229
245
 
230
- // First time setting params
231
- this.syncParams = params;
232
- if (params.streamOptions?.flags?.broadcastLogs) {
233
- this.logger = this.broadCastLogger;
234
- }
246
+ if (this.syncParams) {
247
+ // Cannot modify already existing sync implementation params
248
+ return;
249
+ }
235
250
 
236
- self.onerror = (event) => {
237
- // Share any uncaught events on the broadcast logger
238
- this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
239
- };
251
+ // First time setting params
252
+ this.syncParams = params;
253
+ if (params.streamOptions?.flags?.broadcastLogs) {
254
+ this.logger = this.broadCastLogger;
255
+ }
240
256
 
241
- if (!this.dbAdapter) {
242
- await this.openInternalDB();
257
+ const lockedAdapter = new LockedAsyncDatabaseAdapter({
258
+ name: params.dbParams.dbFilename,
259
+ openConnection: async () => {
260
+ // Gets a connection from the clients when a new connection is requested.
261
+ const db = await this.openInternalDB();
262
+ db.registerListener({
263
+ closing: () => {
264
+ lockedAdapter.reOpenInternalDB();
265
+ }
266
+ });
267
+ return db;
268
+ },
269
+ logger: this.logger,
270
+ reOpenOnConnectionClosed: true
271
+ });
272
+ this.distributedDB = lockedAdapter;
273
+ await lockedAdapter.init();
274
+
275
+ lockedAdapter.registerListener({
276
+ databaseReOpened: () => {
277
+ // We may have missed some table updates while the database was closed.
278
+ // We can poke the crud in case we missed any updates.
279
+ this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
280
+
281
+ /**
282
+ * FIXME or IMPROVE ME
283
+ * The Rust client implementation stores sync state on the connection level.
284
+ * Reopening the database causes a state machine error which should cause the
285
+ * StreamingSyncImplementation to reconnect. It would be nicer if we could trigger
286
+ * this reconnect earlier.
287
+ * This reconnect is not required for IndexedDB.
288
+ */
243
289
  }
244
-
245
- this.iterateListeners((l) => l.initialized?.());
246
290
  });
291
+
292
+ self.onerror = (event) => {
293
+ // Share any uncaught events on the broadcast logger
294
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
295
+ };
296
+
297
+ this.iterateListeners((l) => l.initialized?.());
247
298
  }
248
299
 
249
300
  async dispose() {
@@ -276,7 +327,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
276
327
  port,
277
328
  clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
278
329
  currentSubscriptions: [],
279
- closeListeners: []
330
+ closeListeners: [],
331
+ isClosing: false
280
332
  } satisfies WrappedSyncPort;
281
333
  this.ports.push(portProvider);
282
334
 
@@ -295,14 +347,17 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
295
347
  * clients.
296
348
  */
297
349
  async removePort(port: WrappedSyncPort) {
350
+ // Ports might be removed faster than we can process them.
351
+ port.isClosing = true;
352
+
298
353
  // Remove the port within a mutex context.
299
354
  // Warns if the port is not found. This should not happen in practice.
300
355
  // We return early if the port is not found.
301
- const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
356
+ return await this.portMutex.runExclusive(async () => {
302
357
  const index = this.ports.findIndex((p) => p == port);
303
358
  if (index < 0) {
304
359
  this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
305
- return {};
360
+ return () => {};
306
361
  }
307
362
 
308
363
  const trackedPort = this.ports[index];
@@ -321,42 +376,15 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
321
376
  }
322
377
  });
323
378
 
324
- const shouldReconnect = !!this.connectionManager.syncStreamImplementation && this.ports.length > 0;
325
- return {
326
- shouldReconnect,
327
- trackedPort
328
- };
329
- });
330
-
331
- if (!trackedPort) {
332
- // We could not find the port to remove
333
- return () => {};
334
- }
335
-
336
- for (const closeListener of trackedPort.closeListeners) {
337
- await closeListener();
338
- }
339
-
340
- if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
341
- // Unconditionally close the connection because the database it's writing to has just been closed.
342
- // The connection has been closed previously, this might throw. We should be able to ignore it.
343
- await this.connectionManager
344
- .disconnect()
345
- .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
346
-
347
- // Clearing the adapter will result in a new one being opened in connect
348
- this.dbAdapter = null;
349
-
350
- if (shouldReconnect) {
351
- await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
379
+ // Close the worker wrapped database connection, we can't accurately rely on this connection
380
+ for (const closeListener of trackedPort.closeListeners) {
381
+ await closeListener();
352
382
  }
353
- }
354
383
 
355
- // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
356
- this.collectActiveSubscriptions();
384
+ this.collectActiveSubscriptions();
357
385
 
358
- // Release proxy
359
- return () => trackedPort.clientProvider[Comlink.releaseProxy]();
386
+ return () => trackedPort.clientProvider[Comlink.releaseProxy]();
387
+ });
360
388
  }
361
389
 
362
390
  triggerCrudUpload() {
@@ -401,11 +429,14 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
401
429
  const syncParams = this.syncParams!;
402
430
  // Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
403
431
  return new WebStreamingSyncImplementation({
404
- adapter: new SqliteBucketStorage(this.dbAdapter!, this.logger),
432
+ adapter: new SqliteBucketStorage(this.distributedDB!, this.logger),
405
433
  remote: new WebRemote(
406
434
  {
407
435
  invalidateCredentials: async () => {
408
- const lastPort = this.ports[this.ports.length - 1];
436
+ const lastPort = await this.getLastWrappedPort();
437
+ if (!lastPort) {
438
+ throw new Error('No client port found to invalidate credentials');
439
+ }
409
440
  try {
410
441
  this.logger.log('calling the last port client provider to invalidate credentials');
411
442
  lastPort.clientProvider.invalidateCredentials();
@@ -414,7 +445,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
414
445
  }
415
446
  },
416
447
  fetchCredentials: async () => {
417
- const lastPort = this.ports[this.ports.length - 1];
448
+ const lastPort = await this.getLastWrappedPort();
449
+ if (!lastPort) {
450
+ throw new Error('No client port found to fetch credentials');
451
+ }
418
452
  return new Promise(async (resolve, reject) => {
419
453
  const abortController = new AbortController();
420
454
  this.fetchCredentialsController = {
@@ -437,7 +471,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
437
471
  this.logger
438
472
  ),
439
473
  uploadCrud: async () => {
440
- const lastPort = this.ports[this.ports.length - 1];
474
+ const lastPort = await this.getLastWrappedPort();
475
+ if (!lastPort) {
476
+ throw new Error('No client port found to upload crud');
477
+ }
441
478
 
442
479
  return new Promise(async (resolve, reject) => {
443
480
  const abortController = new AbortController();
@@ -464,39 +501,91 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
464
501
  });
465
502
  }
466
503
 
504
+ /**
505
+ * Opens a worker wrapped database connection. Using the last connected client port.
506
+ */
467
507
  protected async openInternalDB() {
468
- const lastClient = this.ports[this.ports.length - 1];
469
- if (!lastClient) {
508
+ const client = await this.getRandomWrappedPort();
509
+ if (!client) {
470
510
  // Should not really happen in practice
471
511
  throw new Error(`Could not open DB connection since no client is connected.`);
472
512
  }
473
- const workerPort = await lastClient.clientProvider.getDBWorkerPort();
513
+
514
+ // Fail-safe timeout for opening a database connection.
515
+ const timeout = setTimeout(() => {
516
+ abortController.abort();
517
+ }, 10_000);
518
+
519
+ /**
520
+ * Handle cases where the client might close while opening a connection.
521
+ */
522
+ const abortController = new AbortController();
523
+ const closeListener = () => {
524
+ abortController.abort();
525
+ };
526
+
527
+ const removeCloseListener = () => {
528
+ const index = client.closeListeners.indexOf(closeListener);
529
+ if (index >= 0) {
530
+ client.closeListeners.splice(index, 1);
531
+ }
532
+ };
533
+
534
+ client.closeListeners.push(closeListener);
535
+
536
+ const workerPort = await withAbort({
537
+ action: () => client.clientProvider.getDBWorkerPort(),
538
+ signal: abortController.signal,
539
+ cleanupOnAbort: (port) => {
540
+ port.close();
541
+ }
542
+ }).catch((ex) => {
543
+ removeCloseListener();
544
+ throw ex;
545
+ });
546
+
474
547
  const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
475
548
  const identifier = this.syncParams!.dbParams.dbFilename;
476
- const db = await remote(this.syncParams!.dbParams);
477
- const locked = new LockedAsyncDatabaseAdapter({
478
- name: identifier,
479
- openConnection: async () => {
480
- const wrapped = new WorkerWrappedAsyncDatabaseConnection({
481
- remote,
482
- baseConnection: db,
483
- identifier,
484
- // It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
485
- // that and ensure pending requests are aborted when the tab is closed.
486
- remoteCanCloseUnexpectedly: true
487
- });
488
- lastClient.closeListeners.push(async () => {
489
- this.logger.info('Aborting open connection because associated tab closed.');
490
- await wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
491
- wrapped.markRemoteClosed();
492
- });
493
549
 
494
- return wrapped;
495
- },
496
- logger: this.logger
550
+ /**
551
+ * The open could fail if the tab is closed while we're busy opening the database.
552
+ * This operation is typically executed inside an exclusive portMutex lock.
553
+ * We typically execute the closeListeners using the portMutex in a different context.
554
+ * We can't rely on the closeListeners to abort the operation if the tab is closed.
555
+ */
556
+ const db = await withAbort({
557
+ action: () => remote(this.syncParams!.dbParams),
558
+ signal: abortController.signal,
559
+ cleanupOnAbort: (db) => {
560
+ db.close();
561
+ }
562
+ }).finally(() => {
563
+ // We can remove the close listener here since we no longer need it past this point.
564
+ removeCloseListener();
565
+ });
566
+
567
+ clearTimeout(timeout);
568
+
569
+ const wrapped = new WorkerWrappedAsyncDatabaseConnection({
570
+ remote,
571
+ baseConnection: db,
572
+ identifier,
573
+ // It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
574
+ // that and ensure pending requests are aborted when the tab is closed.
575
+ remoteCanCloseUnexpectedly: true
497
576
  });
498
- await locked.init();
499
- this.dbAdapter = lastClient.db = locked;
577
+ client.closeListeners.push(async () => {
578
+ this.logger.info('Aborting open connection because associated tab closed.');
579
+ /**
580
+ * Don't await this close operation. It might never resolve if the tab is closed.
581
+ * We mark the remote as closed first, this will reject any pending requests.
582
+ * We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
583
+ */
584
+ wrapped.markRemoteClosed();
585
+ wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
586
+ });
587
+
588
+ return wrapped;
500
589
  }
501
590
 
502
591
  /**
@@ -507,17 +596,43 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
507
596
  this.syncStatus = new SyncStatus(status);
508
597
  this.ports.forEach((p) => p.clientProvider.statusChanged(status));
509
598
  }
599
+ }
510
600
 
511
- /**
512
- * A function only used for unit tests which updates the internal
513
- * sync stream client and all tab client's sync status
514
- */
515
- async _testUpdateAllStatuses(status: SyncStatusOptions) {
516
- if (!this.connectionManager.syncStreamImplementation) {
517
- throw new Error('Cannot update status without a sync stream implementation');
601
+ /**
602
+ * Runs the action with an abort controller.
603
+ */
604
+ function withAbort<T>(options: {
605
+ action: () => Promise<T>;
606
+ signal: AbortSignal;
607
+ cleanupOnAbort?: (result: T) => void;
608
+ }): Promise<T> {
609
+ const { action, signal, cleanupOnAbort } = options;
610
+ return new Promise((resolve, reject) => {
611
+ if (signal.aborted) {
612
+ reject(new AbortOperation('Operation aborted by abort controller'));
613
+ return;
518
614
  }
519
- // Only assigning, don't call listeners for this test
520
- this.connectionManager.syncStreamImplementation!.syncStatus = new SyncStatus(status);
521
- this.updateAllStatuses(status);
522
- }
615
+
616
+ function handleAbort() {
617
+ signal.removeEventListener('abort', handleAbort);
618
+ reject(new AbortOperation('Operation aborted by abort controller'));
619
+ }
620
+
621
+ signal.addEventListener('abort', handleAbort, { once: true });
622
+
623
+ function completePromise(action: () => void) {
624
+ signal.removeEventListener('abort', handleAbort);
625
+ action();
626
+ }
627
+
628
+ action()
629
+ .then((data) => {
630
+ // We already rejected due to the abort, allow for cleanup
631
+ if (signal.aborted) {
632
+ return completePromise(() => cleanupOnAbort?.(data));
633
+ }
634
+ completePromise(() => resolve(data));
635
+ })
636
+ .catch((e) => completePromise(() => reject(e)));
637
+ });
523
638
  }
@@ -1,6 +1,6 @@
1
1
  import { createBaseLogger } from '@powersync/common';
2
- import { SharedSyncImplementation } from './SharedSyncImplementation';
3
- import { WorkerClient } from './WorkerClient';
2
+ import { SharedSyncImplementation } from './SharedSyncImplementation.js';
3
+ import { WorkerClient } from './WorkerClient.js';
4
4
 
5
5
  const _self: SharedWorkerGlobalScope = self as any;
6
6
  const logger = createBaseLogger();
@@ -10,5 +10,5 @@ const sharedSyncImplementation = new SharedSyncImplementation();
10
10
 
11
11
  _self.onconnect = async function (event: MessageEvent<string>) {
12
12
  const port = event.ports[0];
13
- await new WorkerClient(sharedSyncImplementation, port).initialize();
13
+ new WorkerClient(sharedSyncImplementation, port);
14
14
  };
@@ -1,13 +1,13 @@
1
+ import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common';
1
2
  import * as Comlink from 'comlink';
3
+ import { getNavigatorLocks } from '../../shared/navigator.js';
2
4
  import {
3
5
  ManualSharedSyncPayload,
4
6
  SharedSyncClientEvent,
5
7
  SharedSyncImplementation,
6
8
  SharedSyncInitOptions,
7
9
  WrappedSyncPort
8
- } from './SharedSyncImplementation';
9
- import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
10
- import { getNavigatorLocks } from '../../shared/navigator';
10
+ } from './SharedSyncImplementation.js';
11
11
 
12
12
  /**
13
13
  * A client to the shared sync worker.
@@ -17,13 +17,13 @@ import { getNavigatorLocks } from '../../shared/navigator';
17
17
  */
18
18
  export class WorkerClient {
19
19
  private resolvedPort: WrappedSyncPort | null = null;
20
+ protected resolvedPortPromise: Promise<WrappedSyncPort> | null = null;
20
21
 
21
22
  constructor(
22
23
  private readonly sync: SharedSyncImplementation,
23
24
  private readonly port: MessagePort
24
- ) {}
25
-
26
- async initialize() {
25
+ ) {
26
+ Comlink.expose(this, this.port);
27
27
  /**
28
28
  * Adds an extra listener which can remove this port
29
29
  * from the list of monitored ports.
@@ -34,9 +34,6 @@ export class WorkerClient {
34
34
  await this.removePort();
35
35
  }
36
36
  });
37
-
38
- this.resolvedPort = await this.sync.addPort(this.port);
39
- Comlink.expose(this, this.port);
40
37
  }
41
38
 
42
39
  private async removePort() {
@@ -59,7 +56,10 @@ export class WorkerClient {
59
56
  * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
60
57
  * it can consider the connection to be closed.
61
58
  */
62
- addLockBasedCloseSignal(name: string) {
59
+ async addLockBasedCloseSignal(name: string) {
60
+ // Only add the port once the lock has been obtained on the client.
61
+ this.resolvedPort = await this.sync.addPort(this.port);
62
+ // Don't await this lock request
63
63
  getNavigatorLocks().request(name, async () => {
64
64
  await this.removePort();
65
65
  });
@@ -99,8 +99,4 @@ export class WorkerClient {
99
99
  disconnect() {
100
100
  return this.sync.disconnect();
101
101
  }
102
-
103
- async _testUpdateAllStatuses(status: SyncStatusOptions) {
104
- return this.sync._testUpdateAllStatuses(status);
105
- }
106
102
  }