@powersync/web 0.0.0-dev-20251201150812 → 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 (39) hide show
  1. package/dist/index.umd.js +2415 -58
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +1884 -39
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +1809 -8
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js +0 -1203
  8. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js.map +1 -1
  9. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +0 -1203
  10. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  11. package/lib/package.json +2 -2
  12. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +4 -1
  13. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +52 -28
  14. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +3 -3
  15. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +1 -1
  16. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -2
  17. package/lib/src/worker/db/WASQLiteDB.worker.js +0 -1
  18. package/lib/src/worker/db/opfs.d.ts +96 -0
  19. package/lib/src/worker/db/opfs.js +582 -0
  20. package/lib/src/worker/sync/SharedSyncImplementation.js +23 -4
  21. package/lib/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +5 -5
  23. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +71 -48
  24. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +3 -4
  25. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +3 -3
  26. package/src/worker/db/WASQLiteDB.worker.ts +0 -2
  27. package/src/worker/db/opfs.ts +623 -0
  28. package/src/worker/sync/SharedSyncImplementation.ts +29 -8
  29. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js +0 -1813
  30. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +0 -1
  31. package/lib/src/worker/sync/MockSyncService.d.ts +0 -2
  32. package/lib/src/worker/sync/MockSyncService.js +0 -3
  33. package/lib/src/worker/sync/MockSyncServiceTypes.d.ts +0 -101
  34. package/lib/src/worker/sync/MockSyncServiceTypes.js +0 -1
  35. package/lib/src/worker/sync/MockSyncServiceWorker.d.ts +0 -56
  36. package/lib/src/worker/sync/MockSyncServiceWorker.js +0 -369
  37. package/src/worker/sync/MockSyncService.ts +0 -3
  38. package/src/worker/sync/MockSyncServiceTypes.ts +0 -71
  39. package/src/worker/sync/MockSyncServiceWorker.ts +0 -406
@@ -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';
@@ -113,23 +113,29 @@ export class LockedAsyncDatabaseAdapter
113
113
  }
114
114
 
115
115
  protected async openInternalDB() {
116
- // Dispose any previous table change listener.
117
- this._disposeTableChangeListener?.();
118
- this._disposeTableChangeListener = null;
119
-
120
- const isReOpen = !!this._db;
121
-
122
- this._db = await this.options.openConnection();
123
- await this._db.init();
124
- this._config = await this._db.getConfig();
125
- await this.registerOnChangeListener(this._db);
126
- if (isReOpen) {
127
- this.iterateListeners((cb) => cb.databaseReOpened?.());
128
- }
129
116
  /**
130
- * This is only required for the long-lived shared IndexedDB connections.
117
+ * Execute opening of the db in a lock in order not to interfere with other operations.
131
118
  */
132
- this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
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
+ });
133
139
  }
134
140
 
135
141
  protected _reOpen() {
@@ -154,7 +160,23 @@ export class LockedAsyncDatabaseAdapter
154
160
  }
155
161
 
156
162
  protected async _init() {
157
- 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
+ }
158
180
  this.iterateListeners((cb) => cb.initialized?.());
159
181
  }
160
182
 
@@ -252,13 +274,10 @@ export class LockedAsyncDatabaseAdapter
252
274
  );
253
275
  }
254
276
 
255
- protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
256
- await this.waitForInitialized();
257
-
277
+ protected async _acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
258
278
  if (this.closing) {
259
279
  throw new Error(`Cannot acquire lock, the database is closing`);
260
280
  }
261
-
262
281
  const abortController = new AbortController();
263
282
  this.pendingAbortControllers.add(abortController);
264
283
  const { timeoutMs } = options ?? {};
@@ -278,37 +297,41 @@ export class LockedAsyncDatabaseAdapter
278
297
  if (timeoutId) {
279
298
  clearTimeout(timeoutId);
280
299
  }
281
- let holdId: string | null = null;
282
- try {
283
- // The database is being opened in the background. Wait for it here.
284
- if (this.databaseOpenPromise) {
285
- try {
286
- await this.databaseOpenPromise;
287
- } catch (ex) {
288
- // This will cause a retry of opening the database.
289
- const wrappedError = new ConnectionClosedError('Could not open database');
290
- wrappedError.cause = ex;
291
- throw wrappedError;
292
- }
293
- }
300
+ return await callback();
301
+ }
302
+ );
303
+ }
294
304
 
295
- holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
296
- return await callback();
297
- } catch (ex) {
298
- if (ex instanceof ConnectionClosedError) {
299
- if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
300
- // Immediately re-open the database. We need to miss as little table updates as possible.
301
- this.reOpenInternalDB();
302
- }
303
- }
304
- throw ex;
305
- } finally {
306
- if (holdId) {
307
- 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();
308
326
  }
309
327
  }
328
+ throw ex;
329
+ } finally {
330
+ if (holdId) {
331
+ await this.baseDB.releaseHold(holdId);
332
+ }
310
333
  }
311
- );
334
+ }, options);
312
335
  }
313
336
 
314
337
  async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
@@ -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;
@@ -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;