@powersync/web 1.29.1 → 1.31.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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/0b19af1befc07ce338dd.wasm +0 -0
  3. package/dist/2632c3bda9473da74fd5.wasm +0 -0
  4. package/dist/64f5351ba3784bfe2f3e.wasm +0 -0
  5. package/dist/9318ca94aac4d0fe0135.wasm +0 -0
  6. package/dist/index.umd.js +7258 -1847
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/worker/SharedSyncImplementation.umd.js +548 -290
  9. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  10. package/dist/worker/WASQLiteDB.umd.js +192 -126
  11. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  12. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +3 -3
  13. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -1
  14. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +22 -9
  15. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
  16. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +22 -9
  17. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
  18. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +22 -9
  19. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
  20. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +22 -9
  21. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
  22. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js +9 -9
  23. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js.map +1 -1
  24. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +32 -35
  25. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  26. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js +27 -20
  27. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +1 -1
  28. package/lib/package.json +5 -5
  29. package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
  30. package/lib/src/db/PowerSyncDatabase.js +4 -4
  31. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +17 -0
  32. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +109 -19
  33. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
  34. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +14 -7
  35. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +11 -2
  36. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +1 -1
  37. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -2
  38. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -5
  39. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +51 -35
  40. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +18 -8
  41. package/lib/src/worker/sync/SharedSyncImplementation.js +204 -108
  42. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
  43. package/lib/src/worker/sync/WorkerClient.d.ts +4 -5
  44. package/lib/src/worker/sync/WorkerClient.js +7 -9
  45. package/lib/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +6 -6
  47. package/src/db/PowerSyncDatabase.ts +13 -15
  48. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +126 -25
  49. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +18 -6
  50. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +11 -3
  51. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +3 -3
  52. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +65 -47
  53. package/src/worker/db/WASQLiteDB.worker.ts +0 -1
  54. package/src/worker/sync/SharedSyncImplementation.ts +234 -119
  55. package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
  56. package/src/worker/sync/WorkerClient.ts +9 -13
  57. package/dist/10072fe45f0a8fab0a0e.wasm +0 -0
  58. package/dist/6e435e51534839845554.wasm +0 -0
  59. package/dist/a730f7ca717b02234beb.wasm +0 -0
  60. package/dist/aa2f408d64445fed090e.wasm +0 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BaseObserver,
3
+ ConnectionClosedError,
3
4
  DBAdapter,
4
5
  DBAdapterListener,
5
6
  DBGetUtils,
@@ -10,7 +11,7 @@ import {
10
11
  createLogger,
11
12
  type ILogger
12
13
  } from '@powersync/common';
13
- import { getNavigatorLocks } from '../..//shared/navigator';
14
+ import { getNavigatorLocks } from '../../shared/navigator';
14
15
  import { AsyncDatabaseConnection } from './AsyncDatabaseConnection';
15
16
  import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
16
17
  import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
@@ -26,10 +27,16 @@ export interface LockedAsyncDatabaseAdapterOptions {
26
27
  openConnection: () => Promise<AsyncDatabaseConnection>;
27
28
  debugMode?: boolean;
28
29
  logger?: ILogger;
30
+ defaultLockTimeoutMs?: number;
31
+ reOpenOnConnectionClosed?: boolean;
29
32
  }
30
33
 
31
34
  export type LockedAsyncDatabaseAdapterListener = DBAdapterListener & {
32
35
  initialized?: () => void;
36
+ /**
37
+ * Fired when the database is re-opened after being closed.
38
+ */
39
+ databaseReOpened?: () => void;
33
40
  };
34
41
 
35
42
  /**
@@ -51,6 +58,7 @@ export class LockedAsyncDatabaseAdapter
51
58
  private _config: ResolvedWebSQLOpenOptions | null = null;
52
59
  protected pendingAbortControllers: Set<AbortController>;
53
60
  protected requiresHolds: boolean | null;
61
+ protected databaseOpenPromise: Promise<void> | null = null;
54
62
 
55
63
  closing: boolean;
56
64
  closed: boolean;
@@ -105,16 +113,72 @@ export class LockedAsyncDatabaseAdapter
105
113
  return this.initPromise;
106
114
  }
107
115
 
116
+ protected async openInternalDB() {
117
+ /**
118
+ * Execute opening of the db in a lock in order not to interfere with other operations.
119
+ */
120
+ return this._acquireLock(async () => {
121
+ // Dispose any previous table change listener.
122
+ this._disposeTableChangeListener?.();
123
+ this._disposeTableChangeListener = null;
124
+ this._db?.close().catch((ex) => this.logger.warn(`Error closing database before opening new instance`, ex));
125
+ const isReOpen = !!this._db;
126
+ this._db = null;
127
+
128
+ this._db = await this.options.openConnection();
129
+ await this._db.init();
130
+ this._config = await this._db.getConfig();
131
+ await this.registerOnChangeListener(this._db);
132
+ if (isReOpen) {
133
+ this.iterateListeners((cb) => cb.databaseReOpened?.());
134
+ }
135
+ /**
136
+ * This is only required for the long-lived shared IndexedDB connections.
137
+ */
138
+ this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
139
+ });
140
+ }
141
+
142
+ protected _reOpen() {
143
+ this.databaseOpenPromise = this.openInternalDB().finally(() => {
144
+ this.databaseOpenPromise = null;
145
+ });
146
+ return this.databaseOpenPromise;
147
+ }
148
+
149
+ /**
150
+ * Re-opens the underlying database.
151
+ * Returns a pending operation if one is already in progress.
152
+ */
153
+ async reOpenInternalDB(): Promise<void> {
154
+ if (!this.options.reOpenOnConnectionClosed) {
155
+ throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
156
+ }
157
+ if (this.databaseOpenPromise) {
158
+ return this.databaseOpenPromise;
159
+ }
160
+ return this._reOpen();
161
+ }
162
+
108
163
  protected async _init() {
109
- this._db = await this.options.openConnection();
110
- await this._db.init();
111
- this._config = await this._db.getConfig();
112
- await this.registerOnChangeListener(this._db);
113
- this.iterateListeners((cb) => cb.initialized?.());
114
164
  /**
115
- * This is only required for the long-lived shared IndexedDB connections.
165
+ * For OPFS, we can see this open call sometimes fail due to NoModificationAllowedError.
166
+ * We should be able to recover from this by re-opening the database.
116
167
  */
117
- this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
168
+ const maxAttempts = 3;
169
+ for (let count = 0; count < maxAttempts; count++) {
170
+ try {
171
+ await this.openInternalDB();
172
+ break;
173
+ } catch (ex) {
174
+ if (count == maxAttempts - 1) {
175
+ throw ex;
176
+ }
177
+ this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex);
178
+ await new Promise((resolve) => setTimeout(resolve, 1000));
179
+ }
180
+ }
181
+ this.iterateListeners((cb) => cb.initialized?.());
118
182
  }
119
183
 
120
184
  getConfiguration(): ResolvedWebSQLOpenOptions {
@@ -170,7 +234,14 @@ export class LockedAsyncDatabaseAdapter
170
234
  */
171
235
  async close() {
172
236
  this.closing = true;
173
- this._disposeTableChangeListener?.();
237
+ /**
238
+ * Note that we obtain a reference to the callback to avoid calling the callback with `this` as the context.
239
+ * This is to avoid Comlink attempting to clone `this` when calling the method.
240
+ */
241
+ const dispose = this._disposeTableChangeListener;
242
+ if (dispose) {
243
+ dispose();
244
+ }
174
245
  this.pendingAbortControllers.forEach((controller) => controller.abort('Closed'));
175
246
  await this.baseDB?.close?.();
176
247
  this.closed = true;
@@ -196,7 +267,7 @@ export class LockedAsyncDatabaseAdapter
196
267
  return this.acquireLock(
197
268
  async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
198
269
  {
199
- timeoutMs: options?.timeoutMs
270
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
200
271
  }
201
272
  );
202
273
  }
@@ -206,23 +277,20 @@ export class LockedAsyncDatabaseAdapter
206
277
  return this.acquireLock(
207
278
  async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
208
279
  {
209
- timeoutMs: options?.timeoutMs
280
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
210
281
  }
211
282
  );
212
283
  }
213
284
 
214
- protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
215
- await this.waitForInitialized();
216
-
285
+ protected async _acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
217
286
  if (this.closing) {
218
287
  throw new Error(`Cannot acquire lock, the database is closing`);
219
288
  }
220
-
221
289
  const abortController = new AbortController();
222
290
  this.pendingAbortControllers.add(abortController);
223
291
  const { timeoutMs } = options ?? {};
224
292
 
225
- const timoutId = timeoutMs
293
+ const timeoutId = timeoutMs
226
294
  ? setTimeout(() => {
227
295
  abortController.abort(`Timeout after ${timeoutMs}ms`);
228
296
  this.pendingAbortControllers.delete(abortController);
@@ -234,19 +302,52 @@ export class LockedAsyncDatabaseAdapter
234
302
  { signal: abortController.signal },
235
303
  async () => {
236
304
  this.pendingAbortControllers.delete(abortController);
237
- if (timoutId) {
238
- clearTimeout(timoutId);
305
+ if (timeoutId) {
306
+ clearTimeout(timeoutId);
239
307
  }
240
- const holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
241
- try {
242
- return await callback();
243
- } finally {
244
- if (holdId) {
245
- await this.baseDB.releaseHold(holdId);
308
+ return await callback();
309
+ }
310
+ );
311
+ }
312
+
313
+ protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
314
+ await this.waitForInitialized();
315
+
316
+ // The database is being opened in the background. Wait for it here.
317
+ if (this.databaseOpenPromise) {
318
+ await this.databaseOpenPromise;
319
+ }
320
+
321
+ return this._acquireLock(async () => {
322
+ let holdId: string | null = null;
323
+ try {
324
+ /**
325
+ * We can't await this since it uses the same lock as we're in now.
326
+ * If there is a pending open, this call will throw.
327
+ * If there is no pending open, but there is also no database - the open
328
+ * might have failed. We need to re-open the database.
329
+ */
330
+ if (this.databaseOpenPromise || !this._db) {
331
+ throw new ConnectionClosedError('Connection is busy re-opening');
332
+ }
333
+
334
+ holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
335
+ return await callback();
336
+ } catch (ex) {
337
+ if (ConnectionClosedError.MATCHES(ex)) {
338
+ if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
339
+ // Immediately re-open the database. We need to miss as little table updates as possible.
340
+ // Note, don't await this since it uses the same lock as we're in now.
341
+ this.reOpenInternalDB();
246
342
  }
247
343
  }
344
+ throw ex;
345
+ } finally {
346
+ if (holdId) {
347
+ await this.baseDB.releaseHold(holdId);
348
+ }
248
349
  }
249
- );
350
+ }, options);
250
351
  }
251
352
 
252
353
  async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
@@ -1,3 +1,4 @@
1
+ import { BaseObserver, ConnectionClosedError } from '@powersync/common';
1
2
  import * as Comlink from 'comlink';
2
3
  import {
3
4
  AsyncDatabaseConnection,
@@ -23,17 +24,23 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
23
24
  onClose?: () => void;
24
25
  };
25
26
 
27
+ export type WorkerWrappedAsyncDatabaseConnectionListener = {
28
+ closing: () => void;
29
+ };
26
30
  /**
27
31
  * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
28
32
  * functions for worker listeners.
29
33
  */
30
34
  export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions>
35
+ extends BaseObserver<WorkerWrappedAsyncDatabaseConnectionListener>
31
36
  implements AsyncDatabaseConnection
32
37
  {
33
38
  protected lockAbortController = new AbortController();
34
39
  protected notifyRemoteClosed: AbortController | undefined;
35
40
 
36
41
  constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
42
+ super();
43
+
37
44
  if (options.remoteCanCloseUnexpectedly) {
38
45
  this.notifyRemoteClosed = new AbortController();
39
46
  }
@@ -72,18 +79,21 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
72
79
  return this.withRemote(() => this.baseConnection.isAutoCommit());
73
80
  }
74
81
 
75
- private withRemote<T>(workerPromise: () => Promise<T>): Promise<T> {
82
+ private withRemote<T>(workerPromise: () => Promise<T>, fireActionOnAbort = false): Promise<T> {
76
83
  const controller = this.notifyRemoteClosed;
77
84
  if (controller) {
78
85
  return new Promise((resolve, reject) => {
79
86
  if (controller.signal.aborted) {
80
- reject(new Error('Called operation on closed remote'));
81
- // Don't run the operation if we're going to reject
82
- return;
87
+ reject(new ConnectionClosedError('Called operation on closed remote'));
88
+ if (!fireActionOnAbort) {
89
+ // Don't run the operation if we're going to reject
90
+ // We might want to fire-and-forget the operation in some cases (like a close operation)
91
+ return;
92
+ }
83
93
  }
84
94
 
85
95
  function handleAbort() {
86
- reject(new Error('Remote peer closed with request in flight'));
96
+ reject(new ConnectionClosedError('Remote peer closed with request in flight'));
87
97
  }
88
98
 
89
99
  function completePromise(action: () => void) {
@@ -164,10 +174,12 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
164
174
  // Abort any pending lock requests.
165
175
  this.lockAbortController.abort();
166
176
  try {
167
- await this.withRemote(() => this.baseConnection.close());
177
+ // fire and forget the close operation
178
+ await this.withRemote(() => this.baseConnection.close(), true);
168
179
  } finally {
169
180
  this.options.remote[Comlink.releaseProxy]();
170
181
  this.options.onClose?.();
182
+ this.iterateListeners((l) => l.closing?.());
171
183
  }
172
184
  }
173
185
 
@@ -3,7 +3,6 @@ import { BaseObserver, BatchedUpdateNotification } from '@powersync/common';
3
3
  import { Mutex } from 'async-mutex';
4
4
  import { AsyncDatabaseConnection, OnTableChangeCallback, ProxiedQueryResult } from '../AsyncDatabaseConnection';
5
5
  import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory';
6
-
7
6
  /**
8
7
  * List of currently tested virtual filesystems
9
8
  */
@@ -126,9 +125,10 @@ export const DEFAULT_MODULE_FACTORIES = {
126
125
  }
127
126
  // @ts-expect-error The types for this static method are missing upstream
128
127
  const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js');
128
+ const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module);
129
129
  return {
130
130
  module,
131
- vfs: await OPFSCoopSyncVFS.create(options.dbFileName, module)
131
+ vfs
132
132
  };
133
133
  }
134
134
  };
@@ -387,7 +387,15 @@ export class WASqliteConnection
387
387
 
388
388
  async close() {
389
389
  this.broadcastChannel?.close();
390
- await this.sqliteAPI.close(this.dbP);
390
+ await this.acquireExecuteLock(async () => {
391
+ /**
392
+ * Running the close operation inside the same execute mutex prevents errors like:
393
+ * ```
394
+ * unable to close due to unfinalized statements or unfinished backups
395
+ * ```
396
+ */
397
+ await this.sqliteAPI.close(this.dbP);
398
+ });
391
399
  }
392
400
 
393
401
  async registerOnTableChange(callback: OnTableChangeCallback) {
@@ -1,17 +1,17 @@
1
- import { type ILogLevel, DBAdapter } from '@powersync/common';
1
+ import { DBAdapter, type ILogLevel } from '@powersync/common';
2
2
  import * as Comlink from 'comlink';
3
3
  import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database';
4
4
  import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory';
5
5
  import { AsyncDatabaseConnection, OpenAsyncDatabaseConnection } from '../AsyncDatabaseConnection';
6
6
  import { LockedAsyncDatabaseAdapter } from '../LockedAsyncDatabaseAdapter';
7
+ import { WorkerWrappedAsyncDatabaseConnection } from '../WorkerWrappedAsyncDatabaseConnection';
7
8
  import {
8
9
  DEFAULT_CACHE_SIZE_KB,
9
10
  ResolvedWebSQLOpenOptions,
10
11
  TemporaryStorageOption,
11
12
  WebSQLOpenFactoryOptions
12
13
  } from '../web-sql-flags';
13
- import { WorkerWrappedAsyncDatabaseConnection } from '../WorkerWrappedAsyncDatabaseConnection';
14
- import { WASqliteConnection, WASQLiteVFS } from './WASQLiteConnection';
14
+ import { WASQLiteVFS, WASqliteConnection } from './WASQLiteConnection';
15
15
 
16
16
  export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions {
17
17
  vfs?: WASQLiteVFS;
@@ -2,20 +2,19 @@ import {
2
2
  PowerSyncConnectionOptions,
3
3
  PowerSyncCredentials,
4
4
  SubscribedStream,
5
- SyncStatus,
6
5
  SyncStatusOptions
7
6
  } from '@powersync/common';
8
7
  import * as Comlink from 'comlink';
8
+ import { getNavigatorLocks } from '../../shared/navigator';
9
9
  import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
10
10
  import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
11
- import { DEFAULT_CACHE_SIZE_KB, resolveWebSQLFlags, TemporaryStorageOption } from '../adapters/web-sql-flags';
11
+ import { WorkerClient } from '../../worker/sync/WorkerClient';
12
12
  import { WebDBAdapter } from '../adapters/WebDBAdapter';
13
+ import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption, resolveWebSQLFlags } from '../adapters/web-sql-flags';
13
14
  import {
14
15
  WebStreamingSyncImplementation,
15
16
  WebStreamingSyncImplementationOptions
16
17
  } from './WebStreamingSyncImplementation';
17
- import { WorkerClient } from '../../worker/sync/WorkerClient';
18
- import { getNavigatorLocks } from '../../shared/navigator';
19
18
 
20
19
  /**
21
20
  * The shared worker will trigger methods on this side of the message port
@@ -146,7 +145,25 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
146
145
  ).port;
147
146
  }
148
147
 
148
+ /**
149
+ * Pass along any sync status updates to this listener
150
+ */
151
+ this.clientProvider = new SharedSyncClientProvider(
152
+ this.webOptions,
153
+ (status) => {
154
+ this.updateSyncStatus(status);
155
+ },
156
+ options.db
157
+ );
158
+
149
159
  this.syncManager = Comlink.wrap<WorkerClient>(this.messagePort);
160
+ /**
161
+ * The sync worker will call this client provider when it needs
162
+ * to fetch credentials or upload data.
163
+ * This performs bi-directional method calling.
164
+ */
165
+ Comlink.expose(this.clientProvider, this.messagePort);
166
+
150
167
  this.syncManager.setLogLevel(this.logger.getLevel());
151
168
 
152
169
  this.triggerCrudUpload = this.syncManager.triggerCrudUpload;
@@ -157,10 +174,49 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
157
174
  * DB worker, but a port to the DB worker can be transferred to the
158
175
  * sync worker.
159
176
  */
177
+
178
+ this.isInitialized = this._init();
179
+ }
180
+
181
+ protected async _init() {
182
+ /**
183
+ * The general flow of initialization is:
184
+ * - The client requests a unique navigator lock.
185
+ * - Once the lock is acquired, we register the lock with the shared worker.
186
+ * - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
187
+ * - Once the shared worker knows the client's lock, we can guarentee that the shared worker will detect if the client has been closed.
188
+ * - This makes the client safe for the shared worker to use.
189
+ * - The client is only added to the SharedSyncImplementation once the lock has been registered.
190
+ * This ensures we don't ever keep track of dead clients (tabs that closed before the lock was registered).
191
+ * - The client side lock is held until the client is disposed.
192
+ * - We resolve the top-level promise after the lock has been registered with the shared worker.
193
+ * - The client sends the params to the shared worker after locks have been registered.
194
+ */
195
+ await new Promise<void>((resolve) => {
196
+ // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
197
+ // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
198
+ // to free resources associated with this tab.
199
+ // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
200
+ getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
201
+ if (this.abortOnClose.signal.aborted) {
202
+ return;
203
+ }
204
+ // Awaiting here ensures the worker is waiting for the lock
205
+ await this.syncManager.addLockBasedCloseSignal(lock!.name);
206
+
207
+ // The lock has been registered, we can continue with the initialization
208
+ resolve();
209
+
210
+ await new Promise<void>((r) => {
211
+ this.abortOnClose.signal.onabort = () => r();
212
+ });
213
+ });
214
+ });
215
+
160
216
  const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
161
217
  const flags = { ...this.webOptions.flags, workers: undefined };
162
218
 
163
- this.isInitialized = this.syncManager.setParams(
219
+ await this.syncManager.setParams(
164
220
  {
165
221
  dbParams: this.dbAdapter.getConfiguration(),
166
222
  streamOptions: {
@@ -170,39 +226,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
170
226
  flags: flags
171
227
  }
172
228
  },
173
- options.subscriptions
229
+ this.options.subscriptions
174
230
  );
175
-
176
- /**
177
- * Pass along any sync status updates to this listener
178
- */
179
- this.clientProvider = new SharedSyncClientProvider(
180
- this.webOptions,
181
- (status) => {
182
- this.iterateListeners((l) => this.updateSyncStatus(status));
183
- },
184
- options.db
185
- );
186
-
187
- /**
188
- * The sync worker will call this client provider when it needs
189
- * to fetch credentials or upload data.
190
- * This performs bi-directional method calling.
191
- */
192
- Comlink.expose(this.clientProvider, this.messagePort);
193
-
194
- // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
195
- // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
196
- // to free resources associated with this tab.
197
- getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
198
- if (!this.abortOnClose.signal.aborted) {
199
- this.syncManager.addLockBasedCloseSignal(lock!.name);
200
-
201
- await new Promise<void>((r) => {
202
- this.abortOnClose.signal.onabort = () => r();
203
- });
204
- }
205
- });
206
231
  }
207
232
 
208
233
  /**
@@ -231,8 +256,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
231
256
  async dispose(): Promise<void> {
232
257
  await this.waitForReady();
233
258
 
234
- await super.dispose();
235
-
236
259
  await new Promise<void>((resolve) => {
237
260
  // Listen for the close acknowledgment from the worker
238
261
  this.messagePort.addEventListener('message', (event) => {
@@ -249,6 +272,9 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
249
272
  };
250
273
  this.messagePort.postMessage(closeMessagePayload);
251
274
  });
275
+
276
+ await super.dispose();
277
+
252
278
  this.abortOnClose.abort();
253
279
 
254
280
  // Release the proxy
@@ -263,12 +289,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
263
289
  updateSubscriptions(subscriptions: SubscribedStream[]): void {
264
290
  this.syncManager.updateSubscriptions(subscriptions);
265
291
  }
266
-
267
- /**
268
- * Used in tests to force a connection states
269
- */
270
- private async _testUpdateStatus(status: SyncStatus) {
271
- await this.isInitialized;
272
- return this.syncManager._testUpdateAllStatuses(status.toJSON());
273
- }
274
292
  }
@@ -17,7 +17,6 @@ const logger = createLogger('db-worker');
17
17
 
18
18
  const DBMap = new Map<string, SharedDBWorkerConnection>();
19
19
  const OPEN_DB_LOCK = 'open-wasqlite-db';
20
-
21
20
  let nextClientId = 1;
22
21
 
23
22
  const openDBShared = async (options: WorkerDBOpenerOptions): Promise<AsyncDatabaseConnection> => {