]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Demonstrate completely transient and a semi-transient (until page reload) kvvfs insta...
authorstephan <stephan@noemail.net>
Sat, 22 Nov 2025 02:43:56 +0000 (02:43 +0000)
committerstephan <stephan@noemail.net>
Sat, 22 Nov 2025 02:43:56 +0000 (02:43 +0000)
FossilOrigin-Name: 3f9ff9873303c3900dd3cba6e922bfb8cdb1f595353b692796b62e3025013517

ext/wasm/api/sqlite3-api-prologue.js
ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js
ext/wasm/api/sqlite3-wasm.c
ext/wasm/tester1.c-pp.js
manifest
manifest.uuid
src/os_kv.c

index 065ea532e660222d506a0dad36eae540a57106f9..2ddf2ac15c04a1314645000685a78ee5d3176873 100644 (file)
@@ -752,6 +752,11 @@ globalThis.sqlite3ApiBootstrap = async function sqlite3ApiBootstrap(
     toss: function(...args){throw new Error(args.join(' '))},
     toss3,
     typedArrayPart: wasm.typedArrayPart,
+    assert: function(arg,msg){
+      if( !arg ){
+        util.toss("Assertion failed:",msg);
+      }
+    },
     /**
        Given a byte array or ArrayBuffer, this function throws if the
        lead bytes of that buffer do not hold a SQLite3 database header,
index 2d69be891b1af4976e681d8e536f974587ed2f4b..4ecde974ba702058e4c7679ea40450637e84f79c 100644 (file)
@@ -31,24 +31,30 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
 
   if( !pKvvfs ) return /* nothing to do */;
 
-  const util = sqlite3.util;
-
-  if( !util.isUIThread() ){
-    /* One test currently relies on this VFS not being visible in
-       Workers. Once we add generic object storage, we can retain this
-       VFS in Workers, we just can't provide local/sessionStorage
-       access there. */
-    capi.sqlite3_vfs_unregister(pKvvfs);
-    return;
-  }
-
-  const wasm = sqlite3.wasm,
+  const util = sqlite3.util,
+        wasm = sqlite3.wasm,
         hop = (o,k)=>Object.prototype.hasOwnProperty.call(o,k);
+
+  const cache = Object.assign(Object.create(null),{
+    rxJournalSuffix: /^-journal$/ // TOOD: lazily init once we figure out where
+  });
+
+  const debug = function(){
+    sqlite3.config.debug("kvvfs:", ...arguments);
+  };
+  const warn = function(){
+    sqlite3.config.warn("kvvfs:", ...arguments);
+  };
+
   /**
-     Implementation of JS's Storage interface for use
-     as backing store of the kvvfs.
+     Implementation of JS's Storage interface for use as backing store
+     of the kvvfs. Storage's constructor cannot be legally called from
+     JS, making it impossible to directly subclass Storage.
+
+     This impl simply proxies a plain, prototype-less Object, suitable
+     for JSON-ing.
   */
-  class ObjectStorage /* extends Storage (ctor may not be legally called) */ {
+  class TransientStorage {
     #map;
     #keys;
     #getKeys(){return this.#keys ??= Object.keys(this.#map);}
@@ -87,49 +93,99 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
     get length() {
       return this.#getKeys().length;
     }
-  }/*ObjectStorage*/;
+  }/*TransientStorage*/;
 
   /**
-     Internal helper for sqlite3_js_kvvfs_clear() and friends.
-     Its argument should be one of ('local','session',"").
+     Map of JS-stringified KVVfsFile::zClass names to
+     reference-counted Storage objects. These objects are creates in
+     xOpen(). Their refcount is decremented in xClose(), and the
+     record is destroyed if the refcount reaches 0. We refcount so
+     that concurrent active xOpen()s on a given name, and within a
+     given thread, use the same storage object.
   */
-  const __kvfsWhich = function(which){
-    const rc = Object.create(null);
-    rc.prefix = 'kvvfs-'+which;
-    rc.stores = [];
-    if( globalThis.sessionStorage
-        && ('session'===which || ""===which)){
-      rc.stores.push(globalThis.sessionStorage);
-    }
-    if( globalThis.localStorage
-        && ('local'===which || ""===which) ){
-      rc.stores.push(globalThis.localStorage);
+  cache.jzClassToStorage = Object.assign(Object.create(null),{
+    /* Start off with mappings for well-known names. */
+    global: {refc: 3/*never reaches 0*/, s: new TransientStorage}
+  });
+  if( globalThis.localStorage ){
+    cache.jzClassToStorage.local =
+      {refc: 3/*never reaches 0*/, s: globalThis.localStorage};
+  }
+  if( globalThis.sessionStorage ){
+    cache.jzClassToStorage.session =
+      {refc: 3/*never reaches 0*/, s: globalThis.sessionStorage}
+  }
+  for(const k of Object.keys(cache.jzClassToStorage)){
+    /* Journals in kvvfs are are stored as individual records within
+       their Storage-ish object, named "kvvfs-${zClass}-jrnl". We
+       always create mappings for both the db file's name and the
+       journal's name referring to the same Storage object. */
+    cache.jzClassToStorage[k+'-journal'] = cache.jzClassToStorage[k];
+  }
+
+  /**
+     Internal helper for sqlite3_js_kvvfs_clear() and friends.  Its
+     argument should be one of ('local','session',"") or the name of
+     an opened transient kvvfs db.
+
+     It returns an object in the form:
+
+     .prefix = the key prefix for this storage: "kvvfs-"+which.
+     (FIXME: we need to teach the underlying pieces to elide the
+     "-..." part for non-sessionSession/non-localStorage entries.
+     If we don't, each storage's keys will always be prefixed
+     by their name, which is wasteful.)
+
+     .stores = [ array of Storage-like objects ]. Will only have >1
+     element if which is falsy, in which case it contains (if called
+     from the main thread) localStorage and sessionStorage. It will
+     be empty if no mapping is found.
+  */
+  const kvfsWhich = function callee(which){
+    const rc = Object.assign(Object.create(null),{
+      prefix: 'kvvfs-' + which,
+      stores: []
+    });
+    if( which ){
+      const s = cache.jzClassToStorage[which];
+      if( s ) rc.stores.push(s.s);
+    }else{
+      if( globalThis.sessionStorage ) rc.stores.push(globalThis.sessionStorage);
+      if( globalThis.localStorage ) rc.stores.push(globalThis.localStorage);
     }
+    //debug("kvvfsWhich",which,rc);
     return rc;
   };
 
   /**
      Clears all storage used by the kvvfs DB backend, deleting any
-     DB(s) stored there. Its argument must be either 'session',
-     'local', or "". In the first two cases, only sessionStorage
-     resp. localStorage is cleared. If it's an empty string (the
-     default) then both are cleared. Only storage keys which match
-     the pattern used by kvvfs are cleared: any other client-side
-     data are retained.
+     DB(s) stored there.
+
+     Its argument must be either 'session', 'local', "", or the name
+     of a transient kvvfs storage object file. In the first two cases,
+     only sessionStorage resp. localStorage is cleared. If which is an
+     empty string (the default) then both localStorage and
+     sessionStorage are cleared. Only storage keys which match the
+     pattern used by kvvfs are cleared: any other client-side data are
+     retained.
 
-     This function is only available in the main window thread.
+     This function only manipulates localStorage and sessionStorage in
+     the main UI thread (they don't exist in Worker threads).
+     It affects transient kvvfs objects in any thread.
 
      Returns the number of entries cleared.
   */
   capi.sqlite3_js_kvvfs_clear = function(which=""){
     let rc = 0;
-    const kvWhich = __kvfsWhich(which);
-    kvWhich.stores.forEach((s)=>{
+    const store = kvfsWhich(which);
+    store.stores.forEach((s)=>{
       const toRm = [] /* keys to remove */;
-      let i;
-      for( i = 0; i < s.length; ++i ){
+      let i, n = s.length;
+      //debug("kvvfs_clear",store,s);
+      for( i = 0; i < n; ++i ){
         const k = s.key(i);
-        if(k.startsWith(kvWhich.prefix)) toRm.push(k);
+        //debug("kvvfs_clear ?",k);
+        if(k.startsWith(store.prefix)) toRm.push(k);
       }
       toRm.forEach((kk)=>s.removeItem(kk));
       rc += toRm.length;
@@ -139,14 +195,18 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
 
   /**
      This routine guesses the approximate amount of
-     window.localStorage and/or window.sessionStorage in use by the
-     kvvfs database backend. Its argument must be one of ('session',
-     'local', ""). In the first two cases, only sessionStorage
-     resp. localStorage is counted. If it's an empty string (the
-     default) then both are counted. Only storage keys which match
-     the pattern used by kvvfs are counted. The returned value is
-     twice the "length" value of every matching key and value,
-     noting that JavaScript stores each character in 2 bytes.
+     storage used by the given kvvfs back-end.
+
+     The 'which' argument is as documented for
+     sqlite3_js_kvvfs_clear(), only the operation this performs is
+     different:
+
+     The returned value is twice the "length" value of every matching
+     key and value, noting that JavaScript stores each character in 2
+     bytes.
+
+     If passed 'local' or 'session' or '' from a thread other than the
+     main UI thread, this is effectively a no-op and returns 0.
 
      The returned size is not authoritative from the perspective of
      how much data can fit into localStorage and sessionStorage, as
@@ -156,12 +216,12 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
   */
   capi.sqlite3_js_kvvfs_size = function(which=""){
     let sz = 0;
-    const kvWhich = __kvfsWhich(which);
-    kvWhich.stores.forEach((s)=>{
+    const store = kvfsWhich(which);
+    store?.stores?.forEach?.((s)=>{
       let i;
       for(i = 0; i < s.length; ++i){
         const k = s.key(i);
-        if(k.startsWith(kvWhich.prefix)){
+        if(k.startsWith(store.prefix)){
           sz += k.length;
           sz += s.getItem(k).length;
         }
@@ -172,33 +232,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
 
   const kvvfsMakeKey = wasm.exports.sqlite3__wasm_kvvfsMakeKeyOnPstack;
   const pstack = wasm.pstack;
-  const cache = Object.create(null);
-  cache.jzClassToStorage = Object.assign(Object.create(null),{
-    /* Map of JS-stringified KVVfsFile::zClass names to
-       reference-counted Storage objects. We refcount so that xClose()
-       does not pull one out from another instance. */
-    local:             {refc: 2, s: globalThis.localStorage},
-    session:           {refc: 2, s: globalThis.sessionStorage}
-  });
-  cache.jzClassToStorage['local-journal'] =
-    cache.jzClassToStorage.local;
-  cache.jzClassToStorage['session-journal'] =
-    cache.jzClassToStorage.session;
-
-
-  const kvvfsStorage = function(zClass){
-    const s = wasm.cstrToJs(zClass);
-    if( cache.jzClassToStorage[s] ){
-      return cache.jzClassToStorage[s].s;
-    }
-    if( !cache.rxSession ){
-      cache.rxSession = /^session(-journal)?$/;
-      cache.rxLocal = /^local(-journal)?$/;
-    }
-    if( cache.rxSession.test(s) ) return sessionStorage;
-    if( cache.rxLocal.test(s) ) return localStorage;
-    return cache.jzClassToStorage(s)?.s;
-  }.bind(Object.create(null));
+  const storageForZClass =
+        (zClass)=>cache.jzClassToStorage[wasm.cstrToJs(zClass)];
 
   const pFileHandles = new Map(
     /* sqlite3_file pointers => objects, each of which has:
@@ -208,9 +243,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
     */
   );
 
-  const debug = sqlite3.config.debug.bind(sqlite3.config);
-  const warn = sqlite3.config.warn.bind(sqlite3.config);
-
   {
     /**
        Original WASM functions for methods we partially override.
@@ -226,6 +258,13 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
     const originalIoMethods = (kvvfsFile)=>
           originalMethods[kvvfsFile.$isJournal ? 'ioJrnl' : 'ioDb'];
 
+    const kvvfsMethods = new sqlite3_kvvfs_methods(
+      /* Wraps the static sqlite3_api_methods singleton */
+      wasm.exports.sqlite3__wasm_kvvfs_methods()
+    );
+    const pVfs = new capi.sqlite3_vfs(kvvfsMethods.$pVfs);
+    const pIoDb = new capi.sqlite3_io_methods(kvvfsMethods.$pIoDb);
+    const pIoJrnl = new capi.sqlite3_io_methods(kvvfsMethods.$pIoJrnl);
     /**
        Implementations for members of the object referred to by
        sqlite3__wasm_kvvfs_methods(). We swap out the native
@@ -238,7 +277,13 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
     */
     sqlite3_kvvfs_methods.override = {
 
-      /* sqlite3_kvvfs_methods's own direct methods */
+      /**
+        sqlite3_kvvfs_methods's member methods.  These perform the
+        fetching, setting, and removal of storage keys on behalf of
+        kvvfs. In the native impl these write each db page to a
+        separate file. This impl stores each db page as a single
+        record in a Storage object which is mapped to zClass.
+      */
       recordHandler: {
         xRcrdRead: (zClass, zKey, zBuf, nBuf)=>{
           const stack = pstack.pointer,
@@ -246,18 +291,21 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
           try{
             const zXKey = kvvfsMakeKey(zClass,zKey);
             if(!zXKey) return -3/*OOM*/;
-            const jKey = wasm.cstrToJs(zXKey);
-            const jV = kvvfsStorage(zClass).getItem(jKey);
+            const jV = storageForZClass(zClass)
+                  .s.getItem(wasm.cstrToJs(zXKey));
             if(!jV) return -1;
             const nV = jV.length /* We are relying 100% on v being
-                                    ASCII so that jV.length is equal
-                                    to the C-string's byte length. */;
+                                 ** ASCII so that jV.length is equal
+                                 ** to the C-string's byte length. */;
             if(nBuf<=0) return nV;
             else if(1===nBuf){
               wasm.poke(zBuf, 0);
               return nV;
             }
-            const zV = wasm.scopedAllocCString(jV);
+            const zV = wasm.scopedAllocCString(jV)
+            /* TODO: allocate a single 128kb buffer (largest page
+               size) for reuse here, or maybe even preallocate
+               it somewhere in sqlite3__wasm_kvvfs_...(). */;
             if(nBuf > nV + 1) nBuf = nV + 1;
             wasm.heap8u().copyWithin(
               Number(zBuf), Number(zV), wasm.ptr.addn(zV, nBuf,- 1)
@@ -278,8 +326,10 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
           try {
             const zXKey = kvvfsMakeKey(zClass,zKey);
             if(!zXKey) return SQLITE_NOMEM;
-            const jKey = wasm.cstrToJs(zXKey);
-            kvvfsStorage(zClass).setItem(jKey, wasm.cstrToJs(zData));
+            storageForZClass(zClass).s.setItem(
+              wasm.cstrToJs(zXKey),
+              wasm.cstrToJs(zData)
+            );
             return 0;
           }catch(e){
             sqlite3.config.error("kvrecordWrite()",e);
@@ -294,7 +344,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
           try {
             const zXKey = kvvfsMakeKey(zClass,zKey);
             if(!zXKey) return capi.SQLITE_NOMEM;
-            kvvfsStorage(zClass).removeItem(wasm.cstrToJs(zXKey));
+            storageForZClass(zClass).s.removeItem(wasm.cstrToJs(zXKey));
             return 0;
           }catch(e){
             sqlite3.config.error("kvrecordDelete()",e);
@@ -304,50 +354,60 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
           }
         }
       }/*recordHandler*/,
+
       /**
-         After initial refactoring to support the use of arbitrary Storage
-         objects (the interface from which localStorage and sessionStorage
-         dervie), we will apparently need to override some of the
-         associated sqlite3_vfs and sqlite3_io_methods members.
-
-         We can call back into the native impls when needed, but we
-         need to override certain operations here to bypass its strict
-         db-naming rules (which, funnily enough, are in place because
-         they're relevant (only) for what should soon be the previous
-         version of this browser-side implementation). Apropos: the
-         port to generic objects would also make non-persistent kvvfs
-         available in Worker threads and non-browser builds. They
-         could optionally be exported to/from JSON.
+         Override certain operations of the underlying sqlite3_vfs and
+         two sqlite3_io_methods instances so that we can tie Storage
+         objects to db names.
       */
-      /* sqlite3_kvvfs_methods::pVfs's methods */
       vfs:{
-        /**
-         */
+        /* sqlite3_kvvfs_methods::pVfs's methods */
         xOpen: function(pProtoVfs,zName,pProtoFile,flags,pOutFlags){
           try{
+            //cache.zReadBuf ??= wasm.malloc(kvvfsMethods.$nBufferSize);
+            const n = wasm.cstrlen(zName);
+            if( n > kvvfsMethods.$nKeySize - 8 /*"-journal"*/ - 1 ){
+              warn("file name is too long:", wasm.cstrToJs(zName));
+              return capi.SQLITE_RANGE;
+            }
             const rc = originalMethods.vfs.xOpen(pProtoVfs, zName, pProtoFile,
                                                  flags, pOutFlags);
             if( 0==rc ){
               const jzName = wasm.cstrToJs(zName);
               const f = new KVVfsFile(pProtoFile);
               let s = cache.jzClassToStorage[jzName];
+              debug("xOpen", jzName, s);
               if( s ){
                 ++s.refc;
               }else{
-                s = cache.jzClassToStorage[jzName] = {
-                  refc: 1,
-                  s: new ObjectStorage
-                };
+                /* TODO: a url flag which tells it to keep the storage
+                   around forever so that future xOpen()s get the same
+                   Storage-ish objects. We can accomplish that by
+                   simply increasing the refcount once more. */
+                util.assert( !f.$isJournal, "Opening a journal before its db? "+jzName );
+                const other = f.$isJournal
+                      ? jzName.replace(cache.rxJournalSuffix,'')
+                      : jzName + '-journal';
+                s = cache.jzClassToStorage[jzName]
+                  = cache.jzClassToStorage[other]
+                  = Object.assign(Object.create(null),{
+                    refc: 1/* if this is a db-open, the journal open
+                              will follow soon enough and bump the
+                              refcount. If we start at 2 here, that
+                              pending open will increment it again. */,
+                    s: new TransientStorage
+                  });
+                debug("xOpen installed storage handles [",
+                      jzName, other,"]", s);
               }
-              debug("kvvfs xOpen", f, jzName, s);
               pFileHandles.set(pProtoFile, {s,f,n:jzName});
             }
             return rc;
           }catch(e){
-            warn("kvvfs xOpen:",e);
+            warn("xOpen:",e);
             return capi.SQLITE_ERROR;
           }
-        },
+        }/*xOpen()*/,
 //#if nope
         xDelete: function(pVfs, zName, iSyncFlag){},
         xAccess:function(pProtoVfs, zPath, flags, pResOut){},
@@ -365,9 +425,10 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
           return i;
         }
       },
+
       /**
          kvvfs has separate sqlite3_api_methods impls for some of the
-         methods, depending on whether it's a db or journal file. Some
+         methods depending on whether it's a db or journal file. Some
          of the methods use shared impls but others are specific to
          either db or journal files.
       */
@@ -376,19 +437,29 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
         xClose: function(pFile){
           try{
             const h = pFileHandles.get(pFile);
-            debug("kvvfs xClose", pFile, h);
-            pFileHandles.delete(pFile);
-            const s = cache.jzClassToStorage[h.n];
-            if( 0===--s.refc ){
-              delete cache.jzClassToStorage[h.n];
-              delete s.s;
-              delete s.refc;
+            debug("xClose", pFile, h);
+            if( h ){
+              pFileHandles.delete(pFile);
+              const s = cache.jzClassToStorage[h.n];
+              util.assert(s, "Missing jzClassToStorage["+h.n+"]");
+              if( 0===--s.refc ){
+                const other = h.f.$isJournal
+                      ? h.n.replace(cache.rxJournalSuffix,'')
+                      : h.n+'-journal';
+                debug("cleaning up storage handles [", h.n, other,"]",s);
+                delete cache.jzClassToStorage[h.n];
+                delete cache.jzClassToStorage[other];
+                delete s.s;
+                delete s.refc;
+              }
+              originalIoMethods(h.f).xClose(pFile);
+              h.f.dispose();
+            }else{
+              /* Can happen if xOpen fails */
             }
-            originalIoMethods(h.f).xClose(pFile);
-            h.f.dispose();
             return 0;
           }catch(e){
-            warn("kvvfs xClose",e);
+            warn("xClose",e);
             return capi.SQLITE_ERROR;
           }
         },
@@ -407,6 +478,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
         xDeviceCharacteristics: function(pFile){}
 //#endif
       },
+
       ioJrnl:{
         /* sqlite3_kvvfs_methods::pIoJrnl's methods. Those set to true
            are copied as-is from the ioDb objects. Others are specific
@@ -430,14 +502,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
     }/*sqlite3_kvvfs_methods.override*/;
 
     const ov = sqlite3_kvvfs_methods.override;
-    const kvvfsMethods = new sqlite3_kvvfs_methods(
-      /* Wraps the static sqlite3_api_methods singleton */
-      wasm.exports.sqlite3__wasm_kvvfs_methods()
-    );
-    const pVfs = new capi.sqlite3_vfs(kvvfsMethods.$pVfs);
-    const pIoDb = new capi.sqlite3_io_methods(kvvfsMethods.$pIoDb);
-    const pIoJrnl = new capi.sqlite3_io_methods(kvvfsMethods.$pIoJrnl);
-    debug("pVfs and friends",pVfs, pIoDb, pIoJrnl);
+    debug("pVfs and friends", pVfs, pIoDb, pIoJrnl);
     try {
       for(const e of Object.entries(ov.recordHandler)){
         // Overwrite kvvfsMethods's callbacks
@@ -446,38 +511,34 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
       }
       for(const e of Object.entries(ov.vfs)){
         // Overwrite some pVfs entries and stash the original impls
-        const k = e[0],
-              f = e[1],
-              km = pVfs.memberKey(k),
-              mbr = pVfs.structInfo.members[k] || util.toss("Missing pVfs member ",km);
-        originalMethods.vfs[k] = wasm.functionEntry(pVfs[km]);
-        pVfs[km] = wasm.installFunction(mbr.signature, f);
+        const k = e[0], f = e[1], km = pVfs.memberKey(k),
+              member = pVfs.structInfo.members[k]
+              || util.toss("Missing pVfs.structInfo[",k,"]");
+        originalMethods.vfs[k] = wasm.functionEntry(pVfs[km])
+          || util.toss("Missing native pVfs[",km,"]");
+        pVfs[km] = wasm.installFunction(member.signature, f);
       }
       for(const e of Object.entries(ov.ioDb)){
         // Similar treatment for pVfs.$pIoDb a.k.a. pIoDb...
-        const k = e[0],
-              f = e[1],
-              km = pIoDb.memberKey(k),
-              mbr = pIoDb.structInfo.members[k];
-        if( !mbr ){
-          warn("Missinog pIoDb member",k,km,pIoDb.structInfo);
-          util.toss("Missing pIoDb member",k,km);
-        }
-        originalMethods.ioDb[k] = wasm.functionEntry(pIoDb[km]);
-        pIoDb[km] = wasm.installFunction(mbr.signature, f);
+        const k = e[0], f = e[1], km = pIoDb.memberKey(k),
+              member = pIoDb.structInfo.members[k]
+              || util.toss("Missing pIoDb.structInfo[",k,"]");
+        originalMethods.ioDb[k] = wasm.functionEntry(pIoDb[km])
+          || util.toss("Missing native pIoDb[",km,"]");
+        pIoDb[km] = wasm.installFunction(member.signature, f);
       }
       for(const e of Object.entries(ov.ioJrnl)){
         // Similar treatment for pVfs.$pIoJrnl a.k.a. pIoJrnl...
-        const k = e[0],
-              f = e[1],
-              km = pIoJrnl.memberKey(k);
-        originalMethods.ioJrnl[k] = wasm.functionEntry(pIoJrnl[km]);
+        const k = e[0], f = e[1], km = pIoJrnl.memberKey(k);
+        originalMethods.ioJrnl[k] = wasm.functionEntry(pIoJrnl[km])
+          || util.toss("Missing native pIoJrnl[",km,"]");
         if( true===f ){
           /* use pIoDb's copy */
-          pIoJrnl[km] = pIoDb[km] || util.toss("Missing copied pIoDb member",km);
+          pIoJrnl[km] = pIoDb[km] || util.toss("Missing copied pIoDb[",km,"]");
         }else{
-          const mbr = pIoJrnl.structInfo.members[k] || util.toss("Missing pIoJrnl member",km)
-          pIoJrnl[km] = wasm.installFunction(mbr.signature, f);
+          const member = pIoJrnl.structInfo.members[k]
+                || util.toss("Missing pIoJrnl.structInfo[",k,"]");
+          pIoJrnl[km] = wasm.installFunction(pIoJrnl.memberSignature(k), f);
         }
       }
     }finally{
@@ -509,10 +570,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
       storageName = sqlite3.oo1.JsStorageDb.defaultStorageName
     ){
       const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...arguments);
-      storageName = opt.filename;
-      if('session'!==storageName && 'local'!==storageName){
-        util.toss3("JsStorageDb db name must be one of 'session' or 'local'.");
-      }
       opt.vfs = 'kvvfs';
       sqlite3.oo1.DB.dbCtorHelper.call(this, opt);
     };
index e6dc7590e0a807b4157c54a53fff4eb5342f39e5..2cda7a0561d07de079e110e1821e789bb66ff0d8 100644 (file)
@@ -1019,13 +1019,15 @@ const char * sqlite3__wasm_enum_json(void){
   /** ^^^ indirection needed to expand CurrentStruct */
 #define StructBinder StructBinder_(CurrentStruct)
 #define _StructBinder CloseBrace(2)
-#define M(MEMBER,SIG)                                         \
-  outf("%s\"%s\": "                                           \
-       "{\"offset\":%d,\"sizeof\": %d,\"signature\":\"%s\"}", \
-       (n++ ? ", " : ""), #MEMBER,                            \
-       (int)offsetof(CurrentStruct,MEMBER),                   \
-       (int)sizeof(((CurrentStruct*)0)->MEMBER),              \
-       SIG)
+#define M3(MEMBER,SIG,READONLY)                                \
+  outf("%s\"%s\": "                                            \
+       "{\"offset\":%d,\"sizeof\":%d,\"signature\":\"%s\"%s}", \
+       (n++ ? ", " : ""), #MEMBER,                             \
+       (int)offsetof(CurrentStruct,MEMBER),                    \
+       (int)sizeof(((CurrentStruct*)0)->MEMBER),               \
+       SIG, (READONLY ? ",\"readOnly\":true" : ""))
+#define M(MEMBER,SIG) M3(MEMBER,SIG,0)
+#define MRO(MEMBER,SIG) M3(MEMBER,SIG,1)
 
   nStruct = 0;
   out(", \"structs\": ["); {
@@ -1093,7 +1095,8 @@ const char * sqlite3__wasm_enum_json(void){
       M(xRcrdRead,         "i(sspi)");
       M(xRcrdWrite,        "i(sss)");
       M(xRcrdDelete,       "i(ss)");
-      M(nKeySize,          "i");
+      MRO(nKeySize,        "i");
+      MRO(nBufferSize,     "i");
       M(pVfs,              "p");
       M(pIoDb,             "p");
       M(pIoJrnl,           "p");
@@ -1269,6 +1272,8 @@ const char * sqlite3__wasm_enum_json(void){
 #undef StructBinder_
 #undef StructBinder__
 #undef M
+#undef MRO
+#undef M3
 #undef _StructBinder
 #undef CloseBrace
 #undef out
index 33bf733e9cc2885e6fb9fd9ed597263d0500f1d3..c8de3f625f1b478dc546a50fe80d0d463d828050 100644 (file)
@@ -2879,25 +2879,14 @@ globalThis.sqlite3InitModule = sqlite3InitModule;
   ////////////////////////////////////////////////////////////////////////
   T.g('kvvfs')
     .t({
-      name: 'kvvfs is disabled in worker',
-      predicate: ()=>(isWorker() || "test is only valid in a Worker"),
-      test: function(sqlite3){
-        T.assert(
-          !capi.sqlite3_vfs_find('kvvfs'),
-          "Expecting kvvfs to be unregistered."
-        );
-      }
-    })
-    .t({
-      name: 'kvvfs in main thread',
-      predicate: ()=>(isUIThread()
-                      || "local/sessionStorage are unavailable in a Worker"),
+      name: 'kvvfs sessionStorage',
+      predicate: ()=>(globalThis.sessionStorage || "sessionStorage is unavailable"),
       test: function(sqlite3){
         const filename = this.kvvfsDbFile = 'session';
         const pVfs = capi.sqlite3_vfs_find('kvvfs');
         T.assert(looksLikePtr(pVfs));
-        const JDb = this.JDb = sqlite3.oo1.JsStorageDb;
-        const unlink = this.kvvfsUnlink = ()=>JDb.clearStorage(this.kvvfsDbFile);
+        const JDb = sqlite3.oo1.JsStorageDb;
+        const unlink = this.kvvfsUnlink = ()=>JDb.clearStorage(filename);
         unlink();
         let db = new JDb(filename);
         try {
@@ -2916,6 +2905,55 @@ globalThis.sqlite3InitModule = sqlite3InitModule;
         }
       }
     }/*kvvfs sanity checks*/)
+    .t({
+      name: 'transient kvvfs',
+      test: function(sqlite3){
+        const filename = 'global' /* preinstalled instance */;
+        const JDb = sqlite3.oo1.JsStorageDb;
+        const unlink = ()=>JDb.clearStorage(filename);
+        unlink();
+        let db = new JDb(filename);
+        const sqlSetup = [
+          'create table kvvfs(a);',
+          'insert into kvvfs(a) values(1),(2),(3)'
+        ];
+        try {
+          db.exec(sqlSetup);
+          const close = ()=>{
+            db.close();
+            db = undefined;
+          };
+          T.assert(3 === db.selectValue('select count(*) from kvvfs'));
+          close();
+
+          db = new JDb(filename);
+          db.exec('insert into kvvfs(a) values(4),(5),(6)');
+          T.assert(6 === db.selectValue('select count(*) from kvvfs'));
+          close();
+
+          db = new JDb('new-storage');
+          db.exec(sqlSetup);
+          T.assert(3 === db.selectValue('select count(*) from kvvfs'));
+          close();
+
+          T.mustThrow(function(){
+            /* Ensure that 'new-storage' was deleted when its refcount
+               went to 0. TODO is a way to tell these instances to
+               hang around after that, such that 'new-instance' could
+               be semi-persistent (until the page is reloaded).
+            */
+            let ddb = new JDb('new-storage');
+            try{
+              ddb.selectValue('select a from kvvfs');
+            }finally{
+              ddb.close();
+            }
+          }, "Expecting new-storage to be empty.");
+        }finally{
+          if( db ) db.close();
+        }
+      }
+    }/*transient kvvfs*/)
 //#if enable-see
     .t({
       name: 'kvvfs with SEE encryption',
index 0236d0c8db010e5ddcb35a8ab0904cb66d387cda..064f7d61e5898f49a970d03f797537bca229f393 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Latest\sside-stream\sjaccwabyt/wasmutil.
-D 2025-11-22T02:23:02.637
+C Demonstrate\scompletely\stransient\sand\sa\ssemi-transient\s(until\spage\sreload)\skvvfs\sinstances.
+D 2025-11-22T02:43:56.155
 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -595,16 +595,16 @@ F ext/wasm/api/post-js-header.js d24bd0d065f3489c8b78ddf3ead6321e5d047187a162cd5
 F ext/wasm/api/pre-js.c-pp.js ad2546290e0c8ce5ca2081bff6e85cc25eeb904a3303921f1184290a7ff1b32f
 F ext/wasm/api/sqlite3-api-glue.c-pp.js 9eaed1801be392f6687aa7da8e3a5a41d03de19993d8fe62ee6c52617eab4985
 F ext/wasm/api/sqlite3-api-oo1.c-pp.js 7d8850f754b4a9aecd5a4a92a357e05f720cd7f5cf962909343b40c914237256
-F ext/wasm/api/sqlite3-api-prologue.js 7004b569624765c5132984bfecee2305bef928a6adf44e0202dacc9cbc5c8e2a
+F ext/wasm/api/sqlite3-api-prologue.js d78114039f77ab79bdffb682a58db34000ba2a263efbb210e3c51aa74d206e1c
 F ext/wasm/api/sqlite3-api-worker1.c-pp.js 1041dd645e8e821c082b628cd8d9acf70c667430f9d45167569633ffc7567938
 F ext/wasm/api/sqlite3-license-version-header.js 0c807a421f0187e778dc1078f10d2994b915123c1223fe752b60afdcd1263f89
 F ext/wasm/api/sqlite3-opfs-async-proxy.js 9654b565b346dc609b75d15337f20acfa7af7d9d558da1afeb9b6d8eaa404966
 F ext/wasm/api/sqlite3-vfs-helper.c-pp.js 3f828cc66758acb40e9c5b4dcfd87fd478a14c8fb7f0630264e6c7fa0e57515d
-F ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js b8e37020d69e09348e16111a989f29ffa41fc719412d0ea7ffd1e8f51db91417
+F ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js 1df90559ff3e01175fcfa5b653088a331c23474ec019a156e10d468dbbfacfac
 F ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js 26cb41d5a62f46a106b6371eb00fef02de3cdbfaa51338ba087a45f53028e0d0
 F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js aa330fa0e8ef35cbd92eb0d52e05fbaa07e61540c5cb164e693c82428ce1d763
 F ext/wasm/api/sqlite3-vtab-helper.c-pp.js 9097074724172e31e56ce20ccd7482259cf72a76124213cbc9469d757676da86
-F ext/wasm/api/sqlite3-wasm.c e10ae835b1d8cb3bbc0974200a8339a96e66caed96d45ca962a5cebfd1a04e36
+F ext/wasm/api/sqlite3-wasm.c 1b49686314999e267de6f243a1ebacb86c6bebc528646b26096c44c26e50ae42
 F ext/wasm/api/sqlite3-worker1-promiser.c-pp.js bda1c75bd674a92a0e27cc2f3d46dbbf21e422413f8046814515a0bd7409328a
 F ext/wasm/api/sqlite3-worker1.c-pp.js 802d69ead8c38dc1be52c83afbfc77e757da8a91a2e159e7ed3ecda8b8dba2e7
 F ext/wasm/c-pp-lite.c 943be1a36774d58385dca32de36fc18d4f432fe79f7aa35e6c85dd6a6b825bd0
@@ -647,7 +647,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555
 F ext/wasm/test-opfs-vfs.js 1618670e466f424aa289859fe0ec8ded223e42e9e69b5c851f809baaaca1a00c
 F ext/wasm/tester1-worker.c-pp.html 0e432ec2c0d99cd470484337066e8d27e7aee4641d97115338f7d962bf7b081a
 F ext/wasm/tester1.c-pp.html 52d88fe2c6f21a046030a36410b4839b632f4424028197a45a3d5669ea724ddb
-F ext/wasm/tester1.c-pp.js 015b4133cc3a5fb41d6236a6b39d23d996cc2d61a4877acde31a1f69574d4ce3
+F ext/wasm/tester1.c-pp.js cbfee01ad8ca3d981c24a44807aa1c686bc0e01c86fc8532044d7da17e584de7
 F ext/wasm/tests/opfs/concurrency/index.html 657578a6e9ce1e9b8be951549ed93a6a471f4520a99e5b545928668f4285fb5e
 F ext/wasm/tests/opfs/concurrency/test.js d08889a5bb6e61937d0b8cbb78c9efbefbf65ad09f510589c779b7cc6a803a88
 F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2
@@ -717,7 +717,7 @@ F src/notify.c 57c2d1a2805d6dee32acd5d250d928ab94e02d76369ae057dee7d445fd64e878
 F src/os.c 509452169d5ea739723e213b8e2481cf0e587f0e88579a912d200db5269f5f6d
 F src/os.h 1ff5ae51d339d0e30d8a9d814f4b8f8e448169304d83a7ed9db66a65732f3e63
 F src/os_common.h 6c0eb8dd40ef3e12fe585a13e709710267a258e2c8dd1c40b1948a1d14582e06
-F src/os_kv.c b5de11f31634c36ac5ab04b14f6da67801f331b73384f852de0d77c98009818d
+F src/os_kv.c 494f977e4977ec58d754a7ad6e0f29e3bf63ac155f3a8bb78bdcda50d0af92c5
 F src/os_setup.h 8efc64eda6a6c2f221387eefc2e7e45fd5a3d5c8337a7a83519ba4fbd2957ae2
 F src/os_unix.c 7945ede1e85b2d1b910e1b4af9ba342e964b1e30e79f4176480a60736445cb36
 F src/os_win.c a89b501fc195085c7d6c9eec7f5bd782625e94bb2a96b000f4d009703df1083f
@@ -2178,8 +2178,8 @@ F tool/version-info.c 33d0390ef484b3b1cb685d59362be891ea162123cea181cb8e6d2cf6dd
 F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7
 F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
-P 19a3349a2031e2b7fae67847b55643e4f70f8dae863ebc1ace3b09d1f482c8eb
-R f4817f0cbb500ac29ff861fffe4ad699
+P e0b33b51229a977cc3fa8a5a6c8ea59669f8bf566b2a6330fd24da1ad886a716
+R c905cc3949f1a87b4595864b9a20953b
 U stephan
-Z a18118b461e622bb9fd359a51fedd9be
+Z 95fff1b5f36e3d97fba51de647f25d12
 # Remove this line to create a well-formed Fossil manifest.
index a10954d26a858dbf5ddcd55260ddc6b0476cf74a..8210d850e84da7ca6168052bb92c09b3c9761e68 100644 (file)
@@ -1 +1 @@
-e0b33b51229a977cc3fa8a5a6c8ea59669f8bf566b2a6330fd24da1ad886a716
+3f9ff9873303c3900dd3cba6e922bfb8cdb1f595353b692796b62e3025013517
index 4960feedc0634050fe72e69cb66d7fc8a7f7fc52..567c22408b0af78b728e579d97bfbfda9916c38e 100644 (file)
@@ -181,14 +181,25 @@ static int kvrecordRead(const char*, const char *zKey, char *zBuf, int nBuf);
 /* Expand the key name with an appropriate prefix and put the result
 ** in zKeyOut[].  The zKeyOut[] buffer is assumed to hold at least
 ** KVRECORD_KEY_SZ bytes.
+**
+** TODO: we only need to include zClass in the keys for "local" and
+** "session" instances and their "-journal" counterparts.  For other
+** instances (a capability added 3+ years later) we can allow longer
+** db names if we elide zClass. We don't _really_ need that part of
+** the key in JS-side local/session instances (we do in
+** filesystem-side instances), but we can't strip it without
+** invalidating existing JS-side kvvfs dbs.
 */
 static void kvrecordMakeKey(
   const char *zClass,
   const char *zKeyIn,
   char *zKeyOut
 ){
+  assert( zClass );
+  assert( zKeyIn );
+  assert( zKeyOut );
   sqlite3_snprintf(KVRECORD_KEY_SZ, zKeyOut, "kvvfs-%s-%s",
-                   zClass, zKeyIn ? zKeyIn : "");
+                   zClass, zKeyIn);
 }
 
 /* Write content into a key.  zClass is the particular namespace of the
@@ -302,6 +313,7 @@ struct sqlite3_kvvfs_methods {
   int (*xRcrdWrite)(const char*, const char *zKey, const char *zData);
   int (*xRcrdDelete)(const char*, const char *zKey);
   const int nKeySize;
+  const int nBufferSize;
 #ifndef SQLITE_WASM
 #  define MAYBE_CONST const
 #else
@@ -332,6 +344,7 @@ sqlite3_kvvfs_methods sqlite3KvvfsMethods = {
   .xRcrdWrite      = kvrecordWrite,
   .xRcrdDelete     = kvrecordDelete,
   .nKeySize        = KVRECORD_KEY_SZ,
+  .nBufferSize     = SQLITE_KVOS_SZ,
   .pVfs            = &sqlite3OsKvvfsObject,
   .pIoDb           = &kvvfs_db_io_methods,
   .pIoJrnl         = &kvvfs_jrnl_io_methods
@@ -842,6 +855,7 @@ static int kvvfsOpen(
   assert(!pFile->aData);
   assert(!pFile->aJrnl);
   assert(!pFile->nJrnl);
+  assert(!pFile->base.pMethods);
   pFile->szPage = -1;
   pFile->szDb = -1;
   pFile->zName = zName;
@@ -856,17 +870,9 @@ static int kvvfsOpen(
   }else{
     pFile->isJournal = 0;
     pFile->base.pMethods = &kvvfs_db_io_methods;
-    if( 0==strcmp("session",zName) || 0==strcmp("local",zName) ){
-      pFile->zClass = zName;
-    }
   }
   if( !pFile->zClass ){
-#ifndef SQLITE_WASM
-    return SQLITE_CANTOPEN;
-#else
-    /* The JS impl maps these to Storage objects */
     pFile->zClass = zName;
-#endif
   }
   pFile->aData = sqlite3_malloc64(SQLITE_KVOS_SZ);
   if( pFile->aData==0 ){