@powersync/web 0.0.0-dev-20250925184532 → 0.0.0-dev-20251003085035

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 (30) hide show
  1. package/dist/{fbde47713220d7baec73.wasm → 10072fe45f0a8fab0a0e.wasm} +0 -0
  2. package/dist/{d0a1e43030b814ed322f.wasm → 6e435e51534839845554.wasm} +0 -0
  3. package/dist/a730f7ca717b02234beb.wasm +0 -0
  4. package/dist/aa2f408d64445fed090e.wasm +0 -0
  5. package/dist/index.umd.js +82 -26
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +76 -21
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +2 -2
  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/lib/package.json +4 -4
  20. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +11 -0
  21. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +53 -9
  22. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +1 -0
  23. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -0
  24. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +2 -0
  25. package/lib/src/worker/sync/SharedSyncImplementation.js +18 -9
  26. package/lib/src/worker/sync/WorkerClient.js +3 -1
  27. package/lib/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +5 -5
  29. package/dist/31ba59416bad61e8fb1f.wasm +0 -0
  30. package/dist/f4ad8bfeb6e6e5326142.wasm +0 -0
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "1.26.2",
3
+ "version": "1.27.0",
4
4
  "description": "PowerSync web SDK. Sync Postgres, MongoDB or MySQL with SQLite in your web app",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -60,8 +60,8 @@
60
60
  "author": "JOURNEYAPPS",
61
61
  "license": "Apache-2.0",
62
62
  "peerDependencies": {
63
- "@journeyapps/wa-sqlite": "^1.3.1",
64
- "@powersync/common": "workspace:^1.38.1"
63
+ "@journeyapps/wa-sqlite": "^1.3.2",
64
+ "@powersync/common": "workspace:^1.39.0"
65
65
  },
66
66
  "dependencies": {
67
67
  "@powersync/common": "workspace:*",
@@ -71,7 +71,7 @@
71
71
  "commander": "^12.1.0"
72
72
  },
73
73
  "devDependencies": {
74
- "@journeyapps/wa-sqlite": "^1.3.1",
74
+ "@journeyapps/wa-sqlite": "^1.3.2",
75
75
  "@types/uuid": "^9.0.6",
76
76
  "crypto-browserify": "^3.12.0",
77
77
  "p-defer": "^4.0.1",
@@ -8,6 +8,7 @@ export type SharedConnectionWorker = {
8
8
  export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> = {
9
9
  baseConnection: AsyncDatabaseConnection;
10
10
  identifier: string;
11
+ remoteCanCloseUnexpectedly: boolean;
11
12
  /**
12
13
  * Need a remote in order to keep a reference to the Proxied worker
13
14
  */
@@ -21,9 +22,19 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
21
22
  export declare class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> implements AsyncDatabaseConnection {
22
23
  protected options: WrappedWorkerConnectionOptions<Config>;
23
24
  protected lockAbortController: AbortController;
25
+ protected notifyRemoteClosed: AbortController | undefined;
24
26
  constructor(options: WrappedWorkerConnectionOptions<Config>);
25
27
  protected get baseConnection(): AsyncDatabaseConnection<ResolvedWebSQLOpenOptions>;
26
28
  init(): Promise<void>;
29
+ /**
30
+ * Marks the remote as closed.
31
+ *
32
+ * This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
33
+ * it happens, all methods on the {@link baseConnection} would never resolve. To avoid livelocks in this scenario, we
34
+ * throw on all outstanding promises and forbid new calls.
35
+ */
36
+ markRemoteClosed(): void;
37
+ private withRemote;
27
38
  /**
28
39
  * Get a MessagePort which can be used to share the internals of this connection.
29
40
  */
@@ -5,10 +5,13 @@ import * as Comlink from 'comlink';
5
5
  */
6
6
  export class WorkerWrappedAsyncDatabaseConnection {
7
7
  options;
8
- lockAbortController;
8
+ lockAbortController = new AbortController();
9
+ notifyRemoteClosed;
9
10
  constructor(options) {
10
11
  this.options = options;
11
- this.lockAbortController = new AbortController();
12
+ if (options.remoteCanCloseUnexpectedly) {
13
+ this.notifyRemoteClosed = new AbortController();
14
+ }
12
15
  }
13
16
  get baseConnection() {
14
17
  return this.options.baseConnection;
@@ -16,6 +19,43 @@ export class WorkerWrappedAsyncDatabaseConnection {
16
19
  init() {
17
20
  return this.baseConnection.init();
18
21
  }
22
+ /**
23
+ * Marks the remote as closed.
24
+ *
25
+ * This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
26
+ * it happens, all methods on the {@link baseConnection} would never resolve. To avoid livelocks in this scenario, we
27
+ * throw on all outstanding promises and forbid new calls.
28
+ */
29
+ markRemoteClosed() {
30
+ // Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
31
+ // set.
32
+ this.notifyRemoteClosed.abort();
33
+ }
34
+ withRemote(workerPromise) {
35
+ const controller = this.notifyRemoteClosed;
36
+ if (controller) {
37
+ return new Promise((resolve, reject) => {
38
+ if (controller.signal.aborted) {
39
+ reject(new Error('Called operation on closed remote'));
40
+ }
41
+ function handleAbort() {
42
+ reject(new Error('Remote peer closed with request in flight'));
43
+ }
44
+ function completePromise(action) {
45
+ controller.signal.removeEventListener('abort', handleAbort);
46
+ action();
47
+ }
48
+ controller.signal.addEventListener('abort', handleAbort);
49
+ workerPromise()
50
+ .then((data) => completePromise(() => resolve(data)))
51
+ .catch((e) => completePromise(() => reject(e)));
52
+ });
53
+ }
54
+ else {
55
+ // Can't close, so just return the inner worker promise unguarded.
56
+ return workerPromise();
57
+ }
58
+ }
19
59
  /**
20
60
  * Get a MessagePort which can be used to share the internals of this connection.
21
61
  */
@@ -66,20 +106,24 @@ export class WorkerWrappedAsyncDatabaseConnection {
66
106
  async close() {
67
107
  // Abort any pending lock requests.
68
108
  this.lockAbortController.abort();
69
- await this.baseConnection.close();
70
- this.options.remote[Comlink.releaseProxy]();
71
- this.options.onClose?.();
109
+ try {
110
+ await this.withRemote(() => this.baseConnection.close());
111
+ }
112
+ finally {
113
+ this.options.remote[Comlink.releaseProxy]();
114
+ this.options.onClose?.();
115
+ }
72
116
  }
73
117
  execute(sql, params) {
74
- return this.baseConnection.execute(sql, params);
118
+ return this.withRemote(() => this.baseConnection.execute(sql, params));
75
119
  }
76
120
  executeRaw(sql, params) {
77
- return this.baseConnection.executeRaw(sql, params);
121
+ return this.withRemote(() => this.baseConnection.executeRaw(sql, params));
78
122
  }
79
123
  executeBatch(sql, params) {
80
- return this.baseConnection.executeBatch(sql, params);
124
+ return this.withRemote(() => this.baseConnection.executeBatch(sql, params));
81
125
  }
82
126
  getConfig() {
83
- return this.baseConnection.getConfig();
127
+ return this.withRemote(() => this.baseConnection.getConfig());
84
128
  }
85
129
  }
@@ -17,6 +17,7 @@ export class WASQLiteDBAdapter extends LockedAsyncDatabaseAdapter {
17
17
  const remote = Comlink.wrap(workerPort);
18
18
  return new WorkerWrappedAsyncDatabaseConnection({
19
19
  remote,
20
+ remoteCanCloseUnexpectedly: false,
20
21
  identifier: options.dbFilename,
21
22
  baseConnection: await remote({
22
23
  ...options,
@@ -45,6 +45,8 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
45
45
  const workerDBOpener = Comlink.wrap(workerPort);
46
46
  return new WorkerWrappedAsyncDatabaseConnection({
47
47
  remote: workerDBOpener,
48
+ // This tab owns the worker, so we're guaranteed to outlive it.
49
+ remoteCanCloseUnexpectedly: false,
48
50
  baseConnection: await workerDBOpener({
49
51
  dbFilename: this.options.dbFilename,
50
52
  vfs,
@@ -44,6 +44,7 @@ export type WrappedSyncPort = {
44
44
  clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
45
45
  db?: DBAdapter;
46
46
  currentSubscriptions: SubscribedStream[];
47
+ closeListeners: (() => void)[];
47
48
  };
48
49
  /**
49
50
  * @internal
@@ -100,6 +101,7 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
100
101
  port: MessagePort;
101
102
  clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
102
103
  currentSubscriptions: never[];
104
+ closeListeners: never[];
103
105
  }>;
104
106
  /**
105
107
  * Removes a message port client from this manager's managed
@@ -179,7 +179,8 @@ export class SharedSyncImplementation extends BaseObserver {
179
179
  const portProvider = {
180
180
  port,
181
181
  clientProvider: Comlink.wrap(port),
182
- currentSubscriptions: []
182
+ currentSubscriptions: [],
183
+ closeListeners: []
183
184
  };
184
185
  this.ports.push(portProvider);
185
186
  // Give the newly connected client the latest status
@@ -226,19 +227,18 @@ export class SharedSyncImplementation extends BaseObserver {
226
227
  // We could not find the port to remove
227
228
  return () => { };
228
229
  }
230
+ for (const closeListener of trackedPort.closeListeners) {
231
+ closeListener();
232
+ }
229
233
  if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
230
- if (shouldReconnect) {
231
- await this.connectionManager.disconnect();
232
- }
234
+ // Unconditionally close the connection because the database it's writing to has just been closed.
235
+ await this.connectionManager.disconnect();
233
236
  // Clearing the adapter will result in a new one being opened in connect
234
237
  this.dbAdapter = null;
235
238
  if (shouldReconnect) {
236
239
  await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
237
240
  }
238
241
  }
239
- if (trackedPort.db) {
240
- await trackedPort.db.close();
241
- }
242
242
  // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
243
243
  this.collectActiveSubscriptions();
244
244
  // Release proxy
@@ -353,11 +353,20 @@ export class SharedSyncImplementation extends BaseObserver {
353
353
  const locked = new LockedAsyncDatabaseAdapter({
354
354
  name: identifier,
355
355
  openConnection: async () => {
356
- return new WorkerWrappedAsyncDatabaseConnection({
356
+ const wrapped = new WorkerWrappedAsyncDatabaseConnection({
357
357
  remote,
358
358
  baseConnection: db,
359
- identifier
359
+ identifier,
360
+ // It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
361
+ // that and ensure pending requests are aborted when the tab is closed.
362
+ remoteCanCloseUnexpectedly: true
363
+ });
364
+ lastClient.closeListeners.push(() => {
365
+ this.logger.info('Aborting open connection because associated tab closed.');
366
+ wrapped.close();
367
+ wrapped.markRemoteClosed();
360
368
  });
369
+ return wrapped;
361
370
  },
362
371
  logger: this.logger
363
372
  });
@@ -31,7 +31,9 @@ export class WorkerClient {
31
31
  }
32
32
  async removePort() {
33
33
  if (this.resolvedPort) {
34
- const release = await this.sync.removePort(this.resolvedPort);
34
+ const resolved = this.resolvedPort;
35
+ this.resolvedPort = null;
36
+ const release = await this.sync.removePort(resolved);
35
37
  this.resolvedPort = null;
36
38
  this.port.postMessage({
37
39
  event: SharedSyncClientEvent.CLOSE_ACK,