delete capi.KVVfsFile;
if( !pKvvfs ) return /* nothing to do */;
+ if( 0 ){
+ /* This working would be our proverbial holy grail. */
+ capi.sqlite3_vfs_register(pKvvfs, 1);
+ }
const util = sqlite3.util,
wasm = sqlite3.wasm,
Create a new instance of the objects which go into
cache.storagePool.
*/
- const createStorageObj = (name,storage)=>Object.assign(Object.create(null),{
+ const newStorageObj = (name,storage)=>Object.assign(Object.create(null),{
+ /**
+ JS string value of this KVVfsFile::$zClass. i.e. the storage's
+ name.
+ */
jzClass: name,
+ /**
+ Refcount to keep dbs and journals pointing to the same storage
+ for the life of both. Managed by xOpen() and xClose().
+ */
refc: 1,
+ /**
+ isTransient objects will be removed by xClose() when refc
+ reaches 0. The others will persist, to give the illusion of
+ real back-end storage. Managed by xOpen().
+ */
+ isTransient: false,
+ /**
+ The backing store. Must implement the Storage interface.
+ */
storage: storage || new KVVfsStorage,
- /* This is the storage prefix used for kvvfs keys. It is
+ /**
+ The storage prefix used for kvvfs keys. It is
"kvvfs-STORAGENAME-" for local/session storage and an empty
- string for transient storage. local/session storage must
- use the long form for backwards compatibility.
+ string for transient storage. local/session storage must use
+ the long form (A) for backwards compatibility and (B) so that
+ kvvfs can coexist with non-db client data in those backends.
+ Neither (A) nor (B) are concerns for KVVfsStorage objects.
This prefix mirrors the one generated by os_kv.c's
kvrecordMakeKey() and must stay in sync with that one.
*/
keyPrefix: kvvfsKeyPrefix(name),
- files: [/*KVVfsFile instances currently using this storage*/]
+ /**
+ KVVfsFile instances currently using this storage. Managed by
+ xOpen() and xClose().
+ */
+ files: []
});
that concurrent active xOpen()s on a given name, and within a
given thread, use the same storage object.
*/
- /* Start off with mappings for well-known names. */
cache.storagePool = Object.assign(Object.create(null),{
- localThread: createStorageObj('localThread')
+ /* Start off with mappings for well-known names. */
+ localThread: newStorageObj('localThread')
});
if( globalThis.Storage ){
if( globalThis.localStorage instanceof globalThis.Storage ){
- cache.storagePool.local = createStorageObj('local');
+ cache.storagePool.local = newStorageObj('local', globalThis.localStorage);
}
if( globalThis.sessionStorage instanceof globalThis.Storage ){
- cache.storagePool.session = createStorageObj('session');
+ cache.storagePool.session = newStorageObj('session', globalThis.sessionStorage);
}
}
argument should be one of ('local','session',"") or the name of
an opened transient kvvfs db.
+ Its handling of which=='' is historical: it means to consider
+ both local and session storage. which=='' is also a valid kvvfs
+ storage unit name, though. Prior to that possibility, this API
+ was only availbe in the main UI thread, so the empty string
+ continues to unconditionally work that way in the main UI
+ thread. When, however, it's running where local/session storage
+ are not availble, it treats which=='' as a storage unit name.
+
It returns an object in the form:
.prefix = the key prefix for this storage. Typically
in the current environment (e.g. a worker thread).
.cts = the underlying storagePool entry. This will only be set
- of which is not empty.
+ if which is not empty.
*/
const kvvfsWhich = function callee(which){
const rc = Object.assign(Object.create(null),{
stores: []
});
- if( which ){
+ if( which || !globalThis.Storage ){
const s = storageForZClass(which);
if( s ){
//debug("kvvfsWhich",s.jzClass,rc.prefix, s.s);
rc.prefix = undefined;
}
}else{
- // Legacy behavior: return both local and session storage.
rc.prefix = 'kvvfs-';
- if( globalThis.sessionStorage ) rc.stores.push(globalThis.sessionStorage);
- if( globalThis.localStorage ) rc.stores.push(globalThis.localStorage);
+ // Legacy behavior: return both local and session storage.
+ if( cache.storagePool.local ) {
+ rc.stores.push(cache.storagePool.local.storage);
+ }
+ if( cache.storagePool.session ) {
+ rc.stores.push(cache.storagePool.session.storage);
+ }
}
//debug("kvvfsWhich",which,rc);
return rc;
(sqlite3_file*). On success it returns a new KVVfsFile instance
wrapping that pointer, which the caller must eventual call
dispose() on (which won't free the underlying pointer, just the
- wrapper).
- */
+ wrapper). Returns null if no handle is found (which would
+ indicate either that pDb is not using kvvfs or a severe bug in
+ its management).
+ */
const fileForDb = function(pDb){
const stack = pstack.pointer;
try{
pstack.restore(stack);
}
};
-//#endif nope
/**
Expects an object from the storagePool map. The $szPage and
throw e;
}
};
-
- /**
- Clears all storage used by the kvvfs DB backend, deleting any
- 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 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 store = kvvfsWhich(which);
- const keyPrefix = store.prefix;
- /**
- Historically this code had no way to check whether the storage
- was in use before wiping it, so could not error in that
- case. Whether or not it should, not that it can (as of late
- 2025-11), is TBD.
- */
- store.stores.forEach((s)=>{
- const toRm = [] /* keys to remove */;
- let i, n = s.length;
- //debug("kvvfs_clear",store,s);
- for( i = 0; i < n; ++i ){
- const k = s.key(i);
- //debug("kvvfs_clear ?",k);
- if(!keyPrefix || k.startsWith(keyPrefix)) toRm.push(k);
- }
- toRm.forEach((kk)=>s.removeItem(kk));
- rc += toRm.length;
- });
- if( store.cts ) alertFilesToReload(store.cts);
- return rc;
- };
-
- /**
- This routine guesses the approximate amount of
- 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
- the precise algorithms for determining those limits are
- unspecified and may include per-entry overhead invisible to
- clients.
- */
- capi.sqlite3_js_kvvfs_size = function(which=""){
- let sz = 0;
- const store = kvvfsWhich(which);
- //warn("kvvfs_size storage",store);
- store?.stores?.forEach?.((s)=>{
- //warn("kvvfs_size backend",s);
- let i;
- for(i = 0; i < s.length; ++i){
- const k = s.key(i);
- if(k.startsWith(store.prefix)){
- sz += k.length;
- sz += s.getItem(k).length;
- }
- }
- });
- return sz * 2 /* because JS uses 2-byte char encoding */;
- };
+//#endif nope
/** pstack-allocates a key. Caller must eventually restore
the pstack to free the memory. */
zName = (cache.zEmpty ??= wasm.allocCString(""));
}
const n = wasm.cstrlen(zName);
- if( n > kvvfsMethods.$nKeySize - 8 /*"-journal"*/ - 1 ){
+ if( !n ){
+ warn("file name may not be empty (backwards compatibilty constraint)");
+ return capi.SQLITE_RANGE;
+ }else if( n > kvvfsMethods.$nKeySize - 8 /*"-journal"*/ - 1 ){
warn("file name is too long:", wasm.cstrToJs(zName));
return capi.SQLITE_RANGE;
}
const jzClass = wasm.cstrToJs(zName);
if( jzClass?.length != n ){
- warn("kvvfs file name must be ASCII-only");
+ warn("kvvfs file name must be ASCII-only:",jzClass);
/* This limitation is to avoide any issues with
truncating multi-byte characters in kvvfs's key-size
limit. */
const rc = originalMethods.vfs.xOpen(pProtoVfs, zName, pProtoFile,
flags, pOutFlags);
if( rc ) return rc;
+ let transient = false;
+ if(n && wasm.isPtr(zName)){
+ if(capi.sqlite3_uri_boolean(zName, "transient", 0)){
+ transient = true;
+ //warn("transient=",transient);
+ }
+ if(capi.sqlite3_uri_boolean(zName, "wipe-before-open", 0)){
+ // TODO
+ }
+ }
const f = new KVVfsFile(pProtoFile);
util.assert(f.$zClass, "Missing f.$zClass");
let s = storageForZClass(jzClass);
//debug("xOpen", jzClass, s);
if( s ){
++s.refc;
+ if( true===transient ) s.isTransient = true;
s.files.push(f);
}else{
/* TODO: a url flag which tells it to keep the storage
: jzClass + '-journal';
s = cache.storagePool[jzClass]
= cache.storagePool[other]
- = createStorageObj(jzClass);
+ = newStorageObj(jzClass);
s.files.push(f);
+ s.isTransient = transient;
debug("xOpen installed storage handles [",
jzClass, other,"]", s);
}
pFileHandles.delete(pFile);
const s = storageForZClass(h.jzClass);
s.files = s.files.filter((v)=>v!==h.file);
- if( 0===--s.refc ){
+ if( --s.refc<=0 && s.isTransient ){
const other = h.file.$isJournal
? h.jzClass.replace(cache.rxJournalSuffix,'')
: h.jzClass+'-journal';
}
}/*native method overrides*/
+ /**
+ Clears all storage used by the kvvfs DB backend, deleting any
+ DB(s) stored there.
+
+ Its argument must be the name of a kvvfs storage object:
+
+ - 'session'
+ - 'local'
+ - An empty string means both of 'local' and 'session' storage.
+ - A transient kvvfs storage object name.
+
+ In the first two cases, only sessionStorage resp. localStorage is
+ cleared. If passed an an empty string (the default) then its
+ behavior depends on:
+
+ - If called in the main thread an empty stream means to act on
+ both localStorage and sessionStorage. Only storage keys which
+ match the pattern used by kvvfs are cleared: any other
+ client-side data are retained. Version 1 of this API always
+ behaves this way. (This is backwards-compatibility behavior to
+ account for an admitted misuse of "".)
+
+ - If called from a thread where globalThis.Storage is not available
+ then an empty string is considered to be a legal storage unit name.
+
+ Returns the number of entries cleared.
+
+ As of kvvfs version 2:
+
+ This API is available in Worker threads but does not have access
+ to localStorage or sessionStorage in them. Prior versions did not
+ include this API in Worker threads.
+
+ Differences in this function in version 2:
+
+ - It accepts an arbitrary storage name. In v1 this was a silent
+ no-op for any names other than ('local','session','').
+
+ - In Worker threads, an empty string argument is treated as a
+ storage unit name.
+ */
+ capi.sqlite3_js_kvvfs_clear = function(which=""){
+ let rc = 0;
+ const store = kvvfsWhich(which);
+ const keyPrefix = store.prefix;
+ /**
+ Historically this code had no way to check whether the storage
+ was in use before wiping it, so could not error in that
+ case. Whether or not it should, now that it can (as of late
+ 2025-11), is TBD.
+ */
+ store.stores.forEach((s)=>{
+ const toRm = [] /* keys to remove */;
+ let i, n = s.length;
+ //debug("kvvfs_clear",store,s);
+ for( i = 0; i < n; ++i ){
+ const k = s.key(i);
+ //debug("kvvfs_clear ?",k);
+ if(!keyPrefix || k.startsWith(keyPrefix)) toRm.push(k);
+ }
+ toRm.forEach((kk)=>s.removeItem(kk));
+ rc += toRm.length;
+ });
+ //if( store.cts ) alertFilesToReload(store.cts);
+ return rc;
+ };
+
+ /**
+ This routine guesses the approximate amount of
+ 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
+ the precise algorithms for determining those limits are
+ unspecified and may include per-entry overhead invisible to
+ clients.
+ */
+ capi.sqlite3_js_kvvfs_size = function(which=""){
+ let sz = 0;
+ const store = kvvfsWhich(which);
+ //warn("kvvfs_size storage",store);
+ store?.stores?.forEach?.((s)=>{
+ //warn("kvvfs_size backend",s);
+ let i;
+ for(i = 0; i < s.length; ++i){
+ const k = s.key(i);
+ if(!store.prefix || k.startsWith(store.prefix)){
+ sz += k.length;
+ sz += s.getItem(k).length;
+ }
+ }
+ });
+ return sz * 2 /* because JS uses 2-byte char encoding */;
+ };
/**
Copies the entire contents of the given transient storage object
util.toss3(capi.SQLITE_NOTFOUND,
"There is no kvvfs storage named",storageName);
}
- debug("store to export=",store);
+ //debug("store to export=",store);
const s = store.storage;
const rc = Object.assign(Object.create(null),{
name: store.jzClass,
"Cowardly refusing to create storage with a",
"'-journal' suffix.");
}
- store = createStorageObj(exp.name);
+ store = newStorageObj(exp.name);
cache.storagePool[exp.name] =
cache.storagePool[exp.name+'-journal'] = store;
//warn("Installing new storage:",store);
filter.test(error.message) passes. If it's a function, the test
passes if filter(error) returns truthy. If it's a string, the
test passes if the filter matches the exception message
- precisely. In all other cases the test fails, throwing an
- Error.
+ precisely. If filter is a number then it is compared against
+ the resultCode property of the exception. In all other cases
+ the test fails, throwing an Error.
If it throws, msg is used as the error report unless it's falsy,
in which case a default is used.
if(filter instanceof RegExp) pass = filter.test(err.message);
else if(filter instanceof Function) pass = filter(err);
else if('string' === typeof filter) pass = (err.message === filter);
+ else if('number' === typeof filter) pass = (err.resultCode === filter);
if(!pass){
+ console.error("Filter",filter,"rejected exception",err);
throw new Error(msg || ("Filter rejected this exception: <<"+err.message+">>"));
}
return this;
test: function(sqlite3){
const filename = 'localThread' /* preinstalled instance */;
const JDb = sqlite3.oo1.JsStorageDb;
+ const DB = sqlite3.oo1.DB;
JDb.clearStorage(filename);
let db = new JDb(filename);
const sqlSetup = [
console.debug("kvvfs to Object:",exp);
close();
- db = new JDb('new-storage');
+ const dbFileRaw = 'file:new-storage?vfs=kvvfs&transient=1';
+ db = new DB(dbFileRaw);
db.exec(sqlSetup);
+ const dbFilename = db.dbFilename();
+ console.warn("db.dbFilename() =",dbFilename);
T.assert(3 === db.selectValue('select count(*) from kvvfs'));
- console.debug("kvvfs to Object:",exportDb(db.filename));
- const n = db.storageSize();
+ console.debug("kvvfs to Object:",exportDb(dbFilename));
+ const n = capi.sqlite3_js_kvvfs_size( dbFilename );
T.assert( n>0, "Db size count failed" );
if( 1 ){
// Concurrent open of that same name uses the same storage
- const x = new JDb(db.filename);
+ const x = new JDb(dbFilename);
T.assert(3 === db.selectValue('select count(*) from kvvfs'));
x.close();
}
// disappears...
T.mustThrowMatching(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');
+ went to 0, because of its 'transient' flag. By default
+ the objects are retained, like a filesystem would. */
+ let ddb = new JDb(dbFilename);
try{ddb.selectValue('select a from kvvfs')}
finally{ddb.close()}
- }, /.*no such table: kvvfs.*/);
+ }, /no such table: kvvfs/);
}finally{
if( db ) db.close();
}
//predicate: ()=>false,
test: function(sqlite3){
const filename = 'my';
+ const DB = sqlite3.oo1.DB;
const JDb = sqlite3.oo1.JsStorageDb;
let db;
let duo;
const sqlCount = 'select count(*) from kvvfs';
try {
const exportDb = capi.sqlite3_js_kvvfs_export_storage;
- db = new JDb(filename);
- db.clearStorage(/*must not throw*/);
+ const dbFileRaw = 'file:'+filename+'?vfs=kvvfs&transient=1';
+ db = new DB(dbFileRaw);
+ capi.sqlite3_js_kvvfs_clear(filename);
db.exec(sqlSetup);
T.assert(3 === db.selectValue(sqlCount));
duo = new JDb(filename);
T.mustThrowMatching(()=>importDb(exp,true), /.*in use.*/);
duo.close();
- importDb(exp);
+ importDb(exp, true);
duo = new JDb(filename);
T.assert(6 === duo.selectValue(sqlCount));
duo.close();