@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
@@ -10,7 +10,7 @@ import {
10
10
  createLogger,
11
11
  type ILogger
12
12
  } from '@powersync/common';
13
- import { getNavigatorLocks } from '../..//shared/navigator';
13
+ import { getNavigatorLocks } from '../../shared/navigator';
14
14
  import { AsyncDatabaseConnection, ConnectionClosedError } from './AsyncDatabaseConnection';
15
15
  import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
16
16
  import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
@@ -57,7 +57,6 @@ export class LockedAsyncDatabaseAdapter
57
57
  private _config: ResolvedWebSQLOpenOptions | null = null;
58
58
  protected pendingAbortControllers: Set<AbortController>;
59
59
  protected requiresHolds: boolean | null;
60
- protected requiresReOpen: boolean;
61
60
  protected databaseOpenPromise: Promise<void> | null = null;
62
61
 
63
62
  closing: boolean;
@@ -71,7 +70,6 @@ export class LockedAsyncDatabaseAdapter
71
70
  this.closed = false;
72
71
  this.closing = false;
73
72
  this.requiresHolds = null;
74
- this.requiresReOpen = false;
75
73
  // Set the name if provided. We can query for the name if not available yet
76
74
  this.debugMode = options.debugMode ?? false;
77
75
  if (this.debugMode) {
@@ -115,27 +113,70 @@ export class LockedAsyncDatabaseAdapter
115
113
  }
116
114
 
117
115
  protected async openInternalDB() {
118
- // Dispose any previous table change listener.
119
- this._disposeTableChangeListener?.();
120
- this._disposeTableChangeListener = null;
116
+ /**
117
+ * Execute opening of the db in a lock in order not to interfere with other operations.
118
+ */
119
+ return this._acquireLock(async () => {
120
+ // Dispose any previous table change listener.
121
+ this._disposeTableChangeListener?.();
122
+ this._disposeTableChangeListener = null;
123
+ this._db?.close().catch((ex) => this.logger.warn(`Error closing database before opening new instance`, ex));
124
+ const isReOpen = !!this._db;
125
+ this._db = null;
126
+
127
+ this._db = await this.options.openConnection();
128
+ await this._db.init();
129
+ this._config = await this._db.getConfig();
130
+ await this.registerOnChangeListener(this._db);
131
+ if (isReOpen) {
132
+ this.iterateListeners((cb) => cb.databaseReOpened?.());
133
+ }
134
+ /**
135
+ * This is only required for the long-lived shared IndexedDB connections.
136
+ */
137
+ this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
138
+ });
139
+ }
121
140
 
122
- const isReOpen = !!this._db;
141
+ protected _reOpen() {
142
+ this.databaseOpenPromise = this.openInternalDB().finally(() => {
143
+ this.databaseOpenPromise = null;
144
+ });
145
+ return this.databaseOpenPromise;
146
+ }
123
147
 
124
- this._db = await this.options.openConnection();
125
- await this._db.init();
126
- this._config = await this._db.getConfig();
127
- await this.registerOnChangeListener(this._db);
128
- if (isReOpen) {
129
- this.iterateListeners((cb) => cb.databaseReOpened?.());
148
+ /**
149
+ * Re-opens the underlying database.
150
+ * Returns a pending operation if one is already in progress.
151
+ */
152
+ async reOpenInternalDB(): Promise<void> {
153
+ if (!this.options.reOpenOnConnectionClosed) {
154
+ throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
130
155
  }
131
- /**
132
- * This is only required for the long-lived shared IndexedDB connections.
133
- */
134
- this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
156
+ if (this.databaseOpenPromise) {
157
+ return this.databaseOpenPromise;
158
+ }
159
+ return this._reOpen();
135
160
  }
136
161
 
137
162
  protected async _init() {
138
- await this.openInternalDB();
163
+ /**
164
+ * For OPFS, we can see this open call sometimes fail due to NoModificationAllowedError.
165
+ * We should be able to recover from this by re-opening the database.
166
+ */
167
+ const maxAttempts = 3;
168
+ for (let count = 0; count < maxAttempts; count++) {
169
+ try {
170
+ await this.openInternalDB();
171
+ break;
172
+ } catch (ex) {
173
+ if (count == maxAttempts - 1) {
174
+ throw ex;
175
+ }
176
+ this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex);
177
+ await new Promise((resolve) => setTimeout(resolve, 1000));
178
+ }
179
+ }
139
180
  this.iterateListeners((cb) => cb.initialized?.());
140
181
  }
141
182
 
@@ -233,13 +274,10 @@ export class LockedAsyncDatabaseAdapter
233
274
  );
234
275
  }
235
276
 
236
- protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
237
- await this.waitForInitialized();
238
-
277
+ protected async _acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
239
278
  if (this.closing) {
240
279
  throw new Error(`Cannot acquire lock, the database is closing`);
241
280
  }
242
-
243
281
  const abortController = new AbortController();
244
282
  this.pendingAbortControllers.add(abortController);
245
283
  const { timeoutMs } = options ?? {};
@@ -259,39 +297,41 @@ export class LockedAsyncDatabaseAdapter
259
297
  if (timeoutId) {
260
298
  clearTimeout(timeoutId);
261
299
  }
262
- let holdId: string | null = null;
263
- try {
264
- // The database is being opened in the background. Wait for it here.
265
- if (this.databaseOpenPromise) {
266
- try {
267
- await this.databaseOpenPromise;
268
- } catch (ex) {
269
- // This will cause a retry of opening the database.
270
- const wrappedError = new ConnectionClosedError('Could not open database');
271
- wrappedError.cause = ex;
272
- throw wrappedError;
273
- }
274
- }
300
+ return await callback();
301
+ }
302
+ );
303
+ }
275
304
 
276
- holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
277
- return await callback();
278
- } catch (ex) {
279
- if (ex instanceof ConnectionClosedError) {
280
- if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
281
- // Immediately re-open the database. We need to miss as little table updates as possible.
282
- this.databaseOpenPromise = this.openInternalDB().finally(() => {
283
- this.databaseOpenPromise = null;
284
- });
285
- }
286
- }
287
- throw ex;
288
- } finally {
289
- if (holdId) {
290
- await this.baseDB.releaseHold(holdId);
305
+ protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
306
+ await this.waitForInitialized();
307
+
308
+ return this._acquireLock(async () => {
309
+ let holdId: string | null = null;
310
+ try {
311
+ // The database is being opened in the background. Wait for it here.
312
+ if (this.databaseOpenPromise) {
313
+ /**
314
+ * We can't await this since it uses the same lock as we're in now.
315
+ */
316
+ throw new ConnectionClosedError('Connection is busy re-opening');
317
+ }
318
+
319
+ holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
320
+ return await callback();
321
+ } catch (ex) {
322
+ if (ex instanceof ConnectionClosedError || (ex instanceof Error && ex.name === 'NoModificationAllowedError')) {
323
+ if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
324
+ // Immediately re-open the database. We need to miss as little table updates as possible.
325
+ this.reOpenInternalDB();
291
326
  }
292
327
  }
328
+ throw ex;
329
+ } finally {
330
+ if (holdId) {
331
+ await this.baseDB.releaseHold(holdId);
332
+ }
293
333
  }
294
- );
334
+ }, options);
295
335
  }
296
336
 
297
337
  async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
@@ -1,3 +1,4 @@
1
+ import { BaseObserver } from '@powersync/common';
1
2
  import * as Comlink from 'comlink';
2
3
  import {
3
4
  AsyncDatabaseConnection,
@@ -24,17 +25,23 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
24
25
  onClose?: () => void;
25
26
  };
26
27
 
28
+ export type WorkerWrappedAsyncDatabaseConnectionListener = {
29
+ closing: () => void;
30
+ };
27
31
  /**
28
32
  * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
29
33
  * functions for worker listeners.
30
34
  */
31
35
  export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions>
36
+ extends BaseObserver<WorkerWrappedAsyncDatabaseConnectionListener>
32
37
  implements AsyncDatabaseConnection
33
38
  {
34
39
  protected lockAbortController = new AbortController();
35
40
  protected notifyRemoteClosed: AbortController | undefined;
36
41
 
37
42
  constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
43
+ super();
44
+
38
45
  if (options.remoteCanCloseUnexpectedly) {
39
46
  this.notifyRemoteClosed = new AbortController();
40
47
  }
@@ -73,14 +80,17 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
73
80
  return this.withRemote(() => this.baseConnection.isAutoCommit());
74
81
  }
75
82
 
76
- private withRemote<T>(workerPromise: () => Promise<T>): Promise<T> {
83
+ private withRemote<T>(workerPromise: () => Promise<T>, fireActionOnAbort = false): Promise<T> {
77
84
  const controller = this.notifyRemoteClosed;
78
85
  if (controller) {
79
86
  return new Promise((resolve, reject) => {
80
87
  if (controller.signal.aborted) {
81
88
  reject(new ConnectionClosedError('Called operation on closed remote'));
82
- // Don't run the operation if we're going to reject
83
- return;
89
+ if (!fireActionOnAbort) {
90
+ // Don't run the operation if we're going to reject
91
+ // We might want to fire-and-forget the operation in some cases (like a close operation)
92
+ return;
93
+ }
84
94
  }
85
95
 
86
96
  function handleAbort() {
@@ -165,10 +175,12 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
165
175
  // Abort any pending lock requests.
166
176
  this.lockAbortController.abort();
167
177
  try {
168
- await this.withRemote(() => this.baseConnection.close());
178
+ // fire and forget the close operation
179
+ await this.withRemote(() => this.baseConnection.close(), true);
169
180
  } finally {
170
181
  this.options.remote[Comlink.releaseProxy]();
171
182
  this.options.onClose?.();
183
+ this.iterateListeners((l) => l.closing?.());
172
184
  }
173
185
  }
174
186
 
@@ -1,9 +1,9 @@
1
1
  import * as SQLite from '@journeyapps/wa-sqlite';
2
2
  import { BaseObserver, BatchedUpdateNotification } from '@powersync/common';
3
3
  import { Mutex } from 'async-mutex';
4
+ import { OPFSCoopSyncVFS } from '../../../worker/db/opfs';
4
5
  import { AsyncDatabaseConnection, OnTableChangeCallback, ProxiedQueryResult } from '../AsyncDatabaseConnection';
5
6
  import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory';
6
-
7
7
  /**
8
8
  * List of currently tested virtual filesystems
9
9
  */
@@ -124,11 +124,10 @@ export const DEFAULT_MODULE_FACTORIES = {
124
124
  } else {
125
125
  module = await SyncWASQLiteModuleFactory();
126
126
  }
127
- // @ts-expect-error The types for this static method are missing upstream
128
- const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js');
127
+ const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module);
129
128
  return {
130
129
  module,
131
- vfs: await OPFSCoopSyncVFS.create(options.dbFileName, module)
130
+ vfs: vfs as any
132
131
  };
133
132
  }
134
133
  };
@@ -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,7 +2,6 @@ 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';
@@ -187,6 +186,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
187
186
  * - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
188
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.
189
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).
190
191
  * - The client side lock is held until the client is disposed.
191
192
  * - We resolve the top-level promise after the lock has been registered with the shared worker.
192
193
  * - The client sends the params to the shared worker after locks have been registered.
@@ -255,8 +256,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
255
256
  async dispose(): Promise<void> {
256
257
  await this.waitForReady();
257
258
 
258
- await super.dispose();
259
-
260
259
  await new Promise<void>((resolve) => {
261
260
  // Listen for the close acknowledgment from the worker
262
261
  this.messagePort.addEventListener('message', (event) => {
@@ -273,6 +272,9 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
273
272
  };
274
273
  this.messagePort.postMessage(closeMessagePayload);
275
274
  });
275
+
276
+ await super.dispose();
277
+
276
278
  this.abortOnClose.abort();
277
279
 
278
280
  // Release the proxy
@@ -287,12 +289,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
287
289
  updateSubscriptions(subscriptions: SubscribedStream[]): void {
288
290
  this.syncManager.updateSubscriptions(subscriptions);
289
291
  }
290
-
291
- /**
292
- * Used in tests to force a connection states
293
- */
294
- private async _testUpdateStatus(status: SyncStatus) {
295
- await this.isInitialized;
296
- return this.syncManager._testUpdateAllStatuses(status.toJSON());
297
- }
298
292
  }
@@ -17,11 +17,9 @@ 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> => {
24
- // Prevent multiple simultaneous opens from causing race conditions
25
23
  return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
26
24
  const clientId = nextClientId++;
27
25
  const { dbFilename, logLevel } = options;