@livestore/wa-sqlite 1.0.1-dev.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/wa-sqlite-async.mjs +16 -0
- package/dist/wa-sqlite-async.wasm +0 -0
- package/dist/wa-sqlite-jspi.mjs +16 -0
- package/dist/wa-sqlite-jspi.wasm +0 -0
- package/dist/wa-sqlite.mjs +16 -0
- package/dist/wa-sqlite.wasm +0 -0
- package/package.json +45 -0
- package/src/FacadeVFS.js +508 -0
- package/src/VFS.js +222 -0
- package/src/WebLocksMixin.js +412 -0
- package/src/examples/AccessHandlePoolVFS.js +458 -0
- package/src/examples/IDBBatchAtomicVFS.js +820 -0
- package/src/examples/IDBMirrorVFS.js +875 -0
- package/src/examples/MemoryAsyncVFS.js +100 -0
- package/src/examples/MemoryVFS.js +176 -0
- package/src/examples/OPFSAdaptiveVFS.js +437 -0
- package/src/examples/OPFSAnyContextVFS.js +300 -0
- package/src/examples/OPFSCoopSyncVFS.js +590 -0
- package/src/examples/OPFSPermutedVFS.js +1214 -0
- package/src/examples/README.md +89 -0
- package/src/examples/tag.js +82 -0
- package/src/sqlite-api.js +914 -0
- package/src/sqlite-constants.js +275 -0
- package/src/types/globals.d.ts +60 -0
- package/src/types/index.d.ts +1302 -0
- package/src/types/tsconfig.json +6 -0
- package/test/AccessHandlePoolVFS.test.js +27 -0
- package/test/IDBBatchAtomicVFS.test.js +97 -0
- package/test/IDBMirrorVFS.test.js +27 -0
- package/test/MemoryAsyncVFS.test.js +27 -0
- package/test/MemoryVFS.test.js +27 -0
- package/test/OPFSAdaptiveVFS.test.js +27 -0
- package/test/OPFSAnyContextVFS.test.js +27 -0
- package/test/OPFSCoopSyncVFS.test.js +27 -0
- package/test/OPFSPermutedVFS.test.js +27 -0
- package/test/TestContext.js +96 -0
- package/test/WebLocksMixin.test.js +521 -0
- package/test/api.test.js +49 -0
- package/test/api_exec.js +89 -0
- package/test/api_misc.js +63 -0
- package/test/api_statements.js +426 -0
- package/test/callbacks.test.js +373 -0
- package/test/sql.test.js +64 -0
- package/test/sql_0001.js +49 -0
- package/test/sql_0002.js +52 -0
- package/test/sql_0003.js +83 -0
- package/test/sql_0004.js +81 -0
- package/test/sql_0005.js +76 -0
- package/test/test-worker.js +204 -0
- package/test/vfs_xAccess.js +2 -0
- package/test/vfs_xClose.js +52 -0
- package/test/vfs_xOpen.js +91 -0
- package/test/vfs_xRead.js +38 -0
- package/test/vfs_xWrite.js +36 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
|
|
2
|
+
import { FacadeVFS } from '../FacadeVFS.js';
|
|
3
|
+
import * as VFS from '../VFS.js';
|
|
4
|
+
import { WebLocksMixin } from '../WebLocksMixin.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef Metadata
|
|
8
|
+
* @property {string} name
|
|
9
|
+
* @property {number} fileSize
|
|
10
|
+
* @property {number} version
|
|
11
|
+
* @property {number} [pendingVersion]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class File {
|
|
15
|
+
/** @type {string} */ path;
|
|
16
|
+
/** @type {number} */ flags;
|
|
17
|
+
|
|
18
|
+
/** @type {Metadata} */ metadata;
|
|
19
|
+
/** @type {number} */ fileSize = 0;
|
|
20
|
+
|
|
21
|
+
/** @type {boolean} */ needsMetadataSync = false;
|
|
22
|
+
/** @type {Metadata} */ rollback = null;
|
|
23
|
+
/** @type {Set<number>} */ changedPages = new Set();
|
|
24
|
+
|
|
25
|
+
/** @type {string} */ synchronous = 'full';
|
|
26
|
+
/** @type {IDBTransactionOptions} */ txOptions = { durability: 'strict' };
|
|
27
|
+
|
|
28
|
+
constructor(path, flags, metadata) {
|
|
29
|
+
this.path = path;
|
|
30
|
+
this.flags = flags;
|
|
31
|
+
this.metadata = metadata;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class IDBBatchAtomicVFS extends WebLocksMixin(FacadeVFS) {
|
|
36
|
+
/** @type {Map<number, File>} */ mapIdToFile = new Map();
|
|
37
|
+
lastError = null;
|
|
38
|
+
|
|
39
|
+
log = null; // console.log
|
|
40
|
+
|
|
41
|
+
/** @type {Promise} */ #isReady;
|
|
42
|
+
/** @type {IDBContext} */ #idb;
|
|
43
|
+
|
|
44
|
+
static async create(name, module, options) {
|
|
45
|
+
const vfs = new IDBBatchAtomicVFS(name, module, options);
|
|
46
|
+
await vfs.isReady();
|
|
47
|
+
return vfs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
constructor(name, module, options = {}) {
|
|
51
|
+
super(name, module, options);
|
|
52
|
+
this.#isReady = this.#initialize(options.idbName ?? name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async #initialize(name) {
|
|
56
|
+
this.#idb = await IDBContext.create(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
this.#idb.close();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async isReady() {
|
|
64
|
+
await super.isReady();
|
|
65
|
+
await this.#isReady;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getFilename(fileId) {
|
|
69
|
+
const pathname = this.mapIdToFile.get(fileId).path;
|
|
70
|
+
return `IDB(${this.name}):${pathname}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string?} zName
|
|
75
|
+
* @param {number} fileId
|
|
76
|
+
* @param {number} flags
|
|
77
|
+
* @param {DataView} pOutFlags
|
|
78
|
+
* @returns {Promise<number>}
|
|
79
|
+
*/
|
|
80
|
+
async jOpen(zName, fileId, flags, pOutFlags) {
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://');
|
|
83
|
+
const path = url.pathname;
|
|
84
|
+
|
|
85
|
+
let meta = await this.#idb.q(({ metadata }) => metadata.get(path));
|
|
86
|
+
if (!meta && (flags & VFS.SQLITE_OPEN_CREATE)) {
|
|
87
|
+
meta = {
|
|
88
|
+
name: path,
|
|
89
|
+
fileSize: 0,
|
|
90
|
+
version: 0
|
|
91
|
+
};
|
|
92
|
+
await this.#idb.q(({ metadata }) => metadata.put(meta), 'rw');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!meta) {
|
|
96
|
+
throw new Error(`File ${path} not found`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const file = new File(path, flags, meta);
|
|
100
|
+
this.mapIdToFile.set(fileId, file);
|
|
101
|
+
pOutFlags.setInt32(0, flags, true);
|
|
102
|
+
return VFS.SQLITE_OK;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
this.lastError = e;
|
|
105
|
+
return VFS.SQLITE_CANTOPEN;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} zName
|
|
111
|
+
* @param {number} syncDir
|
|
112
|
+
* @returns {Promise<number>}
|
|
113
|
+
*/
|
|
114
|
+
async jDelete(zName, syncDir) {
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(zName, 'file://');
|
|
117
|
+
const path = url.pathname;
|
|
118
|
+
|
|
119
|
+
this.#idb.q(({ metadata, blocks }) => {
|
|
120
|
+
const range = IDBKeyRange.bound([path, -Infinity], [path, Infinity]);
|
|
121
|
+
blocks.delete(range);
|
|
122
|
+
metadata.delete(path);
|
|
123
|
+
}, 'rw');
|
|
124
|
+
|
|
125
|
+
if (syncDir) {
|
|
126
|
+
await this.#idb.sync(false);
|
|
127
|
+
}
|
|
128
|
+
return VFS.SQLITE_OK;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
this.lastError = e;
|
|
131
|
+
return VFS.SQLITE_IOERR_DELETE;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} zName
|
|
137
|
+
* @param {number} flags
|
|
138
|
+
* @param {DataView} pResOut
|
|
139
|
+
* @returns {Promise<number>}
|
|
140
|
+
*/
|
|
141
|
+
async jAccess(zName, flags, pResOut) {
|
|
142
|
+
try {
|
|
143
|
+
const url = new URL(zName, 'file://');
|
|
144
|
+
const path = url.pathname;
|
|
145
|
+
|
|
146
|
+
const meta = await this.#idb.q(({ metadata }) => metadata.get(path));
|
|
147
|
+
pResOut.setInt32(0, meta ? 1 : 0, true);
|
|
148
|
+
return VFS.SQLITE_OK;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
this.lastError = e;
|
|
151
|
+
return VFS.SQLITE_IOERR_ACCESS;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {number} fileId
|
|
157
|
+
* @returns {Promise<number>}
|
|
158
|
+
*/
|
|
159
|
+
async jClose(fileId) {
|
|
160
|
+
try {
|
|
161
|
+
const file = this.mapIdToFile.get(fileId);
|
|
162
|
+
this.mapIdToFile.delete(fileId);
|
|
163
|
+
|
|
164
|
+
if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
165
|
+
await this.#idb.q(({ metadata, blocks }) => {
|
|
166
|
+
metadata.delete(file.path);
|
|
167
|
+
blocks.delete(IDBKeyRange.bound([file.path, 0], [file.path, Infinity]));
|
|
168
|
+
}, 'rw');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (file.needsMetadataSync) {
|
|
172
|
+
this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw');
|
|
173
|
+
}
|
|
174
|
+
await this.#idb.sync(file.synchronous === 'full');
|
|
175
|
+
return VFS.SQLITE_OK;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
this.lastError = e;
|
|
178
|
+
return VFS.SQLITE_IOERR_CLOSE;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {number} fileId
|
|
184
|
+
* @param {Uint8Array} pData
|
|
185
|
+
* @param {number} iOffset
|
|
186
|
+
* @returns {Promise<number>}
|
|
187
|
+
*/
|
|
188
|
+
async jRead(fileId, pData, iOffset) {
|
|
189
|
+
try {
|
|
190
|
+
const file = this.mapIdToFile.get(fileId);
|
|
191
|
+
|
|
192
|
+
let pDataOffset = 0;
|
|
193
|
+
while (pDataOffset < pData.byteLength) {
|
|
194
|
+
// Fetch the IndexedDB block for this file location.
|
|
195
|
+
const fileOffset = iOffset + pDataOffset;
|
|
196
|
+
const block = await this.#idb.q(({ blocks }) => {
|
|
197
|
+
const range = IDBKeyRange.bound([file.path, -fileOffset], [file.path, Infinity]);
|
|
198
|
+
return blocks.get(range);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!block || block.data.byteLength - block.offset <= fileOffset) {
|
|
202
|
+
pData.fill(0, pDataOffset);
|
|
203
|
+
return VFS.SQLITE_IOERR_SHORT_READ;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Copy block data.
|
|
207
|
+
const dst = pData.subarray(pDataOffset);
|
|
208
|
+
const srcOffset = fileOffset + block.offset;
|
|
209
|
+
const nBytesToCopy = Math.min(
|
|
210
|
+
Math.max(block.data.byteLength - srcOffset, 0),
|
|
211
|
+
dst.byteLength);
|
|
212
|
+
dst.set(block.data.subarray(srcOffset, srcOffset + nBytesToCopy));
|
|
213
|
+
pDataOffset += nBytesToCopy;
|
|
214
|
+
}
|
|
215
|
+
return VFS.SQLITE_OK;
|
|
216
|
+
} catch (e) {
|
|
217
|
+
this.lastError = e;
|
|
218
|
+
return VFS.SQLITE_IOERR_READ;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {number} fileId
|
|
224
|
+
* @param {Uint8Array} pData
|
|
225
|
+
* @param {number} iOffset
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
jWrite(fileId, pData, iOffset) {
|
|
229
|
+
try {
|
|
230
|
+
const file = this.mapIdToFile.get(fileId);
|
|
231
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
232
|
+
if (!file.rollback) {
|
|
233
|
+
// Begin a new write transaction.
|
|
234
|
+
// Add pendingVersion to the metadata in IndexedDB. If we crash
|
|
235
|
+
// during the transaction, this lets subsequent connections
|
|
236
|
+
// know to remove blocks from the failed transaction.
|
|
237
|
+
const pending = Object.assign(
|
|
238
|
+
{ pendingVersion: file.metadata.version - 1 },
|
|
239
|
+
file.metadata);
|
|
240
|
+
this.#idb.q(({ metadata }) => metadata.put(pending), 'rw', file.txOptions);
|
|
241
|
+
|
|
242
|
+
file.rollback = Object.assign({}, file.metadata);
|
|
243
|
+
file.metadata.version--;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
248
|
+
file.changedPages.add(iOffset);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const data = pData.slice();
|
|
252
|
+
const version = file.metadata.version;
|
|
253
|
+
const isOverwrite = iOffset < file.metadata.fileSize;
|
|
254
|
+
if (!isOverwrite ||
|
|
255
|
+
file.flags & VFS.SQLITE_OPEN_MAIN_DB ||
|
|
256
|
+
file.flags & VFS.SQLITE_OPEN_TEMP_DB) {
|
|
257
|
+
const block = {
|
|
258
|
+
path: file.path,
|
|
259
|
+
offset: -iOffset,
|
|
260
|
+
version: version,
|
|
261
|
+
data: pData.slice()
|
|
262
|
+
};
|
|
263
|
+
this.#idb.q(({ blocks }) => {
|
|
264
|
+
blocks.put(block);
|
|
265
|
+
file.changedPages.add(iOffset);
|
|
266
|
+
}, 'rw', file.txOptions);
|
|
267
|
+
} else {
|
|
268
|
+
this.#idb.q(async ({ blocks }) => {
|
|
269
|
+
// Read the existing block.
|
|
270
|
+
const range = IDBKeyRange.bound(
|
|
271
|
+
[file.path, -iOffset],
|
|
272
|
+
[file.path, Infinity]);
|
|
273
|
+
const block = await blocks.get(range);
|
|
274
|
+
|
|
275
|
+
// Modify the block data.
|
|
276
|
+
// @ts-ignore
|
|
277
|
+
block.data.subarray(iOffset + block.offset).set(data);
|
|
278
|
+
|
|
279
|
+
// Write back.
|
|
280
|
+
blocks.put(block);
|
|
281
|
+
}, 'rw', file.txOptions);
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (file.metadata.fileSize < iOffset + pData.length) {
|
|
286
|
+
file.metadata.fileSize = iOffset + pData.length;
|
|
287
|
+
file.needsMetadataSync = true;
|
|
288
|
+
}
|
|
289
|
+
return VFS.SQLITE_OK;
|
|
290
|
+
} catch (e) {
|
|
291
|
+
this.lastError = e;
|
|
292
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {number} fileId
|
|
298
|
+
* @param {number} iSize
|
|
299
|
+
* @returns {number}
|
|
300
|
+
*/
|
|
301
|
+
jTruncate(fileId, iSize) {
|
|
302
|
+
try {
|
|
303
|
+
const file = this.mapIdToFile.get(fileId);
|
|
304
|
+
if (iSize < file.metadata.fileSize) {
|
|
305
|
+
this.#idb.q(({ blocks }) => {
|
|
306
|
+
const range = IDBKeyRange.bound(
|
|
307
|
+
[file.path, -Infinity],
|
|
308
|
+
[file.path, -iSize, Infinity]);
|
|
309
|
+
blocks.delete(range);
|
|
310
|
+
}, 'rw', file.txOptions);
|
|
311
|
+
file.metadata.fileSize = iSize;
|
|
312
|
+
file.needsMetadataSync = true;
|
|
313
|
+
}
|
|
314
|
+
return VFS.SQLITE_OK;
|
|
315
|
+
} catch (e) {
|
|
316
|
+
this.lastError = e;
|
|
317
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {number} fileId
|
|
323
|
+
* @param {number} flags
|
|
324
|
+
* @returns {Promise<number>}
|
|
325
|
+
*/
|
|
326
|
+
async jSync(fileId, flags) {
|
|
327
|
+
try {
|
|
328
|
+
const file = this.mapIdToFile.get(fileId);
|
|
329
|
+
if (file.needsMetadataSync) {
|
|
330
|
+
this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw', file.txOptions);
|
|
331
|
+
file.needsMetadataSync = false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
335
|
+
// Sync is only needed here for durability. Visibility for other
|
|
336
|
+
// connections is ensured in jUnlock().
|
|
337
|
+
if (file.synchronous === 'full') {
|
|
338
|
+
await this.#idb.sync(true);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
await this.#idb.sync(file.synchronous === 'full');
|
|
342
|
+
}
|
|
343
|
+
return VFS.SQLITE_OK;
|
|
344
|
+
} catch (e) {
|
|
345
|
+
this.lastError = e;
|
|
346
|
+
return VFS.SQLITE_IOERR_FSYNC;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @param {number} fileId
|
|
352
|
+
* @param {DataView} pSize64
|
|
353
|
+
* @returns {number}
|
|
354
|
+
*/
|
|
355
|
+
jFileSize(fileId, pSize64) {
|
|
356
|
+
try {
|
|
357
|
+
const file = this.mapIdToFile.get(fileId);
|
|
358
|
+
pSize64.setBigInt64(0, BigInt(file.metadata.fileSize), true);
|
|
359
|
+
return VFS.SQLITE_OK;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
this.lastError = e;
|
|
362
|
+
return VFS.SQLITE_IOERR_FSTAT;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @param {number} fileId
|
|
368
|
+
* @param {number} lockType
|
|
369
|
+
* @returns {Promise<number>}
|
|
370
|
+
*/
|
|
371
|
+
async jLock(fileId, lockType) {
|
|
372
|
+
// Call the actual lock implementation.
|
|
373
|
+
const file = this.mapIdToFile.get(fileId);
|
|
374
|
+
const result = await super.jLock(fileId, lockType);
|
|
375
|
+
|
|
376
|
+
if (lockType === VFS.SQLITE_LOCK_SHARED) {
|
|
377
|
+
// Update metadata.
|
|
378
|
+
file.metadata = await this.#idb.q(async ({ metadata, blocks }) => {
|
|
379
|
+
// @ts-ignore
|
|
380
|
+
/** @type {Metadata} */ const m = await metadata.get(file.path);
|
|
381
|
+
if (m.pendingVersion) {
|
|
382
|
+
console.warn(`removing failed transaction ${m.pendingVersion}`);
|
|
383
|
+
await new Promise((resolve, reject) => {
|
|
384
|
+
const range = IDBKeyRange.bound([m.name, -Infinity], [m.name, Infinity]);
|
|
385
|
+
const request = blocks.openCursor(range);
|
|
386
|
+
request.onsuccess = () => {
|
|
387
|
+
const cursor = request.result;
|
|
388
|
+
if (cursor) {
|
|
389
|
+
const block = cursor.value;
|
|
390
|
+
if (block.version < m.version) {
|
|
391
|
+
cursor.delete();
|
|
392
|
+
}
|
|
393
|
+
cursor.continue();
|
|
394
|
+
} else {
|
|
395
|
+
resolve();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
request.onerror = () => reject(request.error);
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
delete m.pendingVersion;
|
|
402
|
+
metadata.put(m);
|
|
403
|
+
}
|
|
404
|
+
return m;
|
|
405
|
+
}, 'rw', file.txOptions);
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @param {number} fileId
|
|
412
|
+
* @param {number} lockType
|
|
413
|
+
* @returns {Promise<number>}
|
|
414
|
+
*/
|
|
415
|
+
async jUnlock(fileId, lockType) {
|
|
416
|
+
if (lockType === VFS.SQLITE_LOCK_NONE) {
|
|
417
|
+
const file = this.mapIdToFile.get(fileId);
|
|
418
|
+
await this.#idb.sync(file.synchronous === 'full');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Call the actual unlock implementation.
|
|
422
|
+
return super.jUnlock(fileId, lockType);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {number} fileId
|
|
427
|
+
* @param {number} op
|
|
428
|
+
* @param {DataView} pArg
|
|
429
|
+
* @returns {number|Promise<number>}
|
|
430
|
+
*/
|
|
431
|
+
jFileControl(fileId, op, pArg) {
|
|
432
|
+
try {
|
|
433
|
+
const file = this.mapIdToFile.get(fileId);
|
|
434
|
+
switch (op) {
|
|
435
|
+
case VFS.SQLITE_FCNTL_PRAGMA:
|
|
436
|
+
const key = extractString(pArg, 4);
|
|
437
|
+
const value = extractString(pArg, 8);
|
|
438
|
+
this.log?.('xFileControl', file.path, 'PRAGMA', key, value);
|
|
439
|
+
const setPragmaResponse = response => {
|
|
440
|
+
const encoded = new TextEncoder().encode(response);
|
|
441
|
+
const out = this._module._sqlite3_malloc(encoded.byteLength);
|
|
442
|
+
const outArray = this._module.HEAPU8.subarray(out, out + encoded.byteLength);
|
|
443
|
+
outArray.set(encoded);
|
|
444
|
+
pArg.setUint32(0, out, true);
|
|
445
|
+
return VFS.SQLITE_ERROR;
|
|
446
|
+
};
|
|
447
|
+
switch (key.toLowerCase()) {
|
|
448
|
+
case 'page_size':
|
|
449
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
450
|
+
// Don't allow changing the page size.
|
|
451
|
+
if (value && file.metadata.fileSize) {
|
|
452
|
+
return VFS.SQLITE_ERROR;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case 'synchronous':
|
|
457
|
+
if (value) {
|
|
458
|
+
switch (value.toLowerCase()) {
|
|
459
|
+
case '0':
|
|
460
|
+
case 'off':
|
|
461
|
+
file.synchronous = 'off';
|
|
462
|
+
file.txOptions = { durability: 'relaxed' };
|
|
463
|
+
break;
|
|
464
|
+
case '1':
|
|
465
|
+
case 'normal':
|
|
466
|
+
file.synchronous = 'normal';
|
|
467
|
+
file.txOptions = { durability: 'relaxed' };
|
|
468
|
+
break;
|
|
469
|
+
case '2':
|
|
470
|
+
case '3':
|
|
471
|
+
case 'full':
|
|
472
|
+
case 'extra':
|
|
473
|
+
file.synchronous = 'full';
|
|
474
|
+
file.txOptions = { durability: 'strict' };
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
case 'write_hint':
|
|
480
|
+
return super.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null);
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
case VFS.SQLITE_FCNTL_SYNC:
|
|
484
|
+
this.log?.('xFileControl', file.path, 'SYNC');
|
|
485
|
+
const commitMetadata = Object.assign({}, file.metadata);
|
|
486
|
+
const prevFileSize = file.rollback.fileSize
|
|
487
|
+
this.#idb.q(({ metadata, blocks }) => {
|
|
488
|
+
metadata.put(commitMetadata);
|
|
489
|
+
|
|
490
|
+
// Remove old page versions.
|
|
491
|
+
for (const offset of file.changedPages) {
|
|
492
|
+
if (offset < prevFileSize) {
|
|
493
|
+
const range = IDBKeyRange.bound(
|
|
494
|
+
[file.path, -offset, commitMetadata.version],
|
|
495
|
+
[file.path, -offset, Infinity],
|
|
496
|
+
true);
|
|
497
|
+
blocks.delete(range);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
file.changedPages.clear();
|
|
501
|
+
}, 'rw', file.txOptions);
|
|
502
|
+
file.needsMetadataSync = false;
|
|
503
|
+
file.rollback = null;
|
|
504
|
+
break;
|
|
505
|
+
case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
|
|
506
|
+
// Every write transaction is atomic, so this is a no-op.
|
|
507
|
+
this.log?.('xFileControl', file.path, 'BEGIN_ATOMIC_WRITE');
|
|
508
|
+
return VFS.SQLITE_OK;
|
|
509
|
+
case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
|
|
510
|
+
// Every write transaction is atomic, so this is a no-op.
|
|
511
|
+
this.log?.('xFileControl', file.path, 'COMMIT_ATOMIC_WRITE');
|
|
512
|
+
return VFS.SQLITE_OK;
|
|
513
|
+
case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
|
|
514
|
+
this.log?.('xFileControl', file.path, 'ROLLBACK_ATOMIC_WRITE');
|
|
515
|
+
file.metadata = file.rollback;
|
|
516
|
+
const rollbackMetadata = Object.assign({}, file.metadata);
|
|
517
|
+
this.#idb.q(({ metadata, blocks }) => {
|
|
518
|
+
metadata.put(rollbackMetadata);
|
|
519
|
+
|
|
520
|
+
// Remove pages.
|
|
521
|
+
for (const offset of file.changedPages) {
|
|
522
|
+
blocks.delete([file.path, -offset, rollbackMetadata.version - 1]);
|
|
523
|
+
}
|
|
524
|
+
file.changedPages.clear();
|
|
525
|
+
}, 'rw', file.txOptions);
|
|
526
|
+
file.needsMetadataSync = false;
|
|
527
|
+
file.rollback = null;
|
|
528
|
+
return VFS.SQLITE_OK;
|
|
529
|
+
}
|
|
530
|
+
} catch (e) {
|
|
531
|
+
this.lastError = e;
|
|
532
|
+
return VFS.SQLITE_IOERR;
|
|
533
|
+
}
|
|
534
|
+
return super.jFileControl(fileId, op, pArg);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* @param {number} pFile
|
|
539
|
+
* @returns {number|Promise<number>}
|
|
540
|
+
*/
|
|
541
|
+
jDeviceCharacteristics(pFile) {
|
|
542
|
+
return 0
|
|
543
|
+
| VFS.SQLITE_IOCAP_BATCH_ATOMIC
|
|
544
|
+
| VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @param {Uint8Array} zBuf
|
|
549
|
+
* @returns {number|Promise<number>}
|
|
550
|
+
*/
|
|
551
|
+
jGetLastError(zBuf) {
|
|
552
|
+
if (this.lastError) {
|
|
553
|
+
console.error(this.lastError);
|
|
554
|
+
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
|
|
555
|
+
const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
|
|
556
|
+
zBuf[written] = 0;
|
|
557
|
+
}
|
|
558
|
+
return VFS.SQLITE_OK
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function extractString(dataView, offset) {
|
|
563
|
+
const p = dataView.getUint32(offset, true);
|
|
564
|
+
if (p) {
|
|
565
|
+
const chars = new Uint8Array(dataView.buffer, p);
|
|
566
|
+
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)));
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export class IDBContext {
|
|
572
|
+
/** @type {IDBDatabase} */ #database;
|
|
573
|
+
|
|
574
|
+
/** @type {Promise} */ #chain = null;
|
|
575
|
+
/** @type {Promise<any>} */ #txComplete = Promise.resolve();
|
|
576
|
+
/** @type {IDBRequest?} */ #request = null;
|
|
577
|
+
/** @type {WeakSet<IDBTransaction>} */ #txPending = new WeakSet();
|
|
578
|
+
|
|
579
|
+
log = null;
|
|
580
|
+
|
|
581
|
+
static async create(name) {
|
|
582
|
+
const database = await new Promise((resolve, reject) => {
|
|
583
|
+
const request = indexedDB.open(name, 6);
|
|
584
|
+
request.onupgradeneeded = async event => {
|
|
585
|
+
const db = request.result;
|
|
586
|
+
if (event.oldVersion) {
|
|
587
|
+
console.log(`Upgrading IndexedDB from version ${event.oldVersion}`);
|
|
588
|
+
}
|
|
589
|
+
switch (event.oldVersion) {
|
|
590
|
+
case 0:
|
|
591
|
+
// Start with the original schema.
|
|
592
|
+
db.createObjectStore('blocks', { keyPath: ['path', 'offset', 'version']})
|
|
593
|
+
.createIndex('version', ['path', 'version']);
|
|
594
|
+
// fall through intentionally
|
|
595
|
+
case 5:
|
|
596
|
+
const tx = request.transaction;
|
|
597
|
+
const blocks = tx.objectStore('blocks');
|
|
598
|
+
blocks.deleteIndex('version');
|
|
599
|
+
const metadata = db.createObjectStore('metadata', { keyPath: 'name' });
|
|
600
|
+
|
|
601
|
+
await new Promise((resolve, reject) => {
|
|
602
|
+
// Iterate over all the blocks.
|
|
603
|
+
let lastBlock = {};
|
|
604
|
+
const request = tx.objectStore('blocks').openCursor();
|
|
605
|
+
request.onsuccess = () => {
|
|
606
|
+
const cursor = request.result;
|
|
607
|
+
if (cursor) {
|
|
608
|
+
const block = cursor.value;
|
|
609
|
+
if (typeof block.offset !== 'number' ||
|
|
610
|
+
(block.path === lastBlock.path && block.offset === lastBlock.offset)) {
|
|
611
|
+
// Remove superceded block (or the "purge" info).
|
|
612
|
+
cursor.delete();
|
|
613
|
+
} else if (block.offset === 0) {
|
|
614
|
+
// Move metadata to its own store.
|
|
615
|
+
metadata.put({
|
|
616
|
+
name: block.path,
|
|
617
|
+
fileSize: block.fileSize,
|
|
618
|
+
version: block.version
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
delete block.fileSize;
|
|
622
|
+
cursor.update(block);
|
|
623
|
+
}
|
|
624
|
+
lastBlock = block;
|
|
625
|
+
cursor.continue();
|
|
626
|
+
} else {
|
|
627
|
+
resolve();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
request.onerror = () => reject(request.error);
|
|
631
|
+
});
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
request.onsuccess = () => resolve(request.result);
|
|
636
|
+
request.onerror = () => reject(request.error);
|
|
637
|
+
});
|
|
638
|
+
return new IDBContext(database);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
constructor(database) {
|
|
642
|
+
this.#database = database;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
close() {
|
|
646
|
+
this.#database.close();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* @param {(stores: Object.<string, IDBObjectStore>) => any} f
|
|
651
|
+
* @param {'ro'|'rw'} mode
|
|
652
|
+
* @returns {Promise<any>}
|
|
653
|
+
*/
|
|
654
|
+
q(f, mode = 'ro', options = {}) {
|
|
655
|
+
/** @type {IDBTransactionMode} */
|
|
656
|
+
const txMode = mode === 'ro' ? 'readonly' : 'readwrite';
|
|
657
|
+
const txOptions = Object.assign({
|
|
658
|
+
/** @type {IDBTransactionDurability} */ durability: 'default'
|
|
659
|
+
}, options);
|
|
660
|
+
|
|
661
|
+
// Ensure that queries run sequentially. If any function rejects,
|
|
662
|
+
// or any request has an error, or the transaction does not commit,
|
|
663
|
+
// then no subsequent functions will run until sync() or reset().
|
|
664
|
+
this.#chain = (this.#chain || Promise.resolve())
|
|
665
|
+
.then(() => this.#q(f, txMode, txOptions));
|
|
666
|
+
return this.#chain;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* @param {(stores: Object.<string, IDBObjectStore>) => any} f
|
|
671
|
+
* @param {IDBTransactionMode} mode
|
|
672
|
+
* @param {IDBTransactionOptions} options
|
|
673
|
+
* @returns {Promise<any>}
|
|
674
|
+
*/
|
|
675
|
+
async #q(f, mode, options) {
|
|
676
|
+
/** @type {IDBTransaction} */ let tx;
|
|
677
|
+
if (this.#request &&
|
|
678
|
+
this.#txPending.has(this.#request.transaction) &&
|
|
679
|
+
this.#request.transaction.mode >= mode &&
|
|
680
|
+
this.#request.transaction.durability === options.durability) {
|
|
681
|
+
// The previous request transaction is compatible and has
|
|
682
|
+
// not yet completed.
|
|
683
|
+
tx = this.#request.transaction;
|
|
684
|
+
|
|
685
|
+
// If the previous request is pending, wait for it to complete.
|
|
686
|
+
// This ensures that the transaction will be active.
|
|
687
|
+
if (this.#request.readyState === 'pending') {
|
|
688
|
+
await new Promise(resolve => {
|
|
689
|
+
this.#request.addEventListener('success', resolve, { once: true });
|
|
690
|
+
this.#request.addEventListener('error', resolve, { once: true });
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
for (let i = 0; i < 2; ++i) {
|
|
696
|
+
if (!tx) {
|
|
697
|
+
// The current transaction is missing or doesn't match so
|
|
698
|
+
// replace it with a new one. wait for the previous
|
|
699
|
+
// transaction to complete so the lifetimes do not overlap.
|
|
700
|
+
await this.#txComplete;
|
|
701
|
+
|
|
702
|
+
// Create the new transaction.
|
|
703
|
+
// @ts-ignore
|
|
704
|
+
tx = this.#database.transaction(this.#database.objectStoreNames, mode, options);
|
|
705
|
+
this.log?.('IDBTransaction open', mode);
|
|
706
|
+
this.#txPending.add(tx);
|
|
707
|
+
this.#txComplete = new Promise((resolve, reject) => {
|
|
708
|
+
tx.addEventListener('complete', () => {
|
|
709
|
+
this.log?.('IDBTransaction complete');
|
|
710
|
+
this.#txPending.delete(tx);
|
|
711
|
+
resolve();
|
|
712
|
+
});
|
|
713
|
+
tx.addEventListener('abort', () => {
|
|
714
|
+
this.#txPending.delete(tx);
|
|
715
|
+
reject(new Error('transaction aborted'));
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// @ts-ignore
|
|
721
|
+
// Create object store proxies.
|
|
722
|
+
const objectStores = [...tx.objectStoreNames].map(name => {
|
|
723
|
+
return [name, this.proxyStoreOrIndex(tx.objectStore(name))];
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
// Execute the function.
|
|
728
|
+
return await f(Object.fromEntries(objectStores));
|
|
729
|
+
} catch (e) {
|
|
730
|
+
// Use a new transaction if this one was inactive. This will
|
|
731
|
+
// happen if the last request in the transaction completed
|
|
732
|
+
// in a previous task but the transaction has not yet committed.
|
|
733
|
+
if (!i && e.name === 'TransactionInactiveError') {
|
|
734
|
+
this.log?.('TransactionInactiveError, retrying');
|
|
735
|
+
tx = null;
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
throw e;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Object store methods that return an IDBRequest, except for cursor
|
|
745
|
+
* creation, are wrapped to return a Promise. In addition, the
|
|
746
|
+
* request is used internally for chaining.
|
|
747
|
+
* @param {IDBObjectStore} objectStore
|
|
748
|
+
* @returns
|
|
749
|
+
*/
|
|
750
|
+
proxyStoreOrIndex(objectStore) {
|
|
751
|
+
return new Proxy(objectStore, {
|
|
752
|
+
get: (target, property, receiver) => {
|
|
753
|
+
const result = Reflect.get(target, property, receiver);
|
|
754
|
+
if (typeof result === 'function') {
|
|
755
|
+
return (...args) => {
|
|
756
|
+
const maybeRequest = Reflect.apply(result, target, args);
|
|
757
|
+
// @ts-ignore
|
|
758
|
+
if (maybeRequest instanceof IDBRequest && !property.endsWith('Cursor')) {
|
|
759
|
+
// // Debug logging.
|
|
760
|
+
// this.log?.(`${target.name}.${String(property)}`, args);
|
|
761
|
+
// maybeRequest.addEventListener('success', () => {
|
|
762
|
+
// this.log?.(`${target.name}.${String(property)} success`, maybeRequest.result);
|
|
763
|
+
// });
|
|
764
|
+
// maybeRequest.addEventListener('error', () => {
|
|
765
|
+
// this.log?.(`${target.name}.${String(property)} error`, maybeRequest.error);
|
|
766
|
+
// });
|
|
767
|
+
|
|
768
|
+
// Save the request.
|
|
769
|
+
this.#request = maybeRequest;
|
|
770
|
+
|
|
771
|
+
// Abort the transaction on error.
|
|
772
|
+
maybeRequest.addEventListener('error', () => {
|
|
773
|
+
console.error(maybeRequest.error);
|
|
774
|
+
maybeRequest.transaction.abort();
|
|
775
|
+
}, { once: true });
|
|
776
|
+
|
|
777
|
+
// Return a Promise.
|
|
778
|
+
return wrap(maybeRequest);
|
|
779
|
+
}
|
|
780
|
+
return maybeRequest;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return result;
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @param {boolean} durable
|
|
790
|
+
*/
|
|
791
|
+
async sync(durable) {
|
|
792
|
+
if (this.#chain) {
|
|
793
|
+
// This waits for all IndexedDB calls to be made.
|
|
794
|
+
await this.#chain;
|
|
795
|
+
if (durable) {
|
|
796
|
+
// This waits for the final transaction to commit.
|
|
797
|
+
await this.#txComplete;
|
|
798
|
+
}
|
|
799
|
+
this.reset();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
reset() {
|
|
804
|
+
this.#chain = null;
|
|
805
|
+
this.#txComplete = Promise.resolve();
|
|
806
|
+
this.#request = null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* @param {IDBRequest} request
|
|
812
|
+
* @returns {Promise}
|
|
813
|
+
*/
|
|
814
|
+
function wrap(request) {
|
|
815
|
+
return new Promise((resolve, reject) => {
|
|
816
|
+
request.onsuccess = () => resolve(request.result);
|
|
817
|
+
request.onerror = () => reject(request.error);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|