@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,227 @@
1
+ import { DBGetUtilsDefaultMixin, BaseObserver, ConnectionClosedError } from '@powersync/common';
2
+ import * as Comlink from 'comlink';
3
+ /**
4
+ * A single-connection {@link ConnectionPool} implementation based on a worker connection.
5
+ */
6
+ export class DatabaseClient extends BaseObserver {
7
+ options;
8
+ config;
9
+ #connection;
10
+ #shareConnectionAbortController = new AbortController();
11
+ #receiveTableUpdates;
12
+ constructor(options, config) {
13
+ super();
14
+ this.options = options;
15
+ this.config = config;
16
+ this.#connection = {
17
+ connection: options.connection,
18
+ notifyRemoteClosed: options.remoteCanCloseUnexpectedly ? new AbortController() : undefined,
19
+ traceQueries: config.debugMode === true
20
+ };
21
+ const { port1, port2 } = new MessageChannel();
22
+ options.connection.setUpdateListener(Comlink.transfer(port1, [port1]));
23
+ this.#receiveTableUpdates = port2;
24
+ port2.onmessage = (event) => {
25
+ const tables = event.data;
26
+ const notification = {
27
+ tables,
28
+ groupedUpdates: {},
29
+ rawUpdates: []
30
+ };
31
+ this.iterateListeners((l) => {
32
+ l.tablesUpdated && l.tablesUpdated(notification);
33
+ });
34
+ };
35
+ }
36
+ get name() {
37
+ return this.config.dbFilename;
38
+ }
39
+ /**
40
+ * Marks the remote as closed.
41
+ *
42
+ * This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
43
+ * it happens, all outstanding requests on this pool would never resolve. To avoid livelocks in this scenario, we
44
+ * throw on all outstanding promises and forbid new calls.
45
+ */
46
+ markRemoteClosed() {
47
+ // Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
48
+ // set.
49
+ this.#connection.notifyRemoteClosed.abort();
50
+ }
51
+ async close() {
52
+ // This connection is no longer shared, so we can close locks held for shareConnection calls.
53
+ this.#shareConnectionAbortController.abort();
54
+ this.#receiveTableUpdates.close();
55
+ await useConnectionState(this.#connection, (c) => c.close(), true);
56
+ this.options.onClose?.();
57
+ this.options.source?.[Comlink.releaseProxy]();
58
+ }
59
+ readLock(fn, options) {
60
+ return this.#lock(false, fn, options);
61
+ }
62
+ writeLock(fn, options) {
63
+ return this.#lock(true, fn, options);
64
+ }
65
+ async #lock(write, fn, options) {
66
+ const token = await useConnectionState(this.#connection, (c) => c.requestAccess(write, options?.timeoutMs));
67
+ try {
68
+ return await fn(new ClientLockContext(this.#connection, token));
69
+ }
70
+ finally {
71
+ await useConnectionState(this.#connection, (c) => c.completeAccess(token));
72
+ }
73
+ }
74
+ async refreshSchema() {
75
+ // Currently a no-op on the web.
76
+ }
77
+ async shareConnection() {
78
+ /**
79
+ * Hold a navigator lock in order to avoid features such as Chrome's frozen tabs,
80
+ * or Edge's sleeping tabs from pausing the thread for this connection.
81
+ * This promise resolves once a lock is obtained.
82
+ * This lock will be held as long as this connection is open.
83
+ * The `shareConnection` method should not be called on multiple tabs concurrently.
84
+ */
85
+ const abort = this.#shareConnectionAbortController;
86
+ const source = this.options.source;
87
+ if (source == null) {
88
+ throw new Error(`shareConnection() is only available for connections based by workers.`);
89
+ }
90
+ await new Promise((resolve, reject) => navigator.locks
91
+ .request(`shared-connection-${this.name}-${Date.now()}-${Math.round(Math.random() * 10000)}`, {
92
+ signal: abort.signal
93
+ }, async () => {
94
+ resolve();
95
+ // Free the lock when the connection is already closed.
96
+ if (abort.signal.aborted) {
97
+ return;
98
+ }
99
+ // Hold the lock while the shared connection is in use.
100
+ await new Promise((releaseLock) => {
101
+ abort.signal.addEventListener('abort', () => {
102
+ releaseLock();
103
+ });
104
+ });
105
+ })
106
+ // We aren't concerned with abort errors here
107
+ .catch((ex) => {
108
+ if (ex.name == 'AbortError') {
109
+ resolve();
110
+ }
111
+ else {
112
+ reject(ex);
113
+ }
114
+ }));
115
+ const newPort = await source[Comlink.createEndpoint]();
116
+ return { port: newPort, identifier: this.name };
117
+ }
118
+ getConfiguration() {
119
+ return this.config;
120
+ }
121
+ }
122
+ /**
123
+ * A {@link SqlExecutor} implemented by sending commands to a worker.
124
+ *
125
+ * While an instance is active, it has exclusive access to the underlying database connection (as represented by its
126
+ * token).
127
+ */
128
+ class ClientSqlExecutor {
129
+ #connection;
130
+ #token;
131
+ constructor(connection, token) {
132
+ this.#connection = connection;
133
+ this.#token = token;
134
+ }
135
+ /**
136
+ * Requests an operation from the worker, potentially tracing it if that option has been enabled.
137
+ */
138
+ async maybeTrace(fn, describeForTrace) {
139
+ if (this.#connection.traceQueries) {
140
+ const start = performance.now();
141
+ const description = describeForTrace();
142
+ try {
143
+ const r = await useConnectionState(this.#connection, fn);
144
+ performance.measure(`[SQL] ${description}`, { start });
145
+ return r;
146
+ }
147
+ catch (e) {
148
+ performance.measure(`[SQL] [ERROR: ${e.message}] ${description}`, { start });
149
+ throw e;
150
+ }
151
+ }
152
+ else {
153
+ return useConnectionState(this.#connection, fn);
154
+ }
155
+ }
156
+ async execute(query, params) {
157
+ const rs = await this.#executeOnWorker(query, params);
158
+ let rows;
159
+ if (rs.resultSet) {
160
+ const resultSet = rs.resultSet;
161
+ function rowToJavaScriptObject(row) {
162
+ const obj = {};
163
+ resultSet.columns.forEach((key, idx) => (obj[key] = row[idx]));
164
+ return obj;
165
+ }
166
+ const mapped = resultSet.rows.map(rowToJavaScriptObject);
167
+ rows = {
168
+ _array: mapped,
169
+ length: mapped.length,
170
+ item: (idx) => mapped[idx]
171
+ };
172
+ }
173
+ return {
174
+ rowsAffected: rs.changes,
175
+ insertId: rs.lastInsertRowId,
176
+ rows
177
+ };
178
+ }
179
+ async executeRaw(query, params) {
180
+ const rs = await this.#executeOnWorker(query, params);
181
+ return rs.resultSet?.rows ?? [];
182
+ }
183
+ async #executeOnWorker(query, params) {
184
+ return this.maybeTrace((c) => c.execute(this.#token, query, params), () => query);
185
+ }
186
+ async executeBatch(query, params = []) {
187
+ const results = await this.maybeTrace((c) => c.executeBatch(this.#token, query, params), () => `${query} (batch of ${params.length})`);
188
+ const result = { insertId: undefined, rowsAffected: 0 };
189
+ for (const source of results) {
190
+ result.insertId = source.lastInsertRowId;
191
+ result.rowsAffected += source.changes;
192
+ }
193
+ return result;
194
+ }
195
+ }
196
+ class ClientLockContext extends DBGetUtilsDefaultMixin(ClientSqlExecutor) {
197
+ }
198
+ async function useConnectionState(state, workerPromise, fireActionOnAbort = false) {
199
+ const controller = state.notifyRemoteClosed;
200
+ if (controller) {
201
+ return new Promise((resolve, reject) => {
202
+ if (controller.signal.aborted) {
203
+ reject(new ConnectionClosedError('Called operation on closed remote'));
204
+ if (!fireActionOnAbort) {
205
+ // Don't run the operation if we're going to reject
206
+ // We might want to fire-and-forget the operation in some cases (like a close operation)
207
+ return;
208
+ }
209
+ }
210
+ function handleAbort() {
211
+ reject(new ConnectionClosedError('Remote peer closed with request in flight'));
212
+ }
213
+ function completePromise(action) {
214
+ controller.signal.removeEventListener('abort', handleAbort);
215
+ action();
216
+ }
217
+ controller.signal.addEventListener('abort', handleAbort);
218
+ workerPromise(state.connection)
219
+ .then((data) => completePromise(() => resolve(data)))
220
+ .catch((e) => completePromise(() => reject(e)));
221
+ });
222
+ }
223
+ else {
224
+ // Can't close, so just return the inner worker promise unguarded.
225
+ return workerPromise(state.connection);
226
+ }
227
+ }
@@ -0,0 +1,47 @@
1
+ import { ILogger } from '@powersync/common';
2
+ import { ConcurrentSqliteConnection } from './ConcurrentConnection.js';
3
+ import { RawQueryResult } from './RawSqliteConnection.js';
4
+ export interface DatabaseServerOptions {
5
+ inner: ConcurrentSqliteConnection;
6
+ onClose: () => void;
7
+ logger: ILogger;
8
+ }
9
+ /**
10
+ * Access to a WA-sqlite connection that can be shared with multiple clients sending queries over an RPC protocol built
11
+ * with the Comlink package.
12
+ */
13
+ export declare class DatabaseServer {
14
+ #private;
15
+ constructor(options: DatabaseServerOptions);
16
+ /**
17
+ * Called by clients when they wish to connect to this database.
18
+ *
19
+ * @param lockName A lock that is currently held by the client. When the lock is returned, we know the client is gone
20
+ * and that we need to clean up resources.
21
+ */
22
+ connect(lockName?: string): Promise<ClientConnectionView>;
23
+ forceClose(): Promise<void>;
24
+ }
25
+ export interface ClientConnectionView {
26
+ close(): Promise<void>;
27
+ /**
28
+ * Only used for testing purposes.
29
+ */
30
+ debugIsAutoCommit(): Promise<boolean>;
31
+ /**
32
+ * Requests exclusive access to this database connection.
33
+ *
34
+ * Returns a token that can be used with the query methods. It must be returned with {@link completeAccess} to
35
+ * give other clients access to the database afterwards.
36
+ */
37
+ requestAccess(write: boolean, timeoutMs?: number): Promise<string>;
38
+ execute(token: string, sql: string, params: any[] | undefined): Promise<RawQueryResult>;
39
+ executeBatch(token: string, sql: string, params: any[][]): Promise<RawQueryResult[]>;
40
+ completeAccess(token: string): Promise<void>;
41
+ /**
42
+ * Sends update notifications to the given message port.
43
+ *
44
+ * Update notifications are posted as a `string[]` message.
45
+ */
46
+ setUpdateListener(listener: MessagePort): Promise<void>;
47
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Access to a WA-sqlite connection that can be shared with multiple clients sending queries over an RPC protocol built
3
+ * with the Comlink package.
4
+ */
5
+ export class DatabaseServer {
6
+ #options;
7
+ #nextClientId = 0;
8
+ #activeClients = new Set();
9
+ // TODO: Don't use a broadcast channel for connections managed by a shared worker.
10
+ #updateBroadcastChannel;
11
+ #clientTableListeners = new Set();
12
+ constructor(options) {
13
+ this.#options = options;
14
+ const inner = options.inner;
15
+ this.#updateBroadcastChannel = new BroadcastChannel(`${inner.options.dbFilename}-table-updates`);
16
+ this.#updateBroadcastChannel.onmessage = ({ data }) => {
17
+ this.#pushTableUpdateToClients(data);
18
+ };
19
+ }
20
+ #pushTableUpdateToClients(changedTables) {
21
+ for (const listener of this.#clientTableListeners) {
22
+ listener.postMessage(changedTables);
23
+ }
24
+ }
25
+ get #inner() {
26
+ return this.#options.inner;
27
+ }
28
+ get #logger() {
29
+ return this.#options.logger;
30
+ }
31
+ /**
32
+ * Called by clients when they wish to connect to this database.
33
+ *
34
+ * @param lockName A lock that is currently held by the client. When the lock is returned, we know the client is gone
35
+ * and that we need to clean up resources.
36
+ */
37
+ async connect(lockName) {
38
+ let isOpen = true;
39
+ const clientId = this.#nextClientId++;
40
+ this.#activeClients.add(clientId);
41
+ let connectionLeases = new Map();
42
+ let currentTableListener;
43
+ function requireOpen() {
44
+ if (!isOpen) {
45
+ throw new Error('Client has already been closed');
46
+ }
47
+ }
48
+ function requireOpenAndLease(lease) {
49
+ requireOpen();
50
+ const token = connectionLeases.get(lease);
51
+ if (!token) {
52
+ throw new Error('Attempted to use a connection lease that has already been returned.');
53
+ }
54
+ return token;
55
+ }
56
+ const close = async () => {
57
+ if (isOpen) {
58
+ isOpen = false;
59
+ if (currentTableListener) {
60
+ this.#clientTableListeners.delete(currentTableListener);
61
+ }
62
+ // If the client holds a connection lease it hasn't returned, return that now.
63
+ for (const { lease } of connectionLeases.values()) {
64
+ this.#logger.debug(`Closing connection lease that hasn't been returned.`);
65
+ await lease.returnLease();
66
+ }
67
+ this.#activeClients.delete(clientId);
68
+ if (this.#activeClients.size == 0) {
69
+ await this.forceClose();
70
+ }
71
+ else {
72
+ this.#logger.debug('Keeping underlying connection active since its used by other clients.');
73
+ }
74
+ }
75
+ };
76
+ if (lockName) {
77
+ navigator.locks.request(lockName, {}, () => {
78
+ close();
79
+ });
80
+ }
81
+ return {
82
+ close,
83
+ debugIsAutoCommit: async () => {
84
+ return this.#inner.unsafeUseInner().isAutoCommit();
85
+ },
86
+ requestAccess: async (write, timeoutMs) => {
87
+ requireOpen();
88
+ const lease = await this.#inner.acquireConnection(timeoutMs != null ? AbortSignal.timeout(timeoutMs) : undefined);
89
+ if (!isOpen) {
90
+ // Race between requestAccess and close(), the connection was closed while we tried to acquire a lease.
91
+ await lease.returnLease();
92
+ return requireOpen();
93
+ }
94
+ const token = crypto.randomUUID();
95
+ connectionLeases.set(token, { lease, write });
96
+ return token;
97
+ },
98
+ completeAccess: async (token) => {
99
+ const lease = requireOpenAndLease(token);
100
+ connectionLeases.delete(token);
101
+ try {
102
+ if (lease.write) {
103
+ // Collect update hooks invoked while the client had the write connection.
104
+ const { resultSet } = await lease.lease.use((conn) => conn.execute(`SELECT powersync_update_hooks('get')`));
105
+ if (resultSet) {
106
+ const updatedTables = JSON.parse(resultSet.rows[0][0]);
107
+ if (updatedTables.length) {
108
+ this.#updateBroadcastChannel.postMessage(updatedTables);
109
+ this.#pushTableUpdateToClients(updatedTables);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ finally {
115
+ await lease.lease.returnLease();
116
+ }
117
+ },
118
+ execute: async (token, sql, params) => {
119
+ const { lease } = requireOpenAndLease(token);
120
+ return await lease.use((db) => db.execute(sql, params));
121
+ },
122
+ executeBatch: async (token, sql, params) => {
123
+ const { lease } = requireOpenAndLease(token);
124
+ return await lease.use((db) => db.executeBatch(sql, params));
125
+ },
126
+ setUpdateListener: async (listener) => {
127
+ requireOpen();
128
+ if (currentTableListener) {
129
+ this.#clientTableListeners.delete(currentTableListener);
130
+ }
131
+ currentTableListener = listener;
132
+ if (listener) {
133
+ this.#clientTableListeners.add(listener);
134
+ }
135
+ }
136
+ };
137
+ }
138
+ async forceClose() {
139
+ this.#logger.debug(`Closing connection to ${this.#inner.options}.`);
140
+ const connection = this.#inner;
141
+ this.#options.onClose();
142
+ this.#updateBroadcastChannel.close();
143
+ await connection.close();
144
+ }
145
+ }
@@ -0,0 +1,46 @@
1
+ import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory.js';
2
+ export interface RawResultSet {
3
+ columns: string[];
4
+ rows: SQLiteCompatibleType[][];
5
+ }
6
+ export interface RawQueryResult {
7
+ changes: number;
8
+ lastInsertRowId: number;
9
+ autocommit: boolean;
10
+ resultSet: RawResultSet | undefined;
11
+ }
12
+ /**
13
+ * A small wrapper around WA-sqlite to help with opening databases and running statements by preparing them internally.
14
+ *
15
+ * This is an internal class, and it must never be used directly. Wrappers are required to ensure raw connections aren't
16
+ * used concurrently across tabs.
17
+ */
18
+ export declare class RawSqliteConnection {
19
+ readonly options: ResolvedWASQLiteOpenFactoryOptions;
20
+ private _sqliteAPI;
21
+ /**
22
+ * The `sqlite3*` connection pointer.
23
+ */
24
+ private db;
25
+ private _moduleFactory;
26
+ constructor(options: ResolvedWASQLiteOpenFactoryOptions);
27
+ get isOpen(): boolean;
28
+ init(): Promise<void>;
29
+ private openSQLiteAPI;
30
+ requireSqlite(): SQLiteAPI;
31
+ /**
32
+ * Checks if the database connection is in autocommit mode.
33
+ * @returns true if in autocommit mode, false if in a transaction
34
+ */
35
+ isAutoCommit(): boolean;
36
+ execute(sql: string, bindings?: any[]): Promise<RawQueryResult>;
37
+ executeBatch(sql: string, bindings: any[][]): Promise<RawQueryResult[]>;
38
+ private wrapQueryResults;
39
+ /**
40
+ * This executes a single statement using SQLite3 and returns the results as a {@link RawResultSet}.
41
+ */
42
+ private executeSingleStatementRaw;
43
+ executeRaw(sql: string, bindings?: any[]): Promise<RawResultSet[]>;
44
+ private stepThroughStatement;
45
+ close(): Promise<void>;
46
+ }
@@ -0,0 +1,147 @@
1
+ import { Factory as WaSqliteFactory, SQLITE_ROW } from '@journeyapps/wa-sqlite';
2
+ import { DEFAULT_MODULE_FACTORIES } from './vfs.js';
3
+ /**
4
+ * A small wrapper around WA-sqlite to help with opening databases and running statements by preparing them internally.
5
+ *
6
+ * This is an internal class, and it must never be used directly. Wrappers are required to ensure raw connections aren't
7
+ * used concurrently across tabs.
8
+ */
9
+ export class RawSqliteConnection {
10
+ options;
11
+ _sqliteAPI = null;
12
+ /**
13
+ * The `sqlite3*` connection pointer.
14
+ */
15
+ db = 0;
16
+ _moduleFactory;
17
+ constructor(options) {
18
+ this.options = options;
19
+ this._moduleFactory = DEFAULT_MODULE_FACTORIES[this.options.vfs];
20
+ }
21
+ get isOpen() {
22
+ return this.db != 0;
23
+ }
24
+ async init() {
25
+ const api = (this._sqliteAPI = await this.openSQLiteAPI());
26
+ this.db = await api.open_v2(this.options.dbFilename, this.options.isReadOnly ? 1 /* SQLITE_OPEN_READONLY */ : 6 /* SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE */);
27
+ await this.executeRaw(`PRAGMA temp_store = ${this.options.temporaryStorage};`);
28
+ if (this.options.encryptionKey) {
29
+ const escapedKey = this.options.encryptionKey.replace("'", "''");
30
+ await this.executeRaw(`PRAGMA key = '${escapedKey}'`);
31
+ }
32
+ await this.executeRaw(`PRAGMA cache_size = -${this.options.cacheSizeKb};`);
33
+ await this.executeRaw(`SELECT powersync_update_hooks('install');`);
34
+ }
35
+ async openSQLiteAPI() {
36
+ const { module, vfs } = await this._moduleFactory({
37
+ dbFileName: this.options.dbFilename,
38
+ encryptionKey: this.options.encryptionKey
39
+ });
40
+ const sqlite3 = WaSqliteFactory(module);
41
+ sqlite3.vfs_register(vfs, true);
42
+ /**
43
+ * Register the PowerSync core SQLite extension
44
+ */
45
+ module.ccall('powersync_init_static', 'int', []);
46
+ /**
47
+ * Create the multiple cipher vfs if an encryption key is provided
48
+ */
49
+ if (this.options.encryptionKey) {
50
+ const createResult = module.ccall('sqlite3mc_vfs_create', 'int', ['string', 'int'], [this.options.dbFilename, 1]);
51
+ if (createResult !== 0) {
52
+ throw new Error('Failed to create multiple cipher vfs, Database encryption will not work');
53
+ }
54
+ }
55
+ return sqlite3;
56
+ }
57
+ requireSqlite() {
58
+ if (!this._sqliteAPI) {
59
+ throw new Error(`Initialization has not completed`);
60
+ }
61
+ return this._sqliteAPI;
62
+ }
63
+ /**
64
+ * Checks if the database connection is in autocommit mode.
65
+ * @returns true if in autocommit mode, false if in a transaction
66
+ */
67
+ isAutoCommit() {
68
+ return this.requireSqlite().get_autocommit(this.db) != 0;
69
+ }
70
+ async execute(sql, bindings) {
71
+ const resultSet = await this.executeSingleStatementRaw(sql, bindings);
72
+ return this.wrapQueryResults(this.requireSqlite(), resultSet);
73
+ }
74
+ async executeBatch(sql, bindings) {
75
+ const results = [];
76
+ const api = this.requireSqlite();
77
+ for await (const stmt of api.statements(this.db, sql)) {
78
+ let columns;
79
+ for (const parameterSet of bindings) {
80
+ const rs = await this.stepThroughStatement(api, stmt, parameterSet, columns, false);
81
+ results.push(this.wrapQueryResults(api, rs));
82
+ }
83
+ // executeBatch can only use a single statement
84
+ break;
85
+ }
86
+ return results;
87
+ }
88
+ wrapQueryResults(api, rs) {
89
+ return {
90
+ changes: api.changes(this.db),
91
+ lastInsertRowId: api.last_insert_id(this.db),
92
+ autocommit: api.get_autocommit(this.db) != 0,
93
+ resultSet: rs
94
+ };
95
+ }
96
+ /**
97
+ * This executes a single statement using SQLite3 and returns the results as a {@link RawResultSet}.
98
+ */
99
+ async executeSingleStatementRaw(sql, bindings) {
100
+ const results = await this.executeRaw(sql, bindings);
101
+ return results.length ? results[0] : undefined;
102
+ }
103
+ async executeRaw(sql, bindings) {
104
+ const results = [];
105
+ const api = this.requireSqlite();
106
+ for await (const stmt of api.statements(this.db, sql)) {
107
+ let columns;
108
+ const rs = await this.stepThroughStatement(api, stmt, bindings ?? [], columns);
109
+ columns = rs.columns;
110
+ if (columns.length) {
111
+ results.push(rs);
112
+ }
113
+ // When binding parameters, only a single statement is executed.
114
+ if (bindings) {
115
+ break;
116
+ }
117
+ }
118
+ return results;
119
+ }
120
+ async stepThroughStatement(api, stmt, bindings, knownColumns, includeResults = true) {
121
+ // TODO not sure why this is needed currently, but booleans break
122
+ bindings.forEach((b, index, arr) => {
123
+ if (typeof b == 'boolean') {
124
+ arr[index] = b ? 1 : 0;
125
+ }
126
+ });
127
+ api.reset(stmt);
128
+ if (bindings) {
129
+ api.bind_collection(stmt, bindings);
130
+ }
131
+ const rows = [];
132
+ while ((await api.step(stmt)) === SQLITE_ROW) {
133
+ if (includeResults) {
134
+ const row = api.row(stmt);
135
+ rows.push(row);
136
+ }
137
+ }
138
+ knownColumns ??= api.column_names(stmt);
139
+ return { columns: knownColumns, rows };
140
+ }
141
+ async close() {
142
+ if (this.isOpen) {
143
+ await this.requireSqlite().close(this.db);
144
+ this.db = 0;
145
+ }
146
+ }
147
+ }
@@ -1,23 +1,45 @@
1
- import { DBAdapter, type ILogLevel } from '@powersync/common';
2
- import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory.js';
3
- import { AsyncDatabaseConnection } from '../AsyncDatabaseConnection.js';
1
+ import { DBAdapter, SQLOpenFactory, type ILogLevel } from '@powersync/common';
4
2
  import { ResolvedWebSQLOpenOptions, WebSQLOpenFactoryOptions } from '../web-sql-flags.js';
5
- import { WASQLiteVFS } from './WASQLiteConnection.js';
3
+ import { WASQLiteVFS } from './vfs.js';
4
+ import { PoolConnection } from '../AsyncWebAdapter.js';
6
5
  export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions {
7
6
  vfs?: WASQLiteVFS;
7
+ /**
8
+ * If the {@link vfs} supports it, an additional amount of read-only connections to open. Using additional read
9
+ * connections can speed up queries by dispatching them to multiple workers running them concurrently.
10
+ *
11
+ * {@link WASQLiteVFS.OPFSWriteAheadVFS} is the only VFS with support for multiple connections, so this option is
12
+ * ignored for other VFS implementations.
13
+ *
14
+ * Defaults to 1.
15
+ */
16
+ additionalReaders?: number;
8
17
  }
9
18
  export interface ResolvedWASQLiteOpenFactoryOptions extends ResolvedWebSQLOpenOptions {
10
19
  vfs: WASQLiteVFS;
20
+ /**
21
+ * Whether this is a read-only connection opened for the `OPFSWriteAheadVFS` file system.
22
+ */
23
+ isReadOnly: boolean;
11
24
  }
12
25
  export interface WorkerDBOpenerOptions extends ResolvedWASQLiteOpenFactoryOptions {
13
26
  logLevel: ILogLevel;
27
+ /**
28
+ * A lock that is currently held by the client. When the lock is returned, we know the client is gone and that we need
29
+ * to clean up resources.
30
+ */
31
+ lockName: string;
14
32
  }
15
33
  /**
16
34
  * Opens a SQLite connection using WA-SQLite.
17
35
  */
18
- export declare class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
36
+ export declare class WASQLiteOpenFactory implements SQLOpenFactory {
37
+ private options;
38
+ private resolvedFlags;
39
+ private logger;
19
40
  constructor(options: WASQLiteOpenFactoryOptions);
20
41
  get waOptions(): WASQLiteOpenFactoryOptions;
21
42
  protected openAdapter(): DBAdapter;
22
- openConnection(): Promise<AsyncDatabaseConnection>;
43
+ openDB(): DBAdapter;
44
+ openConnection(): Promise<PoolConnection>;
23
45
  }