Fixes default log output to console for macOS
[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.
92   */
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
106   });
108   /** Logging routines, from most to least serious. */
109   const loggers = [
110     sqlite3.config.error,
111     sqlite3.config.warn,
112     sqlite3.config.log
113   ];
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);
124   };
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);
131   };
133   /**
134      Impls for the sqlite3_io_methods methods. Maintenance reminder:
135      members are in alphabetical order to simplify finding them.
136   */
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;
144     },
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);
156           }
157         }catch(e){
158           return pool.storeErr(e, capi.SQLITE_IOERR);
159         }
160       }
161       return 0;
162     },
163     xDeviceCharacteristics: function(pFile){
164       return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
165     },
166     xFileControl: function(pFile, opId, pArg){
167       return capi.SQLITE_NOTFOUND;
168     },
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;
177     },
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;
185     },
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)}
195         );
196         if(nRead < n){
197           wasm.heap8u().fill(0, pDest + nRead, pDest + n);
198           return capi.SQLITE_IOERR_SHORT_READ;
199         }
200         return 0;
201       }catch(e){
202         return pool.storeErr(e, capi.SQLITE_IOERR);
203       }
204     },
205     xSectorSize: function(pFile){
206       return SECTOR_SIZE;
207     },
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);
219       }
220     },
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);
232       }
233     },
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;
240     },
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) }
250         );
251         return n===nBytes ? 0 : toss("Unknown write() failure.");
252       }catch(e){
253         return pool.storeErr(e, capi.SQLITE_IOERR);
254       }
255     }
256   }/*ioMethods*/;
258   const opfsIoMethods = new capi.sqlite3_io_methods();
259   opfsIoMethods.$iVersion = 1;
260   sqlite3.vfs.installVfs({
261     io: {struct: opfsIoMethods, methods: ioMethods}
262   });
264   /**
265      Impls for the sqlite3_vfs methods. Maintenance reminder: members
266      are in alphabetical order to simplify finding them.
267   */
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);
279       }
280       return 0;
281     },
282     xCurrentTime: function(pVfs,pOut){
283       wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000),
284                 'double');
285       return 0;
286     },
287     xCurrentTimeInt64: function(pVfs,pOut){
288       wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(),
289                 'i64');
290       return 0;
291     },
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;
302       }
303     },
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;
309     },
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);
324         }
325       }
326       return e ? (e.sqlite3Rc || capi.SQLITE_IOERR) : 0;
327     },
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);
347           }
348         }
349         if(!sah){
350           toss('file not found:',path);
351         }
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;
365       }
366     }/*xOpen()*/
367   }/*vfsMethods*/;
369   /**
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.
379   */
380   const createOpfsVfs = function(vfsName){
381     if( sqlite3.capi.sqlite3_vfs_find(vfsName)){
382       toss3("VFS name is already registered:", vfsName);
383     }
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)
398     );
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();
406     }
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;
414       };
415     }
416     if(!opfsVfs.$xSleep && !vfsMethods.xSleep){
417       vfsMethods.xSleep = (pVfs,ms)=>0;
418     }
419     sqlite3.vfs.installVfs({
420       vfs: {struct: opfsVfs, methods: vfsMethods}
421     });
422     return opfsVfs;
423   };
425   /**
426      Class for managing OPFS-related state for the
427      OPFS SharedAccessHandle Pool sqlite3_vfs.
428   */
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);
482         });
483     }
485     #logImpl(level,...args){
486       if(this.#verbosity>level) loggers[level](this.vfsName+":",...args);
487     };
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;
507     }
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 //    }
520     /**
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.
524     */
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);
533       }
534       return this.getCapacity();
535     }
537     /**
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.
541     */
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;
547         }
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;
555       }
556       return nRm;
557     }
559     /**
560        Releases all currently-opened SAHs. The only legal
561        operation after this is acquireAccessHandles().
562     */
563     releaseAccessHandles(){
564       for(const ah of this.#mapSAHToName.keys()) ah.close();
565       this.#mapSAHToName.clear();
566       this.#mapFilenameToSAH.clear();
567       this.#availableSAH.clear();
568     }
570     /**
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.
580     */
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]);
586         }
587       }
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);
601             }
602           }
603         }catch(e){
604           this.storeErr(e);
605           this.releaseAccessHandles();
606           throw e;
607         }
608       }));
609     }
611     /**
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.
617     */
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 '';
630       }
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);
642         }
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 '';
651       }
652     }
654     /**
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.
659     */
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);
664       }
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);
680       }
681     }
683     /**
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.
689     */
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);
696       }
697       return new Uint32Array([h1>>>0, h2>>>0]);
698     }
700     /**
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.
706     */
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});
715         }
716       }
717       this.#dhVfsRoot = h;
718       this.#dhVfsParent = prev;
719       this.#dhOpaque = await this.#dhVfsRoot.getDirectoryHandle(
720         OPAQUE_DIR_NAME,{create:true}
721       );
722       this.releaseAccessHandles();
723       return this.acquireAccessHandles(clearFiles);
724     }
726     /**
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.
736     */
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;
742     }
744     /**
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.
748     */
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);
755       }
756       return !!sah;
757     }
759     /**
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.
766     */
767     storeErr(e,code){
768       if(e){
769         e.sqlite3Rc = code || capi.SQLITE_IOERR;
770         this.error(e);
771       }
772       this.$error = e;
773       return code;
774     }
775     /**
776        Pops this object's Error object and returns
777        it (a falsy value if no error is set).
778     */
779     popErr(){
780       const rc = this.$error;
781       this.$error = undefined;
782       return rc;
783     }
785     /**
786        Returns the next available SAH without removing
787        it from the set.
788     */
789     nextAvailableSAH(){
790       const [rc] = this.#availableSAH.keys();
791       return rc;
792     }
794     /**
795        Given an (sqlite3_file*), returns the mapped
796        xOpen file object.
797     */
798     getOFileForS3File(pFile){
799       return this.#mapS3FileToOFile_.get(pFile);
800     }
801     /**
802        Maps or unmaps (if file is falsy) the given (sqlite3_file*)
803        to an xOpen file object and to this pool object.
804     */
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);
812       }
813     }
815     /**
816        Returns true if the given client-defined file name is in this
817        object's name-to-SAH map.
818     */
819     hasFilename(name){
820       return this.#mapFilenameToSAH.has(name)
821     }
823     /**
824        Returns the SAH associated with the given
825        client-defined file name.
826     */
827     getSAHForPath(path){
828       return this.#mapFilenameToSAH.get(path);
829     }
831     /**
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.
841     */
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}
852         );
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*/
857       }
858       return true;
859     }
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+".");
871         }
872       }
873       return b;
874     }
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;
889           }
890           sah.write(chunk, {at:  HEADER_OFFSET_DATA + nWrote});
891           nWrote += chunk.byteLength;
892         }
893         if( nWrote < 512 || 0!==nWrote % 512 ){
894           toss("Input size",nWrote,"is not correct for an SQLite database.");
895         }
896         if( !checkedHeader ){
897           const header = new Uint8Array(20);
898           sah.read( header, {at: 0} );
899           util.affirmDbHeader( header );
900         }
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;
907       }
908       this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB);
909       return nWrote;
910     }
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.");
922       }
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.");
927         }
928       }
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);
937       }
938       return nWrote;
939     }
941   }/*class OpfsSAHPool*/;
944   /**
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().
952   */
953   class OpfsSAHPoolUtil {
954     /* This object's associated OpfsSAHPool. */
955     #p;
957     constructor(sahPool){
958       this.#p = sahPool;
959       this.vfsName = sahPool.vfsName;
960     }
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;
974     }
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 */;
988   /**
989      Returns a resolved Promise if the current environment
990      has a "fully-sync" SAH impl, else a rejected Promise.
991   */
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.");
1003     }
1004     return true;
1005   };
1007   /** Only for testing a rejection case. */
1008   let instanceCounter = 0;
1010   /**
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.
1208   */
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.");
1213     }
1214     if(initPromises[vfsName]){
1215       //console.warn("Returning same OpfsSAHPool result",options,vfsName,initPromises[vfsName]);
1216       return initPromises[vfsName];
1217     }
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.")));
1224     }
1226     /**
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.
1239     */
1240     return initPromises[vfsName] = apiVersionCheck().then(async function(){
1241       if(options.$testThrowInInit){
1242         throw options.$testThrowInInit;
1243       }
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);
1256           };
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);
1268             }
1269           );
1270         }/*extend sqlite3.oo1*/
1271         thePool.log("VFS initialized.");
1272         return poolUtil;
1273       }).catch(async (e)=>{
1274         await thePool.removeVfs().catch(()=>{});
1275         throw e;
1276       });
1277     }).catch((err)=>{
1278       //error("rejecting promise:",err);
1279       return initPromises[vfsName] = Promise.reject(err);
1280     });
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