@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.
- package/dist/2075a31bb151adbb9767.wasm +0 -0
- package/dist/3322bc84de986b63c2cd.wasm +0 -0
- package/dist/8e97452e297be23b5e50.wasm +0 -0
- package/dist/fbc178b70d530e8ce02b.wasm +0 -0
- package/dist/index.umd.js +4323 -156
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +24 -15
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +86 -78
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-2fb422.umd.js +3562 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/lib/package.json +3 -3
- package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
- package/lib/src/db/PowerSyncDatabase.js +0 -8
- package/lib/src/db/adapters/AsyncWebAdapter.d.ts +13 -3
- package/lib/src/db/adapters/AsyncWebAdapter.js +115 -21
- package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +1 -2
- package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +1 -1
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +16 -2
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +60 -38
- package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +7 -18
- package/lib/src/db/adapters/wa-sqlite/vfs.js +34 -49
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +4 -0
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +4 -0
- package/lib/src/worker/db/MultiDatabaseServer.js +4 -1
- package/lib/src/worker/db/open-worker-database.js +2 -2
- package/lib/src/worker/sync/SharedSyncImplementation.js +4 -8
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/db/PowerSyncDatabase.ts +1 -9
- package/src/db/adapters/AsyncWebAdapter.ts +138 -22
- package/src/db/adapters/wa-sqlite/DatabaseServer.ts +4 -2
- package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +4 -1
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +89 -44
- package/src/db/adapters/wa-sqlite/vfs.ts +33 -49
- package/src/db/sync/SSRWebStreamingSyncImplementation.ts +5 -0
- package/src/worker/db/MultiDatabaseServer.ts +4 -1
- package/src/worker/db/open-worker-database.ts +2 -2
- package/src/worker/sync/SharedSyncImplementation.ts +4 -8
- package/dist/26d61ca9f5694d064635.wasm +0 -0
- package/dist/b4c6283dc473b6b3fd24.wasm +0 -0
- package/dist/c78985091a0b22aaef03.wasm +0 -0
- package/dist/ca59e199e1138b553fad.wasm +0 -0
- 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
- 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
- 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
- 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
- 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
- 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-
|
|
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-
|
|
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-
|
|
65
|
+
"@powersync/common": "0.0.0-dev-20260503073249"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@journeyapps/wa-sqlite": "^1.
|
|
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
|
|
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<
|
|
26
|
+
inner: Promise<PoolConnection>,
|
|
24
27
|
readonly name: string
|
|
25
28
|
) {
|
|
26
|
-
this.
|
|
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.
|
|
33
|
-
|
|
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.
|
|
45
|
+
await this.state;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
async close() {
|
|
42
|
-
const
|
|
43
|
-
|
|
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
|
|
48
|
-
return
|
|
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
|
|
53
|
-
return
|
|
59
|
+
const state = await this.state;
|
|
60
|
+
return state.withConnection(false, fn, options);
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
async refreshSchema(): Promise<void> {
|
|
57
|
-
|
|
64
|
+
const state = await this.state;
|
|
65
|
+
await state.refreshSchema();
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
registerListener(listener: Partial<DBAdapterListener>): () => void {
|
|
61
|
-
if (this.
|
|
62
|
-
return this.
|
|
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
|
|
81
|
-
return
|
|
196
|
+
const state = await this.state;
|
|
197
|
+
return state.writer.shareConnection();
|
|
82
198
|
}
|
|
83
199
|
|
|
84
200
|
getConfiguration(): WebDBAdapterConfiguration {
|
|
85
|
-
if (this.
|
|
86
|
-
return this.
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
workerPort
|
|
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
|
-
|
|
194
|
+
client = new DatabaseClient(
|
|
195
|
+
{ connection, source: null, remoteCanCloseUnexpectedly: false },
|
|
196
|
+
{
|
|
197
|
+
...resolvedOptions,
|
|
198
|
+
requiresPersistentTriggers
|
|
199
|
+
}
|
|
200
|
+
);
|
|
156
201
|
}
|
|
157
202
|
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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
|
-
|
|
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
|
|
14
|
+
const needsDedicated = vfs && vfsRequiresDedicatedWorkers(vfs);
|
|
15
15
|
|
|
16
16
|
if (worker) {
|
|
17
17
|
return !needsDedicated && multipleTabs
|