@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,325 @@
1
+ import {
2
+ QueryResult,
3
+ LockContext,
4
+ DBLockOptions,
5
+ DBAdapterListener,
6
+ ConnectionPool,
7
+ SqlExecutor,
8
+ DBGetUtilsDefaultMixin,
9
+ BatchedUpdateNotification,
10
+ BaseObserver,
11
+ ConnectionClosedError,
12
+ SQLOpenOptions
13
+ } from '@powersync/common';
14
+ import { SharedConnectionWorker, WebDBAdapterConfiguration } from '../WebDBAdapter.js';
15
+ import { ClientConnectionView } from './DatabaseServer.js';
16
+ import { RawQueryResult } from './RawSqliteConnection.js';
17
+ import * as Comlink from 'comlink';
18
+ import { WorkerDBOpenerOptions } from './WASQLiteOpenFactory.js';
19
+
20
+ export interface OpenWorkerConnection {
21
+ connect(config: WorkerDBOpenerOptions): Promise<ClientConnectionView>;
22
+ connectToExisting(options: { identifier: string; lockName: string }): Promise<ClientConnectionView>;
23
+ }
24
+
25
+ export interface ClientOptions {
26
+ connection: ClientConnectionView;
27
+ /**
28
+ * The remote from which the {@link connection} has been obtained.
29
+ *
30
+ * We use this to be able to expose this port to other clients wanting to connect to the database (e.g. the shared
31
+ * sync worker).
32
+ *
33
+ * For sources not based on workers, returns null.
34
+ */
35
+ source: Comlink.Remote<OpenWorkerConnection> | null;
36
+ /**
37
+ * Whether the remote we're connecting to can close unexpectedly (e.g. because we're a shared worker connecting to a
38
+ * dedicated worker handle we've received from a tab).
39
+ */
40
+ remoteCanCloseUnexpectedly: boolean;
41
+ onClose?: () => void;
42
+ }
43
+
44
+ /**
45
+ * A single-connection {@link ConnectionPool} implementation based on a worker connection.
46
+ */
47
+ export class DatabaseClient<Config extends SQLOpenOptions = WebDBAdapterConfiguration>
48
+ extends BaseObserver<DBAdapterListener>
49
+ implements ConnectionPool
50
+ {
51
+ #connection: ConnectionState;
52
+ #shareConnectionAbortController = new AbortController();
53
+ #receiveTableUpdates: MessagePort;
54
+
55
+ constructor(
56
+ private readonly options: ClientOptions,
57
+ private readonly config: Config
58
+ ) {
59
+ super();
60
+ this.#connection = {
61
+ connection: options.connection,
62
+ notifyRemoteClosed: options.remoteCanCloseUnexpectedly ? new AbortController() : undefined,
63
+ traceQueries: config.debugMode === true
64
+ };
65
+
66
+ const { port1, port2 } = new MessageChannel();
67
+ options.connection.setUpdateListener(Comlink.transfer(port1, [port1]));
68
+
69
+ this.#receiveTableUpdates = port2;
70
+ port2.onmessage = (event) => {
71
+ const tables = event.data as string[];
72
+ const notification: BatchedUpdateNotification = {
73
+ tables,
74
+ groupedUpdates: {},
75
+ rawUpdates: []
76
+ };
77
+ this.iterateListeners((l) => {
78
+ l.tablesUpdated && l.tablesUpdated(notification);
79
+ });
80
+ };
81
+ }
82
+
83
+ get name(): string {
84
+ return this.config.dbFilename;
85
+ }
86
+
87
+ /**
88
+ * Marks the remote as closed.
89
+ *
90
+ * This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
91
+ * it happens, all outstanding requests on this pool would never resolve. To avoid livelocks in this scenario, we
92
+ * throw on all outstanding promises and forbid new calls.
93
+ */
94
+ markRemoteClosed() {
95
+ // Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
96
+ // set.
97
+ this.#connection.notifyRemoteClosed!.abort();
98
+ }
99
+
100
+ async close(): Promise<void> {
101
+ // This connection is no longer shared, so we can close locks held for shareConnection calls.
102
+ this.#shareConnectionAbortController.abort();
103
+ this.#receiveTableUpdates.close();
104
+
105
+ await useConnectionState(this.#connection, (c) => c.close(), true);
106
+ this.options.onClose?.();
107
+ this.options.source?.[Comlink.releaseProxy]();
108
+ }
109
+
110
+ readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
111
+ return this.#lock(false, fn, options);
112
+ }
113
+
114
+ writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
115
+ return this.#lock(true, fn, options);
116
+ }
117
+
118
+ async #lock<T>(write: boolean, fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
119
+ const token = await useConnectionState(this.#connection, (c) => c.requestAccess(write, options?.timeoutMs));
120
+ try {
121
+ return await fn(new ClientLockContext(this.#connection, token));
122
+ } finally {
123
+ await useConnectionState(this.#connection, (c) => c.completeAccess(token));
124
+ }
125
+ }
126
+
127
+ async refreshSchema(): Promise<void> {
128
+ // Currently a no-op on the web.
129
+ }
130
+
131
+ async shareConnection(): Promise<SharedConnectionWorker> {
132
+ /**
133
+ * Hold a navigator lock in order to avoid features such as Chrome's frozen tabs,
134
+ * or Edge's sleeping tabs from pausing the thread for this connection.
135
+ * This promise resolves once a lock is obtained.
136
+ * This lock will be held as long as this connection is open.
137
+ * The `shareConnection` method should not be called on multiple tabs concurrently.
138
+ */
139
+ const abort = this.#shareConnectionAbortController;
140
+ const source = this.options.source;
141
+ if (source == null) {
142
+ throw new Error(`shareConnection() is only available for connections based by workers.`);
143
+ }
144
+
145
+ await new Promise<void>((resolve, reject) =>
146
+ navigator.locks
147
+ .request(
148
+ `shared-connection-${this.name}-${Date.now()}-${Math.round(Math.random() * 10000)}`,
149
+ {
150
+ signal: abort.signal
151
+ },
152
+ async () => {
153
+ resolve();
154
+
155
+ // Free the lock when the connection is already closed.
156
+ if (abort.signal.aborted) {
157
+ return;
158
+ }
159
+
160
+ // Hold the lock while the shared connection is in use.
161
+ await new Promise<void>((releaseLock) => {
162
+ abort.signal.addEventListener('abort', () => {
163
+ releaseLock();
164
+ });
165
+ });
166
+ }
167
+ )
168
+ // We aren't concerned with abort errors here
169
+ .catch((ex) => {
170
+ if (ex.name == 'AbortError') {
171
+ resolve();
172
+ } else {
173
+ reject(ex);
174
+ }
175
+ })
176
+ );
177
+
178
+ const newPort = await source[Comlink.createEndpoint]();
179
+ return { port: newPort, identifier: this.name };
180
+ }
181
+
182
+ getConfiguration(): Config {
183
+ return this.config;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * A {@link SqlExecutor} implemented by sending commands to a worker.
189
+ *
190
+ * While an instance is active, it has exclusive access to the underlying database connection (as represented by its
191
+ * token).
192
+ */
193
+ class ClientSqlExecutor implements SqlExecutor {
194
+ readonly #connection: ConnectionState;
195
+ readonly #token: string;
196
+
197
+ constructor(connection: ConnectionState, token: string) {
198
+ this.#connection = connection;
199
+ this.#token = token;
200
+ }
201
+
202
+ /**
203
+ * Requests an operation from the worker, potentially tracing it if that option has been enabled.
204
+ */
205
+ private async maybeTrace<T>(
206
+ fn: (connection: ClientConnectionView) => Promise<T>,
207
+ describeForTrace: () => string
208
+ ): Promise<T> {
209
+ if (this.#connection.traceQueries) {
210
+ const start = performance.now();
211
+ const description = describeForTrace();
212
+
213
+ try {
214
+ const r = await useConnectionState(this.#connection, fn);
215
+ performance.measure(`[SQL] ${description}`, { start });
216
+ return r;
217
+ } catch (e: any) {
218
+ performance.measure(`[SQL] [ERROR: ${e.message}] ${description}`, { start });
219
+ throw e;
220
+ }
221
+ } else {
222
+ return useConnectionState(this.#connection, fn);
223
+ }
224
+ }
225
+
226
+ async execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
227
+ const rs = await this.#executeOnWorker(query, params);
228
+ let rows: QueryResult['rows'] | undefined;
229
+ if (rs.resultSet) {
230
+ const resultSet = rs.resultSet;
231
+
232
+ function rowToJavaScriptObject(row: any[]): Record<string, any> {
233
+ const obj: Record<string, any> = {};
234
+ resultSet.columns.forEach((key, idx) => (obj[key] = row[idx]));
235
+ return obj;
236
+ }
237
+
238
+ const mapped = resultSet.rows.map(rowToJavaScriptObject);
239
+
240
+ rows = {
241
+ _array: mapped,
242
+ length: mapped.length,
243
+ item: (idx: number) => mapped[idx]
244
+ };
245
+ }
246
+
247
+ return {
248
+ rowsAffected: rs.changes,
249
+ insertId: rs.lastInsertRowId,
250
+ rows
251
+ };
252
+ }
253
+
254
+ async executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
255
+ const rs = await this.#executeOnWorker(query, params);
256
+ return rs.resultSet?.rows ?? [];
257
+ }
258
+
259
+ async #executeOnWorker(query: string, params: any[] | undefined): Promise<RawQueryResult> {
260
+ return this.maybeTrace(
261
+ (c) => c.execute(this.#token, query, params),
262
+ () => query
263
+ );
264
+ }
265
+
266
+ async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
267
+ const results = await this.maybeTrace(
268
+ (c) => c.executeBatch(this.#token, query, params),
269
+ () => `${query} (batch of ${params.length})`
270
+ );
271
+ const result: QueryResult = { insertId: undefined, rowsAffected: 0 };
272
+ for (const source of results) {
273
+ result.insertId = source.lastInsertRowId;
274
+ result.rowsAffected += source.changes;
275
+ }
276
+
277
+ return result;
278
+ }
279
+ }
280
+
281
+ class ClientLockContext extends DBGetUtilsDefaultMixin(ClientSqlExecutor) implements LockContext {}
282
+
283
+ interface ConnectionState {
284
+ connection: ClientConnectionView;
285
+ notifyRemoteClosed: AbortController | undefined;
286
+ traceQueries: boolean;
287
+ }
288
+
289
+ async function useConnectionState<T>(
290
+ state: ConnectionState,
291
+ workerPromise: (connection: ClientConnectionView) => Promise<T>,
292
+ fireActionOnAbort = false
293
+ ): Promise<T> {
294
+ const controller = state.notifyRemoteClosed;
295
+ if (controller) {
296
+ return new Promise((resolve, reject) => {
297
+ if (controller.signal.aborted) {
298
+ reject(new ConnectionClosedError('Called operation on closed remote'));
299
+ if (!fireActionOnAbort) {
300
+ // Don't run the operation if we're going to reject
301
+ // We might want to fire-and-forget the operation in some cases (like a close operation)
302
+ return;
303
+ }
304
+ }
305
+
306
+ function handleAbort() {
307
+ reject(new ConnectionClosedError('Remote peer closed with request in flight'));
308
+ }
309
+
310
+ function completePromise(action: () => void) {
311
+ controller!.signal.removeEventListener('abort', handleAbort);
312
+ action();
313
+ }
314
+
315
+ controller.signal.addEventListener('abort', handleAbort);
316
+
317
+ workerPromise(state.connection)
318
+ .then((data) => completePromise(() => resolve(data)))
319
+ .catch((e) => completePromise(() => reject(e)));
320
+ });
321
+ } else {
322
+ // Can't close, so just return the inner worker promise unguarded.
323
+ return workerPromise(state.connection);
324
+ }
325
+ }
@@ -0,0 +1,203 @@
1
+ import { ILogger } from '@powersync/common';
2
+ import { ConcurrentSqliteConnection, ConnectionLeaseToken } from './ConcurrentConnection.js';
3
+ import { RawQueryResult } from './RawSqliteConnection.js';
4
+
5
+ export interface DatabaseServerOptions {
6
+ inner: ConcurrentSqliteConnection;
7
+ onClose: () => void;
8
+ logger: ILogger;
9
+ }
10
+
11
+ /**
12
+ * Access to a WA-sqlite connection that can be shared with multiple clients sending queries over an RPC protocol built
13
+ * with the Comlink package.
14
+ */
15
+ export class DatabaseServer {
16
+ #options: DatabaseServerOptions;
17
+ #nextClientId = 0;
18
+ #activeClients = new Set<number>();
19
+
20
+ // TODO: Don't use a broadcast channel for connections managed by a shared worker.
21
+ #updateBroadcastChannel: BroadcastChannel;
22
+ #clientTableListeners = new Set<MessagePort>();
23
+
24
+ constructor(options: DatabaseServerOptions) {
25
+ this.#options = options;
26
+ const inner = options.inner;
27
+ this.#updateBroadcastChannel = new BroadcastChannel(`${inner.options.dbFilename}-table-updates`);
28
+
29
+ this.#updateBroadcastChannel.onmessage = ({ data }) => {
30
+ this.#pushTableUpdateToClients(data as string[]);
31
+ };
32
+ }
33
+
34
+ #pushTableUpdateToClients(changedTables: string[]) {
35
+ for (const listener of this.#clientTableListeners) {
36
+ listener.postMessage(changedTables);
37
+ }
38
+ }
39
+
40
+ get #inner() {
41
+ return this.#options.inner;
42
+ }
43
+
44
+ get #logger() {
45
+ return this.#options.logger;
46
+ }
47
+
48
+ /**
49
+ * Called by clients when they wish to connect to this database.
50
+ *
51
+ * @param lockName A lock that is currently held by the client. When the lock is returned, we know the client is gone
52
+ * and that we need to clean up resources.
53
+ */
54
+ async connect(lockName?: string): Promise<ClientConnectionView> {
55
+ let isOpen = true;
56
+ const clientId = this.#nextClientId++;
57
+ this.#activeClients.add(clientId);
58
+
59
+ let connectionLeases = new Map<string, { lease: ConnectionLeaseToken; write: boolean }>();
60
+ let currentTableListener: MessagePort | undefined;
61
+
62
+ function requireOpen() {
63
+ if (!isOpen) {
64
+ throw new Error('Client has already been closed');
65
+ }
66
+ }
67
+
68
+ function requireOpenAndLease(lease: string) {
69
+ requireOpen();
70
+ const token = connectionLeases.get(lease);
71
+ if (!token) {
72
+ throw new Error('Attempted to use a connection lease that has already been returned.');
73
+ }
74
+
75
+ return token;
76
+ }
77
+
78
+ const close = async () => {
79
+ if (isOpen) {
80
+ isOpen = false;
81
+
82
+ if (currentTableListener) {
83
+ this.#clientTableListeners.delete(currentTableListener);
84
+ }
85
+
86
+ // If the client holds a connection lease it hasn't returned, return that now.
87
+ for (const { lease } of connectionLeases.values()) {
88
+ this.#logger.debug(`Closing connection lease that hasn't been returned.`);
89
+ await lease.returnLease();
90
+ }
91
+
92
+ this.#activeClients.delete(clientId);
93
+
94
+ if (this.#activeClients.size == 0) {
95
+ await this.forceClose();
96
+ } else {
97
+ this.#logger.debug('Keeping underlying connection active since its used by other clients.');
98
+ }
99
+ }
100
+ };
101
+
102
+ if (lockName) {
103
+ navigator.locks!.request(lockName, {}, () => {
104
+ close();
105
+ });
106
+ }
107
+
108
+ return {
109
+ close,
110
+ debugIsAutoCommit: async () => {
111
+ return this.#inner.unsafeUseInner().isAutoCommit();
112
+ },
113
+ requestAccess: async (write, timeoutMs) => {
114
+ requireOpen();
115
+
116
+ const lease = await this.#inner.acquireConnection(
117
+ timeoutMs != null ? AbortSignal.timeout(timeoutMs) : undefined
118
+ );
119
+ if (!isOpen) {
120
+ // Race between requestAccess and close(), the connection was closed while we tried to acquire a lease.
121
+ await lease.returnLease();
122
+ return requireOpen() as never;
123
+ }
124
+
125
+ const token = crypto.randomUUID();
126
+ connectionLeases.set(token, { lease, write });
127
+ return token;
128
+ },
129
+ completeAccess: async (token) => {
130
+ const lease = requireOpenAndLease(token);
131
+ connectionLeases.delete(token);
132
+
133
+ try {
134
+ if (lease.write) {
135
+ // Collect update hooks invoked while the client had the write connection.
136
+ const { resultSet } = await lease.lease.use((conn) => conn.execute(`SELECT powersync_update_hooks('get')`));
137
+ if (resultSet) {
138
+ const updatedTables: string[] = JSON.parse(resultSet.rows[0][0] as string);
139
+ if (updatedTables.length) {
140
+ this.#updateBroadcastChannel.postMessage(updatedTables);
141
+ this.#pushTableUpdateToClients(updatedTables);
142
+ }
143
+ }
144
+ }
145
+ } finally {
146
+ await lease.lease.returnLease();
147
+ }
148
+ },
149
+ execute: async (token, sql, params) => {
150
+ const { lease } = requireOpenAndLease(token);
151
+ return await lease.use((db) => db.execute(sql, params));
152
+ },
153
+ executeBatch: async (token, sql, params) => {
154
+ const { lease } = requireOpenAndLease(token);
155
+ return await lease.use((db) => db.executeBatch(sql, params));
156
+ },
157
+ setUpdateListener: async (listener) => {
158
+ requireOpen();
159
+ if (currentTableListener) {
160
+ this.#clientTableListeners.delete(currentTableListener);
161
+ }
162
+
163
+ currentTableListener = listener;
164
+ if (listener) {
165
+ this.#clientTableListeners.add(listener);
166
+ }
167
+ }
168
+ };
169
+ }
170
+
171
+ async forceClose() {
172
+ this.#logger.debug(`Closing connection to ${this.#inner.options}.`);
173
+ const connection = this.#inner;
174
+ this.#options.onClose();
175
+ this.#updateBroadcastChannel.close();
176
+ await connection.close();
177
+ }
178
+ }
179
+
180
+ export interface ClientConnectionView {
181
+ close(): Promise<void>;
182
+ /**
183
+ * Only used for testing purposes.
184
+ */
185
+ debugIsAutoCommit(): Promise<boolean>;
186
+ /**
187
+ * Requests exclusive access to this database connection.
188
+ *
189
+ * Returns a token that can be used with the query methods. It must be returned with {@link completeAccess} to
190
+ * give other clients access to the database afterwards.
191
+ */
192
+ requestAccess(write: boolean, timeoutMs?: number): Promise<string>;
193
+ execute(token: string, sql: string, params: any[] | undefined): Promise<RawQueryResult>;
194
+ executeBatch(token: string, sql: string, params: any[][]): Promise<RawQueryResult[]>;
195
+ completeAccess(token: string): Promise<void>;
196
+
197
+ /**
198
+ * Sends update notifications to the given message port.
199
+ *
200
+ * Update notifications are posted as a `string[]` message.
201
+ */
202
+ setUpdateListener(listener: MessagePort): Promise<void>;
203
+ }