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
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
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){
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";
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),{
100 directory: undefined /* derived from .name */,
103 /* Logging verbosity 3+ == everything, 2 == warnings+errors, 1 ==
108 /** Logging routines, from most to least serious. */
110 sqlite3.config.error,
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.
138 xCheckReservedLock: function(pFile,pOut){
139 const pool = getPoolForPFile(pFile);
140 pool.log('xCheckReservedLock');
142 wasm.poke32(pOut, 1);
145 xClose: function(pFile){
146 const pool = getPoolForPFile(pFile);
148 const file = pool.getOFileForS3File(pFile);
151 pool.log(`xClose ${file.path}`);
152 pool.mapS3FileToOFile(pFile, false);
154 if(file.flags & capi.SQLITE_OPEN_DELETEONCLOSE){
155 pool.deletePath(file.path);
158 return pool.storeErr(e, capi.SQLITE_IOERR);
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));
178 xLock: function(pFile,lockType){
179 const pool = getPoolForPFile(pFile);
180 pool.log(`xLock ${lockType}`);
182 const file = pool.getOFileForS3File(pFile);
183 file.lockType = lockType;
186 xRead: function(pFile,pDest,n,offset64){
187 const pool = getPoolForPFile(pFile);
189 const file = pool.getOFileForS3File(pFile);
190 pool.log(`xRead ${file.path} ${n} @ ${offset64}`);
192 const nRead = file.sah.read(
193 wasm.heap8u().subarray(pDest, pDest+n),
194 {at: HEADER_OFFSET_DATA + Number(offset64)}
197 wasm.heap8u().fill(0, pDest + nRead, pDest + n);
198 return capi.SQLITE_IOERR_SHORT_READ;
202 return pool.storeErr(e, capi.SQLITE_IOERR);
205 xSectorSize: function(pFile){
208 xSync: function(pFile,flags){
209 const pool = getPoolForPFile(pFile);
210 pool.log(`xSync ${flags}`);
212 const file = pool.getOFileForS3File(pFile);
213 //log(`xSync ${file.path} ${flags}`);
218 return pool.storeErr(e, capi.SQLITE_IOERR);
221 xTruncate: function(pFile,sz64){
222 const pool = getPoolForPFile(pFile);
223 pool.log(`xTruncate ${sz64}`);
225 const file = pool.getOFileForS3File(pFile);
226 //log(`xTruncate ${file.path} ${iSize}`);
228 file.sah.truncate(HEADER_OFFSET_DATA + Number(sz64));
231 return pool.storeErr(e, capi.SQLITE_IOERR);
234 xUnlock: function(pFile,lockType){
235 const pool = getPoolForPFile(pFile);
237 const file = pool.getOFileForS3File(pFile);
238 file.lockType = lockType;
241 xWrite: function(pFile,pSrc,n,offset64){
242 const pool = getPoolForPFile(pFile);
244 const file = pool.getOFileForS3File(pFile);
245 pool.log(`xWrite ${file.path} ${n} ${offset64}`);
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.");
253 return pool.storeErr(e, capi.SQLITE_IOERR);
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.
269 xAccess: function(pVfs,zName,flags,pOut){
270 //log(`xAccess ${wasm.cstrToJs(zName)}`);
271 const pool = getPoolForVfs(pVfs);
274 const name = pool.getPath(zName);
275 wasm.poke32(pOut, pool.hasFilename(name) ? 1 : 0);
278 wasm.poke32(pOut, 0);
282 xCurrentTime: function(pVfs,pOut){
283 wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000),
287 xCurrentTimeInt64: function(pVfs,pOut){
288 wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(),
292 xDelete: function(pVfs, zName, doSyncDir){
293 const pool = getPoolForVfs(pVfs);
294 pool.log(`xDelete ${wasm.cstrToJs(zName)}`);
297 pool.deletePath(pool.getPath(zName));
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);
315 const scope = wasm.scopedAllocPush();
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);
321 return capi.SQLITE_NOMEM;
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);
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)
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);
345 // File pool is full.
346 toss('SAH pool is full. Cannot create file',path);
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;
360 wasm.poke32(pOutFlags, flags);
364 return capi.SQLITE_CANTOPEN;
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
387 const pDVfs = capi.sqlite3_vfs_find(null);
389 ? new capi.sqlite3_vfs(pDVfs)
390 : null /* dVfs will be null when sqlite3 is built with
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)
401 /* Inherit certain VFS members from the default VFS,
403 opfsVfs.$xRandomness = dVfs.$xRandomness;
404 opfsVfs.$xSleep = dVfs.$xSleep;
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();
412 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
416 if(!opfsVfs.$xSleep && !vfsMethods.xSleep){
417 vfsMethods.xSleep = (pVfs,ms)=>0;
419 sqlite3.vfs.installVfs({
420 vfs: {struct: opfsVfs, methods: vfsMethods}
426 Class for managing OPFS-related state for the
427 OPFS SharedAccessHandle Pool sqlite3_vfs.
430 /* OPFS dir in which VFS metadata is stored. */
432 /* Directory handle to this.vfsDir. */
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
439 /* Directory handle to this.dhVfsRoot's parent dir. Needed
440 for a VFS-wipe op. */
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
460 // associated sqlite3_vfs instance
463 // Logging verbosity. See optionDefaults.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);
473 new DataView(this.#apBody.buffer, this.#apBody.byteOffset);
475 .reset(!!(options.clearOnInit ?? optionDefaults.clearOnInit))
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. */
504 const iter = this.#mapFilenameToSAH.keys();
505 for(const n of iter) rc.push(n);
509 // #createFileObject(sah,clientName,opaqueName){
510 // const f = Object.assign(Object.create(null),{
511 // clientName, opaqueName
513 // this.#mapSAHToMeta.set(sah, f);
516 // #unmapFileObject(sah){
517 // this.#mapSAHToMeta.delete(sah);
521 Adds n files to the pool's capacity. This change is
522 persistent across settings. Returns a Promise which resolves
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){
544 for(const ah of Array.from(this.#availableSAH)){
545 if(nRm === n || this.getFileCount() === this.getCapacity()){
548 const name = this.#mapSAHToName.get(ah);
549 //this.#unmapFileObject(ah);
551 await this.#dhOpaque.removeEntry(name);
552 this.#mapSAHToName.delete(ah);
553 this.#availableSAH.delete(ah);
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
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){
583 for await (const [name,h] of this.#dhOpaque){
585 files.push([name,h]);
588 return Promise.all(files.map(async([name,h])=>{
590 const ah = await h.createSyncAccessHandle()
591 this.#mapSAHToName.set(ah, name);
593 ah.truncate(HEADER_OFFSET_DATA);
594 this.setAssociatedPath(ah, '', 0);
596 const path = this.getAssociatedPath(ah);
598 this.#mapFilenameToSAH.set(path, ah);
600 this.#availableSAH.add(ah);
605 this.releaseAccessHandles();
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)}`,
628 this.setAssociatedPath(sah, '', 0);
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])){
637 const pathBytes = this.#apBody.findIndex((v)=>0===v);
639 // This file is unassociated, so truncate it to avoid
640 // leaving stale db data laying around.
641 sah.truncate(HEADER_OFFSET_DATA);
644 ? textDecoder.decode(this.#apBody.subarray(0,pathBytes))
648 warn('Disassociating file with bad digest.');
649 this.setAssociatedPath(sah, '', 0);
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
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});
674 this.#mapFilenameToSAH.set(path, sah);
675 this.#availableSAH.delete(sah);
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
690 computeDigest(byteArray){
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
707 async reset(clearFiles){
709 let h = await navigator.storage.getDirectory();
711 for(const d of this.vfsDir.split('/')){
714 h = await h.getDirectoryHandle(d,{create:true});
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,
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
738 if(wasm.isPtr(arg)) arg = wasm.cstrToJs(arg);
739 return ((arg instanceof URL)
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.
750 const sah = this.#mapFilenameToSAH.get(path);
752 // Un-associate the name from the SAH.
753 this.#mapFilenameToSAH.delete(path);
754 this.setAssociatedPath(sah, '', 0);
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.
769 e.sqlite3Rc = code || capi.SQLITE_IOERR;
776 Pops this object's Error object and returns
777 it (a falsy value if no error is set).
780 const rc = this.$error;
781 this.$error = undefined;
786 Returns the next available SAH without removing
790 const [rc] = this.#availableSAH.keys();
795 Given an (sqlite3_file*), returns the mapped
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){
807 this.#mapS3FileToOFile_.set(pFile, file);
808 setPoolForPFile(pFile, this);
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.
820 return this.#mapFilenameToSAH.has(name)
824 Returns the SAH associated with the given
825 client-defined file name.
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.
843 if(!this.#cVfs.pointer || !this.#dhOpaque) return false;
844 capi.sqlite3_vfs_unregister(this.#cVfs.pointer);
845 this.#cVfs.dispose();
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;
855 sqlite3.config.error(this.vfsName,"removeVfs() failed:",e);
856 /*otherwise ignored - there is no recovery strategy*/
862 //! Documented elsewhere in this file.
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);
868 const nRead = sah.read(b, {at: HEADER_OFFSET_DATA});
870 toss("Expected to read "+n+" bytes but read "+nRead+".");
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.");
882 let nWrote = 0, chunk, checkedHeader = false, err = false;
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*/);
905 this.setAssociatedPath(sah, '', 0);
908 this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB);
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});
931 this.setAssociatedPath(sah, '', 0);
932 toss("Expected to write "+n+" bytes but wrote "+nWrote+".");
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);
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
950 Class docs are in the client-level docs for
951 installOpfsSAHPoolVfs().
953 class OpfsSAHPoolUtil {
954 /* This object's associated OpfsSAHPool. */
957 constructor(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();
999 await dh.removeEntry(fn);
1001 toss("The local OPFS API is too old for opfs-sahpool:",
1002 "it has an async FileSystemSyncAccessHandle.close() method.");
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
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
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
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
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
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
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
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
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,
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
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);
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(
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;"
1270 }/*extend sqlite3.oo1*/
1271 thePool.log("VFS initialized.");
1273 }).catch(async (e)=>{
1274 await thePool.removeVfs().catch(()=>{});
1278 //error("rejecting promise:",err);
1279 return initPromises[vfsName] = Promise.reject(err);
1281 }/*installOpfsSAHPoolVfs()*/;
1282 }/*sqlite3ApiBootstrap.initializers*/);
1285 The OPFS SAH Pool VFS parts are elided from builds targeting
1288 //#endif target=node