@ehegnes/wa-sqlite 2.0.0-beta.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 +2 -0
- package/dist/wa-sqlite-async.wasm +0 -0
- package/dist/wa-sqlite-jspi.mjs +2 -0
- package/dist/wa-sqlite-jspi.wasm +0 -0
- package/dist/wa-sqlite.mjs +2 -0
- package/dist/wa-sqlite.wasm +0 -0
- package/package.json +44 -0
- package/src/FacadeVFS.js +681 -0
- package/src/VFS.js +222 -0
- package/src/WebLocksMixin.js +411 -0
- package/src/examples/AccessHandlePoolVFS.js +458 -0
- package/src/examples/IDBBatchAtomicVFS.js +827 -0
- package/src/examples/IDBMirrorVFS.js +889 -0
- package/src/examples/LazyLock.js +90 -0
- package/src/examples/Lock.js +69 -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 +597 -0
- package/src/examples/OPFSPermutedVFS.js +1217 -0
- package/src/examples/OPFSWriteAheadVFS.js +960 -0
- package/src/examples/README.md +81 -0
- package/src/examples/WriteAhead.js +1174 -0
- package/src/examples/tag.js +82 -0
- package/src/sqlite-api.js +924 -0
- package/src/sqlite-constants.js +275 -0
- package/src/types/globals.d.ts +60 -0
- package/src/types/index.d.ts +1228 -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/OPFSWriteAheadVFS.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 +447 -0
- package/test/callbacks.test.js +581 -0
- package/test/data/idbv5.json +1 -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,960 @@
|
|
|
1
|
+
import { FacadeVFS } from "../FacadeVFS.js";
|
|
2
|
+
import * as VFS from '../VFS.js';
|
|
3
|
+
import { LazyLock } from "./LazyLock.js";
|
|
4
|
+
import { WriteAhead } from "./WriteAhead.js";
|
|
5
|
+
|
|
6
|
+
const LIBRARY_FILES_ROOT = '.wa-sqlite';
|
|
7
|
+
const DEFAULT_TEMP_FILES = 6;
|
|
8
|
+
|
|
9
|
+
const finalizationRegistry = new FinalizationRegistry((/** @type {() => void} */ f) => f());
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef FileEntry
|
|
13
|
+
* @property {string} zName
|
|
14
|
+
* @property {number} flags
|
|
15
|
+
* @property {FileSystemSyncAccessHandle} [accessHandle]
|
|
16
|
+
|
|
17
|
+
* Main database file properties:
|
|
18
|
+
* @property {*} [retryResult]
|
|
19
|
+
* @property {FileSystemSyncAccessHandle[]} [waHandles]
|
|
20
|
+
*
|
|
21
|
+
* @property {'reserved'|'exclusive'|null} [writeHint]
|
|
22
|
+
* @property {'normal'|'exclusive'} [lockingMode]
|
|
23
|
+
* @property {number} [lockState] SQLITE_LOCK_*
|
|
24
|
+
* @property {LazyLock} [readLock]
|
|
25
|
+
* @property {LazyLock} [writeLock]
|
|
26
|
+
* @property {'none'|'read'|'write'|'readwrite'} [useLazyLock]
|
|
27
|
+
* @property {number} [timeout]
|
|
28
|
+
* @property {0|1|2|3} [synchronous]
|
|
29
|
+
* @property {number?} [pageSize]
|
|
30
|
+
* @property {boolean} [overwrite]
|
|
31
|
+
*
|
|
32
|
+
* @property {WriteAhead} [writeAhead]
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef OPFSWriteAheadOptions
|
|
37
|
+
* @property {number} [nTmpFiles]
|
|
38
|
+
* @property {number} [autoCheckpoint]
|
|
39
|
+
* @property {number} [backstopInterval]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export class OPFSWriteAheadVFS extends FacadeVFS {
|
|
43
|
+
lastError = null;
|
|
44
|
+
log = null;
|
|
45
|
+
|
|
46
|
+
/** @type {Map<number, FileEntry>} */ mapIdToFile = new Map();
|
|
47
|
+
/** @type {Map<string, FileEntry>} */ mapPathToFile = new Map();
|
|
48
|
+
|
|
49
|
+
/** @type {Map<string, FileSystemSyncAccessHandle>} */ boundTempFiles = new Map();
|
|
50
|
+
/** @type {Set<FileSystemSyncAccessHandle>} */ unboundTempFiles = new Set();
|
|
51
|
+
/** @type {OPFSWriteAheadOptions} */ options = {
|
|
52
|
+
nTmpFiles: DEFAULT_TEMP_FILES
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
_ready;
|
|
56
|
+
|
|
57
|
+
static async create(name, module, options) {
|
|
58
|
+
const vfs = new OPFSWriteAheadVFS(name, module);
|
|
59
|
+
Object.assign(vfs.options, options);
|
|
60
|
+
await vfs.isReady();
|
|
61
|
+
return vfs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
constructor(name, module) {
|
|
65
|
+
super(name, module);
|
|
66
|
+
this._ready = (async () => {
|
|
67
|
+
// Ensure the library files root directory exists.
|
|
68
|
+
let dirHandle = await navigator.storage.getDirectory();
|
|
69
|
+
dirHandle = await dirHandle.getDirectoryHandle(LIBRARY_FILES_ROOT, { create: true });
|
|
70
|
+
|
|
71
|
+
// Clean up any stale session directories.
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
for await (const name of dirHandle.keys()) {
|
|
74
|
+
if (name.startsWith('.session-')) {
|
|
75
|
+
// Acquire a lock on the session directory to ensure it is not in use.
|
|
76
|
+
await navigator.locks.request(name, { ifAvailable: true }, async lock => {
|
|
77
|
+
if (lock) {
|
|
78
|
+
// This directory is not in use.
|
|
79
|
+
try {
|
|
80
|
+
await dirHandle.removeEntry(name, { recursive: true });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Ignore errors, will try again next time.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create our session directory.
|
|
90
|
+
const dirName = `.session-${Math.random().toString(16).slice(2)}`;
|
|
91
|
+
await new Promise(resolve => {
|
|
92
|
+
navigator.locks.request(dirName, () => {
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
resolve();
|
|
95
|
+
return new Promise(release => {
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
finalizationRegistry.register(this, release);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
dirHandle = await dirHandle.getDirectoryHandle(dirName, { create: true });
|
|
102
|
+
|
|
103
|
+
// Create temporary files.
|
|
104
|
+
for (let i = 0; i < this.options.nTmpFiles; i++) {
|
|
105
|
+
const fileHandle= await dirHandle.getFileHandle(i.toString(), { create: true });
|
|
106
|
+
const accessHandle = await fileHandle.createSyncAccessHandle();
|
|
107
|
+
finalizationRegistry.register(this, () => accessHandle.close());
|
|
108
|
+
this.unboundTempFiles.add(accessHandle);
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isReady() {
|
|
114
|
+
return Promise.all([super.isReady(), this._ready]).then(() => true);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string?} zName
|
|
119
|
+
* @param {number} fileId
|
|
120
|
+
* @param {number} flags
|
|
121
|
+
* @param {DataView} pOutFlags
|
|
122
|
+
* @returns {number}
|
|
123
|
+
*/
|
|
124
|
+
jOpen(zName, fileId, flags, pOutFlags) {
|
|
125
|
+
try {
|
|
126
|
+
if (zName === null) {
|
|
127
|
+
// Generate a temporary filename. This will only be used as a
|
|
128
|
+
// key to map to a pre-opened temporary file access handle.
|
|
129
|
+
zName = Math.random().toString(16).slice(2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const file = this.mapPathToFile.get(zName) ?? {
|
|
133
|
+
zName,
|
|
134
|
+
flags,
|
|
135
|
+
retryResult: null,
|
|
136
|
+
};
|
|
137
|
+
this.mapPathToFile.set(zName, file);
|
|
138
|
+
|
|
139
|
+
if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
140
|
+
// Open database and journal files with a retry operation.
|
|
141
|
+
if (file.retryResult === null) {
|
|
142
|
+
// This is the initial open attempt. Start the asynchronous task
|
|
143
|
+
// and return SQLITE_BUSY to force a retry.
|
|
144
|
+
this._module.retryOps.push(this.#retryOpen(zName, flags, fileId, pOutFlags));
|
|
145
|
+
return VFS.SQLITE_BUSY;
|
|
146
|
+
} else if (file.retryResult instanceof Error) {
|
|
147
|
+
const e = file.retryResult;
|
|
148
|
+
file.retryResult = null;
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Initialize database file state.
|
|
153
|
+
file.accessHandle = file.retryResult.accessHandle;
|
|
154
|
+
file.waHandles = file.retryResult.waHandles;
|
|
155
|
+
file.writeAhead = file.retryResult.writeAhead;
|
|
156
|
+
file.retryResult = null;
|
|
157
|
+
|
|
158
|
+
file.lockState = VFS.SQLITE_LOCK_NONE;
|
|
159
|
+
file.lockingMode = 'normal';
|
|
160
|
+
file.readLock = new LazyLock(`${zName}#read`);
|
|
161
|
+
file.writeLock = new LazyLock(`${zName}#write`);
|
|
162
|
+
file.useLazyLock = 'readwrite';
|
|
163
|
+
file.timeout = -1;
|
|
164
|
+
file.synchronous = 1; // NORMAL
|
|
165
|
+
file.writeHint = null;
|
|
166
|
+
file.pageSize = null;
|
|
167
|
+
file.overwrite = false;
|
|
168
|
+
} else if (flags & (VFS.SQLITE_OPEN_WAL | VFS.SQLITE_OPEN_SUPER_JOURNAL)) {
|
|
169
|
+
throw new Error('WAL and super-journal files are not supported');
|
|
170
|
+
} else if (file.accessHandle) {
|
|
171
|
+
// This temporary file already has an access handle, which happens
|
|
172
|
+
// only for tests. Just use it as is.
|
|
173
|
+
} else {
|
|
174
|
+
// This is a temporary file. Use an unbound pre-opened accessHandle.
|
|
175
|
+
if (!(flags & VFS.SQLITE_OPEN_CREATE)) throw new Error('file not found');
|
|
176
|
+
file.accessHandle = this.#openTemporaryFile(zName);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.mapIdToFile.set(fileId, file);
|
|
180
|
+
pOutFlags.setInt32(0, flags, true);
|
|
181
|
+
return VFS.SQLITE_OK;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error(e.stack);
|
|
184
|
+
this.lastError = e;
|
|
185
|
+
this.mapPathToFile.delete(zName);
|
|
186
|
+
return VFS.SQLITE_CANTOPEN;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {string} zName
|
|
192
|
+
* @param {number} syncDir
|
|
193
|
+
* @returns {number}
|
|
194
|
+
*/
|
|
195
|
+
jDelete(zName, syncDir) {
|
|
196
|
+
try {
|
|
197
|
+
if (this.boundTempFiles.has(zName)) {
|
|
198
|
+
const file = this.mapPathToFile.get(zName);
|
|
199
|
+
this.#deleteTemporaryFile(file);
|
|
200
|
+
} else {
|
|
201
|
+
throw new Error(`unexpected file deletion: ${zName}`);
|
|
202
|
+
}
|
|
203
|
+
return VFS.SQLITE_OK;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error(e.stack);
|
|
206
|
+
this.lastError = e;
|
|
207
|
+
return VFS.SQLITE_IOERR_DELETE;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {string} zName
|
|
213
|
+
* @param {number} flags
|
|
214
|
+
* @param {DataView} pResOut
|
|
215
|
+
* @returns {number}
|
|
216
|
+
*/
|
|
217
|
+
jAccess(zName, flags, pResOut) {
|
|
218
|
+
try {
|
|
219
|
+
const file = this.mapPathToFile.get(zName);
|
|
220
|
+
pResOut.setInt32(0, file ? 1 : 0, true);
|
|
221
|
+
return VFS.SQLITE_OK;
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.error(e.stack);
|
|
224
|
+
this.lastError = e;
|
|
225
|
+
return VFS.SQLITE_IOERR_ACCESS;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {number} fileId
|
|
231
|
+
* @returns {number}
|
|
232
|
+
*/
|
|
233
|
+
jClose(fileId) {
|
|
234
|
+
try {
|
|
235
|
+
const file = this.mapIdToFile.get(fileId);
|
|
236
|
+
if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
237
|
+
file.writeAhead.close();
|
|
238
|
+
file.accessHandle.close();
|
|
239
|
+
file.waHandles.forEach(handle => handle.close());
|
|
240
|
+
this.mapPathToFile.delete(file?.zName);
|
|
241
|
+
|
|
242
|
+
file.readLock.close();
|
|
243
|
+
file.writeLock.close();
|
|
244
|
+
} else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
245
|
+
this.#deleteTemporaryFile(file);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Disassociate fileId from file entry.
|
|
249
|
+
this.mapIdToFile.delete(fileId);
|
|
250
|
+
return VFS.SQLITE_OK;
|
|
251
|
+
} catch (e) {
|
|
252
|
+
console.error(e.stack);
|
|
253
|
+
this.lastError = e;
|
|
254
|
+
return VFS.SQLITE_IOERR_CLOSE;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {number} fileId
|
|
260
|
+
* @param {Uint8Array} pData
|
|
261
|
+
* @param {number} iOffset
|
|
262
|
+
* @returns {number}
|
|
263
|
+
*/
|
|
264
|
+
jRead(fileId, pData, iOffset) {
|
|
265
|
+
try {
|
|
266
|
+
const file = this.mapIdToFile.get(fileId);
|
|
267
|
+
|
|
268
|
+
let bytesRead = null;
|
|
269
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
270
|
+
// Try reading from the write-ahead overlays first. A read on the
|
|
271
|
+
// database file is always a complete page, except when reading
|
|
272
|
+
// from the 100-byte header.
|
|
273
|
+
const pageOffset = iOffset < 100 ? iOffset : 0;
|
|
274
|
+
const page = file.writeAhead.read(iOffset - pageOffset);
|
|
275
|
+
if (page) {
|
|
276
|
+
const readData = page.subarray(pageOffset, pageOffset + pData.byteLength);
|
|
277
|
+
pData.set(readData);
|
|
278
|
+
bytesRead = readData.byteLength;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (bytesRead === null) {
|
|
283
|
+
// Read directly from the OPFS file.
|
|
284
|
+
|
|
285
|
+
// On Chrome (at least), passing pData to accessHandle.read() is
|
|
286
|
+
// an error because pData is a Proxy of a Uint8Array. Calling
|
|
287
|
+
// subarray() produces a real Uint8Array and that works.
|
|
288
|
+
bytesRead = file.accessHandle.read(pData.subarray(), { at: iOffset });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (bytesRead < pData.byteLength) {
|
|
292
|
+
pData.fill(0, bytesRead);
|
|
293
|
+
return VFS.SQLITE_IOERR_SHORT_READ;
|
|
294
|
+
}
|
|
295
|
+
return VFS.SQLITE_OK;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.error(e.stack);
|
|
298
|
+
this.lastError = e;
|
|
299
|
+
return VFS.SQLITE_IOERR_READ;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {number} fileId
|
|
305
|
+
* @param {Uint8Array} pData
|
|
306
|
+
* @param {number} iOffset
|
|
307
|
+
* @returns {number}
|
|
308
|
+
*/
|
|
309
|
+
jWrite(fileId, pData, iOffset) {
|
|
310
|
+
try {
|
|
311
|
+
const file = this.mapIdToFile.get(fileId);
|
|
312
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
313
|
+
// Write to the write-ahead overlay.
|
|
314
|
+
const isPageResize = file.overwrite && file.pageSize !== pData.byteLength;
|
|
315
|
+
file.writeAhead.write(iOffset, pData, {
|
|
316
|
+
dstPageSize: isPageResize ? file.pageSize : null
|
|
317
|
+
});
|
|
318
|
+
return VFS.SQLITE_OK;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// On Chrome (at least), passing pData to accessHandle.write() is
|
|
322
|
+
// an error because pData is a Proxy of a Uint8Array. Calling
|
|
323
|
+
// subarray() produces a real Uint8Array and that works.
|
|
324
|
+
file.accessHandle.write(pData.subarray(), { at: iOffset });
|
|
325
|
+
return VFS.SQLITE_OK;
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error(e.stack);
|
|
328
|
+
this.lastError = e;
|
|
329
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @param {number} fileId
|
|
335
|
+
* @param {number} iSize
|
|
336
|
+
* @returns {number}
|
|
337
|
+
*/
|
|
338
|
+
jTruncate(fileId, iSize) {
|
|
339
|
+
try {
|
|
340
|
+
const file = this.mapIdToFile.get(fileId);
|
|
341
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
342
|
+
file.writeAhead.truncate(iSize);
|
|
343
|
+
return VFS.SQLITE_OK;
|
|
344
|
+
}
|
|
345
|
+
file.accessHandle.truncate(iSize);
|
|
346
|
+
return VFS.SQLITE_OK;
|
|
347
|
+
} catch (e) {
|
|
348
|
+
console.error(e.stack);
|
|
349
|
+
this.lastError = e;
|
|
350
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {number} fileId
|
|
356
|
+
* @param {number} flags
|
|
357
|
+
* @returns {number}
|
|
358
|
+
*/
|
|
359
|
+
jSync(fileId, flags) {
|
|
360
|
+
try {
|
|
361
|
+
const file = this.mapIdToFile.get(fileId);
|
|
362
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
363
|
+
const durability = file.synchronous > 1 ? 'strict' : 'relaxed';
|
|
364
|
+
file.writeAhead.sync({ durability });
|
|
365
|
+
} else {
|
|
366
|
+
// This is a temporary file so sync is not needed.
|
|
367
|
+
// Temporary journals are only used for rollback by the
|
|
368
|
+
// connection that created them, not for recovery.
|
|
369
|
+
}
|
|
370
|
+
return VFS.SQLITE_OK;
|
|
371
|
+
} catch (e) {
|
|
372
|
+
console.error(e.stack);
|
|
373
|
+
this.lastError = e;
|
|
374
|
+
return VFS.SQLITE_IOERR_FSYNC;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {number} fileId
|
|
380
|
+
* @param {DataView} pSize64
|
|
381
|
+
* @returns {number}
|
|
382
|
+
*/
|
|
383
|
+
jFileSize(fileId, pSize64) {
|
|
384
|
+
try {
|
|
385
|
+
const file = this.mapIdToFile.get(fileId);
|
|
386
|
+
|
|
387
|
+
let size;
|
|
388
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
389
|
+
size = file.writeAhead.getFileSize() || file.accessHandle.getSize();
|
|
390
|
+
} else {
|
|
391
|
+
size = file.accessHandle.getSize();
|
|
392
|
+
}
|
|
393
|
+
pSize64.setBigInt64(0, BigInt(size), true);
|
|
394
|
+
return VFS.SQLITE_OK;
|
|
395
|
+
} catch (e) {
|
|
396
|
+
console.error(e.stack);
|
|
397
|
+
this.lastError = e;
|
|
398
|
+
return VFS.SQLITE_IOERR_FSTAT;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {number} pFile
|
|
404
|
+
* @param {number} lockType
|
|
405
|
+
* @returns {number|Promise<number>}
|
|
406
|
+
*/
|
|
407
|
+
jLock(pFile, lockType) {
|
|
408
|
+
try {
|
|
409
|
+
const file = this.mapIdToFile.get(pFile);
|
|
410
|
+
if (file.lockState === VFS.SQLITE_LOCK_NONE && lockType === VFS.SQLITE_LOCK_SHARED) {
|
|
411
|
+
// We do all our locking work in this transition.
|
|
412
|
+
if (file.retryResult === null) {
|
|
413
|
+
if (file.lockingMode === 'exclusive') {
|
|
414
|
+
// Exclusive locking mode is treated as a write, and the
|
|
415
|
+
// read lock is also acquired to block readers.
|
|
416
|
+
file.retryResult = {};
|
|
417
|
+
this._module.retryOps.push(this.#retryLockWrite(file));
|
|
418
|
+
return VFS.SQLITE_BUSY;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// With WAL, read and write transactions use separate locks. In
|
|
422
|
+
// each case if the required lock is already held then we can
|
|
423
|
+
// proceed synchronously. Otherwise we need to acquire state
|
|
424
|
+
// asynchronously and retry.
|
|
425
|
+
if (file.writeHint) {
|
|
426
|
+
// Write transaction.
|
|
427
|
+
if (!file.writeLock.acquireIfHeld('exclusive')) {
|
|
428
|
+
file.retryResult = {};
|
|
429
|
+
this._module.retryOps.push(this.#retryLockWrite(file));
|
|
430
|
+
return VFS.SQLITE_BUSY;
|
|
431
|
+
} else {
|
|
432
|
+
file.writeAhead.isolateForWrite();
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
// Read transaction.
|
|
436
|
+
if (!file.readLock.acquireIfHeld('shared')) {
|
|
437
|
+
file.retryResult = {};
|
|
438
|
+
this._module.retryOps.push(this.#retryLockRead(file));
|
|
439
|
+
return VFS.SQLITE_BUSY;
|
|
440
|
+
} else {
|
|
441
|
+
file.writeAhead.isolateForRead();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else if (file.retryResult instanceof Error) {
|
|
445
|
+
const e = file.retryResult;
|
|
446
|
+
file.retryResult = null;
|
|
447
|
+
throw e;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// We have acquired the needed locks, either synchronously or
|
|
451
|
+
// via retry.
|
|
452
|
+
file.retryResult = null;
|
|
453
|
+
} else if (lockType >= VFS.SQLITE_LOCK_RESERVED && !file.writeLock.mode) {
|
|
454
|
+
// This is a write transaction but we don't already have the write
|
|
455
|
+
// lock. This happens when the write hint was not used, which this
|
|
456
|
+
// VFS treats as an error.
|
|
457
|
+
throw new Error('Write transaction cannot use BEGIN DEFERRED');
|
|
458
|
+
}
|
|
459
|
+
file.lockState = lockType;
|
|
460
|
+
return VFS.SQLITE_OK;
|
|
461
|
+
} catch (e) {
|
|
462
|
+
if (e.name === 'TimeoutError') {
|
|
463
|
+
return VFS.SQLITE_BUSY;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.error(e.stack);
|
|
467
|
+
this.lastError = e;
|
|
468
|
+
return VFS.SQLITE_IOERR_LOCK;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* @param {number} pFile
|
|
474
|
+
* @param {number} lockType
|
|
475
|
+
* @returns {number}
|
|
476
|
+
*/
|
|
477
|
+
jUnlock(pFile, lockType) {
|
|
478
|
+
try {
|
|
479
|
+
const file = this.mapIdToFile.get(pFile);
|
|
480
|
+
|
|
481
|
+
// If retryResult is non-null, an asynchronous lock operation is in
|
|
482
|
+
// progress. In that case, don't change any locks.
|
|
483
|
+
if (!file.retryResult && lockType === VFS.SQLITE_LOCK_NONE) {
|
|
484
|
+
// In this VFS, this is the only unlock transition that matters.
|
|
485
|
+
// Exit write-ahead isolation.
|
|
486
|
+
file.writeAhead.rejoin();
|
|
487
|
+
|
|
488
|
+
// Release any locks.
|
|
489
|
+
switch (file.useLazyLock) {
|
|
490
|
+
case 'none':
|
|
491
|
+
file.writeLock.release();
|
|
492
|
+
file.readLock.release();
|
|
493
|
+
break;
|
|
494
|
+
case 'read':
|
|
495
|
+
file.writeLock.release();
|
|
496
|
+
file.readLock.releaseLazy();
|
|
497
|
+
break;
|
|
498
|
+
case 'write':
|
|
499
|
+
file.writeLock.releaseLazy();
|
|
500
|
+
file.readLock.release();
|
|
501
|
+
break;
|
|
502
|
+
case 'readwrite':
|
|
503
|
+
file.writeLock.releaseLazy();
|
|
504
|
+
file.readLock.releaseLazy();
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Reset state for the next transaction.
|
|
509
|
+
file.writeHint = null;
|
|
510
|
+
}
|
|
511
|
+
file.lockState = lockType;
|
|
512
|
+
} catch (e) {
|
|
513
|
+
console.error(e.stack);
|
|
514
|
+
this.lastError = e;
|
|
515
|
+
return VFS.SQLITE_IOERR_UNLOCK;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* @param {number} pFile
|
|
521
|
+
* @param {DataView} pResOut
|
|
522
|
+
* @returns {number}
|
|
523
|
+
*/
|
|
524
|
+
jCheckReservedLock(pFile, pResOut) {
|
|
525
|
+
// A hot journal cannot exist so this method should never be called.
|
|
526
|
+
console.assert(false, 'unexpected');
|
|
527
|
+
pResOut.setInt32(0, 0, true);
|
|
528
|
+
return VFS.SQLITE_OK;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* @param {number} pFile
|
|
533
|
+
* @param {number} op
|
|
534
|
+
* @param {DataView} pArg
|
|
535
|
+
* @returns {number}
|
|
536
|
+
*/
|
|
537
|
+
jFileControl(pFile, op, pArg) {
|
|
538
|
+
try {
|
|
539
|
+
const file = this.mapIdToFile.get(pFile);
|
|
540
|
+
switch (op) {
|
|
541
|
+
case VFS.SQLITE_FCNTL_PRAGMA:
|
|
542
|
+
const key = this._module.UTF8ToString(pArg.getUint32(4, true));
|
|
543
|
+
const valueAddress = pArg.getUint32(8, true);
|
|
544
|
+
const value = valueAddress ? this._module.UTF8ToString(valueAddress) : null;
|
|
545
|
+
this.log?.(`PRAGMA ${key} ${value}`);
|
|
546
|
+
switch (key.toLowerCase()) {
|
|
547
|
+
case 'experimental_pragma_20251114':
|
|
548
|
+
// After entering the SHARED locking state on the next
|
|
549
|
+
// transaction, SQLite intends to immediately transition to
|
|
550
|
+
// RESERVED if value is '1', or EXCLUSIVE if value is '2'.
|
|
551
|
+
switch (value) {
|
|
552
|
+
case '1':
|
|
553
|
+
file.writeHint = 'reserved';
|
|
554
|
+
break;
|
|
555
|
+
case '2':
|
|
556
|
+
file.writeHint = 'exclusive';
|
|
557
|
+
break;
|
|
558
|
+
default:
|
|
559
|
+
throw new Error(`unexpected write hint value: ${value}`);
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
case 'backstop_interval':
|
|
563
|
+
if (value !== null) {
|
|
564
|
+
const millis = parseInt(value);
|
|
565
|
+
file.writeAhead.setBackstopInterval(millis);
|
|
566
|
+
} else {
|
|
567
|
+
// Return current interval.
|
|
568
|
+
const s = file.writeAhead.options.backstopInterval.toString();
|
|
569
|
+
const ptr = this._module._sqlite3_malloc64(s.length + 1);
|
|
570
|
+
this._module.stringToUTF8(s, ptr, s.length + 1);
|
|
571
|
+
pArg.setUint32(0, ptr, true);
|
|
572
|
+
}
|
|
573
|
+
return VFS.SQLITE_OK;
|
|
574
|
+
case 'busy_timeout':
|
|
575
|
+
// Override SQLite's handling of busy timeouts with our
|
|
576
|
+
// blocking lock timeouts.
|
|
577
|
+
if (value !== null) {
|
|
578
|
+
file.timeout = parseInt(value);
|
|
579
|
+
} else {
|
|
580
|
+
// Return current timeout.
|
|
581
|
+
const s = file.timeout.toString();
|
|
582
|
+
const ptr = this._module._sqlite3_malloc64(s.length + 1);
|
|
583
|
+
this._module.stringToUTF8(s, ptr, s.length + 1);
|
|
584
|
+
pArg.setUint32(0, ptr, true);
|
|
585
|
+
}
|
|
586
|
+
return VFS.SQLITE_OK;
|
|
587
|
+
case 'journal_size_limit':
|
|
588
|
+
if (value !== null) {
|
|
589
|
+
const nPages = parseInt(value);
|
|
590
|
+
file.writeAhead.options.journalSizeLimit = nPages;
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
case 'locking_mode':
|
|
594
|
+
// Track SQLite locking mode. Exclusive mode requires a
|
|
595
|
+
// write lock.
|
|
596
|
+
switch (value?.toLowerCase()) {
|
|
597
|
+
case 'normal':
|
|
598
|
+
file.lockingMode = 'normal';
|
|
599
|
+
break;
|
|
600
|
+
case 'exclusive':
|
|
601
|
+
file.lockingMode = 'exclusive';
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
case 'page_size':
|
|
606
|
+
if (value !== null) {
|
|
607
|
+
// Valid page sizes are 1 (which maps to 65536) or powers of
|
|
608
|
+
// two from 512 to 32768.
|
|
609
|
+
const n = parseInt(value);
|
|
610
|
+
if (n === 1 || (n >= 512 && n <= 32768 && (n & (n - 1)) === 0)) {
|
|
611
|
+
file.pageSize = n === 1 ? 65536 : n;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
case 'synchronous':
|
|
616
|
+
// Track SQLite synchronous mode. Write-ahead transactions
|
|
617
|
+
// trade durability for performance on values 1 (NORMAL) or
|
|
618
|
+
// lower.
|
|
619
|
+
if (value !== null) {
|
|
620
|
+
switch (value.toLowerCase()) {
|
|
621
|
+
case 'off':
|
|
622
|
+
case '0':
|
|
623
|
+
file.synchronous = 0;
|
|
624
|
+
break;
|
|
625
|
+
case 'normal':
|
|
626
|
+
case '1':
|
|
627
|
+
file.synchronous = 1;
|
|
628
|
+
break;
|
|
629
|
+
case 'full':
|
|
630
|
+
case '2':
|
|
631
|
+
file.synchronous = 2;
|
|
632
|
+
break;
|
|
633
|
+
case 'extra':
|
|
634
|
+
case '3':
|
|
635
|
+
file.synchronous = 3;
|
|
636
|
+
break;
|
|
637
|
+
default:
|
|
638
|
+
throw new Error(`unexpected synchronous value: ${value}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
642
|
+
case 'vfs_trace':
|
|
643
|
+
// This is a trace feature for debugging only.
|
|
644
|
+
if (value !== null) {
|
|
645
|
+
this.log = parseInt(value) !== 0 ? console.debug : null;
|
|
646
|
+
file.writeAhead.log = this.log;
|
|
647
|
+
}
|
|
648
|
+
return VFS.SQLITE_OK;
|
|
649
|
+
case 'wal_autocheckpoint':
|
|
650
|
+
// A setting greater than zero enables automatic checkpoints
|
|
651
|
+
// with this connection (enabled by default).
|
|
652
|
+
if (value !== null) {
|
|
653
|
+
file.writeAhead.options.autoCheckpoint = parseInt(value);
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
case 'wal_checkpoint':
|
|
657
|
+
const checkpointMode = (value ?? 'passive').toLowerCase();
|
|
658
|
+
switch (checkpointMode) {
|
|
659
|
+
case 'passive':
|
|
660
|
+
this._module.pendingOps.push(this.#pendingCheckpoint(file, checkpointMode));
|
|
661
|
+
break;
|
|
662
|
+
case 'full':
|
|
663
|
+
case 'restart':
|
|
664
|
+
case 'truncate':
|
|
665
|
+
if (file.writeAhead.isTransactionPending()) {
|
|
666
|
+
throw new Error('invalid while a transaction is in progress');
|
|
667
|
+
}
|
|
668
|
+
this._module.pendingOps.push(this.#pendingCheckpoint(file, checkpointMode));
|
|
669
|
+
break;
|
|
670
|
+
case 'noop':
|
|
671
|
+
break;
|
|
672
|
+
default:
|
|
673
|
+
throw new Error(`unexpected wal_checkpoint mode: ${value}`);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Return the approximate number of pages in the WAL before
|
|
677
|
+
// checkpointing. SQLite returns different information, but
|
|
678
|
+
// that is not feasible from a VFS.
|
|
679
|
+
{
|
|
680
|
+
const s = file.writeAhead.getWriteAheadSize().toString();
|
|
681
|
+
const ptr = this._module._sqlite3_malloc64(s.length + 1);
|
|
682
|
+
this._module.stringToUTF8(s, ptr, s.length + 1);
|
|
683
|
+
pArg.setUint32(0, ptr, true);
|
|
684
|
+
}
|
|
685
|
+
return VFS.SQLITE_OK;
|
|
686
|
+
case 'lazy_lock':
|
|
687
|
+
// Lazy locks don't actually release their Web Lock until
|
|
688
|
+
// they receive a message requesting it. Typically a setting
|
|
689
|
+
// of 'readwrite' (default) or 'read' is best.
|
|
690
|
+
if (value !== null) {
|
|
691
|
+
const useLazyLock = value.toLowerCase();
|
|
692
|
+
switch (useLazyLock) {
|
|
693
|
+
case 'read':
|
|
694
|
+
case 'write':
|
|
695
|
+
case 'readwrite':
|
|
696
|
+
case 'none':
|
|
697
|
+
file.useLazyLock = useLazyLock;
|
|
698
|
+
break;
|
|
699
|
+
default:
|
|
700
|
+
throw new Error(`unexpected value for lazy_lock: ${value}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
{
|
|
704
|
+
const s = file.useLazyLock;
|
|
705
|
+
const ptr = this._module._sqlite3_malloc64(s.length + 1);
|
|
706
|
+
this._module.stringToUTF8(s, ptr, s.length + 1);
|
|
707
|
+
pArg.setUint32(0, ptr, true);
|
|
708
|
+
}
|
|
709
|
+
return VFS.SQLITE_OK;
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
|
|
713
|
+
// Support SQLite batch atomic write transactions.
|
|
714
|
+
case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
|
|
715
|
+
case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
|
|
716
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
717
|
+
return VFS.SQLITE_OK;
|
|
718
|
+
}
|
|
719
|
+
break;
|
|
720
|
+
case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
|
|
721
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
722
|
+
file.writeAhead.rollback();
|
|
723
|
+
return VFS.SQLITE_OK;
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
726
|
+
|
|
727
|
+
case VFS.SQLITE_FCNTL_SYNC:
|
|
728
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
729
|
+
file.writeAhead.commit();
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
|
|
733
|
+
case VFS.SQLITE_FCNTL_OVERWRITE:
|
|
734
|
+
file.overwrite = true;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
} catch (e) {
|
|
738
|
+
console.error(e.stack);
|
|
739
|
+
this.lastError = e;
|
|
740
|
+
return VFS.SQLITE_IOERR;
|
|
741
|
+
}
|
|
742
|
+
return VFS.SQLITE_NOTFOUND;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* @param {number} pFile
|
|
747
|
+
* @returns {number}
|
|
748
|
+
*/
|
|
749
|
+
jDeviceCharacteristics(pFile) {
|
|
750
|
+
return VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
|
|
751
|
+
| VFS.SQLITE_IOCAP_BATCH_ATOMIC;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* @param {Uint8Array} zBuf
|
|
756
|
+
* @returns {number}
|
|
757
|
+
*/
|
|
758
|
+
jGetLastError(zBuf) {
|
|
759
|
+
if (this.lastError) {
|
|
760
|
+
console.error(this.lastError);
|
|
761
|
+
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
|
|
762
|
+
const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
|
|
763
|
+
zBuf[written] = 0;
|
|
764
|
+
}
|
|
765
|
+
return VFS.SQLITE_OK
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* @param {string} zName
|
|
770
|
+
* @returns {FileSystemSyncAccessHandle}
|
|
771
|
+
*/
|
|
772
|
+
#openTemporaryFile(zName) {
|
|
773
|
+
if (this.unboundTempFiles.size === 0) {
|
|
774
|
+
throw new Error('no temporary files available');
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Bind an access handle from the temporary pool.
|
|
778
|
+
const accessHandle = this.unboundTempFiles.values().next().value;
|
|
779
|
+
this.unboundTempFiles.delete(accessHandle);
|
|
780
|
+
this.boundTempFiles.set(zName, accessHandle);
|
|
781
|
+
return accessHandle;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* @param {FileEntry} file
|
|
786
|
+
*/
|
|
787
|
+
#deleteTemporaryFile(file) {
|
|
788
|
+
file.accessHandle.truncate(0);
|
|
789
|
+
|
|
790
|
+
// Temporary files are not actually deleted, just returned to the pool.
|
|
791
|
+
this.mapPathToFile.delete(file.zName);
|
|
792
|
+
this.unboundTempFiles.add(file.accessHandle);
|
|
793
|
+
this.boundTempFiles.delete(file.zName);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* @param {string} dbName
|
|
798
|
+
* @param {number} i
|
|
799
|
+
* @returns {string}
|
|
800
|
+
*/
|
|
801
|
+
#getWriteAheadNameFromDbName(dbName, i) {
|
|
802
|
+
// Our WAL file is not compatible with SQLite WAL, so use a distinct name.
|
|
803
|
+
return `${dbName}-wa${i}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Asynchronous PRAGMA operation to checkpoint the write-ahead log.
|
|
808
|
+
* @param {FileEntry} file
|
|
809
|
+
* @param {'passive'|'full'|'restart'|'truncate'} mode
|
|
810
|
+
*/
|
|
811
|
+
async #pendingCheckpoint(file, mode) {
|
|
812
|
+
const onFinally = [];
|
|
813
|
+
try {
|
|
814
|
+
if (mode !== 'passive' && file.lockState === VFS.SQLITE_LOCK_NONE) {
|
|
815
|
+
await file.writeLock.acquire('exclusive');
|
|
816
|
+
onFinally.push(() => file.writeLock.release());
|
|
817
|
+
|
|
818
|
+
file.writeAhead.isolateForWrite();
|
|
819
|
+
onFinally.push(() => file.writeAhead.rejoin());
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
await file.writeAhead.checkpoint({ isPassive: mode === 'passive' });
|
|
823
|
+
} catch (e) {
|
|
824
|
+
if (e.name === 'AbortError') {
|
|
825
|
+
e.code = VFS.SQLITE_BUSY;
|
|
826
|
+
}
|
|
827
|
+
throw e;
|
|
828
|
+
} finally {
|
|
829
|
+
while (onFinally.length) {
|
|
830
|
+
onFinally.pop()();
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* @param {FileEntry} file
|
|
837
|
+
*/
|
|
838
|
+
async #retryLockRead(file) {
|
|
839
|
+
const onError = [];
|
|
840
|
+
try {
|
|
841
|
+
await file.readLock.acquire('shared', file.timeout);
|
|
842
|
+
onError.push(() => file.readLock.release());
|
|
843
|
+
|
|
844
|
+
file.writeAhead.isolateForRead();
|
|
845
|
+
file.retryResult = {};
|
|
846
|
+
} catch (e) {
|
|
847
|
+
while (onError.length) {
|
|
848
|
+
onError.pop()();
|
|
849
|
+
}
|
|
850
|
+
file.retryResult = e;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* @param {FileEntry} file
|
|
856
|
+
*/
|
|
857
|
+
async #retryLockWrite(file) {
|
|
858
|
+
const onError = [];
|
|
859
|
+
try {
|
|
860
|
+
// Exclusive locking mode requires both read and write locks.
|
|
861
|
+
// Otherwise, only the write lock is needed.
|
|
862
|
+
if (file.lockingMode === 'exclusive') {
|
|
863
|
+
await file.readLock.acquire('exclusive', file.timeout);
|
|
864
|
+
onError.push(() => file.readLock.release());
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
await file.writeLock.acquire('exclusive', file.timeout);
|
|
868
|
+
onError.push(() => file.writeLock.release());
|
|
869
|
+
|
|
870
|
+
file.writeAhead.isolateForWrite();
|
|
871
|
+
file.retryResult = {};
|
|
872
|
+
} catch (e) {
|
|
873
|
+
while (onError.length) {
|
|
874
|
+
onError.pop()();
|
|
875
|
+
}
|
|
876
|
+
file.retryResult = e;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Handle asynchronous jOpen() tasks.
|
|
882
|
+
* @param {string} zName
|
|
883
|
+
* @param {number} flags
|
|
884
|
+
* @param {number} fileId
|
|
885
|
+
* @param {DataView} pOutFlags
|
|
886
|
+
* @returns {Promise<void>}
|
|
887
|
+
*/
|
|
888
|
+
async #retryOpen(zName, flags, fileId, pOutFlags) {
|
|
889
|
+
/** @type {(() => void)[]} */ const onError = [];
|
|
890
|
+
const file = this.mapPathToFile.get(zName);
|
|
891
|
+
try {
|
|
892
|
+
await navigator.locks.request(`${zName}#ckpt`, async lock => {
|
|
893
|
+
// Parse the path components.
|
|
894
|
+
const directoryNames = zName.split('/').filter(d => d);
|
|
895
|
+
const dbName = directoryNames.pop();
|
|
896
|
+
|
|
897
|
+
// Get the OPFS directory handle.
|
|
898
|
+
let dirHandle = await navigator.storage.getDirectory();
|
|
899
|
+
const create = !!(flags & VFS.SQLITE_OPEN_CREATE);
|
|
900
|
+
for (const directoryName of directoryNames) {
|
|
901
|
+
dirHandle = await dirHandle.getDirectoryHandle(directoryName, { create });
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const isNewDatabase = create && await (async function() {
|
|
905
|
+
try {
|
|
906
|
+
await dirHandle.getFileHandle(dbName);
|
|
907
|
+
return false;
|
|
908
|
+
} catch (e) {
|
|
909
|
+
if (e.name === 'NotFoundError') {
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
throw e;
|
|
913
|
+
}
|
|
914
|
+
})();
|
|
915
|
+
|
|
916
|
+
// Convenience function for opening access handles.
|
|
917
|
+
async function openFile(
|
|
918
|
+
/** @type {string} */ filename,
|
|
919
|
+
/** @type {FileSystemGetFileOptions} */ options) {
|
|
920
|
+
const fileHandle = await dirHandle.getFileHandle(filename, options);
|
|
921
|
+
// @ts-ignore
|
|
922
|
+
const accessHandle = await fileHandle.createSyncAccessHandle({
|
|
923
|
+
mode: 'readwrite-unsafe'
|
|
924
|
+
});
|
|
925
|
+
onError.push(() => {
|
|
926
|
+
accessHandle.close();
|
|
927
|
+
if (isNewDatabase) {
|
|
928
|
+
dirHandle.removeEntry(filename);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
return accessHandle;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Open the main database OPFS file.
|
|
935
|
+
const accessHandle = await openFile(dbName, { create });
|
|
936
|
+
|
|
937
|
+
// Open WAL files.
|
|
938
|
+
const waHandles = await Promise.all([0, 1].map(async i => {
|
|
939
|
+
const waName = this.#getWriteAheadNameFromDbName(dbName, i);
|
|
940
|
+
const waHandle = await openFile(waName, { create: true });
|
|
941
|
+
if (isNewDatabase) {
|
|
942
|
+
waHandle.truncate(0);
|
|
943
|
+
}
|
|
944
|
+
return waHandle;
|
|
945
|
+
}));
|
|
946
|
+
|
|
947
|
+
// Create the write-ahead manager.
|
|
948
|
+
const writeAhead = new WriteAhead(zName, accessHandle, waHandles);
|
|
949
|
+
await writeAhead.ready();
|
|
950
|
+
|
|
951
|
+
file.retryResult = { accessHandle, waHandles, writeAhead };
|
|
952
|
+
});
|
|
953
|
+
} catch (e) {
|
|
954
|
+
while (onError.length) {
|
|
955
|
+
onError.pop()();
|
|
956
|
+
}
|
|
957
|
+
file.retryResult = e;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|