From: stephan Date: Sat, 22 Nov 2025 02:43:56 +0000 (+0000) Subject: Demonstrate completely transient and a semi-transient (until page reload) kvvfs insta... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=fc6096b01386ef558ddb4139c5637071330ada5f;p=thirdparty%2Fsqlite.git Demonstrate completely transient and a semi-transient (until page reload) kvvfs instances. FossilOrigin-Name: 3f9ff9873303c3900dd3cba6e922bfb8cdb1f595353b692796b62e3025013517 --- diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index 065ea532e6..2ddf2ac15c 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -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, diff --git a/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js b/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js index 2d69be891b..4ecde974ba 100644 --- a/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js +++ b/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js @@ -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); }; diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c index e6dc7590e0..2cda7a0561 100644 --- a/ext/wasm/api/sqlite3-wasm.c +++ b/ext/wasm/api/sqlite3-wasm.c @@ -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 diff --git a/ext/wasm/tester1.c-pp.js b/ext/wasm/tester1.c-pp.js index 33bf733e9c..c8de3f625f 100644 --- a/ext/wasm/tester1.c-pp.js +++ b/ext/wasm/tester1.c-pp.js @@ -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', diff --git a/manifest b/manifest index 0236d0c8db..064f7d61e5 100644 --- 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. diff --git a/manifest.uuid b/manifest.uuid index a10954d26a..8210d850e8 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -e0b33b51229a977cc3fa8a5a6c8ea59669f8bf566b2a6330fd24da1ad886a716 +3f9ff9873303c3900dd3cba6e922bfb8cdb1f595353b692796b62e3025013517 diff --git a/src/os_kv.c b/src/os_kv.c index 4960feedc0..567c22408b 100644 --- a/src/os_kv.c +++ b/src/os_kv.c @@ -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 ){