@powersync/web 0.0.0-dev-20260311103504 → 0.0.0-dev-20260503073249

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 (121) hide show
  1. package/dist/2075a31bb151adbb9767.wasm +0 -0
  2. package/dist/3322bc84de986b63c2cd.wasm +0 -0
  3. package/dist/8e97452e297be23b5e50.wasm +0 -0
  4. package/dist/fbc178b70d530e8ce02b.wasm +0 -0
  5. package/dist/index.umd.js +5341 -1279
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +1113 -3526
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +1397 -1332
  10. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  11. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-9af0a7.umd.js +31 -0
  12. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-9af0a7.umd.js.map +1 -0
  13. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-bbf5a9.umd.js +31 -0
  14. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-bbf5a9.umd.js.map +1 -0
  15. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-c26e0f.umd.js +31 -0
  16. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-cc5fcc.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-c26e0f.umd.js.map} +1 -1
  17. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +31 -0
  18. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map} +1 -1
  19. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-2fb422.umd.js +3562 -0
  20. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-2fb422.umd.js.map +1 -0
  21. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-0df390.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-96fb23.umd.js} +16 -16
  22. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-0df390.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-96fb23.umd.js.map} +1 -1
  23. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-151024.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-c89911.umd.js} +12 -12
  24. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-151024.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-c89911.umd.js.map} +1 -1
  25. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-c01ef0.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-ec4eb1.umd.js} +14 -14
  26. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-c01ef0.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-ec4eb1.umd.js.map} +1 -1
  27. package/lib/package.json +4 -5
  28. package/lib/src/db/PowerSyncDatabase.d.ts +2 -3
  29. package/lib/src/db/PowerSyncDatabase.js +3 -12
  30. package/lib/src/db/adapters/AsyncWebAdapter.d.ts +50 -0
  31. package/lib/src/db/adapters/AsyncWebAdapter.js +163 -0
  32. package/lib/src/db/adapters/SSRDBAdapter.d.ts +1 -2
  33. package/lib/src/db/adapters/SSRDBAdapter.js +5 -6
  34. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.d.ts +56 -0
  35. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.js +121 -0
  36. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.d.ts +54 -0
  37. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.js +227 -0
  38. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.d.ts +47 -0
  39. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +145 -0
  40. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +46 -0
  41. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +147 -0
  42. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +28 -6
  43. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +104 -55
  44. package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +50 -0
  45. package/lib/src/db/adapters/wa-sqlite/vfs.js +76 -0
  46. package/lib/src/db/adapters/web-sql-flags.d.ts +5 -0
  47. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +5 -2
  48. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +6 -3
  49. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +4 -19
  50. package/lib/src/index.d.ts +1 -4
  51. package/lib/src/index.js +1 -4
  52. package/lib/src/shared/tab_close_signal.d.ts +11 -0
  53. package/lib/src/shared/tab_close_signal.js +26 -0
  54. package/lib/src/worker/db/MultiDatabaseServer.d.ts +17 -0
  55. package/lib/src/worker/db/MultiDatabaseServer.js +89 -0
  56. package/lib/src/worker/db/WASQLiteDB.worker.js +9 -48
  57. package/lib/src/worker/db/open-worker-database.d.ts +3 -3
  58. package/lib/src/worker/db/open-worker-database.js +2 -2
  59. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +5 -6
  60. package/lib/src/worker/sync/SharedSyncImplementation.js +88 -54
  61. package/lib/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +5 -6
  63. package/src/db/PowerSyncDatabase.ts +4 -12
  64. package/src/db/adapters/AsyncWebAdapter.ts +207 -0
  65. package/src/db/adapters/SSRDBAdapter.ts +7 -7
  66. package/src/db/adapters/wa-sqlite/ConcurrentConnection.ts +137 -0
  67. package/src/db/adapters/wa-sqlite/DatabaseClient.ts +325 -0
  68. package/src/db/adapters/wa-sqlite/DatabaseServer.ts +203 -0
  69. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +194 -0
  70. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +152 -63
  71. package/src/db/adapters/wa-sqlite/vfs.ts +96 -0
  72. package/src/db/adapters/web-sql-flags.ts +6 -0
  73. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +7 -3
  74. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +4 -20
  75. package/src/index.ts +1 -4
  76. package/src/shared/tab_close_signal.ts +28 -0
  77. package/src/worker/db/MultiDatabaseServer.ts +107 -0
  78. package/src/worker/db/WASQLiteDB.worker.ts +10 -57
  79. package/src/worker/db/open-worker-database.ts +4 -4
  80. package/src/worker/sync/SharedSyncImplementation.ts +114 -58
  81. package/dist/26d61ca9f5694d064635.wasm +0 -0
  82. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +0 -1878
  83. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +0 -1
  84. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js +0 -555
  85. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js.map +0 -1
  86. package/dist/b4c6283dc473b6b3fd24.wasm +0 -0
  87. package/dist/c78985091a0b22aaef03.wasm +0 -0
  88. package/dist/ca59e199e1138b553fad.wasm +0 -0
  89. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-b9c070.umd.js +0 -31
  90. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-b9c070.umd.js.map +0 -1
  91. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-c99c07.umd.js +0 -31
  92. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-c99c07.umd.js.map +0 -1
  93. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-cc5fcc.umd.js +0 -31
  94. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +0 -31
  95. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +0 -17
  96. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +0 -33
  97. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +0 -49
  98. package/lib/src/db/adapters/AsyncDatabaseConnection.js +0 -1
  99. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +0 -109
  100. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +0 -401
  101. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +0 -59
  102. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +0 -147
  103. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +0 -12
  104. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +0 -19
  105. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +0 -155
  106. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +0 -401
  107. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +0 -32
  108. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +0 -49
  109. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +0 -42
  110. package/lib/src/worker/db/SharedWASQLiteConnection.js +0 -90
  111. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +0 -9
  112. package/lib/src/worker/db/WorkerWASQLiteConnection.js +0 -12
  113. package/src/db/adapters/AbstractWebSQLOpenFactory.ts +0 -48
  114. package/src/db/adapters/AsyncDatabaseConnection.ts +0 -55
  115. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +0 -490
  116. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +0 -201
  117. package/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.ts +0 -23
  118. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +0 -497
  119. package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +0 -86
  120. package/src/worker/db/SharedWASQLiteConnection.ts +0 -131
  121. package/src/worker/db/WorkerWASQLiteConnection.ts +0 -14
@@ -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,107 @@
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
+ // We don't need navigator locks for shared workers because all queries run in this shared worker exclusively.
65
+ // For read-only connections, we use a VFS that supports concurrent reads (so a single lock on the connection is
66
+ // fine).
67
+ const needsNavigatorLocks = !(isSharedWorker || options.isReadOnly);
68
+ const connection = new RawSqliteConnection(options);
69
+ const withSafeConcurrency = new ConcurrentSqliteConnection(connection, needsNavigatorLocks);
70
+
71
+ // Initializing the RawSqliteConnection will run some pragmas that might write to the database file, so we want
72
+ // to do that in an exclusive lock. Note that OPEN_DB_LOCK is not enough for that, as another tab might have
73
+ // already created a connection (and is thus outside of OPEN_DB_LOCK) while currently writing to it.
74
+ const returnLease = await withSafeConcurrency.acquireMutex();
75
+ try {
76
+ await connection.init();
77
+ } catch (e) {
78
+ returnLease();
79
+ await connection.close();
80
+ throw e;
81
+ }
82
+ returnLease();
83
+
84
+ const onClose = () => this.activeDatabases.delete(dbFilename);
85
+ server = new DatabaseServer({
86
+ inner: withSafeConcurrency,
87
+ logger: this.logger,
88
+ onClose
89
+ });
90
+ this.activeDatabases.set(dbFilename, server);
91
+ }
92
+
93
+ return server;
94
+ });
95
+ }
96
+
97
+ closeAll() {
98
+ const existingDatabases = [...this.activeDatabases.values()];
99
+ return Promise.all(
100
+ existingDatabases.map((db) => {
101
+ db.forceClose();
102
+ })
103
+ );
104
+ }
105
+ }
106
+
107
+ 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 { vfsRequiresDedicatedWorkers, 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
@@ -11,7 +11,7 @@ export function openWorkerDatabasePort(
11
11
  worker: string | URL = '',
12
12
  vfs?: WASQLiteVFS
13
13
  ) {
14
- const needsDedicated = vfs == WASQLiteVFS.AccessHandlePoolVFS || vfs == WASQLiteVFS.OPFSCoopSyncVFS;
14
+ const needsDedicated = vfs && vfsRequiresDedicatedWorkers(vfs);
15
15
 
16
16
  if (worker) {
17
17
  return !needsDedicated && multipleTabs
@@ -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,97 @@ 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
+ // The Rust client implementation stores sync state on the connection level. Reopening the database causes a
622
+ // disruption of the connection state and forces us to reconnect. We want to do that as soon as possible to
623
+ // minimize downtime.
624
+ impl?.markConnectionMayHaveChanged();
625
+ }
626
+ }
627
+
628
+ async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
629
+ const db = await this.connect();
630
+ return db.readLock(fn, options);
631
+ }
632
+
633
+ async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
634
+ const db = await this.connect();
635
+ return db.writeLock(fn, options);
636
+ }
637
+
638
+ async refreshSchema(): Promise<void> {
639
+ // Not used by sync client.
640
+ }
641
+ }
587
642
 
588
- return wrapped;
643
+ const Adapter = DBAdapterDefaultMixin(ReconnectPool);
644
+ return new Adapter();
589
645
  }
590
646
 
591
647
  /**
Binary file