@powersync/web 2.1.1 → 2.3.0
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/README.md +1 -1
- package/dist/index.react_native_web.js +224 -121
- package/dist/index.react_native_web.js.map +1 -1
- package/dist/worker/AccessHandlePoolVFS-BPUHfZME.js.map +1 -1
- package/dist/worker/FacadeVFS-d1ZDvud7.js.map +1 -1
- package/dist/worker/IDBBatchAtomicVFS-DbkDb777.js.map +1 -1
- package/dist/worker/MemoryVFS-DVJL5F8j.js.map +1 -1
- package/dist/worker/OPFSCoopSyncVFS-BgTiWPfa.js.map +1 -1
- package/dist/worker/OPFSWriteAheadVFS-Rt5CGCZP.js +2 -0
- package/dist/worker/OPFSWriteAheadVFS-Rt5CGCZP.js.map +1 -0
- package/dist/worker/mc-wa-sqlite-DDFgWP93.js.map +1 -1
- package/dist/worker/mc-wa-sqlite-async-lGclTjKJ.js.map +1 -1
- package/dist/worker/wa-sqlite-B0tZMM0j.js.map +1 -1
- package/dist/worker/wa-sqlite-async-CM6BmfRh.js.map +1 -1
- package/dist/worker/worker.js +1 -1
- package/dist/worker/worker.js.map +1 -1
- package/lib/db/PowerSyncDatabase.d.ts +1 -2
- package/lib/db/PowerSyncDatabase.js +19 -24
- package/lib/db/PowerSyncDatabase.js.map +1 -1
- package/lib/db/adapters/AsyncWebAdapter.js +4 -55
- package/lib/db/adapters/AsyncWebAdapter.js.map +1 -1
- package/lib/db/adapters/acquireFromPool.d.ts +7 -0
- package/lib/db/adapters/acquireFromPool.js +60 -0
- package/lib/db/adapters/acquireFromPool.js.map +1 -0
- package/lib/db/adapters/memory-pool/client.d.ts +70 -0
- package/lib/db/adapters/memory-pool/client.js +269 -0
- package/lib/db/adapters/memory-pool/client.js.map +1 -0
- package/lib/db/adapters/memory-pool/shared.d.ts +51 -0
- package/lib/db/adapters/memory-pool/shared.js +16 -0
- package/lib/db/adapters/memory-pool/shared.js.map +1 -0
- package/lib/db/adapters/memory-pool/vfs.d.ts +19 -0
- package/lib/db/adapters/memory-pool/vfs.js +247 -0
- package/lib/db/adapters/memory-pool/vfs.js.map +1 -0
- package/lib/db/adapters/memory-pool/worker.d.ts +1 -0
- package/lib/db/adapters/memory-pool/worker.js +45 -0
- package/lib/db/adapters/memory-pool/worker.js.map +1 -0
- package/lib/db/adapters/options.d.ts +7 -0
- package/lib/db/adapters/options.js.map +1 -1
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +8 -0
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js +56 -5
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js.map +1 -1
- package/lib/db/adapters/wa-sqlite/StatementCache.d.ts +16 -0
- package/lib/db/adapters/wa-sqlite/StatementCache.js +44 -0
- package/lib/db/adapters/wa-sqlite/StatementCache.js.map +1 -0
- package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +7 -4
- package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js.map +1 -1
- package/lib/db/sync/SSRWebStreamingSyncImplementation.d.ts +1 -8
- package/lib/db/sync/SSRWebStreamingSyncImplementation.js +3 -12
- package/lib/db/sync/SSRWebStreamingSyncImplementation.js.map +1 -1
- package/lib/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -1
- package/lib/db/sync/SharedWebStreamingSyncImplementation.js +6 -4
- package/lib/db/sync/SharedWebStreamingSyncImplementation.js.map +1 -1
- package/lib/worker/db/MultiDatabaseServer.d.ts +1 -2
- package/lib/worker/db/MultiDatabaseServer.js +28 -17
- package/lib/worker/db/MultiDatabaseServer.js.map +1 -1
- package/lib/worker/sync/AbstractSharedSyncClientProvider.d.ts +1 -0
- package/lib/worker/sync/AbstractSharedSyncClientProvider.js.map +1 -1
- package/lib/worker/sync/SharedSyncImplementation.d.ts +3 -11
- package/lib/worker/sync/SharedSyncImplementation.js +32 -64
- package/lib/worker/sync/SharedSyncImplementation.js.map +1 -1
- package/lib/worker/sync/WorkerClient.d.ts +1 -1
- package/lib/worker/sync/WorkerClient.js +3 -3
- package/lib/worker/sync/WorkerClient.js.map +1 -1
- package/package.json +15 -8
- package/src/db/PowerSyncDatabase.ts +20 -24
- package/src/db/adapters/AsyncWebAdapter.ts +13 -61
- package/src/db/adapters/acquireFromPool.ts +73 -0
- package/src/db/adapters/memory-pool/client.ts +367 -0
- package/src/db/adapters/memory-pool/shared.ts +79 -0
- package/src/db/adapters/memory-pool/vfs.ts +293 -0
- package/src/db/adapters/memory-pool/worker.ts +53 -0
- package/src/db/adapters/options.ts +8 -0
- package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +71 -6
- package/src/db/adapters/wa-sqlite/StatementCache.ts +50 -0
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +17 -5
- package/src/db/sync/SSRWebStreamingSyncImplementation.ts +4 -14
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +8 -5
- package/src/worker/db/MultiDatabaseServer.ts +29 -21
- package/src/worker/sync/AbstractSharedSyncClientProvider.ts +1 -0
- package/src/worker/sync/SharedSyncImplementation.ts +41 -76
- package/src/worker/sync/WorkerClient.ts +4 -4
- package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js +0 -2
- package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js.map +0 -1
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
import {
|
|
3
|
+
BaseQueryResult,
|
|
4
|
+
BatchedUpdateNotification,
|
|
5
|
+
DBAdapter,
|
|
6
|
+
DBLockOptions,
|
|
7
|
+
LockContext,
|
|
8
|
+
QueryResult,
|
|
9
|
+
queryResultWithoutRows,
|
|
10
|
+
RawQueryResult
|
|
11
|
+
} from '@powersync/common';
|
|
12
|
+
import {
|
|
13
|
+
applyWalChanges,
|
|
14
|
+
DatabaseServer,
|
|
15
|
+
emptyWalState,
|
|
16
|
+
WalIndexChange,
|
|
17
|
+
WalOverlayEntry,
|
|
18
|
+
WriteAheadBuffers
|
|
19
|
+
} from './shared.js';
|
|
20
|
+
import { Mutex, Semaphore } from '@powersync/shared-internals';
|
|
21
|
+
import type { RawWaSqliteDatabaseOptions } from '../wa-sqlite/RawSqliteConnection.js';
|
|
22
|
+
import { acquireFromPool } from '../acquireFromPool.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options for a {@link InMemoryWriteAheadLogPool}.
|
|
26
|
+
*/
|
|
27
|
+
export interface InMemoryWriteAheadLogPoolOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The amount of workers to use, must at least be `1`.
|
|
30
|
+
*/
|
|
31
|
+
numWorkers: number;
|
|
32
|
+
/**
|
|
33
|
+
* Optional additional options used to configure how the database is opened in workers.
|
|
34
|
+
*/
|
|
35
|
+
database?: Partial<RawWaSqliteDatabaseOptions>;
|
|
36
|
+
/**
|
|
37
|
+
* The maximum size of the database buffer, in bytes.
|
|
38
|
+
*
|
|
39
|
+
* This VFS uses a growable buffer, so not all of this is allocated immediately. By default, the VFS attempts to use
|
|
40
|
+
* 16 GB and falls back to 1 GB if that fails (e.g. on 32-bit platforms).
|
|
41
|
+
*/
|
|
42
|
+
maxDatabaseSize?: number;
|
|
43
|
+
/**
|
|
44
|
+
* The maximum size of the write-ahead log, in bytes.
|
|
45
|
+
*
|
|
46
|
+
* The size of the write-ahead log constraints the amount of data that can be written in a single transaction.
|
|
47
|
+
*
|
|
48
|
+
* This VFS uses a growable buffer, so not all of this is allocated immediately. By default, the VFS attempts to use
|
|
49
|
+
* 1 GB.
|
|
50
|
+
*/
|
|
51
|
+
maxWriteAheadLogSize?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createWriteAheadLogBuffers(options: InMemoryWriteAheadLogPoolOptions): WriteAheadBuffers {
|
|
55
|
+
const gigabyte = 1024 * 1024 * 1024;
|
|
56
|
+
let database: SharedArrayBuffer;
|
|
57
|
+
|
|
58
|
+
if (options.maxDatabaseSize) {
|
|
59
|
+
database = new SharedArrayBuffer(0, { maxByteLength: options.maxDatabaseSize });
|
|
60
|
+
} else {
|
|
61
|
+
try {
|
|
62
|
+
database = new SharedArrayBuffer(0, { maxByteLength: 16 * gigabyte });
|
|
63
|
+
} catch {
|
|
64
|
+
database = new SharedArrayBuffer(0, { maxByteLength: 1 * gigabyte });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
database,
|
|
70
|
+
writeAheadLog: new SharedArrayBuffer(0, { maxByteLength: options.maxWriteAheadLogSize ?? 1 * gigabyte })
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An in-memory database backed by a pool of workers.
|
|
76
|
+
*
|
|
77
|
+
* Multiple workers can execute readonly transactions in parallel, and a single writer is allowed to write to the
|
|
78
|
+
* database in parallel to readers. To allow multiple workers to access the same database, this is based on shared array
|
|
79
|
+
* buffers and requires the page to be [cross-origin isolated](https://web.dev/articles/cross-origin-isolation-guide).
|
|
80
|
+
*
|
|
81
|
+
* To use this VFS, pass it to the {@link PowerSyncDatabase} constructor:
|
|
82
|
+
*
|
|
83
|
+
* ```TypeScript
|
|
84
|
+
* import { PowerSyncDatabase, Schema } from '@powersync/web';
|
|
85
|
+
* import InMemoryWriteAheadLogPool from '@powersync/web/extra/shared-memory-pool';
|
|
86
|
+
*
|
|
87
|
+
* const db = new PowerSyncDatabase({
|
|
88
|
+
* opened: new InMemoryWriteAheadLogPool({numWorkers: 3}),
|
|
89
|
+
* schema: new Schema(...),
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* This database uses a concept known as a write-ahead log: Instead of writing changes directly to the database (which
|
|
94
|
+
* would cause conflicts with readers), the writer appends modified database pages into an append-only overlay log. When
|
|
95
|
+
* readers start a transaction, they consider the main database and items from the log until a specific position,
|
|
96
|
+
* meaning that future appends won't disrupt existing readers.
|
|
97
|
+
*
|
|
98
|
+
* To avoid the write-ahead log from growing indefinitely, it is regularly copied back into the main database file. This
|
|
99
|
+
* process is called a checkpoint, and in this implementation it blocks all other database access. However, it only
|
|
100
|
+
* needs to copy between memory and is usually fairly fast. This implementation checkpoints after a transaction
|
|
101
|
+
* completes and at least 10% of the WAL are used. We might fine-tune this later.
|
|
102
|
+
*/
|
|
103
|
+
export class InMemoryWriteAheadLogPool extends DBAdapter {
|
|
104
|
+
// Note that this name is also used as an identifier for sync navigator locks, so we want it to be unique. As there is
|
|
105
|
+
// no persistence, the name is not configurable and serves no other purpose.
|
|
106
|
+
override readonly name: string = `in-memory-${crypto.randomUUID()}`;
|
|
107
|
+
|
|
108
|
+
readonly #buffers: WriteAheadBuffers;
|
|
109
|
+
|
|
110
|
+
// There is nothing that tells the read and write workers apart, and we will use a write worker for reads too. But we
|
|
111
|
+
// must have a single designated write worker, because sync state is stored on the connection and the sync client
|
|
112
|
+
// relies on this.
|
|
113
|
+
readonly #readers?: Semaphore<PoolWorker>;
|
|
114
|
+
readonly #underlyingReaders: PoolWorker[] = [];
|
|
115
|
+
readonly #writer: PoolWorker;
|
|
116
|
+
readonly #writeLock = new Mutex();
|
|
117
|
+
|
|
118
|
+
readonly #walState = emptyWalState();
|
|
119
|
+
readonly #checkpointThreshold: number;
|
|
120
|
+
|
|
121
|
+
constructor(options: InMemoryWriteAheadLogPoolOptions) {
|
|
122
|
+
super();
|
|
123
|
+
if (options.numWorkers < 1) {
|
|
124
|
+
throw new Error('Need at least one worker');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.#buffers = createWriteAheadLogBuffers(options);
|
|
128
|
+
this.#checkpointThreshold = this.#buffers.writeAheadLog.maxByteLength / 10;
|
|
129
|
+
|
|
130
|
+
this.#writer = new PoolWorker(this.#buffers, options.database ?? {});
|
|
131
|
+
for (let i = 0; i < options.numWorkers - 1; i++) {
|
|
132
|
+
this.#underlyingReaders.push(new PoolWorker(this.#buffers, options.database ?? {}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.#underlyingReaders.length) {
|
|
136
|
+
this.#readers = new Semaphore(this.#underlyingReaders);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async #withWorker<T>(
|
|
141
|
+
allowReadOnly: boolean,
|
|
142
|
+
fn: (worker: PoolWorker) => Promise<T>,
|
|
143
|
+
lockOptions?: DBLockOptions
|
|
144
|
+
): Promise<T> {
|
|
145
|
+
return acquireFromPool(
|
|
146
|
+
this.#writeLock,
|
|
147
|
+
this.#writer,
|
|
148
|
+
this.#readers,
|
|
149
|
+
async (worker) => {
|
|
150
|
+
await worker.pushWalState();
|
|
151
|
+
return await fn(worker);
|
|
152
|
+
},
|
|
153
|
+
lockOptions,
|
|
154
|
+
allowReadOnly
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
159
|
+
return this.#withWorker(true, fn, options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Moves data from the write-ahead log overlay into the main database file.
|
|
164
|
+
*
|
|
165
|
+
* This may only be called with the writers and all readers locked.
|
|
166
|
+
*/
|
|
167
|
+
#checkpoint(changes: WalIndexChange) {
|
|
168
|
+
const walBuffer = this.#buffers.writeAheadLog;
|
|
169
|
+
const databaseBuffer = this.#buffers.database;
|
|
170
|
+
const newFileSize = changes.fileSize;
|
|
171
|
+
|
|
172
|
+
// Note: Checkpointing can fail (e.g. due to us exceeding the max db size), in which case we must tell the
|
|
173
|
+
// writer to drop the WAL changes it has made to consistently roll back the failure after throwing.
|
|
174
|
+
try {
|
|
175
|
+
if (databaseBuffer.byteLength < newFileSize) {
|
|
176
|
+
databaseBuffer.grow(newFileSize);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function copyFromWal(dbOffset: number, overlayEntry: WalOverlayEntry) {
|
|
180
|
+
const source = new Uint8Array(walBuffer, overlayEntry.logOffset, overlayEntry.size);
|
|
181
|
+
new Uint8Array(databaseBuffer, dbOffset).set(source);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Checkpoint existing entries from previous writes.
|
|
185
|
+
for (const [pageOffset, overlayEntry] of this.#walState.overlay.entries()) {
|
|
186
|
+
copyFromWal(pageOffset, overlayEntry);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Checkpoint changes from the last writeLock call that haven't been applied yet.
|
|
190
|
+
for (let i = 0; i < changes.added.length; i += 2) {
|
|
191
|
+
const dbOffset = changes.added[i] as number;
|
|
192
|
+
const walEntry = changes.added[i + 1] as WalOverlayEntry;
|
|
193
|
+
copyFromWal(dbOffset, walEntry);
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// Checkpointing failed, reset the state in the write worker to the local state here.
|
|
197
|
+
const allEntries: (number | WalOverlayEntry)[] = [];
|
|
198
|
+
this.#walState.overlay.forEach((entry, offset) => allEntries.push(offset, entry));
|
|
199
|
+
|
|
200
|
+
this.#writer.addChanges({
|
|
201
|
+
cleared: true,
|
|
202
|
+
fileSize: this.#walState.fileSize,
|
|
203
|
+
walEnd: this.#walState.walEnd,
|
|
204
|
+
added: allEntries
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Checkpoint complete, inform all workers to reset their WAL view when they're used again.
|
|
211
|
+
const cleared: WalIndexChange = { cleared: true, fileSize: newFileSize, walEnd: 0, added: [] };
|
|
212
|
+
applyWalChanges(this.#walState, cleared);
|
|
213
|
+
this.#writer.addChanges(cleared);
|
|
214
|
+
for (const worker of this.#underlyingReaders) {
|
|
215
|
+
worker.addChanges(cleared);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Propagates WAL additions from the writer to other workers, checkpointing if necessary.
|
|
221
|
+
*
|
|
222
|
+
* The caller must hold the write lock for the duration of the call.
|
|
223
|
+
*/
|
|
224
|
+
async #propagateChangesFromWriter(writer: PoolWorker) {
|
|
225
|
+
const changes = await writer.takeWalChanges();
|
|
226
|
+
|
|
227
|
+
if (changes.walEnd > this.#checkpointThreshold) {
|
|
228
|
+
// Instead of applying these changes to the local overlay and other workers, checkpoint! This can't run
|
|
229
|
+
// concurrently with anything else, so acquire read workers workers (we already have the write lock when this
|
|
230
|
+
// gets called).
|
|
231
|
+
const acquiredReaders = await this.#readers?.requestAll();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
this.#checkpoint(changes);
|
|
235
|
+
} finally {
|
|
236
|
+
acquiredReaders?.release();
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// No checkpoint necessary, just forward WAL changes to other workers.
|
|
240
|
+
applyWalChanges(this.#walState, changes);
|
|
241
|
+
|
|
242
|
+
for (const otherWorker of this.#underlyingReaders) {
|
|
243
|
+
otherWorker.addChanges(changes);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
249
|
+
return this.#withWorker(
|
|
250
|
+
false, // Don't allow read-only connection
|
|
251
|
+
async (worker) => {
|
|
252
|
+
try {
|
|
253
|
+
const res = await fn(worker);
|
|
254
|
+
|
|
255
|
+
const {
|
|
256
|
+
rawRows: [[updates]]
|
|
257
|
+
} = await worker.executeRaw(`SELECT powersync_update_hooks('get')`);
|
|
258
|
+
const updatedTables: string[] = JSON.parse(updates as string);
|
|
259
|
+
if (updatedTables.length) {
|
|
260
|
+
const notification: BatchedUpdateNotification = { tables: updatedTables };
|
|
261
|
+
this.iterateListeners((l) => l.tablesUpdated?.(notification));
|
|
262
|
+
}
|
|
263
|
+
return res;
|
|
264
|
+
} finally {
|
|
265
|
+
await this.#propagateChangesFromWriter(worker);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
options
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
override async refreshSchema(): Promise<void> {
|
|
273
|
+
// We only have to refresh readers, as the schema change itself was made on the write connection.
|
|
274
|
+
if (this.#readers) {
|
|
275
|
+
const { items, release } = await this.#readers.requestAll();
|
|
276
|
+
try {
|
|
277
|
+
await Promise.all(
|
|
278
|
+
items.map(async (worker) => {
|
|
279
|
+
await worker.pushWalState();
|
|
280
|
+
await worker.executeRaw("pragma table_info('sqlite_master')");
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
} finally {
|
|
284
|
+
release();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
override async close(): Promise<void> {
|
|
290
|
+
const releaseWriter = await this.#writeLock.acquire();
|
|
291
|
+
const readers = await this.#readers?.requestAll();
|
|
292
|
+
|
|
293
|
+
await Promise.race([this.#writer.close(), ...(readers?.items ?? []).map((e) => e.close())]);
|
|
294
|
+
|
|
295
|
+
releaseWriter();
|
|
296
|
+
readers?.release();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export default InMemoryWriteAheadLogPool;
|
|
301
|
+
|
|
302
|
+
class PoolWorker extends LockContext {
|
|
303
|
+
#buffers: WriteAheadBuffers;
|
|
304
|
+
#options: Partial<RawWaSqliteDatabaseOptions>;
|
|
305
|
+
#worker: Worker;
|
|
306
|
+
#server: Comlink.Remote<DatabaseServer>;
|
|
307
|
+
#isInitialized = false;
|
|
308
|
+
#outstandingChanges: WalIndexChange | null = null;
|
|
309
|
+
|
|
310
|
+
constructor(buffers: WriteAheadBuffers, options: Partial<RawWaSqliteDatabaseOptions>) {
|
|
311
|
+
super();
|
|
312
|
+
this.#buffers = buffers;
|
|
313
|
+
this.#options = options;
|
|
314
|
+
this.#worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
|
|
315
|
+
this.#worker.onerror = (e) => {
|
|
316
|
+
console.error('Worker error', e);
|
|
317
|
+
};
|
|
318
|
+
this.#server = Comlink.wrap(this.#worker);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
addChanges(changes: WalIndexChange) {
|
|
322
|
+
if (changes.cleared) {
|
|
323
|
+
this.#outstandingChanges = { fileSize: changes.fileSize, walEnd: 0, cleared: true, added: [] };
|
|
324
|
+
} else {
|
|
325
|
+
const outstandingChanges = (this.#outstandingChanges ??= { fileSize: 0, walEnd: 0, added: [], cleared: false });
|
|
326
|
+
outstandingChanges.fileSize = changes.fileSize;
|
|
327
|
+
outstandingChanges.walEnd = changes.walEnd;
|
|
328
|
+
outstandingChanges.added.push(...changes.added);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async pushWalState() {
|
|
333
|
+
if (!this.#isInitialized) {
|
|
334
|
+
await this.#server.open(this.#buffers, this.#options);
|
|
335
|
+
this.#isInitialized = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const pending = this.#outstandingChanges;
|
|
339
|
+
if (pending) {
|
|
340
|
+
this.#outstandingChanges = null;
|
|
341
|
+
await this.#server.updateWalState(pending);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
takeWalChanges() {
|
|
346
|
+
return this.#server.takeWalChanges();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
executeRaw<T>(query: string, params?: any[] | undefined): Promise<RawQueryResult> {
|
|
350
|
+
return this.#server.executeRaw(query, params);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async executeBatch(query: string, params?: any[][]): Promise<QueryResult<never>> {
|
|
354
|
+
const results = await this.#server.executeBatch(query, params ?? []);
|
|
355
|
+
const result: BaseQueryResult = { insertId: undefined, rowsAffected: 0 };
|
|
356
|
+
for (const source of results) {
|
|
357
|
+
result.insertId = source.insertId;
|
|
358
|
+
result.rowsAffected = (result.rowsAffected ?? 0) + source.rowsAffected;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return queryResultWithoutRows(result);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
close() {
|
|
365
|
+
this.#worker.terminate();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { RawQueryResult } from '@powersync/common';
|
|
2
|
+
import type { RawWaSqliteDatabaseOptions, RawWebResult } from '../wa-sqlite/RawSqliteConnection.js';
|
|
3
|
+
|
|
4
|
+
export interface WriteAheadBuffers {
|
|
5
|
+
database: SharedArrayBuffer;
|
|
6
|
+
writeAheadLog: SharedArrayBuffer;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WriteAheadState {
|
|
10
|
+
/**
|
|
11
|
+
* Length of the main database file.
|
|
12
|
+
*/
|
|
13
|
+
fileSize: number;
|
|
14
|
+
walEnd: number;
|
|
15
|
+
/**
|
|
16
|
+
* A map of database position offsets to offsets in the WAL logs at which position contents of the page are stored.
|
|
17
|
+
*/
|
|
18
|
+
overlay: Map<number, WalOverlayEntry>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WalOverlayEntry {
|
|
22
|
+
logOffset: number;
|
|
23
|
+
size: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WalIndexChange {
|
|
27
|
+
fileSize: number;
|
|
28
|
+
walEnd: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* New overlay entries to add, encoded as (key, value) pairs of {@link WriteAheadState.overlay}.
|
|
32
|
+
*/
|
|
33
|
+
added: (number | WalOverlayEntry)[];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether to clear the overlay (meaning that we ran a checkpoint).
|
|
37
|
+
*/
|
|
38
|
+
cleared: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DatabaseServer {
|
|
42
|
+
/**
|
|
43
|
+
* Open a database connection from the given database and write-ahead log buffers.
|
|
44
|
+
*/
|
|
45
|
+
open(buffers: WriteAheadBuffers, options: Partial<RawWaSqliteDatabaseOptions>): Promise<void>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Push new wal changes to this worker.
|
|
49
|
+
*/
|
|
50
|
+
updateWalState(overlay: WalIndexChange): Promise<void>;
|
|
51
|
+
|
|
52
|
+
executeRaw(query: string, params?: any[] | undefined): Promise<RawQueryResult>;
|
|
53
|
+
|
|
54
|
+
executeBatch(query: string, params?: any[][] | undefined): Promise<RawWebResult[]>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Takes WAL changes made by this worker during its active write iteration.
|
|
58
|
+
*/
|
|
59
|
+
takeWalChanges(): Promise<WalIndexChange>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function emptyWalState(): WriteAheadState {
|
|
63
|
+
return { fileSize: 0, walEnd: 0, overlay: new Map() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function applyWalChanges(state: WriteAheadState, changes: WalIndexChange) {
|
|
67
|
+
state.fileSize = changes.fileSize;
|
|
68
|
+
state.walEnd = changes.walEnd;
|
|
69
|
+
|
|
70
|
+
if (changes.cleared) {
|
|
71
|
+
state.overlay.clear();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < changes.added.length; i += 2) {
|
|
75
|
+
const dbOffset = changes.added[i] as number;
|
|
76
|
+
const walEntry = changes.added[i + 1] as WalOverlayEntry;
|
|
77
|
+
state.overlay.set(dbOffset, walEntry);
|
|
78
|
+
}
|
|
79
|
+
}
|