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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/1807036ae51c10ee4d23.wasm +0 -0
  2. package/dist/{10072fe45f0a8fab0a0e.wasm → 307d8ce2280e3bae09d5.wasm} +0 -0
  3. package/dist/{6e435e51534839845554.wasm → cd8b9e8f4c87bf81c169.wasm} +0 -0
  4. package/dist/e797080f5ed0b5324166.wasm +0 -0
  5. package/dist/index.umd.js +137 -104
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +137 -110
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +15 -1
  10. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  11. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +2 -2
  12. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
  13. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +2 -2
  14. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
  15. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +2 -2
  16. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
  17. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +2 -2
  18. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
  19. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +20 -23
  20. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  21. package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
  22. package/lib/src/db/PowerSyncDatabase.js +4 -4
  23. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +5 -0
  24. package/lib/src/db/adapters/AsyncDatabaseConnection.js +5 -0
  25. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +6 -1
  26. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +20 -5
  27. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
  28. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +12 -5
  29. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +0 -4
  30. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +3 -8
  31. package/lib/src/worker/sync/MockSyncService.d.ts +2 -0
  32. package/lib/src/worker/sync/MockSyncService.js +3 -0
  33. package/lib/src/worker/sync/MockSyncServiceTypes.d.ts +101 -0
  34. package/lib/src/worker/sync/MockSyncServiceTypes.js +1 -0
  35. package/lib/src/worker/sync/MockSyncServiceWorker.d.ts +56 -0
  36. package/lib/src/worker/sync/MockSyncServiceWorker.js +369 -0
  37. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +6 -11
  38. package/lib/src/worker/sync/SharedSyncImplementation.js +73 -64
  39. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
  40. package/lib/src/worker/sync/WorkerClient.d.ts +1 -3
  41. package/lib/src/worker/sync/WorkerClient.js +3 -27
  42. package/lib/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +3 -3
  44. package/src/db/PowerSyncDatabase.ts +13 -15
  45. package/src/db/adapters/AsyncDatabaseConnection.ts +5 -0
  46. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +22 -5
  47. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +16 -4
  48. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +5 -11
  49. package/src/worker/sync/MockSyncService.ts +3 -0
  50. package/src/worker/sync/MockSyncServiceTypes.ts +71 -0
  51. package/src/worker/sync/MockSyncServiceWorker.ts +406 -0
  52. package/src/worker/sync/SharedSyncImplementation.ts +85 -78
  53. package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
  54. package/src/worker/sync/WorkerClient.ts +4 -30
  55. package/dist/a730f7ca717b02234beb.wasm +0 -0
  56. package/dist/aa2f408d64445fed090e.wasm +0 -0
@@ -89,14 +89,25 @@ export class SharedSyncImplementation extends BaseObserver {
89
89
  /**
90
90
  * Gets the last client port which we know is safe from unexpected closes.
91
91
  */
92
- get lastWrappedPort() {
93
- // Find the last port which is protected from close
94
- for (let i = this.ports.length - 1; i >= 0; i--) {
95
- if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) {
96
- return this.ports[i];
92
+ async getLastWrappedPort() {
93
+ // Find the last port which is not closing
94
+ return await this.portMutex.runExclusive(() => {
95
+ for (let i = this.ports.length - 1; i >= 0; i--) {
96
+ if (!this.ports[i].isClosing) {
97
+ return this.ports[i];
98
+ }
97
99
  }
98
- }
99
- return;
100
+ return;
101
+ });
102
+ }
103
+ /**
104
+ * In some very rare cases a specific tab might not respond to requests.
105
+ * This returns a random port which is not closing.
106
+ */
107
+ async getRandomWrappedPort() {
108
+ return await this.portMutex.runExclusive(() => {
109
+ return this.ports[Math.floor(Math.random() * this.ports.length)];
110
+ });
100
111
  }
101
112
  async waitForStatus(status) {
102
113
  return this.withSyncImplementation(async (sync) => {
@@ -138,39 +149,45 @@ export class SharedSyncImplementation extends BaseObserver {
138
149
  async setParams(params) {
139
150
  await this.portMutex.runExclusive(async () => {
140
151
  this.collectActiveSubscriptions();
141
- if (this.syncParams) {
142
- // Cannot modify already existing sync implementation params
143
- return;
144
- }
145
- // First time setting params
146
- this.syncParams = params;
147
- if (params.streamOptions?.flags?.broadcastLogs) {
148
- this.logger = this.broadCastLogger;
152
+ });
153
+ if (this.syncParams) {
154
+ // Cannot modify already existing sync implementation params
155
+ return;
156
+ }
157
+ // First time setting params
158
+ this.syncParams = params;
159
+ if (params.streamOptions?.flags?.broadcastLogs) {
160
+ this.logger = this.broadCastLogger;
161
+ }
162
+ const lockedAdapter = new LockedAsyncDatabaseAdapter({
163
+ name: params.dbParams.dbFilename,
164
+ openConnection: async () => {
165
+ // Gets a connection from the clients when a new connection is requested.
166
+ const db = await this.openInternalDB();
167
+ db.registerListener({
168
+ closing: () => {
169
+ lockedAdapter.reOpenInternalDB();
170
+ }
171
+ });
172
+ return db;
173
+ },
174
+ logger: this.logger,
175
+ reOpenOnConnectionClosed: true
176
+ });
177
+ this.distributedDB = lockedAdapter;
178
+ await lockedAdapter.init();
179
+ lockedAdapter.registerListener({
180
+ databaseReOpened: () => {
181
+ // We may have missed some table updates while the database was closed.
182
+ // We can poke the crud in case we missed any updates.
183
+ this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
149
184
  }
150
- const lockedAdapter = new LockedAsyncDatabaseAdapter({
151
- name: params.dbParams.dbFilename,
152
- openConnection: async () => {
153
- // Gets a connection from the clients when a new connection is requested.
154
- return await this.openInternalDB();
155
- },
156
- logger: this.logger,
157
- reOpenOnConnectionClosed: true
158
- });
159
- this.distributedDB = lockedAdapter;
160
- await lockedAdapter.init();
161
- lockedAdapter.registerListener({
162
- databaseReOpened: () => {
163
- // We may have missed some table updates while the database was closed.
164
- // We can poke the crud in case we missed any updates.
165
- this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
166
- }
167
- });
168
- self.onerror = (event) => {
169
- // Share any uncaught events on the broadcast logger
170
- this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
171
- };
172
- this.iterateListeners((l) => l.initialized?.());
173
185
  });
186
+ self.onerror = (event) => {
187
+ // Share any uncaught events on the broadcast logger
188
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
189
+ };
190
+ this.iterateListeners((l) => l.initialized?.());
174
191
  }
175
192
  async dispose() {
176
193
  await this.waitForReady();
@@ -200,7 +217,6 @@ export class SharedSyncImplementation extends BaseObserver {
200
217
  clientProvider: Comlink.wrap(port),
201
218
  currentSubscriptions: [],
202
219
  closeListeners: [],
203
- isProtectedFromClose: false,
204
220
  isClosing: false
205
221
  };
206
222
  this.ports.push(portProvider);
@@ -286,7 +302,7 @@ export class SharedSyncImplementation extends BaseObserver {
286
302
  adapter: new SqliteBucketStorage(this.distributedDB, this.logger),
287
303
  remote: new WebRemote({
288
304
  invalidateCredentials: async () => {
289
- const lastPort = this.lastWrappedPort;
305
+ const lastPort = await this.getLastWrappedPort();
290
306
  if (!lastPort) {
291
307
  throw new Error('No client port found to invalidate credentials');
292
308
  }
@@ -299,7 +315,7 @@ export class SharedSyncImplementation extends BaseObserver {
299
315
  }
300
316
  },
301
317
  fetchCredentials: async () => {
302
- const lastPort = this.lastWrappedPort;
318
+ const lastPort = await this.getLastWrappedPort();
303
319
  if (!lastPort) {
304
320
  throw new Error('No client port found to fetch credentials');
305
321
  }
@@ -324,7 +340,7 @@ export class SharedSyncImplementation extends BaseObserver {
324
340
  }
325
341
  }, this.logger),
326
342
  uploadCrud: async () => {
327
- const lastPort = this.lastWrappedPort;
343
+ const lastPort = await this.getLastWrappedPort();
328
344
  if (!lastPort) {
329
345
  throw new Error('No client port found to upload crud');
330
346
  }
@@ -359,11 +375,15 @@ export class SharedSyncImplementation extends BaseObserver {
359
375
  async openInternalDB() {
360
376
  while (true) {
361
377
  try {
362
- const lastClient = this.lastWrappedPort;
363
- if (!lastClient) {
378
+ const client = await this.getRandomWrappedPort();
379
+ if (!client) {
364
380
  // Should not really happen in practice
365
381
  throw new Error(`Could not open DB connection since no client is connected.`);
366
382
  }
383
+ // Fail-safe timeout for opening a database connection.
384
+ const timeout = setTimeout(() => {
385
+ abortController.abort();
386
+ }, 10_000);
367
387
  /**
368
388
  * Handle cases where the client might close while opening a connection.
369
389
  */
@@ -372,13 +392,13 @@ export class SharedSyncImplementation extends BaseObserver {
372
392
  abortController.abort();
373
393
  };
374
394
  const removeCloseListener = () => {
375
- const index = lastClient.closeListeners.indexOf(closeListener);
395
+ const index = client.closeListeners.indexOf(closeListener);
376
396
  if (index >= 0) {
377
- lastClient.closeListeners.splice(index, 1);
397
+ client.closeListeners.splice(index, 1);
378
398
  }
379
399
  };
380
- lastClient.closeListeners.push(closeListener);
381
- const workerPort = await withAbort(() => lastClient.clientProvider.getDBWorkerPort(), abortController.signal).catch((ex) => {
400
+ client.closeListeners.push(closeListener);
401
+ const workerPort = await withAbort(() => client.clientProvider.getDBWorkerPort(), abortController.signal).catch((ex) => {
382
402
  removeCloseListener();
383
403
  throw ex;
384
404
  });
@@ -394,6 +414,7 @@ export class SharedSyncImplementation extends BaseObserver {
394
414
  // We can remove the close listener here since we no longer need it past this point.
395
415
  removeCloseListener();
396
416
  });
417
+ clearTimeout(timeout);
397
418
  const wrapped = new WorkerWrappedAsyncDatabaseConnection({
398
419
  remote,
399
420
  baseConnection: db,
@@ -402,15 +423,15 @@ export class SharedSyncImplementation extends BaseObserver {
402
423
  // that and ensure pending requests are aborted when the tab is closed.
403
424
  remoteCanCloseUnexpectedly: true
404
425
  });
405
- lastClient.closeListeners.push(async () => {
426
+ client.closeListeners.push(async () => {
406
427
  this.logger.info('Aborting open connection because associated tab closed.');
407
428
  /**
408
429
  * Don't await this close operation. It might never resolve if the tab is closed.
409
- * We run the close operation first, before marking the remote as closed. This gives the database some chance
410
- * to close the connection.
430
+ * We mark the remote as closed first, this will reject any pending requests.
431
+ * We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
411
432
  */
412
- wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
413
433
  wrapped.markRemoteClosed();
434
+ wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
414
435
  });
415
436
  return wrapped;
416
437
  }
@@ -428,18 +449,6 @@ export class SharedSyncImplementation extends BaseObserver {
428
449
  this.syncStatus = new SyncStatus(status);
429
450
  this.ports.forEach((p) => p.clientProvider.statusChanged(status));
430
451
  }
431
- /**
432
- * A function only used for unit tests which updates the internal
433
- * sync stream client and all tab client's sync status
434
- */
435
- async _testUpdateAllStatuses(status) {
436
- if (!this.connectionManager.syncStreamImplementation) {
437
- throw new Error('Cannot update status without a sync stream implementation');
438
- }
439
- // Only assigning, don't call listeners for this test
440
- this.connectionManager.syncStreamImplementation.syncStatus = new SyncStatus(status);
441
- this.updateAllStatuses(status);
442
- }
443
452
  }
444
453
  /**
445
454
  * Runs the action with an abort controller.
@@ -7,5 +7,5 @@ logger.useDefaults();
7
7
  const sharedSyncImplementation = new SharedSyncImplementation();
8
8
  _self.onconnect = async function (event) {
9
9
  const port = event.ports[0];
10
- await new WorkerClient(sharedSyncImplementation, port).initialize();
10
+ new WorkerClient(sharedSyncImplementation, port);
11
11
  };
@@ -1,4 +1,4 @@
1
- import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
1
+ import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common';
2
2
  import { SharedSyncImplementation, SharedSyncInitOptions, WrappedSyncPort } from './SharedSyncImplementation';
3
3
  /**
4
4
  * A client to the shared sync worker.
@@ -12,7 +12,6 @@ export declare class WorkerClient {
12
12
  private resolvedPort;
13
13
  protected resolvedPortPromise: Promise<WrappedSyncPort> | null;
14
14
  constructor(sync: SharedSyncImplementation, port: MessagePort);
15
- initialize(): Promise<void>;
16
15
  private removePort;
17
16
  /**
18
17
  * Called by a client after obtaining a lock with a random name.
@@ -29,5 +28,4 @@ export declare class WorkerClient {
29
28
  connect(options?: PowerSyncConnectionOptions): Promise<void>;
30
29
  updateSubscriptions(subscriptions: SubscribedStream[]): void;
31
30
  disconnect(): Promise<void>;
32
- _testUpdateAllStatuses(status: SyncStatusOptions): Promise<void>;
33
31
  }
@@ -16,8 +16,6 @@ export class WorkerClient {
16
16
  this.sync = sync;
17
17
  this.port = port;
18
18
  Comlink.expose(this, this.port);
19
- }
20
- async initialize() {
21
19
  /**
22
20
  * Adds an extra listener which can remove this port
23
21
  * from the list of monitored ports.
@@ -28,15 +26,6 @@ export class WorkerClient {
28
26
  await this.removePort();
29
27
  }
30
28
  });
31
- /**
32
- * Keep a reference to the resolved port promise.
33
- * The init timing is difficult to predict due to the async message passing.
34
- * We only want to use a port if we are know it's been protected from being closed.
35
- * The lock based close signal will be added asynchronously. We need to use the
36
- * added port once the lock is configured.
37
- */
38
- this.resolvedPortPromise = this.sync.addPort(this.port);
39
- this.resolvedPort = await this.resolvedPortPromise;
40
29
  }
41
30
  async removePort() {
42
31
  if (this.resolvedPort) {
@@ -58,19 +47,9 @@ export class WorkerClient {
58
47
  * it can consider the connection to be closed.
59
48
  */
60
49
  async addLockBasedCloseSignal(name) {
61
- if (!this.resolvedPortPromise) {
62
- // The init logic above is actually synchronous, so this should not happen.
63
- this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered');
64
- }
65
- else {
66
- const wrappedPort = await this.resolvedPortPromise;
67
- /**
68
- * The client registered a navigator lock. We now can guarantee detecting if the client has closed.
69
- * E.g. before this point: It's possible some ports might have been created and closed before the
70
- * lock based close signal is added. We should not trust those ports.
71
- */
72
- wrappedPort.isProtectedFromClose = true;
73
- }
50
+ // Only add the port once the lock has been obtained on the client.
51
+ this.resolvedPort = await this.sync.addPort(this.port);
52
+ // Don't await this lock request
74
53
  getNavigatorLocks().request(name, async () => {
75
54
  await this.removePort();
76
55
  });
@@ -102,7 +81,4 @@ export class WorkerClient {
102
81
  disconnect() {
103
82
  return this.sync.disconnect();
104
83
  }
105
- async _testUpdateAllStatuses(status) {
106
- return this.sync._testUpdateAllStatuses(status);
107
- }
108
84
  }