Snapshot of upstream SQLite 3.46.1
[sqlcipher.git] / ext / wasm / api / sqlite3-vfs-opfs-sahpool.c-pp.js
blob3f4182dacc36b3cf371f455e356b5259931b9060
1 //#ifnot target=node
2 /*
3 2023-07-14
5 The author disclaims copyright to this source code. In place of a
6 legal notice, here is a blessing:
8 * May you do good and not evil.
9 * May you find forgiveness for yourself and forgive others.
10 * May you share freely, never taking more than you give.
12 ***********************************************************************
14 This file holds a sqlite3_vfs backed by OPFS storage which uses a
15 different implementation strategy than the "opfs" VFS. This one is a
16 port of Roy Hashimoto's OPFS SyncAccessHandle pool:
18 https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/AccessHandlePoolVFS.js
20 As described at:
22 https://github.com/rhashimoto/wa-sqlite/discussions/67
24 with Roy's explicit permission to permit us to port his to our
25 infrastructure rather than having to clean-room reverse-engineer it:
27 https://sqlite.org/forum/forumpost/e140d84e71
29 Primary differences from the "opfs" VFS include:
31 - This one avoids the need for a sub-worker to synchronize
32 communication between the synchronous C API and the
33 only-partly-synchronous OPFS API.
35 - It does so by opening a fixed number of OPFS files at
36 library-level initialization time, obtaining SyncAccessHandles to
37 each, and manipulating those handles via the synchronous sqlite3_vfs
38 interface. If it cannot open them (e.g. they are already opened by
39 another tab) then the VFS will not be installed.
41 - Because of that, this one lacks all library-level concurrency
42 support.
44 - Also because of that, it does not require the SharedArrayBuffer,
45 so can function without the COOP/COEP HTTP response headers.
47 - It can hypothetically support Safari 16.4+, whereas the "opfs" VFS
48 requires v17 due to a subworker/storage bug in 16.x which makes it
49 incompatible with that VFS.
51 - This VFS requires the "semi-fully-sync" FileSystemSyncAccessHandle
52 (hereafter "SAH") APIs released with Chrome v108 (and all other
53 major browsers released since March 2023). If that API is not
54 detected, the VFS is not registered.
56 globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
57 'use strict';
58 const toss = sqlite3.util.toss;
59 const toss3 = sqlite3.util.toss3;
60 const initPromises = Object.create(null);
61 const capi = sqlite3.capi;
62 const util = sqlite3.util;
63 const wasm = sqlite3.wasm;
64 // Config opts for the VFS...
65 const SECTOR_SIZE = 4096;
66 const HEADER_MAX_PATH_SIZE = 512;
67 const HEADER_FLAGS_SIZE = 4;
68 const HEADER_DIGEST_SIZE = 8;
69 const HEADER_CORPUS_SIZE = HEADER_MAX_PATH_SIZE + HEADER_FLAGS_SIZE;
70 const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE;
71 const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE;
72 const HEADER_OFFSET_DATA = SECTOR_SIZE;
73 /* Bitmask of file types which may persist across sessions.
74 SQLITE_OPEN_xyz types not listed here may be inadvertently
75 left in OPFS but are treated as transient by this VFS and
76 they will be cleaned up during VFS init. */
77 const PERSISTENT_FILE_TYPES =
78 capi.SQLITE_OPEN_MAIN_DB |
79 capi.SQLITE_OPEN_MAIN_JOURNAL |
80 capi.SQLITE_OPEN_SUPER_JOURNAL |
81 capi.SQLITE_OPEN_WAL /* noting that WAL support is
82 unavailable in the WASM build.*/;
84 /** Subdirectory of the VFS's space where "opaque" (randomly-named)
85 files are stored. Changing this effectively invalidates the data
86 stored under older names (orphaning it), so don't do that. */
87 const OPAQUE_DIR_NAME = ".opaque";
89 /**
90 Returns short a string of random alphanumeric characters
91 suitable for use as a random filename.
93 const getRandomName = ()=>Math.random().toString(36).slice(2);
95 const textDecoder = new TextDecoder();
96 const textEncoder = new TextEncoder();
98 const optionDefaults = Object.assign(Object.create(null),{
99 name: 'opfs-sahpool',
100 directory: undefined /* derived from .name */,
101 initialCapacity: 6,
102 clearOnInit: false,
103 /* Logging verbosity 3+ == everything, 2 == warnings+errors, 1 ==
104 errors only. */
105 verbosity: 2
108 /** Logging routines, from most to least serious. */
109 const loggers = [
110 sqlite3.config.error,
111 sqlite3.config.warn,
112 sqlite3.config.log
114 const log = sqlite3.config.log;
115 const warn = sqlite3.config.warn;
116 const error = sqlite3.config.error;
118 /* Maps (sqlite3_vfs*) to OpfsSAHPool instances */
119 const __mapVfsToPool = new Map();
120 const getPoolForVfs = (pVfs)=>__mapVfsToPool.get(pVfs);
121 const setPoolForVfs = (pVfs,pool)=>{
122 if(pool) __mapVfsToPool.set(pVfs, pool);
123 else __mapVfsToPool.delete(pVfs);
125 /* Maps (sqlite3_file*) to OpfsSAHPool instances */
126 const __mapSqlite3File = new Map();
127 const getPoolForPFile = (pFile)=>__mapSqlite3File.get(pFile);
128 const setPoolForPFile = (pFile,pool)=>{
129 if(pool) __mapSqlite3File.set(pFile, pool);
130 else __mapSqlite3File.delete(pFile);
134 Impls for the sqlite3_io_methods methods. Maintenance reminder:
135 members are in alphabetical order to simplify finding them.
137 const ioMethods = {
138 xCheckReservedLock: function(pFile,pOut){
139 const pool = getPoolForPFile(pFile);
140 pool.log('xCheckReservedLock');
141 pool.storeErr();
142 wasm.poke32(pOut, 1);
143 return 0;
145 xClose: function(pFile){
146 const pool = getPoolForPFile(pFile);
147 pool.storeErr();
148 const file = pool.getOFileForS3File(pFile);
149 if(file) {
150 try{
151 pool.log(`xClose ${file.path}`);
152 pool.mapS3FileToOFile(pFile, false);
153 file.sah.flush();
154 if(file.flags & capi.SQLITE_OPEN_DELETEONCLOSE){
155 pool.deletePath(file.path);
157 }catch(e){
158 return pool.storeErr(e, capi.SQLITE_IOERR);
161 return 0;
163 xDeviceCharacteristics: function(pFile){
164 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
166 xFileControl: function(pFile, opId, pArg){
167 return capi.SQLITE_NOTFOUND;
169 xFileSize: function(pFile,pSz64){
170 const pool = getPoolForPFile(pFile);
171 pool.log(`xFileSize`);
172 const file = pool.getOFileForS3File(pFile);
173 const size = file.sah.getSize() - HEADER_OFFSET_DATA;
174 //log(`xFileSize ${file.path} ${size}`);
175 wasm.poke64(pSz64, BigInt(size));
176 return 0;
178 xLock: function(pFile,lockType){
179 const pool = getPoolForPFile(pFile);
180 pool.log(`xLock ${lockType}`);
181 pool.storeErr();
182 const file = pool.getOFileForS3File(pFile);
183 file.lockType = lockType;
184 return 0;
186 xRead: function(pFile,pDest,n,offset64){
187 const pool = getPoolForPFile(pFile);
188 pool.storeErr();
189 const file = pool.getOFileForS3File(pFile);
190 pool.log(`xRead ${file.path} ${n} @ ${offset64}`);
191 try {
192 const nRead = file.sah.read(
193 wasm.heap8u().subarray(pDest, pDest+n),
194 {at: HEADER_OFFSET_DATA + Number(offset64)}
196 if(nRead < n){
197 wasm.heap8u().fill(0, pDest + nRead, pDest + n);
198 return capi.SQLITE_IOERR_SHORT_READ;
200 return 0;
201 }catch(e){
202 return pool.storeErr(e, capi.SQLITE_IOERR);
205 xSectorSize: function(pFile){
206 return SECTOR_SIZE;
208 xSync: function(pFile,flags){
209 const pool = getPoolForPFile(pFile);
210 pool.log(`xSync ${flags}`);
211 pool.storeErr();
212 const file = pool.getOFileForS3File(pFile);
213 //log(`xSync ${file.path} ${flags}`);
214 try{
215 file.sah.flush();
216 return 0;
217 }catch(e){
218 return pool.storeErr(e, capi.SQLITE_IOERR);
221 xTruncate: function(pFile,sz64){
222 const pool = getPoolForPFile(pFile);
223 pool.log(`xTruncate ${sz64}`);
224 pool.storeErr();
225 const file = pool.getOFileForS3File(pFile);
226 //log(`xTruncate ${file.path} ${iSize}`);
227 try{
228 file.sah.truncate(HEADER_OFFSET_DATA + Number(sz64));
229 return 0;
230 }catch(e){
231 return pool.storeErr(e, capi.SQLITE_IOERR);
234 xUnlock: function(pFile,lockType){
235 const pool = getPoolForPFile(pFile);
236 pool.log('xUnlock');
237 const file = pool.getOFileForS3File(pFile);
238 file.lockType = lockType;
239 return 0;
241 xWrite: function(pFile,pSrc,n,offset64){
242 const pool = getPoolForPFile(pFile);
243 pool.storeErr();
244 const file = pool.getOFileForS3File(pFile);
245 pool.log(`xWrite ${file.path} ${n} ${offset64}`);
246 try{
247 const nBytes = file.sah.write(
248 wasm.heap8u().subarray(pSrc, pSrc+n),
249 { at: HEADER_OFFSET_DATA + Number(offset64) }
251 return n===nBytes ? 0 : toss("Unknown write() failure.");
252 }catch(e){
253 return pool.storeErr(e, capi.SQLITE_IOERR);
256 }/*ioMethods*/;
258 const opfsIoMethods = new capi.sqlite3_io_methods();
259 opfsIoMethods.$iVersion = 1;
260 sqlite3.vfs.installVfs({
261 io: {struct: opfsIoMethods, methods: ioMethods}
265 Impls for the sqlite3_vfs methods. Maintenance reminder: members
266 are in alphabetical order to simplify finding them.
268 const vfsMethods = {
269 xAccess: function(pVfs,zName,flags,pOut){
270 //log(`xAccess ${wasm.cstrToJs(zName)}`);
271 const pool = getPoolForVfs(pVfs);
272 pool.storeErr();
273 try{
274 const name = pool.getPath(zName);
275 wasm.poke32(pOut, pool.hasFilename(name) ? 1 : 0);
276 }catch(e){
277 /*ignored*/
278 wasm.poke32(pOut, 0);
280 return 0;
282 xCurrentTime: function(pVfs,pOut){
283 wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000),
284 'double');
285 return 0;
287 xCurrentTimeInt64: function(pVfs,pOut){
288 wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(),
289 'i64');
290 return 0;
292 xDelete: function(pVfs, zName, doSyncDir){
293 const pool = getPoolForVfs(pVfs);
294 pool.log(`xDelete ${wasm.cstrToJs(zName)}`);
295 pool.storeErr();
296 try{
297 pool.deletePath(pool.getPath(zName));
298 return 0;
299 }catch(e){
300 pool.storeErr(e);
301 return capi.SQLITE_IOERR_DELETE;
304 xFullPathname: function(pVfs,zName,nOut,pOut){
305 //const pool = getPoolForVfs(pVfs);
306 //pool.log(`xFullPathname ${wasm.cstrToJs(zName)}`);
307 const i = wasm.cstrncpy(pOut, zName, nOut);
308 return i<nOut ? 0 : capi.SQLITE_CANTOPEN;
310 xGetLastError: function(pVfs,nOut,pOut){
311 const pool = getPoolForVfs(pVfs);
312 const e = pool.popErr();
313 pool.log(`xGetLastError ${nOut} e =`,e);
314 if(e){
315 const scope = wasm.scopedAllocPush();
316 try{
317 const [cMsg, n] = wasm.scopedAllocCString(e.message, true);
318 wasm.cstrncpy(pOut, cMsg, nOut);
319 if(n > nOut) wasm.poke8(pOut + nOut - 1, 0);
320 }catch(e){
321 return capi.SQLITE_NOMEM;
322 }finally{
323 wasm.scopedAllocPop(scope);
326 return e ? (e.sqlite3Rc || capi.SQLITE_IOERR) : 0;
328 //xSleep is optionally defined below
329 xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
330 const pool = getPoolForVfs(pVfs);
331 try{
332 pool.log(`xOpen ${wasm.cstrToJs(zName)} ${flags}`);
333 // First try to open a path that already exists in the file system.
334 const path = (zName && wasm.peek8(zName))
335 ? pool.getPath(zName)
336 : getRandomName();
337 let sah = pool.getSAHForPath(path);
338 if(!sah && (flags & capi.SQLITE_OPEN_CREATE)) {
339 // File not found so try to create it.
340 if(pool.getFileCount() < pool.getCapacity()) {
341 // Choose an unassociated OPFS file from the pool.
342 sah = pool.nextAvailableSAH();
343 pool.setAssociatedPath(sah, path, flags);
344 }else{
345 // File pool is full.
346 toss('SAH pool is full. Cannot create file',path);
349 if(!sah){
350 toss('file not found:',path);
352 // Subsequent I/O methods are only passed the sqlite3_file
353 // pointer, so map the relevant info we need to that pointer.
354 const file = {path, flags, sah};
355 pool.mapS3FileToOFile(pFile, file);
356 file.lockType = capi.SQLITE_LOCK_NONE;
357 const sq3File = new capi.sqlite3_file(pFile);
358 sq3File.$pMethods = opfsIoMethods.pointer;
359 sq3File.dispose();
360 wasm.poke32(pOutFlags, flags);
361 return 0;
362 }catch(e){
363 pool.storeErr(e);
364 return capi.SQLITE_CANTOPEN;
366 }/*xOpen()*/
367 }/*vfsMethods*/;
370 Creates and initializes an sqlite3_vfs instance for an
371 OpfsSAHPool. The argument is the VFS's name (JS string).
373 Throws if the VFS name is already registered or if something
374 goes terribly wrong via sqlite3.vfs.installVfs().
376 Maintenance reminder: the only detail about the returned object
377 which is specific to any given OpfsSAHPool instance is the $zName
378 member. All other state is identical.
380 const createOpfsVfs = function(vfsName){
381 if( sqlite3.capi.sqlite3_vfs_find(vfsName)){
382 toss3("VFS name is already registered:", vfsName);
384 const opfsVfs = new capi.sqlite3_vfs();
385 /* We fetch the default VFS so that we can inherit some
386 methods from it. */
387 const pDVfs = capi.sqlite3_vfs_find(null);
388 const dVfs = pDVfs
389 ? new capi.sqlite3_vfs(pDVfs)
390 : null /* dVfs will be null when sqlite3 is built with
391 SQLITE_OS_OTHER. */;
392 opfsVfs.$iVersion = 2/*yes, two*/;
393 opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
394 opfsVfs.$mxPathname = HEADER_MAX_PATH_SIZE;
395 opfsVfs.addOnDispose(
396 opfsVfs.$zName = wasm.allocCString(vfsName),
397 ()=>setPoolForVfs(opfsVfs.pointer, 0)
400 if(dVfs){
401 /* Inherit certain VFS members from the default VFS,
402 if available. */
403 opfsVfs.$xRandomness = dVfs.$xRandomness;
404 opfsVfs.$xSleep = dVfs.$xSleep;
405 dVfs.dispose();
407 if(!opfsVfs.$xRandomness && !vfsMethods.xRandomness){
408 /* If the default VFS has no xRandomness(), add a basic JS impl... */
409 vfsMethods.xRandomness = function(pVfs, nOut, pOut){
410 const heap = wasm.heap8u();
411 let i = 0;
412 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
413 return i;
416 if(!opfsVfs.$xSleep && !vfsMethods.xSleep){
417 vfsMethods.xSleep = (pVfs,ms)=>0;
419 sqlite3.vfs.installVfs({
420 vfs: {struct: opfsVfs, methods: vfsMethods}
422 return opfsVfs;
426 Class for managing OPFS-related state for the
427 OPFS SharedAccessHandle Pool sqlite3_vfs.
429 class OpfsSAHPool {
430 /* OPFS dir in which VFS metadata is stored. */
431 vfsDir;
432 /* Directory handle to this.vfsDir. */
433 #dhVfsRoot;
434 /* Directory handle to the subdir of this.#dhVfsRoot which holds
435 the randomly-named "opaque" files. This subdir exists in the
436 hope that we can eventually support client-created files in
437 this.#dhVfsRoot. */
438 #dhOpaque;
439 /* Directory handle to this.dhVfsRoot's parent dir. Needed
440 for a VFS-wipe op. */
441 #dhVfsParent;
442 /* Maps SAHs to their opaque file names. */
443 #mapSAHToName = new Map();
444 /* Maps client-side file names to SAHs. */
445 #mapFilenameToSAH = new Map();
446 /* Set of currently-unused SAHs. */
447 #availableSAH = new Set();
448 /* Maps (sqlite3_file*) to xOpen's file objects. */
449 #mapS3FileToOFile_ = new Map();
451 /* Maps SAH to an abstract File Object which contains
452 various metadata about that handle. */
453 //#mapSAHToMeta = new Map();
455 /** Buffer used by [sg]etAssociatedPath(). */
456 #apBody = new Uint8Array(HEADER_CORPUS_SIZE);
457 // DataView for this.#apBody
458 #dvBody;
460 // associated sqlite3_vfs instance
461 #cVfs;
463 // Logging verbosity. See optionDefaults.verbosity.
464 #verbosity;
466 constructor(options = Object.create(null)){
467 this.#verbosity = options.verbosity ?? optionDefaults.verbosity;
468 this.vfsName = options.name || optionDefaults.name;
469 this.#cVfs = createOpfsVfs(this.vfsName);
470 setPoolForVfs(this.#cVfs.pointer, this);
471 this.vfsDir = options.directory || ("."+this.vfsName);
472 this.#dvBody =
473 new DataView(this.#apBody.buffer, this.#apBody.byteOffset);
474 this.isReady = this
475 .reset(!!(options.clearOnInit ?? optionDefaults.clearOnInit))
476 .then(()=>{
477 if(this.$error) throw this.$error;
478 return this.getCapacity()
479 ? Promise.resolve(undefined)
480 : this.addCapacity(options.initialCapacity
481 || optionDefaults.initialCapacity);
485 #logImpl(level,...args){
486 if(this.#verbosity>level) loggers[level](this.vfsName+":",...args);
488 log(...args){this.#logImpl(2, ...args)};
489 warn(...args){this.#logImpl(1, ...args)};
490 error(...args){this.#logImpl(0, ...args)};
492 getVfs(){return this.#cVfs}
494 /* Current pool capacity. */
495 getCapacity(){return this.#mapSAHToName.size}
497 /* Current number of in-use files from pool. */
498 getFileCount(){return this.#mapFilenameToSAH.size}
500 /* Returns an array of the names of all
501 currently-opened client-specified filenames. */
502 getFileNames(){
503 const rc = [];
504 const iter = this.#mapFilenameToSAH.keys();
505 for(const n of iter) rc.push(n);
506 return rc;
509 // #createFileObject(sah,clientName,opaqueName){
510 // const f = Object.assign(Object.create(null),{
511 // clientName, opaqueName
512 // });
513 // this.#mapSAHToMeta.set(sah, f);
514 // return f;
515 // }
516 // #unmapFileObject(sah){
517 // this.#mapSAHToMeta.delete(sah);
518 // }
521 Adds n files to the pool's capacity. This change is
522 persistent across settings. Returns a Promise which resolves
523 to the new capacity.
525 async addCapacity(n){
526 for(let i = 0; i < n; ++i){
527 const name = getRandomName();
528 const h = await this.#dhOpaque.getFileHandle(name, {create:true});
529 const ah = await h.createSyncAccessHandle();
530 this.#mapSAHToName.set(ah,name);
531 this.setAssociatedPath(ah, '', 0);
532 //this.#createFileObject(ah,undefined,name);
534 return this.getCapacity();
538 Reduce capacity by n, but can only reduce up to the limit
539 of currently-available SAHs. Returns a Promise which resolves
540 to the number of slots really removed.
542 async reduceCapacity(n){
543 let nRm = 0;
544 for(const ah of Array.from(this.#availableSAH)){
545 if(nRm === n || this.getFileCount() === this.getCapacity()){
546 break;
548 const name = this.#mapSAHToName.get(ah);
549 //this.#unmapFileObject(ah);
550 ah.close();
551 await this.#dhOpaque.removeEntry(name);
552 this.#mapSAHToName.delete(ah);
553 this.#availableSAH.delete(ah);
554 ++nRm;
556 return nRm;
560 Releases all currently-opened SAHs. The only legal
561 operation after this is acquireAccessHandles().
563 releaseAccessHandles(){
564 for(const ah of this.#mapSAHToName.keys()) ah.close();
565 this.#mapSAHToName.clear();
566 this.#mapFilenameToSAH.clear();
567 this.#availableSAH.clear();
571 Opens all files under this.vfsDir/this.#dhOpaque and acquires
572 a SAH for each. returns a Promise which resolves to no value
573 but completes once all SAHs are acquired. If acquiring an SAH
574 throws, SAHPool.$error will contain the corresponding
575 exception.
577 If clearFiles is true, the client-stored state of each file is
578 cleared when its handle is acquired, including its name, flags,
579 and any data stored after the metadata block.
581 async acquireAccessHandles(clearFiles){
582 const files = [];
583 for await (const [name,h] of this.#dhOpaque){
584 if('file'===h.kind){
585 files.push([name,h]);
588 return Promise.all(files.map(async([name,h])=>{
589 try{
590 const ah = await h.createSyncAccessHandle()
591 this.#mapSAHToName.set(ah, name);
592 if(clearFiles){
593 ah.truncate(HEADER_OFFSET_DATA);
594 this.setAssociatedPath(ah, '', 0);
595 }else{
596 const path = this.getAssociatedPath(ah);
597 if(path){
598 this.#mapFilenameToSAH.set(path, ah);
599 }else{
600 this.#availableSAH.add(ah);
603 }catch(e){
604 this.storeErr(e);
605 this.releaseAccessHandles();
606 throw e;
608 }));
612 Given an SAH, returns the client-specified name of
613 that file by extracting it from the SAH's header.
615 On error, it disassociates SAH from the pool and
616 returns an empty string.
618 getAssociatedPath(sah){
619 sah.read(this.#apBody, {at: 0});
620 // Delete any unexpected files left over by previous
621 // untimely errors...
622 const flags = this.#dvBody.getUint32(HEADER_OFFSET_FLAGS);
623 if(this.#apBody[0] &&
624 ((flags & capi.SQLITE_OPEN_DELETEONCLOSE) ||
625 (flags & PERSISTENT_FILE_TYPES)===0)){
626 warn(`Removing file with unexpected flags ${flags.toString(16)}`,
627 this.#apBody);
628 this.setAssociatedPath(sah, '', 0);
629 return '';
632 const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4);
633 sah.read(fileDigest, {at: HEADER_OFFSET_DIGEST});
634 const compDigest = this.computeDigest(this.#apBody);
635 if(fileDigest.every((v,i) => v===compDigest[i])){
636 // Valid digest
637 const pathBytes = this.#apBody.findIndex((v)=>0===v);
638 if(0===pathBytes){
639 // This file is unassociated, so truncate it to avoid
640 // leaving stale db data laying around.
641 sah.truncate(HEADER_OFFSET_DATA);
643 return pathBytes
644 ? textDecoder.decode(this.#apBody.subarray(0,pathBytes))
645 : '';
646 }else{
647 // Invalid digest
648 warn('Disassociating file with bad digest.');
649 this.setAssociatedPath(sah, '', 0);
650 return '';
655 Stores the given client-defined path and SQLITE_OPEN_xyz flags
656 into the given SAH. If path is an empty string then the file is
657 disassociated from the pool but its previous name is preserved
658 in the metadata.
660 setAssociatedPath(sah, path, flags){
661 const enc = textEncoder.encodeInto(path, this.#apBody);
662 if(HEADER_MAX_PATH_SIZE <= enc.written + 1/*NUL byte*/){
663 toss("Path too long:",path);
665 this.#apBody.fill(0, enc.written, HEADER_MAX_PATH_SIZE);
666 this.#dvBody.setUint32(HEADER_OFFSET_FLAGS, flags);
668 const digest = this.computeDigest(this.#apBody);
669 sah.write(this.#apBody, {at: 0});
670 sah.write(digest, {at: HEADER_OFFSET_DIGEST});
671 sah.flush();
673 if(path){
674 this.#mapFilenameToSAH.set(path, sah);
675 this.#availableSAH.delete(sah);
676 }else{
677 // This is not a persistent file, so eliminate the contents.
678 sah.truncate(HEADER_OFFSET_DATA);
679 this.#availableSAH.add(sah);
684 Computes a digest for the given byte array and returns it as a
685 two-element Uint32Array. This digest gets stored in the
686 metadata for each file as a validation check. Changing this
687 algorithm invalidates all existing databases for this VFS, so
688 don't do that.
690 computeDigest(byteArray){
691 let h1 = 0xdeadbeef;
692 let h2 = 0x41c6ce57;
693 for(const v of byteArray){
694 h1 = 31 * h1 + (v * 307);
695 h2 = 31 * h2 + (v * 307);
697 return new Uint32Array([h1>>>0, h2>>>0]);
701 Re-initializes the state of the SAH pool, releasing and
702 re-acquiring all handles.
704 See acquireAccessHandles() for the specifics of the clearFiles
705 argument.
707 async reset(clearFiles){
708 await this.isReady;
709 let h = await navigator.storage.getDirectory();
710 let prev, prevName;
711 for(const d of this.vfsDir.split('/')){
712 if(d){
713 prev = h;
714 h = await h.getDirectoryHandle(d,{create:true});
717 this.#dhVfsRoot = h;
718 this.#dhVfsParent = prev;
719 this.#dhOpaque = await this.#dhVfsRoot.getDirectoryHandle(
720 OPAQUE_DIR_NAME,{create:true}
722 this.releaseAccessHandles();
723 return this.acquireAccessHandles(clearFiles);
727 Returns the pathname part of the given argument,
728 which may be any of:
730 - a URL object
731 - A JS string representing a file name
732 - Wasm C-string representing a file name
734 All "../" parts and duplicate slashes are resolve/removed from
735 the returned result.
737 getPath(arg) {
738 if(wasm.isPtr(arg)) arg = wasm.cstrToJs(arg);
739 return ((arg instanceof URL)
740 ? arg
741 : new URL(arg, 'file://localhost/')).pathname;
745 Removes the association of the given client-specified file
746 name (JS string) from the pool. Returns true if a mapping
747 is found, else false.
749 deletePath(path) {
750 const sah = this.#mapFilenameToSAH.get(path);
751 if(sah) {
752 // Un-associate the name from the SAH.
753 this.#mapFilenameToSAH.delete(path);
754 this.setAssociatedPath(sah, '', 0);
756 return !!sah;
760 Sets e (an Error object) as this object's current error. Pass a
761 falsy (or no) value to clear it. If code is truthy it is
762 assumed to be an SQLITE_xxx result code, defaulting to
763 SQLITE_IOERR if code is falsy.
765 Returns the 2nd argument.
767 storeErr(e,code){
768 if(e){
769 e.sqlite3Rc = code || capi.SQLITE_IOERR;
770 this.error(e);
772 this.$error = e;
773 return code;
776 Pops this object's Error object and returns
777 it (a falsy value if no error is set).
779 popErr(){
780 const rc = this.$error;
781 this.$error = undefined;
782 return rc;
786 Returns the next available SAH without removing
787 it from the set.
789 nextAvailableSAH(){
790 const [rc] = this.#availableSAH.keys();
791 return rc;
795 Given an (sqlite3_file*), returns the mapped
796 xOpen file object.
798 getOFileForS3File(pFile){
799 return this.#mapS3FileToOFile_.get(pFile);
802 Maps or unmaps (if file is falsy) the given (sqlite3_file*)
803 to an xOpen file object and to this pool object.
805 mapS3FileToOFile(pFile,file){
806 if(file){
807 this.#mapS3FileToOFile_.set(pFile, file);
808 setPoolForPFile(pFile, this);
809 }else{
810 this.#mapS3FileToOFile_.delete(pFile);
811 setPoolForPFile(pFile, false);
816 Returns true if the given client-defined file name is in this
817 object's name-to-SAH map.
819 hasFilename(name){
820 return this.#mapFilenameToSAH.has(name)
824 Returns the SAH associated with the given
825 client-defined file name.
827 getSAHForPath(path){
828 return this.#mapFilenameToSAH.get(path);
832 Removes this object's sqlite3_vfs registration and shuts down
833 this object, releasing all handles, mappings, and whatnot,
834 including deleting its data directory. There is currently no
835 way to "revive" the object and reaquire its resources.
837 This function is intended primarily for testing.
839 Resolves to true if it did its job, false if the
840 VFS has already been shut down.
842 async removeVfs(){
843 if(!this.#cVfs.pointer || !this.#dhOpaque) return false;
844 capi.sqlite3_vfs_unregister(this.#cVfs.pointer);
845 this.#cVfs.dispose();
846 try{
847 this.releaseAccessHandles();
848 await this.#dhVfsRoot.removeEntry(OPAQUE_DIR_NAME, {recursive: true});
849 this.#dhOpaque = undefined;
850 await this.#dhVfsParent.removeEntry(
851 this.#dhVfsRoot.name, {recursive: true}
853 this.#dhVfsRoot = this.#dhVfsParent = undefined;
854 }catch(e){
855 sqlite3.config.error(this.vfsName,"removeVfs() failed:",e);
856 /*otherwise ignored - there is no recovery strategy*/
858 return true;
862 //! Documented elsewhere in this file.
863 exportFile(name){
864 const sah = this.#mapFilenameToSAH.get(name) || toss("File not found:",name);
865 const n = sah.getSize() - HEADER_OFFSET_DATA;
866 const b = new Uint8Array(n>0 ? n : 0);
867 if(n>0){
868 const nRead = sah.read(b, {at: HEADER_OFFSET_DATA});
869 if(nRead != n){
870 toss("Expected to read "+n+" bytes but read "+nRead+".");
873 return b;
876 //! Impl for importDb() when its 2nd arg is a function.
877 async importDbChunked(name, callback){
878 const sah = this.#mapFilenameToSAH.get(name)
879 || this.nextAvailableSAH()
880 || toss("No available handles to import to.");
881 sah.truncate(0);
882 let nWrote = 0, chunk, checkedHeader = false, err = false;
883 try{
884 while( undefined !== (chunk = await callback()) ){
885 if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk);
886 if( 0===nWrote && chunk.byteLength>=15 ){
887 util.affirmDbHeader(chunk);
888 checkedHeader = true;
890 sah.write(chunk, {at: HEADER_OFFSET_DATA + nWrote});
891 nWrote += chunk.byteLength;
893 if( nWrote < 512 || 0!==nWrote % 512 ){
894 toss("Input size",nWrote,"is not correct for an SQLite database.");
896 if( !checkedHeader ){
897 const header = new Uint8Array(20);
898 sah.read( header, {at: 0} );
899 util.affirmDbHeader( header );
901 sah.write(new Uint8Array([1,1]), {
902 at: HEADER_OFFSET_DATA + 18
903 }/*force db out of WAL mode*/);
904 }catch(e){
905 this.setAssociatedPath(sah, '', 0);
906 throw e;
908 this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB);
909 return nWrote;
912 //! Documented elsewhere in this file.
913 importDb(name, bytes){
914 if( bytes instanceof ArrayBuffer ) bytes = new Uint8Array(bytes);
915 else if( bytes instanceof Function ) return this.importDbChunked(name, bytes);
916 const sah = this.#mapFilenameToSAH.get(name)
917 || this.nextAvailableSAH()
918 || toss("No available handles to import to.");
919 const n = bytes.byteLength;
920 if(n<512 || n%512!=0){
921 toss("Byte array size is invalid for an SQLite db.");
923 const header = "SQLite format 3";
924 for(let i = 0; i < header.length; ++i){
925 if( header.charCodeAt(i) !== bytes[i] ){
926 toss("Input does not contain an SQLite database header.");
929 const nWrote = sah.write(bytes, {at: HEADER_OFFSET_DATA});
930 if(nWrote != n){
931 this.setAssociatedPath(sah, '', 0);
932 toss("Expected to write "+n+" bytes but wrote "+nWrote+".");
933 }else{
934 sah.write(new Uint8Array([1,1]), {at: HEADER_OFFSET_DATA+18}
935 /* force db out of WAL mode */);
936 this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB);
938 return nWrote;
941 }/*class OpfsSAHPool*/;
945 A OpfsSAHPoolUtil instance is exposed to clients in order to
946 manipulate an OpfsSAHPool object without directly exposing that
947 object and allowing for some semantic changes compared to that
948 class.
950 Class docs are in the client-level docs for
951 installOpfsSAHPoolVfs().
953 class OpfsSAHPoolUtil {
954 /* This object's associated OpfsSAHPool. */
957 constructor(sahPool){
958 this.#p = sahPool;
959 this.vfsName = sahPool.vfsName;
962 async addCapacity(n){ return this.#p.addCapacity(n) }
964 async reduceCapacity(n){ return this.#p.reduceCapacity(n) }
966 getCapacity(){ return this.#p.getCapacity(this.#p) }
968 getFileCount(){ return this.#p.getFileCount() }
969 getFileNames(){ return this.#p.getFileNames() }
971 async reserveMinimumCapacity(min){
972 const c = this.#p.getCapacity();
973 return (c < min) ? this.#p.addCapacity(min - c) : c;
976 exportFile(name){ return this.#p.exportFile(name) }
978 importDb(name, bytes){ return this.#p.importDb(name,bytes) }
980 async wipeFiles(){ return this.#p.reset(true) }
982 unlink(filename){ return this.#p.deletePath(filename) }
984 async removeVfs(){ return this.#p.removeVfs() }
986 }/* class OpfsSAHPoolUtil */;
989 Returns a resolved Promise if the current environment
990 has a "fully-sync" SAH impl, else a rejected Promise.
992 const apiVersionCheck = async ()=>{
993 const dh = await navigator.storage.getDirectory();
994 const fn = '.opfs-sahpool-sync-check-'+getRandomName();
995 const fh = await dh.getFileHandle(fn, { create: true });
996 const ah = await fh.createSyncAccessHandle();
997 const close = ah.close();
998 await close;
999 await dh.removeEntry(fn);
1000 if(close?.then){
1001 toss("The local OPFS API is too old for opfs-sahpool:",
1002 "it has an async FileSystemSyncAccessHandle.close() method.");
1004 return true;
1007 /** Only for testing a rejection case. */
1008 let instanceCounter = 0;
1011 installOpfsSAHPoolVfs() asynchronously initializes the OPFS
1012 SyncAccessHandle (a.k.a. SAH) Pool VFS. It returns a Promise which
1013 either resolves to a utility object described below or rejects with
1014 an Error value.
1016 Initialization of this VFS is not automatic because its
1017 registration requires that it lock all resources it
1018 will potentially use, even if client code does not want
1019 to use them. That, in turn, can lead to locking errors
1020 when, for example, one page in a given origin has loaded
1021 this VFS but does not use it, then another page in that
1022 origin tries to use the VFS. If the VFS were automatically
1023 registered, the second page would fail to load the VFS
1024 due to OPFS locking errors.
1026 If this function is called more than once with a given "name"
1027 option (see below), it will return the same Promise. Calls for
1028 different names will return different Promises which resolve to
1029 independent objects and refer to different VFS registrations.
1031 On success, the resulting Promise resolves to a utility object
1032 which can be used to query and manipulate the pool. Its API is
1033 described at the end of these docs.
1035 This function accepts an options object to configure certain
1036 parts but it is only acknowledged for the very first call and
1037 ignored for all subsequent calls.
1039 The options, in alphabetical order:
1041 - `clearOnInit`: (default=false) if truthy, contents and filename
1042 mapping are removed from each SAH it is acquired during
1043 initalization of the VFS, leaving the VFS's storage in a pristine
1044 state. Use this only for databases which need not survive a page
1045 reload.
1047 - `initialCapacity`: (default=6) Specifies the default capacity of
1048 the VFS. This should not be set unduly high because the VFS has
1049 to open (and keep open) a file for each entry in the pool. This
1050 setting only has an effect when the pool is initially empty. It
1051 does not have any effect if a pool already exists.
1053 - `directory`: (default="."+`name`) Specifies the OPFS directory
1054 name in which to store metadata for the `"opfs-sahpool"`
1055 sqlite3_vfs. Only one instance of this VFS can be installed per
1056 JavaScript engine, and any two engines with the same storage
1057 directory name will collide with each other, leading to locking
1058 errors and the inability to register the VFS in the second and
1059 subsequent engine. Using a different directory name for each
1060 application enables different engines in the same HTTP origin to
1061 co-exist, but their data are invisible to each other. Changing
1062 this name will effectively orphan any databases stored under
1063 previous names. The default is unspecified but descriptive. This
1064 option may contain multiple path elements, e.g. "foo/bar/baz",
1065 and they are created automatically. In practice there should be
1066 no driving need to change this. ACHTUNG: all files in this
1067 directory are assumed to be managed by the VFS. Do not place
1068 other files in that directory, as they may be deleted or
1069 otherwise modified by the VFS.
1071 - `name`: (default="opfs-sahpool") sets the name to register this
1072 VFS under. Normally this should not be changed, but it is
1073 possible to register this VFS under multiple names so long as
1074 each has its own separate directory to work from. The storage for
1075 each is invisible to all others. The name must be a string
1076 compatible with `sqlite3_vfs_register()` and friends and suitable
1077 for use in URI-style database file names.
1079 Achtung: if a custom `name` is provided, a custom `directory`
1080 must also be provided if any other instance is registered with
1081 the default directory. If no directory is explicitly provided
1082 then a directory name is synthesized from the `name` option.
1084 Peculiarities of this VFS:
1086 - Paths given to it _must_ be absolute. Relative paths will not
1087 be properly recognized. This is arguably a bug but correcting it
1088 requires some hoop-jumping in routines which have no business
1089 doing tricks.
1091 - It is possible to install multiple instances under different
1092 names, each sandboxed from one another inside their own private
1093 directory. This feature exists primarily as a way for disparate
1094 applications within a given HTTP origin to use this VFS without
1095 introducing locking issues between them.
1098 The API for the utility object passed on by this function's
1099 Promise, in alphabetical order...
1101 - [async] number addCapacity(n)
1103 Adds `n` entries to the current pool. This change is persistent
1104 across sessions so should not be called automatically at each app
1105 startup (but see `reserveMinimumCapacity()`). Its returned Promise
1106 resolves to the new capacity. Because this operation is necessarily
1107 asynchronous, the C-level VFS API cannot call this on its own as
1108 needed.
1110 - byteArray exportFile(name)
1112 Synchronously reads the contents of the given file into a Uint8Array
1113 and returns it. This will throw if the given name is not currently
1114 in active use or on I/O error. Note that the given name is _not_
1115 visible directly in OPFS (or, if it is, it's not from this VFS).
1117 - number getCapacity()
1119 Returns the number of files currently contained
1120 in the SAH pool. The default capacity is only large enough for one
1121 or two databases and their associated temp files.
1123 - number getFileCount()
1125 Returns the number of files from the pool currently allocated to
1126 slots. This is not the same as the files being "opened".
1128 - array getFileNames()
1130 Returns an array of the names of the files currently allocated to
1131 slots. This list is the same length as getFileCount().
1133 - void importDb(name, bytes)
1135 Imports the contents of an SQLite database, provided as a byte
1136 array or ArrayBuffer, under the given name, overwriting any
1137 existing content. Throws if the pool has no available file slots,
1138 on I/O error, or if the input does not appear to be a
1139 database. In the latter case, only a cursory examination is made.
1140 Results are undefined if the given db name refers to an opened
1141 db. Note that this routine is _only_ for importing database
1142 files, not arbitrary files, the reason being that this VFS will
1143 automatically clean up any non-database files so importing them
1144 is pointless.
1146 If passed a function for its second argument, its behavior
1147 changes to asynchronous and it imports its data in chunks fed to
1148 it by the given callback function. It calls the callback (which
1149 may be async) repeatedly, expecting either a Uint8Array or
1150 ArrayBuffer (to denote new input) or undefined (to denote
1151 EOF). For so long as the callback continues to return
1152 non-undefined, it will append incoming data to the given
1153 VFS-hosted database file. The result of the resolved Promise when
1154 called this way is the size of the resulting database.
1156 On succes this routine rewrites the database header bytes in the
1157 output file (not the input array) to force disabling of WAL mode.
1159 On a write error, the handle is removed from the pool and made
1160 available for re-use.
1162 - [async] number reduceCapacity(n)
1164 Removes up to `n` entries from the pool, with the caveat that it can
1165 only remove currently-unused entries. It returns a Promise which
1166 resolves to the number of entries actually removed.
1168 - [async] boolean removeVfs()
1170 Unregisters the opfs-sahpool VFS and removes its directory from OPFS
1171 (which means that _all client content_ is removed). After calling
1172 this, the VFS may no longer be used and there is no way to re-add it
1173 aside from reloading the current JavaScript context.
1175 Results are undefined if a database is currently in use with this
1176 VFS.
1178 The returned Promise resolves to true if it performed the removal
1179 and false if the VFS was not installed.
1181 If the VFS has a multi-level directory, e.g. "/foo/bar/baz", _only_
1182 the bottom-most directory is removed because this VFS cannot know for
1183 certain whether the higher-level directories contain data which
1184 should be removed.
1186 - [async] number reserveMinimumCapacity(min)
1188 If the current capacity is less than `min`, the capacity is
1189 increased to `min`, else this returns with no side effects. The
1190 resulting Promise resolves to the new capacity.
1192 - boolean unlink(filename)
1194 If a virtual file exists with the given name, disassociates it from
1195 the pool and returns true, else returns false without side
1196 effects. Results are undefined if the file is currently in active
1197 use.
1199 - string vfsName
1201 The SQLite VFS name under which this pool's VFS is registered.
1203 - [async] void wipeFiles()
1205 Clears all client-defined state of all SAHs and makes all of them
1206 available for re-use by the pool. Results are undefined if any such
1207 handles are currently in use, e.g. by an sqlite3 db.
1209 sqlite3.installOpfsSAHPoolVfs = async function(options=Object.create(null)){
1210 const vfsName = options.name || optionDefaults.name;
1211 if(0 && 2===++instanceCounter){
1212 throw new Error("Just testing rejection.");
1214 if(initPromises[vfsName]){
1215 //console.warn("Returning same OpfsSAHPool result",options,vfsName,initPromises[vfsName]);
1216 return initPromises[vfsName];
1218 if(!globalThis.FileSystemHandle ||
1219 !globalThis.FileSystemDirectoryHandle ||
1220 !globalThis.FileSystemFileHandle ||
1221 !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle ||
1222 !navigator?.storage?.getDirectory){
1223 return (initPromises[vfsName] = Promise.reject(new Error("Missing required OPFS APIs.")));
1227 Maintenance reminder: the order of ASYNC ops in this function
1228 is significant. We need to have them all chained at the very
1229 end in order to be able to catch a race condition where
1230 installOpfsSAHPoolVfs() is called twice in rapid succession,
1231 e.g.:
1233 installOpfsSAHPoolVfs().then(console.warn.bind(console));
1234 installOpfsSAHPoolVfs().then(console.warn.bind(console));
1236 If the timing of the async calls is not "just right" then that
1237 second call can end up triggering the init a second time and chaos
1238 ensues.
1240 return initPromises[vfsName] = apiVersionCheck().then(async function(){
1241 if(options.$testThrowInInit){
1242 throw options.$testThrowInInit;
1244 const thePool = new OpfsSAHPool(options);
1245 return thePool.isReady.then(async()=>{
1246 /** The poolUtil object will be the result of the
1247 resolved Promise. */
1248 const poolUtil = new OpfsSAHPoolUtil(thePool);
1249 if(sqlite3.oo1){
1250 const oo1 = sqlite3.oo1;
1251 const theVfs = thePool.getVfs();
1252 const OpfsSAHPoolDb = function(...args){
1253 const opt = oo1.DB.dbCtorHelper.normalizeArgs(...args);
1254 opt.vfs = theVfs.$zName;
1255 oo1.DB.dbCtorHelper.call(this, opt);
1257 OpfsSAHPoolDb.prototype = Object.create(oo1.DB.prototype);
1258 // yes or no? OpfsSAHPoolDb.PoolUtil = poolUtil;
1259 poolUtil.OpfsSAHPoolDb = OpfsSAHPoolDb;
1260 oo1.DB.dbCtorHelper.setVfsPostOpenSql(
1261 theVfs.pointer,
1262 function(oo1Db, sqlite3){
1263 sqlite3.capi.sqlite3_exec(oo1Db, [
1264 /* See notes in sqlite3-vfs-opfs.js */
1265 "pragma journal_mode=DELETE;",
1266 "pragma cache_size=-16384;"
1267 ], 0, 0, 0);
1270 }/*extend sqlite3.oo1*/
1271 thePool.log("VFS initialized.");
1272 return poolUtil;
1273 }).catch(async (e)=>{
1274 await thePool.removeVfs().catch(()=>{});
1275 throw e;
1277 }).catch((err)=>{
1278 //error("rejecting promise:",err);
1279 return initPromises[vfsName] = Promise.reject(err);
1281 }/*installOpfsSAHPoolVfs()*/;
1282 }/*sqlite3ApiBootstrap.initializers*/);
1283 //#else
1285 The OPFS SAH Pool VFS parts are elided from builds targeting
1286 node.js.
1288 //#endif target=node