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

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 (62) 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 +2551 -161
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +2020 -148
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +1824 -9
  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_AccessHandlePoolVFS_js.umd.js +0 -1203
  20. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js.map +1 -1
  21. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +20 -1226
  22. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  23. package/lib/package.json +2 -2
  24. package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
  25. package/lib/src/db/PowerSyncDatabase.js +4 -4
  26. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +5 -0
  27. package/lib/src/db/adapters/AsyncDatabaseConnection.js +5 -0
  28. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +10 -2
  29. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +72 -33
  30. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
  31. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +12 -5
  32. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +3 -3
  33. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +1 -1
  34. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -2
  35. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +0 -4
  36. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +3 -8
  37. package/lib/src/worker/db/WASQLiteDB.worker.js +0 -1
  38. package/lib/src/worker/db/opfs.d.ts +96 -0
  39. package/lib/src/worker/db/opfs.js +582 -0
  40. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +6 -11
  41. package/lib/src/worker/sync/SharedSyncImplementation.js +95 -67
  42. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
  43. package/lib/src/worker/sync/WorkerClient.d.ts +1 -3
  44. package/lib/src/worker/sync/WorkerClient.js +3 -27
  45. package/lib/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +5 -5
  47. package/src/db/PowerSyncDatabase.ts +13 -15
  48. package/src/db/adapters/AsyncDatabaseConnection.ts +5 -0
  49. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +91 -51
  50. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +16 -4
  51. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +3 -4
  52. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +3 -3
  53. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +5 -11
  54. package/src/worker/db/WASQLiteDB.worker.ts +0 -2
  55. package/src/worker/db/opfs.ts +623 -0
  56. package/src/worker/sync/SharedSyncImplementation.ts +106 -78
  57. package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
  58. package/src/worker/sync/WorkerClient.ts +4 -30
  59. package/dist/a730f7ca717b02234beb.wasm +0 -0
  60. package/dist/aa2f408d64445fed090e.wasm +0 -0
  61. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js +0 -1813
  62. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +0 -1
@@ -76,10 +76,6 @@ export type WrappedSyncPort = {
76
76
  db?: DBAdapter;
77
77
  currentSubscriptions: SubscribedStream[];
78
78
  closeListeners: (() => void | Promise<void>)[];
79
- /**
80
- * If we can use Navigator locks to detect if the client has closed.
81
- */
82
- isProtectedFromClose: boolean;
83
79
  isClosing: boolean;
84
80
  };
85
81
 
@@ -176,14 +172,26 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
176
172
  /**
177
173
  * Gets the last client port which we know is safe from unexpected closes.
178
174
  */
179
- protected get lastWrappedPort(): WrappedSyncPort | undefined {
180
- // Find the last port which is protected from close
181
- for (let i = this.ports.length - 1; i >= 0; i--) {
182
- if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) {
183
- return this.ports[i];
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
+ }
184
182
  }
185
- }
186
- return;
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
+ return this.ports[Math.floor(Math.random() * this.ports.length)];
194
+ });
187
195
  }
188
196
 
189
197
  async waitForStatus(status: SyncStatusOptions): Promise<void> {
@@ -232,44 +240,51 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
232
240
  async setParams(params: SharedSyncInitOptions) {
233
241
  await this.portMutex.runExclusive(async () => {
234
242
  this.collectActiveSubscriptions();
235
- if (this.syncParams) {
236
- // Cannot modify already existing sync implementation params
237
- return;
238
- }
239
-
240
- // First time setting params
241
- this.syncParams = params;
242
- if (params.streamOptions?.flags?.broadcastLogs) {
243
- this.logger = this.broadCastLogger;
244
- }
243
+ });
245
244
 
246
- const lockedAdapter = new LockedAsyncDatabaseAdapter({
247
- name: params.dbParams.dbFilename,
248
- openConnection: async () => {
249
- // Gets a connection from the clients when a new connection is requested.
250
- return await this.openInternalDB();
251
- },
252
- logger: this.logger,
253
- reOpenOnConnectionClosed: true
254
- });
255
- this.distributedDB = lockedAdapter;
256
- await lockedAdapter.init();
257
-
258
- lockedAdapter.registerListener({
259
- databaseReOpened: () => {
260
- // We may have missed some table updates while the database was closed.
261
- // We can poke the crud in case we missed any updates.
262
- this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
263
- }
264
- });
245
+ if (this.syncParams) {
246
+ // Cannot modify already existing sync implementation params
247
+ return;
248
+ }
265
249
 
266
- self.onerror = (event) => {
267
- // Share any uncaught events on the broadcast logger
268
- this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
269
- };
250
+ // First time setting params
251
+ this.syncParams = params;
252
+ if (params.streamOptions?.flags?.broadcastLogs) {
253
+ this.logger = this.broadCastLogger;
254
+ }
270
255
 
271
- this.iterateListeners((l) => l.initialized?.());
256
+ const lockedAdapter = new LockedAsyncDatabaseAdapter({
257
+ name: params.dbParams.dbFilename,
258
+ openConnection: async () => {
259
+ // Gets a connection from the clients when a new connection is requested.
260
+ const db = await this.openInternalDB();
261
+ db.registerListener({
262
+ closing: () => {
263
+ lockedAdapter.reOpenInternalDB();
264
+ }
265
+ });
266
+ return db;
267
+ },
268
+ logger: this.logger,
269
+ reOpenOnConnectionClosed: true
272
270
  });
271
+ this.distributedDB = lockedAdapter;
272
+ await lockedAdapter.init();
273
+
274
+ lockedAdapter.registerListener({
275
+ databaseReOpened: () => {
276
+ // We may have missed some table updates while the database was closed.
277
+ // We can poke the crud in case we missed any updates.
278
+ this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
279
+ }
280
+ });
281
+
282
+ self.onerror = (event) => {
283
+ // Share any uncaught events on the broadcast logger
284
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
285
+ };
286
+
287
+ this.iterateListeners((l) => l.initialized?.());
273
288
  }
274
289
 
275
290
  async dispose() {
@@ -303,7 +318,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
303
318
  clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
304
319
  currentSubscriptions: [],
305
320
  closeListeners: [],
306
- isProtectedFromClose: false,
307
321
  isClosing: false
308
322
  } satisfies WrappedSyncPort;
309
323
  this.ports.push(portProvider);
@@ -409,7 +423,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
409
423
  remote: new WebRemote(
410
424
  {
411
425
  invalidateCredentials: async () => {
412
- const lastPort = this.lastWrappedPort;
426
+ const lastPort = await this.getLastWrappedPort();
413
427
  if (!lastPort) {
414
428
  throw new Error('No client port found to invalidate credentials');
415
429
  }
@@ -421,7 +435,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
421
435
  }
422
436
  },
423
437
  fetchCredentials: async () => {
424
- const lastPort = this.lastWrappedPort;
438
+ const lastPort = await this.getLastWrappedPort();
425
439
  if (!lastPort) {
426
440
  throw new Error('No client port found to fetch credentials');
427
441
  }
@@ -447,7 +461,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
447
461
  this.logger
448
462
  ),
449
463
  uploadCrud: async () => {
450
- const lastPort = this.lastWrappedPort;
464
+ const lastPort = await this.getLastWrappedPort();
451
465
  if (!lastPort) {
452
466
  throw new Error('No client port found to upload crud');
453
467
  }
@@ -483,12 +497,17 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
483
497
  protected async openInternalDB() {
484
498
  while (true) {
485
499
  try {
486
- const lastClient = this.lastWrappedPort;
487
- if (!lastClient) {
500
+ const client = await this.getRandomWrappedPort();
501
+ if (!client) {
488
502
  // Should not really happen in practice
489
503
  throw new Error(`Could not open DB connection since no client is connected.`);
490
504
  }
491
505
 
506
+ // Fail-safe timeout for opening a database connection.
507
+ const timeout = setTimeout(() => {
508
+ abortController.abort();
509
+ }, 10_000);
510
+
492
511
  /**
493
512
  * Handle cases where the client might close while opening a connection.
494
513
  */
@@ -498,18 +517,21 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
498
517
  };
499
518
 
500
519
  const removeCloseListener = () => {
501
- const index = lastClient.closeListeners.indexOf(closeListener);
520
+ const index = client.closeListeners.indexOf(closeListener);
502
521
  if (index >= 0) {
503
- lastClient.closeListeners.splice(index, 1);
522
+ client.closeListeners.splice(index, 1);
504
523
  }
505
524
  };
506
525
 
507
- lastClient.closeListeners.push(closeListener);
526
+ client.closeListeners.push(closeListener);
508
527
 
509
- const workerPort = await withAbort(
510
- () => lastClient.clientProvider.getDBWorkerPort(),
511
- abortController.signal
512
- ).catch((ex) => {
528
+ const workerPort = await withAbort({
529
+ action: () => client.clientProvider.getDBWorkerPort(),
530
+ signal: abortController.signal,
531
+ cleanupOnAbort: (port) => {
532
+ port.close();
533
+ }
534
+ }).catch((ex) => {
513
535
  removeCloseListener();
514
536
  throw ex;
515
537
  });
@@ -523,11 +545,19 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
523
545
  * We typically execute the closeListeners using the portMutex in a different context.
524
546
  * We can't rely on the closeListeners to abort the operation if the tab is closed.
525
547
  */
526
- const db = await withAbort(() => remote(this.syncParams!.dbParams), abortController.signal).finally(() => {
548
+ const db = await withAbort({
549
+ action: () => remote(this.syncParams!.dbParams),
550
+ signal: abortController.signal,
551
+ cleanupOnAbort: (db) => {
552
+ db.close();
553
+ }
554
+ }).finally(() => {
527
555
  // We can remove the close listener here since we no longer need it past this point.
528
556
  removeCloseListener();
529
557
  });
530
558
 
559
+ clearTimeout(timeout);
560
+
531
561
  const wrapped = new WorkerWrappedAsyncDatabaseConnection({
532
562
  remote,
533
563
  baseConnection: db,
@@ -536,15 +566,15 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
536
566
  // that and ensure pending requests are aborted when the tab is closed.
537
567
  remoteCanCloseUnexpectedly: true
538
568
  });
539
- lastClient.closeListeners.push(async () => {
569
+ client.closeListeners.push(async () => {
540
570
  this.logger.info('Aborting open connection because associated tab closed.');
541
571
  /**
542
572
  * Don't await this close operation. It might never resolve if the tab is closed.
543
- * We run the close operation first, before marking the remote as closed. This gives the database some chance
544
- * to close the connection.
573
+ * We mark the remote as closed first, this will reject any pending requests.
574
+ * We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
545
575
  */
546
- wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
547
576
  wrapped.markRemoteClosed();
577
+ wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
548
578
  });
549
579
 
550
580
  return wrapped;
@@ -563,25 +593,17 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
563
593
  this.syncStatus = new SyncStatus(status);
564
594
  this.ports.forEach((p) => p.clientProvider.statusChanged(status));
565
595
  }
566
-
567
- /**
568
- * A function only used for unit tests which updates the internal
569
- * sync stream client and all tab client's sync status
570
- */
571
- async _testUpdateAllStatuses(status: SyncStatusOptions) {
572
- if (!this.connectionManager.syncStreamImplementation) {
573
- throw new Error('Cannot update status without a sync stream implementation');
574
- }
575
- // Only assigning, don't call listeners for this test
576
- this.connectionManager.syncStreamImplementation!.syncStatus = new SyncStatus(status);
577
- this.updateAllStatuses(status);
578
- }
579
596
  }
580
597
 
581
598
  /**
582
599
  * Runs the action with an abort controller.
583
600
  */
584
- function withAbort<T>(action: () => Promise<T>, signal: AbortSignal): Promise<T> {
601
+ function withAbort<T>(options: {
602
+ action: () => Promise<T>;
603
+ signal: AbortSignal;
604
+ cleanupOnAbort?: (result: T) => void;
605
+ }): Promise<T> {
606
+ const { action, signal, cleanupOnAbort } = options;
585
607
  return new Promise((resolve, reject) => {
586
608
  if (signal.aborted) {
587
609
  reject(new AbortOperation('Operation aborted by abort controller'));
@@ -601,7 +623,13 @@ function withAbort<T>(action: () => Promise<T>, signal: AbortSignal): Promise<T>
601
623
  }
602
624
 
603
625
  action()
604
- .then((data) => completePromise(() => resolve(data)))
626
+ .then((data) => {
627
+ // We already rejected due to the abort, allow for cleanup
628
+ if (signal.aborted) {
629
+ return completePromise(() => cleanupOnAbort?.(data));
630
+ }
631
+ completePromise(() => resolve(data));
632
+ })
605
633
  .catch((e) => completePromise(() => reject(e)));
606
634
  });
607
635
  }
@@ -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,4 +1,4 @@
1
- import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
1
+ import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common';
2
2
  import * as Comlink from 'comlink';
3
3
  import { getNavigatorLocks } from '../../shared/navigator';
4
4
  import {
@@ -24,9 +24,6 @@ export class WorkerClient {
24
24
  private readonly port: MessagePort
25
25
  ) {
26
26
  Comlink.expose(this, this.port);
27
- }
28
-
29
- async initialize() {
30
27
  /**
31
28
  * Adds an extra listener which can remove this port
32
29
  * from the list of monitored ports.
@@ -37,16 +34,6 @@ export class WorkerClient {
37
34
  await this.removePort();
38
35
  }
39
36
  });
40
-
41
- /**
42
- * Keep a reference to the resolved port promise.
43
- * The init timing is difficult to predict due to the async message passing.
44
- * We only want to use a port if we are know it's been protected from being closed.
45
- * The lock based close signal will be added asynchronously. We need to use the
46
- * added port once the lock is configured.
47
- */
48
- this.resolvedPortPromise = this.sync.addPort(this.port);
49
- this.resolvedPort = await this.resolvedPortPromise;
50
37
  }
51
38
 
52
39
  private async removePort() {
@@ -70,18 +57,9 @@ export class WorkerClient {
70
57
  * it can consider the connection to be closed.
71
58
  */
72
59
  async addLockBasedCloseSignal(name: string) {
73
- if (!this.resolvedPortPromise) {
74
- // The init logic above is actually synchronous, so this should not happen.
75
- this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered');
76
- } else {
77
- const wrappedPort = await this.resolvedPortPromise;
78
- /**
79
- * The client registered a navigator lock. We now can guarantee detecting if the client has closed.
80
- * E.g. before this point: It's possible some ports might have been created and closed before the
81
- * lock based close signal is added. We should not trust those ports.
82
- */
83
- wrappedPort.isProtectedFromClose = true;
84
- }
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
85
63
  getNavigatorLocks().request(name, async () => {
86
64
  await this.removePort();
87
65
  });
@@ -121,8 +99,4 @@ export class WorkerClient {
121
99
  disconnect() {
122
100
  return this.sync.disconnect();
123
101
  }
124
-
125
- async _testUpdateAllStatuses(status: SyncStatusOptions) {
126
- return this.sync._testUpdateAllStatuses(status);
127
- }
128
102
  }
Binary file
Binary file