@powersync/web 0.0.0-dev-20260414110516 → 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 (64) 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 +4323 -156
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +24 -15
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +86 -78
  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 +3 -3
  28. package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
  29. package/lib/src/db/PowerSyncDatabase.js +0 -8
  30. package/lib/src/db/adapters/AsyncWebAdapter.d.ts +13 -3
  31. package/lib/src/db/adapters/AsyncWebAdapter.js +115 -21
  32. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +1 -2
  33. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +1 -1
  34. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +16 -2
  35. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +60 -38
  36. package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +7 -18
  37. package/lib/src/db/adapters/wa-sqlite/vfs.js +34 -49
  38. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +4 -0
  39. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +4 -0
  40. package/lib/src/worker/db/MultiDatabaseServer.js +4 -1
  41. package/lib/src/worker/db/open-worker-database.js +2 -2
  42. package/lib/src/worker/sync/SharedSyncImplementation.js +4 -8
  43. package/lib/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +4 -4
  45. package/src/db/PowerSyncDatabase.ts +1 -9
  46. package/src/db/adapters/AsyncWebAdapter.ts +138 -22
  47. package/src/db/adapters/wa-sqlite/DatabaseServer.ts +4 -2
  48. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +4 -1
  49. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +89 -44
  50. package/src/db/adapters/wa-sqlite/vfs.ts +33 -49
  51. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +5 -0
  52. package/src/worker/db/MultiDatabaseServer.ts +4 -1
  53. package/src/worker/db/open-worker-database.ts +2 -2
  54. package/src/worker/sync/SharedSyncImplementation.ts +4 -8
  55. package/dist/26d61ca9f5694d064635.wasm +0 -0
  56. package/dist/b4c6283dc473b6b3fd24.wasm +0 -0
  57. package/dist/c78985091a0b22aaef03.wasm +0 -0
  58. package/dist/ca59e199e1138b553fad.wasm +0 -0
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "0.0.0-dev-20260414110516",
3
+ "version": "0.0.0-dev-20260503073249",
4
4
  "description": "PowerSync Web SDK",
5
5
  "main": "lib/src/index.js",
6
6
  "module": "lib/src/index.js",
@@ -56,16 +56,16 @@
56
56
  "license": "Apache-2.0",
57
57
  "peerDependencies": {
58
58
  "@journeyapps/wa-sqlite": "^1.5.0",
59
- "@powersync/common": "0.0.0-dev-20260414110516"
59
+ "@powersync/common": "0.0.0-dev-20260503073249"
60
60
  },
61
61
  "dependencies": {
62
62
  "bson": "^6.10.4",
63
63
  "comlink": "^4.4.2",
64
64
  "commander": "^12.1.0",
65
- "@powersync/common": "0.0.0-dev-20260414110516"
65
+ "@powersync/common": "0.0.0-dev-20260503073249"
66
66
  },
67
67
  "devDependencies": {
68
- "@journeyapps/wa-sqlite": "^1.5.0",
68
+ "@journeyapps/wa-sqlite": "^1.7.0",
69
69
  "@types/uuid": "^9.0.6",
70
70
  "crypto-browserify": "^3.12.0",
71
71
  "glob": "^11.0.0",
@@ -38,6 +38,7 @@ import { AsyncDbAdapter } from './adapters/AsyncWebAdapter.js';
38
38
 
39
39
  export interface WebPowerSyncFlags extends WebSQLFlags {
40
40
  /**
41
+ * @deprecated This flag is no longer used. Navigator locks now handle tab detection automatically.
41
42
  * Externally unload open PowerSync database instances when the window closes.
42
43
  * Setting this to `true` requires calling `close` on all open PowerSyncDatabase
43
44
  * instances before the window unloads
@@ -127,7 +128,6 @@ function assertValidDatabaseOptions(options: WebPowerSyncDatabaseOptions): void
127
128
  export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
128
129
  static SHARED_MUTEX = new Mutex();
129
130
 
130
- protected unloadListener?: () => Promise<void>;
131
131
  protected resolvedFlags: WebPowerSyncFlags;
132
132
 
133
133
  constructor(options: WebPowerSyncDatabaseOptionsWithAdapter);
@@ -140,11 +140,6 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
140
140
  assertValidDatabaseOptions(options);
141
141
 
142
142
  this.resolvedFlags = resolveWebPowerSyncFlags(options.flags);
143
-
144
- if (this.resolvedFlags.enableMultiTabs && !this.resolvedFlags.externallyUnload) {
145
- this.unloadListener = () => this.close({ disconnect: false });
146
- window.addEventListener('unload', this.unloadListener);
147
- }
148
143
  }
149
144
 
150
145
  async _initialize(): Promise<void> {
@@ -190,9 +185,6 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
190
185
  * multiple tabs are not enabled.
191
186
  */
192
187
  close(options?: PowerSyncCloseOptions): Promise<void> {
193
- if (this.unloadListener) {
194
- window.removeEventListener('unload', this.unloadListener);
195
- }
196
188
  return super.close({
197
189
  // Don't disconnect by default if multiple tabs are enabled
198
190
  disconnect: options?.disconnect ?? !this.resolvedFlags.enableMultiTabs
@@ -3,7 +3,10 @@ import {
3
3
  DBAdapterDefaultMixin,
4
4
  DBAdapterListener,
5
5
  DBLockOptions,
6
- LockContext
6
+ LockContext,
7
+ Mutex,
8
+ Semaphore,
9
+ UnlockFn
7
10
  } from '@powersync/common';
8
11
  import { SharedConnectionWorker, WebDBAdapter, WebDBAdapterConfiguration } from './WebDBAdapter.js';
9
12
  import { DatabaseClient } from './wa-sqlite/DatabaseClient.js';
@@ -14,52 +17,57 @@ type PendingListener = { listener: Partial<DBAdapterListener>; closeAfterRegiste
14
17
  * A connection pool implementation delegating to another pool opened asynchronnously.
15
18
  */
16
19
  class AsyncConnectionPool implements ConnectionPool {
17
- protected readonly inner: Promise<DatabaseClient>;
20
+ protected readonly state: Promise<PoolState>;
21
+ protected resolvedWriter?: DatabaseClient;
18
22
 
19
- protected resolvedClient?: DatabaseClient;
20
23
  private readonly pendingListeners = new Set<PendingListener>();
21
24
 
22
25
  constructor(
23
- inner: Promise<DatabaseClient>,
26
+ inner: Promise<PoolConnection>,
24
27
  readonly name: string
25
28
  ) {
26
- this.inner = inner.then((client) => {
29
+ this.state = inner.then((client) => {
27
30
  for (const pending of this.pendingListeners) {
28
- pending.closeAfterRegisteredOnResolvedPool = client.registerListener(pending.listener);
31
+ pending.closeAfterRegisteredOnResolvedPool = client.writer.registerListener(pending.listener);
29
32
  }
30
33
  this.pendingListeners.clear();
31
34
 
32
- this.resolvedClient = client;
33
- return client;
35
+ this.resolvedWriter = client.writer;
36
+ if (client.additionalReaders.length) {
37
+ return readWritePoolState(client.writer, client.additionalReaders);
38
+ }
39
+
40
+ return singleConnectionPoolState(client.writer);
34
41
  });
35
42
  }
36
43
 
37
44
  async init() {
38
- await this.inner;
45
+ await this.state;
39
46
  }
40
47
 
41
48
  async close() {
42
- const inner = await this.inner;
43
- return await inner.close();
49
+ const state = await this.state;
50
+ await state.close();
44
51
  }
45
52
 
46
53
  async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
47
- const inner = await this.inner;
48
- return await inner.readLock(fn, options);
54
+ const state = await this.state;
55
+ return state.withConnection(true, fn, options);
49
56
  }
50
57
 
51
58
  async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
52
- const inner = await this.inner;
53
- return await inner.writeLock(fn, options);
59
+ const state = await this.state;
60
+ return state.withConnection(false, fn, options);
54
61
  }
55
62
 
56
63
  async refreshSchema(): Promise<void> {
57
- await (await this.inner).refreshSchema();
64
+ const state = await this.state;
65
+ await state.refreshSchema();
58
66
  }
59
67
 
60
68
  registerListener(listener: Partial<DBAdapterListener>): () => void {
61
- if (this.resolvedClient) {
62
- return this.resolvedClient.registerListener(listener);
69
+ if (this.resolvedWriter) {
70
+ return this.resolvedWriter.registerListener(listener);
63
71
  } else {
64
72
  const pending: PendingListener = { listener };
65
73
  this.pendingListeners.add(pending);
@@ -75,15 +83,123 @@ class AsyncConnectionPool implements ConnectionPool {
75
83
  }
76
84
  }
77
85
 
86
+ export interface PoolConnection {
87
+ writer: DatabaseClient;
88
+ additionalReaders: DatabaseClient[];
89
+ }
90
+
91
+ interface PoolState {
92
+ writer: DatabaseClient;
93
+ withConnection<T>(allowReadOnly: boolean, fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T>;
94
+ close(): Promise<void>;
95
+ refreshSchema(): Promise<void>;
96
+ }
97
+
98
+ function singleConnectionPoolState(connection: DatabaseClient): PoolState {
99
+ return {
100
+ writer: connection,
101
+ withConnection: (allowReadOnly, fn, options) => {
102
+ if (allowReadOnly) {
103
+ return connection.readLock(fn, options);
104
+ } else {
105
+ return connection.writeLock(fn, options);
106
+ }
107
+ },
108
+ close: () => connection.close(),
109
+ refreshSchema: () => connection.refreshSchema()
110
+ };
111
+ }
112
+
113
+ function readWritePoolState(writer: DatabaseClient, readers: DatabaseClient[]): PoolState {
114
+ // DatabaseClients have locks internally, so these aren't necessary for correctness. However, our mutex and semaphore
115
+ // implementations are very cheap to cancel, which we use to dispatch reads to the first available connection (by
116
+ // simply requesting all of them and sticking with the first connection we get).
117
+ const writerMutex = new Mutex();
118
+ const readerSemaphore = new Semaphore(readers);
119
+
120
+ return {
121
+ writer,
122
+ async withConnection(allowReadOnly, fn, options) {
123
+ const abortController = new AbortController();
124
+ const abortSignal = abortController.signal;
125
+
126
+ let timeout: any = null;
127
+ let release: UnlockFn | undefined;
128
+ if (options?.timeoutMs) {
129
+ timeout = setTimeout(() => abortController.abort, options.timeoutMs);
130
+ }
131
+
132
+ try {
133
+ if (allowReadOnly) {
134
+ let connection: DatabaseClient;
135
+
136
+ // Even if we have a pool of read connections, it's typically very small and we assume that most queries are
137
+ // reads. So, we want to request any connection from the read pool and the dedicated write connection (which
138
+ // can also serve reads). We race for the first connection we can obtain this way, and then abort the other
139
+ // request.
140
+ [connection, release] = await new Promise<[DatabaseClient, UnlockFn]>((resolve, reject) => {
141
+ let didComplete = false;
142
+ function complete() {
143
+ didComplete = true;
144
+ abortController.abort();
145
+ }
146
+
147
+ function completeSuccess(connection: DatabaseClient, returnFn: UnlockFn) {
148
+ if (didComplete) {
149
+ // We're not going to use this connection, so return it immediately.
150
+ returnFn();
151
+ } else {
152
+ complete();
153
+ resolve([connection, returnFn]);
154
+ }
155
+ }
156
+
157
+ function completeError(error: unknown) {
158
+ // We either have a working connection already, or we've rejected the promise. Either way, we don't need
159
+ // to do either thing again.
160
+ if (didComplete) return;
161
+
162
+ complete();
163
+ reject(error);
164
+ }
165
+
166
+ writerMutex.acquire(abortSignal).then((unlock) => completeSuccess(writer, unlock), completeError);
167
+ readerSemaphore
168
+ .requestOne(abortSignal)
169
+ .then(({ item, release }) => completeSuccess(item, release), completeError);
170
+ });
171
+
172
+ return await connection.readLock(fn);
173
+ } else {
174
+ return await writerMutex.runExclusive(() => writer.writeLock(fn), abortSignal);
175
+ }
176
+ } finally {
177
+ if (timeout != null) {
178
+ clearTimeout(timeout);
179
+ }
180
+ release?.();
181
+ }
182
+ },
183
+ async close() {
184
+ await writer.close();
185
+ await Promise.all(readers.map((r) => r.close()));
186
+ },
187
+ async refreshSchema() {
188
+ await writer.refreshSchema();
189
+ await Promise.all(readers.map((r) => r.refreshSchema()));
190
+ }
191
+ };
192
+ }
193
+
78
194
  export class AsyncDbAdapter extends DBAdapterDefaultMixin(AsyncConnectionPool) implements WebDBAdapter {
79
195
  async shareConnection(): Promise<SharedConnectionWorker> {
80
- const inner = await this.inner;
81
- return inner.shareConnection();
196
+ const state = await this.state;
197
+ return state.writer.shareConnection();
82
198
  }
83
199
 
84
200
  getConfiguration(): WebDBAdapterConfiguration {
85
- if (this.resolvedClient) {
86
- return this.resolvedClient.getConfiguration();
201
+ if (this.resolvedWriter) {
202
+ return this.resolvedWriter.getConfiguration();
87
203
  }
88
204
 
89
205
  throw new Error('AsyncDbAdapter.getConfiguration() can only be called after initializing it.');
@@ -112,8 +112,10 @@ export class DatabaseServer {
112
112
  },
113
113
  requestAccess: async (write, timeoutMs) => {
114
114
  requireOpen();
115
- // TODO: Support timeouts, they don't seem to be supported by the async-mutex package.
116
- const lease = await this.#inner.acquireConnection();
115
+
116
+ const lease = await this.#inner.acquireConnection(
117
+ timeoutMs != null ? AbortSignal.timeout(timeoutMs) : undefined
118
+ );
117
119
  if (!isOpen) {
118
120
  // Race between requestAccess and close(), the connection was closed while we tried to acquire a lease.
119
121
  await lease.returnLease();
@@ -39,7 +39,10 @@ export class RawSqliteConnection {
39
39
 
40
40
  async init() {
41
41
  const api = (this._sqliteAPI = await this.openSQLiteAPI());
42
- this.db = await api.open_v2(this.options.dbFilename);
42
+ this.db = await api.open_v2(
43
+ this.options.dbFilename,
44
+ this.options.isReadOnly ? 1 /* SQLITE_OPEN_READONLY */ : 6 /* SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE */
45
+ );
43
46
  await this.executeRaw(`PRAGMA temp_store = ${this.options.temporaryStorage};`);
44
47
  if (this.options.encryptionKey) {
45
48
  const escapedKey = this.options.encryptionKey.replace("'", "''");
@@ -13,16 +13,31 @@ import {
13
13
  import { SSRDBAdapter } from '../SSRDBAdapter.js';
14
14
  import { vfsRequiresDedicatedWorkers, WASQLiteVFS } from './vfs.js';
15
15
  import { MultiDatabaseServer } from '../../../worker/db/MultiDatabaseServer.js';
16
- import { ClientOptions, DatabaseClient, OpenWorkerConnection } from './DatabaseClient.js';
16
+ import { DatabaseClient, OpenWorkerConnection } from './DatabaseClient.js';
17
17
  import { generateTabCloseSignal } from '../../../shared/tab_close_signal.js';
18
- import { AsyncDbAdapter } from '../AsyncWebAdapter.js';
18
+ import { AsyncDbAdapter, PoolConnection } from '../AsyncWebAdapter.js';
19
19
 
20
20
  export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions {
21
21
  vfs?: WASQLiteVFS;
22
+ /**
23
+ * If the {@link vfs} supports it, an additional amount of read-only connections to open. Using additional read
24
+ * connections can speed up queries by dispatching them to multiple workers running them concurrently.
25
+ *
26
+ * {@link WASQLiteVFS.OPFSWriteAheadVFS} is the only VFS with support for multiple connections, so this option is
27
+ * ignored for other VFS implementations.
28
+ *
29
+ * Defaults to 1.
30
+ */
31
+ additionalReaders?: number;
22
32
  }
23
33
 
24
34
  export interface ResolvedWASQLiteOpenFactoryOptions extends ResolvedWebSQLOpenOptions {
25
35
  vfs: WASQLiteVFS;
36
+
37
+ /**
38
+ * Whether this is a read-only connection opened for the `OPFSWriteAheadVFS` file system.
39
+ */
40
+ isReadOnly: boolean;
26
41
  }
27
42
 
28
43
  export interface WorkerDBOpenerOptions extends ResolvedWASQLiteOpenFactoryOptions {
@@ -82,7 +97,7 @@ export class WASQLiteOpenFactory implements SQLOpenFactory {
82
97
  return this.openAdapter();
83
98
  }
84
99
 
85
- async openConnection(): Promise<DatabaseClient> {
100
+ async openConnection(): Promise<PoolConnection> {
86
101
  const { enableMultiTabs, useWebWorker } = this.resolvedFlags;
87
102
  const {
88
103
  vfs = WASQLiteVFS.IDBBatchAtomicVFS,
@@ -95,7 +110,7 @@ export class WASQLiteOpenFactory implements SQLOpenFactory {
95
110
  this.logger.warn('Multiple tabs are not enabled in this browser');
96
111
  }
97
112
 
98
- const resolvedOptions: ResolvedWASQLiteOpenFactoryOptions = {
113
+ const resolveOptions = (isReadOnly: boolean): ResolvedWASQLiteOpenFactoryOptions => ({
99
114
  dbFilename: this.options.dbFilename,
100
115
  dbLocation: this.options.dbLocation,
101
116
  debugMode: this.options.debugMode,
@@ -103,62 +118,92 @@ export class WASQLiteOpenFactory implements SQLOpenFactory {
103
118
  temporaryStorage,
104
119
  cacheSizeKb,
105
120
  flags: this.resolvedFlags,
106
- encryptionKey: encryptionKey
107
- };
121
+ encryptionKey: encryptionKey,
122
+ isReadOnly
123
+ });
108
124
 
109
- let clientOptions: ClientOptions;
125
+ let client: DatabaseClient;
126
+ let additionalReaders: DatabaseClient[] = [];
110
127
  let requiresPersistentTriggers = vfsRequiresDedicatedWorkers(vfs);
111
128
 
112
129
  if (useWebWorker) {
113
130
  const optionsDbWorker = this.options.worker;
114
131
 
115
- const workerPort =
116
- typeof optionsDbWorker == 'function'
117
- ? resolveWorkerDatabasePortFactory(() =>
118
- optionsDbWorker({
119
- ...this.options,
120
- temporaryStorage,
121
- cacheSizeKb,
122
- flags: this.resolvedFlags,
123
- encryptionKey
124
- })
125
- )
126
- : openWorkerDatabasePort(this.options.dbFilename, enableMultiTabs, optionsDbWorker, this.waOptions.vfs);
127
-
128
- const source = Comlink.wrap<OpenWorkerConnection>(workerPort);
129
- const closeSignal = new AbortController();
130
- const connection = await source.connect({
131
- ...resolvedOptions,
132
- logLevel: this.logger.getLevel(),
133
- lockName: await generateTabCloseSignal(closeSignal.signal)
134
- });
135
- clientOptions = {
136
- connection,
137
- source,
138
- // This tab owns the worker, so we're guaranteed to outlive it.
139
- remoteCanCloseUnexpectedly: false,
140
- onClose: () => {
141
- closeSignal.abort();
142
- if (workerPort instanceof Worker) {
143
- workerPort.terminate();
144
- } else {
145
- workerPort.close();
132
+ const openDatabaseWorker = async (
133
+ resolvedOptions: ResolvedWASQLiteOpenFactoryOptions
134
+ ): Promise<DatabaseClient> => {
135
+ const workerPort =
136
+ typeof optionsDbWorker == 'function'
137
+ ? resolveWorkerDatabasePortFactory(() =>
138
+ optionsDbWorker({
139
+ ...this.options,
140
+ temporaryStorage,
141
+ cacheSizeKb,
142
+ flags: this.resolvedFlags,
143
+ encryptionKey
144
+ })
145
+ )
146
+ : openWorkerDatabasePort(this.options.dbFilename, enableMultiTabs, optionsDbWorker, this.waOptions.vfs);
147
+
148
+ const source = Comlink.wrap<OpenWorkerConnection>(workerPort);
149
+ const closeSignal = new AbortController();
150
+ const connection = await source.connect({
151
+ ...resolvedOptions,
152
+ logLevel: this.logger.getLevel(),
153
+ lockName: await generateTabCloseSignal(closeSignal.signal)
154
+ });
155
+ const clientOptions = {
156
+ connection,
157
+ source,
158
+ // This tab owns the worker, so we're guaranteed to outlive it.
159
+ remoteCanCloseUnexpectedly: false,
160
+ onClose: () => {
161
+ closeSignal.abort();
162
+ if (workerPort instanceof Worker) {
163
+ workerPort.terminate();
164
+ } else {
165
+ workerPort.close();
166
+ }
146
167
  }
147
- }
168
+ };
169
+
170
+ return new DatabaseClient(clientOptions, {
171
+ ...resolvedOptions,
172
+ requiresPersistentTriggers
173
+ });
148
174
  };
175
+
176
+ client = await openDatabaseWorker(resolveOptions(false));
177
+
178
+ if (vfs == WASQLiteVFS.OPFSWriteAheadVFS) {
179
+ // This VFS supports concurrent reads, so we can open additional workers to host read-only connections for
180
+ // concurrent reads / writes.
181
+ const additionalReadersCount = this.options.additionalReaders ?? 1;
182
+ for (let i = 0; i < additionalReadersCount; i++) {
183
+ const reader = await openDatabaseWorker(resolveOptions(true));
184
+ additionalReaders.push(reader);
185
+ }
186
+ }
149
187
  } else {
150
188
  // Don't use a web worker. Instead, open the MultiDatabaseServer a worker would use locally.
151
189
  const localServer = new MultiDatabaseServer(this.logger);
152
190
  requiresPersistentTriggers = true;
153
191
 
192
+ const resolvedOptions = resolveOptions(false);
154
193
  const connection = await localServer.openConnectionLocally(resolvedOptions);
155
- clientOptions = { connection, source: null, remoteCanCloseUnexpectedly: false };
194
+ client = new DatabaseClient(
195
+ { connection, source: null, remoteCanCloseUnexpectedly: false },
196
+ {
197
+ ...resolvedOptions,
198
+ requiresPersistentTriggers
199
+ }
200
+ );
156
201
  }
157
202
 
158
- return new DatabaseClient(clientOptions, {
159
- ...resolvedOptions,
160
- requiresPersistentTriggers
161
- });
203
+ return {
204
+ writer: client,
205
+ additionalReaders
206
+ };
162
207
  }
163
208
  }
164
209
 
@@ -6,7 +6,8 @@ import type * as SQLite from '@journeyapps/wa-sqlite';
6
6
  export enum WASQLiteVFS {
7
7
  IDBBatchAtomicVFS = 'IDBBatchAtomicVFS',
8
8
  OPFSCoopSyncVFS = 'OPFSCoopSyncVFS',
9
- AccessHandlePoolVFS = 'AccessHandlePoolVFS'
9
+ AccessHandlePoolVFS = 'AccessHandlePoolVFS',
10
+ OPFSWriteAheadVFS = 'OPFSWriteAheadVFS'
10
11
  }
11
12
 
12
13
  export function vfsRequiresDedicatedWorkers(vfs: WASQLiteVFS) {
@@ -30,49 +31,32 @@ export type WASQLiteModuleFactory = (
30
31
  options: WASQLiteModuleFactoryOptions
31
32
  ) => Promise<{ module: SQLiteModule; vfs: SQLiteVFS }>;
32
33
 
33
- /**
34
- * @internal
35
- */
36
- export const AsyncWASQLiteModuleFactory = async () => {
37
- const { default: factory } = await import('@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs');
38
- return factory();
39
- };
40
-
41
- /**
42
- * @internal
43
- */
44
- export const MultiCipherAsyncWASQLiteModuleFactory = async () => {
45
- const { default: factory } = await import('@journeyapps/wa-sqlite/dist/mc-wa-sqlite-async.mjs');
46
- return factory();
47
- };
48
-
49
- /**
50
- * @internal
51
- */
52
- export const SyncWASQLiteModuleFactory = async () => {
53
- const { default: factory } = await import('@journeyapps/wa-sqlite/dist/wa-sqlite.mjs');
54
- return factory();
55
- };
34
+ async function asyncModuleFactory(encryptionKey: unknown) {
35
+ if (encryptionKey) {
36
+ const { default: factory } = await import('@journeyapps/wa-sqlite/dist/mc-wa-sqlite-async.mjs');
37
+ return factory();
38
+ } else {
39
+ const { default: factory } = await import('@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs');
40
+ return factory();
41
+ }
42
+ }
56
43
 
57
- /**
58
- * @internal
59
- */
60
- export const MultiCipherSyncWASQLiteModuleFactory = async () => {
61
- const { default: factory } = await import('@journeyapps/wa-sqlite/dist/mc-wa-sqlite.mjs');
62
- return factory();
63
- };
44
+ async function syncModuleFactory(encryptionKey: unknown) {
45
+ if (encryptionKey) {
46
+ const { default: factory } = await import('@journeyapps/wa-sqlite/dist/mc-wa-sqlite.mjs');
47
+ return factory();
48
+ } else {
49
+ const { default: factory } = await import('@journeyapps/wa-sqlite/dist/wa-sqlite.mjs');
50
+ return factory();
51
+ }
52
+ }
64
53
 
65
54
  /**
66
55
  * @internal
67
56
  */
68
57
  export const DEFAULT_MODULE_FACTORIES = {
69
58
  [WASQLiteVFS.IDBBatchAtomicVFS]: async (options: WASQLiteModuleFactoryOptions) => {
70
- let module;
71
- if (options.encryptionKey) {
72
- module = await MultiCipherAsyncWASQLiteModuleFactory();
73
- } else {
74
- module = await AsyncWASQLiteModuleFactory();
75
- }
59
+ const module = await asyncModuleFactory(options.encryptionKey);
76
60
  const { IDBBatchAtomicVFS } = await import('@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js');
77
61
  return {
78
62
  module,
@@ -81,12 +65,7 @@ export const DEFAULT_MODULE_FACTORIES = {
81
65
  };
82
66
  },
83
67
  [WASQLiteVFS.AccessHandlePoolVFS]: async (options: WASQLiteModuleFactoryOptions) => {
84
- let module;
85
- if (options.encryptionKey) {
86
- module = await MultiCipherSyncWASQLiteModuleFactory();
87
- } else {
88
- module = await SyncWASQLiteModuleFactory();
89
- }
68
+ const module = await syncModuleFactory(options.encryptionKey);
90
69
  // @ts-expect-error The types for this static method are missing upstream
91
70
  const { AccessHandlePoolVFS } = await import('@journeyapps/wa-sqlite/src/examples/AccessHandlePoolVFS.js');
92
71
  return {
@@ -95,12 +74,7 @@ export const DEFAULT_MODULE_FACTORIES = {
95
74
  };
96
75
  },
97
76
  [WASQLiteVFS.OPFSCoopSyncVFS]: async (options: WASQLiteModuleFactoryOptions) => {
98
- let module;
99
- if (options.encryptionKey) {
100
- module = await MultiCipherSyncWASQLiteModuleFactory();
101
- } else {
102
- module = await SyncWASQLiteModuleFactory();
103
- }
77
+ const module = await syncModuleFactory(options.encryptionKey);
104
78
  // @ts-expect-error The types for this static method are missing upstream
105
79
  const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js');
106
80
  const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module);
@@ -108,5 +82,15 @@ export const DEFAULT_MODULE_FACTORIES = {
108
82
  module,
109
83
  vfs
110
84
  };
85
+ },
86
+ [WASQLiteVFS.OPFSWriteAheadVFS]: async (options: WASQLiteModuleFactoryOptions) => {
87
+ const module = await syncModuleFactory(options.encryptionKey);
88
+ // @ts-expect-error The types for this static method are missing upstream
89
+ const { OPFSWriteAheadVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSWriteAheadVFS.js');
90
+ const vfs = await OPFSWriteAheadVFS.create(options.dbFileName, module, {});
91
+ return {
92
+ module,
93
+ vfs
94
+ };
111
95
  }
112
96
  };
@@ -85,4 +85,9 @@ export class SSRStreamingSyncImplementation extends BaseObserver implements Stre
85
85
  * No-op in SSR mode.
86
86
  */
87
87
  updateSubscriptions(): void {}
88
+
89
+ /**
90
+ * No-op in SSR mode.
91
+ */
92
+ markConnectionMayHaveChanged(): void {}
88
93
  }
@@ -61,7 +61,10 @@ export class MultiDatabaseServer {
61
61
 
62
62
  let server: DatabaseServer | undefined = this.activeDatabases.get(dbFilename);
63
63
  if (server == null) {
64
- const needsNavigatorLocks = !isSharedWorker;
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);
65
68
  const connection = new RawSqliteConnection(options);
66
69
  const withSafeConcurrency = new ConcurrentSqliteConnection(connection, needsNavigatorLocks);
67
70
 
@@ -1,5 +1,5 @@
1
1
  import * as Comlink from 'comlink';
2
- import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js';
2
+ import { vfsRequiresDedicatedWorkers, WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js';
3
3
  import { OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
4
4
 
5
5
  /**
@@ -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