@powersync/web 1.36.0 → 1.37.1

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 (91) hide show
  1. package/dist/index.umd.js +1127 -1235
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +550 -3089
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +797 -854
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/lib/package.json +2 -3
  8. package/lib/src/db/PowerSyncDatabase.d.ts +1 -2
  9. package/lib/src/db/PowerSyncDatabase.js +3 -4
  10. package/lib/src/db/adapters/AsyncWebAdapter.d.ts +40 -0
  11. package/lib/src/db/adapters/AsyncWebAdapter.js +69 -0
  12. package/lib/src/db/adapters/SSRDBAdapter.d.ts +1 -2
  13. package/lib/src/db/adapters/SSRDBAdapter.js +5 -6
  14. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.d.ts +56 -0
  15. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.js +121 -0
  16. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.d.ts +54 -0
  17. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.js +227 -0
  18. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.d.ts +47 -0
  19. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +146 -0
  20. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +46 -0
  21. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +147 -0
  22. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +14 -6
  23. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +66 -39
  24. package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +61 -0
  25. package/lib/src/db/adapters/wa-sqlite/vfs.js +91 -0
  26. package/lib/src/db/adapters/web-sql-flags.d.ts +5 -0
  27. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +1 -2
  28. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +2 -3
  29. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +4 -19
  30. package/lib/src/index.d.ts +1 -4
  31. package/lib/src/index.js +1 -4
  32. package/lib/src/shared/tab_close_signal.d.ts +11 -0
  33. package/lib/src/shared/tab_close_signal.js +26 -0
  34. package/lib/src/worker/db/MultiDatabaseServer.d.ts +17 -0
  35. package/lib/src/worker/db/MultiDatabaseServer.js +86 -0
  36. package/lib/src/worker/db/WASQLiteDB.worker.js +9 -48
  37. package/lib/src/worker/db/open-worker-database.d.ts +3 -3
  38. package/lib/src/worker/db/open-worker-database.js +1 -1
  39. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +5 -6
  40. package/lib/src/worker/sync/SharedSyncImplementation.js +92 -54
  41. package/lib/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +3 -4
  43. package/src/db/PowerSyncDatabase.ts +3 -3
  44. package/src/db/adapters/AsyncWebAdapter.ts +91 -0
  45. package/src/db/adapters/SSRDBAdapter.ts +7 -7
  46. package/src/db/adapters/wa-sqlite/ConcurrentConnection.ts +137 -0
  47. package/src/db/adapters/wa-sqlite/DatabaseClient.ts +325 -0
  48. package/src/db/adapters/wa-sqlite/DatabaseServer.ts +201 -0
  49. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +191 -0
  50. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +87 -43
  51. package/src/db/adapters/wa-sqlite/vfs.ts +112 -0
  52. package/src/db/adapters/web-sql-flags.ts +6 -0
  53. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +2 -3
  54. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +4 -20
  55. package/src/index.ts +1 -4
  56. package/src/shared/tab_close_signal.ts +28 -0
  57. package/src/worker/db/MultiDatabaseServer.ts +104 -0
  58. package/src/worker/db/WASQLiteDB.worker.ts +10 -57
  59. package/src/worker/db/open-worker-database.ts +3 -3
  60. package/src/worker/sync/SharedSyncImplementation.ts +118 -58
  61. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +0 -1881
  62. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +0 -1
  63. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js +0 -555
  64. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js.map +0 -1
  65. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +0 -17
  66. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +0 -33
  67. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +0 -49
  68. package/lib/src/db/adapters/AsyncDatabaseConnection.js +0 -1
  69. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +0 -109
  70. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +0 -404
  71. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +0 -59
  72. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +0 -147
  73. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +0 -12
  74. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +0 -19
  75. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +0 -155
  76. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +0 -401
  77. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +0 -32
  78. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +0 -49
  79. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +0 -42
  80. package/lib/src/worker/db/SharedWASQLiteConnection.js +0 -90
  81. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +0 -9
  82. package/lib/src/worker/db/WorkerWASQLiteConnection.js +0 -12
  83. package/src/db/adapters/AbstractWebSQLOpenFactory.ts +0 -48
  84. package/src/db/adapters/AsyncDatabaseConnection.ts +0 -55
  85. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +0 -489
  86. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +0 -201
  87. package/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.ts +0 -23
  88. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +0 -497
  89. package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +0 -86
  90. package/src/worker/db/SharedWASQLiteConnection.ts +0 -131
  91. package/src/worker/db/WorkerWASQLiteConnection.ts +0 -14
@@ -76,6 +76,12 @@ export interface WebSQLOpenFactoryOptions extends SQLOpenOptions {
76
76
  */
77
77
  worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => Worker | SharedWorker);
78
78
 
79
+ /**
80
+ * Use an existing port to an initialized worker.
81
+ * A worker will be initialized if none is provided
82
+ */
83
+ workerPort?: MessagePort;
84
+
79
85
  logger?: ILogger;
80
86
 
81
87
  /**
@@ -3,13 +3,12 @@ import {
3
3
  BaseObserver,
4
4
  LockOptions,
5
5
  LockType,
6
+ Mutex,
6
7
  PowerSyncConnectionOptions,
7
8
  StreamingSyncImplementation,
8
- SubscribedStream,
9
9
  SyncStatus,
10
10
  SyncStatusOptions
11
11
  } from '@powersync/common';
12
- import { Mutex } from 'async-mutex';
13
12
 
14
13
  export class SSRStreamingSyncImplementation extends BaseObserver implements StreamingSyncImplementation {
15
14
  syncMutex: Mutex;
@@ -29,7 +28,7 @@ export class SSRStreamingSyncImplementation extends BaseObserver implements Stre
29
28
 
30
29
  obtainLock<T>(lockOptions: LockOptions<T>): Promise<T> {
31
30
  const mutex = lockOptions.type == LockType.CRUD ? this.crudMutex : this.syncMutex;
32
- return mutex.runExclusive(lockOptions.callback);
31
+ return mutex.runExclusive(lockOptions.callback, lockOptions.signal);
33
32
  }
34
33
 
35
34
  /**
@@ -15,6 +15,7 @@ import {
15
15
  WebStreamingSyncImplementation,
16
16
  WebStreamingSyncImplementationOptions
17
17
  } from './WebStreamingSyncImplementation.js';
18
+ import { generateTabCloseSignal } from '../../shared/tab_close_signal.js';
18
19
 
19
20
  /**
20
21
  * The shared worker will trigger methods on this side of the message port
@@ -192,26 +193,9 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
192
193
  * - We resolve the top-level promise after the lock has been registered with the shared worker.
193
194
  * - The client sends the params to the shared worker after locks have been registered.
194
195
  */
195
- await new Promise<void>((resolve) => {
196
- // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
197
- // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
198
- // to free resources associated with this tab.
199
- // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
200
- getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
201
- if (this.abortOnClose.signal.aborted) {
202
- return;
203
- }
204
- // Awaiting here ensures the worker is waiting for the lock
205
- await this.syncManager.addLockBasedCloseSignal(lock!.name);
206
-
207
- // The lock has been registered, we can continue with the initialization
208
- resolve();
209
-
210
- await new Promise<void>((r) => {
211
- this.abortOnClose.signal.onabort = () => r();
212
- });
213
- });
214
- });
196
+ const closeSignal = await generateTabCloseSignal(this.abortOnClose.signal);
197
+ // Awaiting here ensures the worker is waiting for the lock
198
+ await this.syncManager.addLockBasedCloseSignal(closeSignal);
215
199
 
216
200
  const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
217
201
  const flags = { ...this.webOptions.flags, workers: undefined };
package/src/index.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  export * from '@powersync/common';
2
2
  export * from './attachments/IndexDBFileSystemAdapter.js';
3
3
  export * from './db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.js';
4
- export * from './db/adapters/AbstractWebSQLOpenFactory.js';
5
- export * from './db/adapters/AsyncDatabaseConnection.js';
6
- export * from './db/adapters/wa-sqlite/WASQLiteConnection.js';
7
- export * from './db/adapters/wa-sqlite/WASQLiteDBAdapter.js';
4
+ export { WASQLiteVFS } from './db/adapters/wa-sqlite/vfs.js';
8
5
  export * from './db/adapters/wa-sqlite/WASQLiteOpenFactory.js';
9
6
  export * from './db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.js';
10
7
  export * from './db/adapters/web-sql-flags.js';
@@ -0,0 +1,28 @@
1
+ import { getNavigatorLocks } from './navigator.js';
2
+
3
+ /**
4
+ * Requests a random lock that will be released once the optional signal is aborted (or, if no signal is given, when the
5
+ * tab is closed).
6
+ *
7
+ * This allows sending the name of the lock to another context (e.g. a shared worker), which will also attempt to
8
+ * acquire it. Since the lock is returned when the tab is closed, this allows the shared worker to free resources
9
+ * assocatiated with this tab.
10
+ *
11
+ * We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
12
+ */
13
+ export function generateTabCloseSignal(abort?: AbortSignal): Promise<string> {
14
+ return new Promise((resolve, reject) => {
15
+ const options: LockOptions = { signal: abort };
16
+ getNavigatorLocks()
17
+ .request(`tab-close-signal-${crypto.randomUUID()}`, options, (lock) => {
18
+ resolve(lock!.name);
19
+
20
+ return new Promise<void>((resolve) => {
21
+ if (abort) {
22
+ abort.addEventListener('abort', () => resolve());
23
+ }
24
+ });
25
+ })
26
+ .catch(reject);
27
+ });
28
+ }
@@ -0,0 +1,104 @@
1
+ import { ILogger } from '@powersync/common';
2
+ import * as Comlink from 'comlink';
3
+ import { ClientConnectionView, DatabaseServer } from '../../db/adapters/wa-sqlite/DatabaseServer.js';
4
+ import {
5
+ ResolvedWASQLiteOpenFactoryOptions,
6
+ WorkerDBOpenerOptions
7
+ } from '../../db/adapters/wa-sqlite/WASQLiteOpenFactory.js';
8
+ import { getNavigatorLocks } from '../../shared/navigator.js';
9
+ import { RawSqliteConnection } from '../../db/adapters/wa-sqlite/RawSqliteConnection.js';
10
+ import { ConcurrentSqliteConnection } from '../../db/adapters/wa-sqlite/ConcurrentConnection.js';
11
+
12
+ const OPEN_DB_LOCK = 'open-wasqlite-db';
13
+
14
+ /**
15
+ * Shared state to manage multiple database connections hosted by a worker.
16
+ */
17
+ export class MultiDatabaseServer {
18
+ private activeDatabases = new Map<string, DatabaseServer>();
19
+
20
+ constructor(readonly logger: ILogger) {}
21
+
22
+ async handleConnection(options: WorkerDBOpenerOptions): Promise<ClientConnectionView> {
23
+ this.logger.setLevel(options.logLevel);
24
+ return Comlink.proxy(await this.openConnectionLocally(options, options.lockName));
25
+ }
26
+
27
+ async connectToExisting(name: string, lockName: string): Promise<ClientConnectionView> {
28
+ return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
29
+ const server = this.activeDatabases.get(name);
30
+ if (server == null) {
31
+ throw new Error(`connectToExisting(${name}) failed because the worker doesn't own a database with that name.`);
32
+ }
33
+
34
+ return Comlink.proxy(await server.connect(lockName));
35
+ });
36
+ }
37
+
38
+ async openConnectionLocally(options: ResolvedWASQLiteOpenFactoryOptions, lockName?: string) {
39
+ // Especially on Firefox, we're sometimes seeing "NoModificationAllowedError"s when opening OPFS databases we can
40
+ // work around by retrying.
41
+ const maxAttempts = 3;
42
+ let server: DatabaseServer | null;
43
+
44
+ for (let count = 0; count < maxAttempts - 1; count++) {
45
+ try {
46
+ server = await this.databaseOpenAttempt(options);
47
+ } catch (ex) {
48
+ this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex);
49
+ await new Promise((resolve) => setTimeout(resolve, 1000));
50
+ }
51
+ }
52
+
53
+ // Final attempt if we haven't been able to open the server - rethrow errors if we still can't open.
54
+ server ??= await this.databaseOpenAttempt(options);
55
+ return server.connect(lockName);
56
+ }
57
+
58
+ private async databaseOpenAttempt(options: ResolvedWASQLiteOpenFactoryOptions): Promise<DatabaseServer> {
59
+ return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
60
+ const { dbFilename } = options;
61
+
62
+ let server: DatabaseServer | undefined = this.activeDatabases.get(dbFilename);
63
+ if (server == null) {
64
+ const needsNavigatorLocks = !isSharedWorker;
65
+ const connection = new RawSqliteConnection(options);
66
+ const withSafeConcurrency = new ConcurrentSqliteConnection(connection, needsNavigatorLocks);
67
+
68
+ // Initializing the RawSqliteConnection will run some pragmas that might write to the database file, so we want
69
+ // to do that in an exclusive lock. Note that OPEN_DB_LOCK is not enough for that, as another tab might have
70
+ // already created a connection (and is thus outside of OPEN_DB_LOCK) while currently writing to it.
71
+ const returnLease = await withSafeConcurrency.acquireMutex();
72
+ try {
73
+ await connection.init();
74
+ } catch (e) {
75
+ returnLease();
76
+ await connection.close();
77
+ throw e;
78
+ }
79
+ returnLease();
80
+
81
+ const onClose = () => this.activeDatabases.delete(dbFilename);
82
+ server = new DatabaseServer({
83
+ inner: withSafeConcurrency,
84
+ logger: this.logger,
85
+ onClose
86
+ });
87
+ this.activeDatabases.set(dbFilename, server);
88
+ }
89
+
90
+ return server;
91
+ });
92
+ }
93
+
94
+ closeAll() {
95
+ const existingDatabases = [...this.activeDatabases.values()];
96
+ return Promise.all(
97
+ existingDatabases.map((db) => {
98
+ db.forceClose();
99
+ })
100
+ );
101
+ }
102
+ }
103
+
104
+ export const isSharedWorker = 'SharedWorkerGlobalScope' in globalThis;
@@ -5,78 +5,31 @@
5
5
  import '@journeyapps/wa-sqlite';
6
6
  import { createBaseLogger, createLogger } from '@powersync/common';
7
7
  import * as Comlink from 'comlink';
8
- import { AsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection.js';
9
- import { WorkerDBOpenerOptions } from '../../db/adapters/wa-sqlite/WASQLiteOpenFactory.js';
10
- import { getNavigatorLocks } from '../../shared/navigator.js';
11
- import { SharedDBWorkerConnection, SharedWASQLiteConnection } from './SharedWASQLiteConnection.js';
12
- import { WorkerWASQLiteConnection } from './WorkerWASQLiteConnection.js';
8
+ import { isSharedWorker, MultiDatabaseServer } from './MultiDatabaseServer.js';
9
+ import { OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
13
10
 
14
11
  const baseLogger = createBaseLogger();
15
12
  baseLogger.useDefaults();
16
13
  const logger = createLogger('db-worker');
17
14
 
18
- const DBMap = new Map<string, SharedDBWorkerConnection>();
19
- const OPEN_DB_LOCK = 'open-wasqlite-db';
20
- let nextClientId = 1;
21
-
22
- const openDBShared = async (options: WorkerDBOpenerOptions): Promise<AsyncDatabaseConnection> => {
23
- // Prevent multiple simultaneous opens from causing race conditions
24
- return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
25
- const clientId = nextClientId++;
26
- const { dbFilename, logLevel } = options;
27
-
28
- logger.setLevel(logLevel);
29
-
30
- if (!DBMap.has(dbFilename)) {
31
- const clientIds = new Set<number>();
32
- // This format returns proxy objects for function callbacks
33
- const connection = new WorkerWASQLiteConnection(options);
34
- await connection.init();
35
-
36
- connection.registerListener({
37
- holdOverwritten: async () => {
38
- /**
39
- * The previous hold has been overwritten, without being released.
40
- * we need to cleanup any resources associated with it.
41
- * We can perform a rollback to release any potential transactions that were started.
42
- */
43
- await connection.execute('ROLLBACK').catch(() => {});
44
- }
45
- });
46
-
47
- DBMap.set(dbFilename, {
48
- clientIds,
49
- db: connection
50
- });
51
- }
52
-
53
- // Associates this clientId with the shared connection entry
54
- const sharedConnection = new SharedWASQLiteConnection({
55
- dbMap: DBMap,
56
- dbFilename,
57
- clientId,
58
- logger
59
- });
60
-
61
- return Comlink.proxy(sharedConnection);
62
- });
15
+ const server = new MultiDatabaseServer(logger);
16
+ const exposedFunctions: OpenWorkerConnection = {
17
+ connect: (config) => server.handleConnection(config),
18
+ connectToExisting: ({ identifier, lockName }) => server.connectToExisting(identifier, lockName)
63
19
  };
64
20
 
65
21
  // Check if we're in a SharedWorker context
66
- if (typeof SharedWorkerGlobalScope !== 'undefined') {
22
+ if (isSharedWorker) {
67
23
  const _self: SharedWorkerGlobalScope = self as any;
68
24
  _self.onconnect = function (event: MessageEvent<string>) {
69
25
  const port = event.ports[0];
70
- Comlink.expose(openDBShared, port);
26
+ Comlink.expose(exposedFunctions, port);
71
27
  };
72
28
  } else {
73
29
  // A dedicated worker can be shared externally
74
- Comlink.expose(openDBShared);
30
+ Comlink.expose(exposedFunctions);
75
31
  }
76
32
 
77
33
  addEventListener('unload', () => {
78
- Array.from(DBMap.values()).forEach(async (dbConnection) => {
79
- const { db } = dbConnection;
80
- db.close?.();
81
- });
34
+ server.closeAll();
82
35
  });
@@ -1,6 +1,6 @@
1
1
  import * as Comlink from 'comlink';
2
- import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection.js';
3
- import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/WASQLiteConnection.js';
2
+ import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js';
3
+ import { OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
4
4
 
5
5
  /**
6
6
  * Opens a shared or dedicated worker which exposes opening of database connections
@@ -49,7 +49,7 @@ export function openWorkerDatabasePort(
49
49
  * a worker.
50
50
  */
51
51
  export function getWorkerDatabaseOpener(workerIdentifier: string, multipleTabs = true, worker: string | URL = '') {
52
- return Comlink.wrap<OpenAsyncDatabaseConnection>(openWorkerDatabasePort(workerIdentifier, multipleTabs, worker));
52
+ return Comlink.wrap<OpenWorkerConnection>(openWorkerDatabasePort(workerIdentifier, multipleTabs, worker));
53
53
  }
54
54
 
55
55
  export function resolveWorkerDatabasePortFactory(worker: () => Worker | SharedWorker) {
@@ -2,12 +2,18 @@ import {
2
2
  AbortOperation,
3
3
  BaseObserver,
4
4
  ConnectionManager,
5
+ ConnectionPool,
5
6
  DBAdapter,
7
+ DBAdapterDefaultMixin,
8
+ DBAdapterListener,
9
+ DBLockOptions,
10
+ LockContext,
6
11
  PowerSyncBackendConnector,
7
12
  SqliteBucketStorage,
8
13
  SubscribedStream,
9
14
  SyncStatus,
10
15
  createLogger,
16
+ Mutex,
11
17
  type ILogLevel,
12
18
  type ILogger,
13
19
  type PowerSyncConnectionOptions,
@@ -15,7 +21,6 @@ import {
15
21
  type StreamingSyncImplementationListener,
16
22
  type SyncStatusOptions
17
23
  } from '@powersync/common';
18
- import { Mutex } from 'async-mutex';
19
24
  import * as Comlink from 'comlink';
20
25
  import { WebRemote } from '../../db/sync/WebRemote.js';
21
26
  import {
@@ -23,12 +28,11 @@ import {
23
28
  WebStreamingSyncImplementationOptions
24
29
  } from '../../db/sync/WebStreamingSyncImplementation.js';
25
30
 
26
- import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection.js';
27
- import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter.js';
28
- import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection.js';
29
31
  import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags.js';
30
32
  import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider.js';
31
33
  import { BroadcastLogger } from './BroadcastLogger.js';
34
+ import { DatabaseClient, OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
35
+ import { generateTabCloseSignal } from '../../shared/tab_close_signal.js';
32
36
 
33
37
  /**
34
38
  * @internal
@@ -116,7 +120,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
116
120
  protected connectionManager: ConnectionManager;
117
121
  syncStatus: SyncStatus;
118
122
  broadCastLogger: ILogger;
119
- protected distributedDB: DBAdapter | null;
123
+ protected readonly database = this.generateReconnectableDatabase();
120
124
 
121
125
  constructor() {
122
126
  super();
@@ -135,9 +139,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
135
139
  });
136
140
  });
137
141
 
138
- // Should be configured once we get params
139
- this.distributedDB = null;
140
-
141
142
  this.syncStatus = new SyncStatus({});
142
143
  this.broadCastLogger = new BroadcastLogger(this.ports);
143
144
 
@@ -254,40 +255,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
254
255
  this.logger = this.broadCastLogger;
255
256
  }
256
257
 
257
- const lockedAdapter = new LockedAsyncDatabaseAdapter({
258
- name: params.dbParams.dbFilename,
259
- openConnection: async () => {
260
- // Gets a connection from the clients when a new connection is requested.
261
- const db = await this.openInternalDB();
262
- db.registerListener({
263
- closing: () => {
264
- lockedAdapter.reOpenInternalDB();
265
- }
266
- });
267
- return db;
268
- },
269
- logger: this.logger,
270
- reOpenOnConnectionClosed: true
271
- });
272
- this.distributedDB = lockedAdapter;
273
- await lockedAdapter.init();
274
-
275
- lockedAdapter.registerListener({
276
- databaseReOpened: () => {
277
- // We may have missed some table updates while the database was closed.
278
- // We can poke the crud in case we missed any updates.
279
- this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
280
-
281
- /**
282
- * FIXME or IMPROVE ME
283
- * The Rust client implementation stores sync state on the connection level.
284
- * Reopening the database causes a state machine error which should cause the
285
- * StreamingSyncImplementation to reconnect. It would be nicer if we could trigger
286
- * this reconnect earlier.
287
- * This reconnect is not required for IndexedDB.
288
- */
289
- }
290
- });
258
+ // Ensure we have a usable database connection, the reconnectable database will connect lazily on first use.
259
+ await this.database.readLock(async () => {});
291
260
 
292
261
  self.onerror = (event) => {
293
262
  // Share any uncaught events on the broadcast logger
@@ -429,7 +398,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
429
398
  const syncParams = this.syncParams!;
430
399
  // Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
431
400
  return new WebStreamingSyncImplementation({
432
- adapter: new SqliteBucketStorage(this.distributedDB!, this.logger),
401
+ adapter: new SqliteBucketStorage(this.database, this.logger),
433
402
  remote: new WebRemote(
434
403
  {
435
404
  invalidateCredentials: async () => {
@@ -502,9 +471,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
502
471
  }
503
472
 
504
473
  /**
505
- * Opens a worker wrapped database connection. Using the last connected client port.
474
+ * Requests a random client to share its database connection with us.
506
475
  */
507
- protected async openInternalDB() {
476
+ private async openInternalDB(
477
+ handleClosed: (db: DatabaseClient<ResolvedWebSQLOpenOptions>) => void
478
+ ): Promise<DatabaseClient<ResolvedWebSQLOpenOptions>> {
508
479
  const client = await this.getRandomWrappedPort();
509
480
  if (!client) {
510
481
  // Should not really happen in practice
@@ -544,9 +515,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
544
515
  throw ex;
545
516
  });
546
517
 
547
- const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
518
+ const remote = Comlink.wrap<OpenWorkerConnection>(workerPort);
548
519
  const identifier = this.syncParams!.dbParams.dbFilename;
549
520
 
521
+ const clientLockName = await generateTabCloseSignal();
522
+
550
523
  /**
551
524
  * The open could fail if the tab is closed while we're busy opening the database.
552
525
  * This operation is typically executed inside an exclusive portMutex lock.
@@ -554,7 +527,19 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
554
527
  * We can't rely on the closeListeners to abort the operation if the tab is closed.
555
528
  */
556
529
  const db = await withAbort({
557
- action: () => remote(this.syncParams!.dbParams),
530
+ action: async () => {
531
+ const clientView = await remote.connectToExisting({ identifier, lockName: clientLockName });
532
+ return new DatabaseClient<ResolvedWebSQLOpenOptions>(
533
+ {
534
+ connection: clientView,
535
+ source: remote,
536
+ // It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
537
+ // that and ensure pending requests are aborted when the tab is closed.
538
+ remoteCanCloseUnexpectedly: true
539
+ },
540
+ this.syncParams!.dbParams
541
+ );
542
+ },
558
543
  signal: abortController.signal,
559
544
  cleanupOnAbort: (db) => {
560
545
  db.close();
@@ -566,26 +551,101 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
566
551
 
567
552
  clearTimeout(timeout);
568
553
 
569
- const wrapped = new WorkerWrappedAsyncDatabaseConnection({
570
- remote,
571
- baseConnection: db,
572
- identifier,
573
- // It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
574
- // that and ensure pending requests are aborted when the tab is closed.
575
- remoteCanCloseUnexpectedly: true
576
- });
577
554
  client.closeListeners.push(async () => {
578
555
  this.logger.info('Aborting open connection because associated tab closed.');
556
+ handleClosed(db);
579
557
  /**
580
558
  * Don't await this close operation. It might never resolve if the tab is closed.
581
559
  * We mark the remote as closed first, this will reject any pending requests.
582
560
  * We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
583
561
  */
584
- wrapped.markRemoteClosed();
585
- wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
562
+ db.markRemoteClosed();
563
+ db.close().catch((ex) => this.logger.warn('error closing database connection', ex));
586
564
  });
565
+ return db;
566
+ }
567
+
568
+ private generateReconnectableDatabase(): DBAdapter {
569
+ const syncParams = this.syncParams;
570
+ const sharedSync = this;
571
+
572
+ class ReconnectPool extends BaseObserver<DBAdapterListener> implements ConnectionPool {
573
+ private connectionState:
574
+ | null
575
+ | DatabaseClient<ResolvedWebSQLOpenOptions>
576
+ | Promise<DatabaseClient<ResolvedWebSQLOpenOptions>> = null;
577
+
578
+ get name(): string {
579
+ return syncParams?.dbParams.dbFilename!;
580
+ }
581
+
582
+ private async connect(): Promise<DatabaseClient<ResolvedWebSQLOpenOptions>> {
583
+ if (this.connectionState == null) {
584
+ const handleClosed = this.handleClientClosed.bind(this);
585
+ this.connectionState = (async () => {
586
+ try {
587
+ const db = await sharedSync.openInternalDB(handleClosed);
588
+ db.registerListener({
589
+ tablesUpdated: (notification) => {
590
+ this.iterateListeners((l) => l.tablesUpdated?.(notification));
591
+ }
592
+ });
593
+ this.connectionState = db;
594
+ return db;
595
+ } catch (e) {
596
+ // Allow reconnecting when the database is used again.
597
+ this.connectionState = null;
598
+ throw e;
599
+ }
600
+ })();
601
+ }
602
+
603
+ return await this.connectionState;
604
+ }
605
+
606
+ async close() {
607
+ if (this.connectionState != null) {
608
+ await (await this.connectionState).close();
609
+ }
610
+ }
611
+
612
+ handleClientClosed(client: DatabaseClient<ResolvedWebSQLOpenOptions>) {
613
+ if (client === this.connectionState) {
614
+ this.connectionState = null;
615
+
616
+ // We may have missed some table updates while the database was closed.
617
+ // We can poke the crud in case we missed any updates.
618
+ const impl = sharedSync.connectionManager.syncStreamImplementation! as WebStreamingSyncImplementation;
619
+ impl?.triggerCrudUpload();
620
+
621
+ /**
622
+ * FIXME or IMPROVE ME
623
+ * The Rust client implementation stores sync state on the connection level.
624
+ * Reopening the database causes a state machine error which should cause the
625
+ * StreamingSyncImplementation to reconnect. It would be nicer if we could trigger
626
+ * this reconnect earlier.
627
+ * This reconnect is not required for IndexedDB.
628
+ */
629
+ }
630
+ }
631
+
632
+ async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
633
+ const db = await this.connect();
634
+ return db.readLock(fn, options);
635
+ }
636
+
637
+ async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
638
+ const db = await this.connect();
639
+ return db.writeLock(fn, options);
640
+ }
641
+
642
+ async refreshSchema(): Promise<void> {
643
+ // Not used by sync client.
644
+ }
645
+ }
587
646
 
588
- return wrapped;
647
+ const Adapter = DBAdapterDefaultMixin(ReconnectPool);
648
+ return new Adapter();
589
649
  }
590
650
 
591
651
  /**