if( !hop(this.#map, k) ){
this.#keys = null;
}
- this.#map[k] = v;
+ this.#map[k] = ''+v;
}
removeItem(k){
}
}/*KVVfsStorage*/;
+ /** True if v is one of the special persistant Storage objects. */
+ const kvvfsIsPersistentName = (v)=>'local'===v || 'session'===v;
+
+ /**
+ Keys in kvvfs have a prefix of "kvvfs-NAME-", where NAME is the
+ db name. This key is redundant in JS but it's how kvvfs works (it
+ saves each key to a separate file, so needs a distinct namespace
+ per data source name). We retain this prefix in 'local' and
+ 'session' storage for backwards compatibility but elide them from
+ "v2" storage, where they're superfluous.
+ */
+ const kvvfsKeyPrefix = (v)=>kvvfsIsPersistentName(v) ? 'kvvfs-'+v+'-' : '';
+
+ /**
+ Create a new instance of the objects which go into
+ cache.storagePool.
+ */
+ const createStorageObj = (name,storage)=>Object.assign(Object.create(null),{
+ jzClass: name,
+ refc: 1,
+ storage: storage || new KVVfsStorage,
+ /* This 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.
+
+ 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*/]
+ });
+
+
/**
Map of JS-stringified KVVfsFile::zClass names to
reference-counted Storage objects. These objects are created in
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),{
- /* Start off with mappings for well-known names. */
- localThread: {
- refc: 3/*never reaches 0*/,
- storage: new KVVfsStorage,
- files: [/*KVVfsFile instances currently using this storage*/]
- }
+ localThread: createStorageObj('localThread')
});
+
if( globalThis.localStorage instanceof globalThis.Storage ){
- cache.storagePool.local = {
- refc: 3/*never reaches 0*/,
- storage: globalThis.localStorage,
- /* This 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.
-
- This prefix mirrors the one generated by os_kv.c's
- kvrecordMakeKey() and must stay in sync with that one.
- */
- keyPrefix: "kvvfs-local-",
- files: []
- };
+ cache.storagePool.local = createStorageObj('local');
}
if( globalThis.sessionStorage instanceof globalThis.Storage ){
- cache.storagePool.session = {
- refc: 3/*never reaches 0*/,
- storage: globalThis.sessionStorage,
- keyPrefix: "kvvfs-session-",
- files: []
- }
+ cache.storagePool.session = createStorageObj('session');
}
+
for(const k of Object.keys(cache.storagePool)){
/* Journals in kvvfs are are stored as individual records within
- their Storage-ish object, named "KEYPREFIXjrnl" (see above
- re. KEYPREFIX). We always map the db and its journal to the
- same Storage object. */
+ their Storage-ish object, named "{storage.keyPrefix}jrnl". We
+ always map the db and its journal to the same Storage
+ object. */
const orig = cache.storagePool[k];
- orig.jzClass = k;
cache.storagePool[k+'-journal'] = orig;
}
+
/**
Returns the storage object mapped to the given string zClass
(C-string pointer or JS string).
? cache.storagePool[zClass]
: cache.storagePool[wasm.cstrToJs(zClass)];
- /** True if v is one of the special persistant Storage objects. */
- const kvvfsIsPersistentName = (v)=>'local'===v || 'session'===v;
-
- /**
- Keys in kvvfs have a prefix of "kvvfs-NAME-", where NAME is the
- db name. This key is redundant in JS but it's how kvvfs works (it
- saves each key to a separate file, so needs a distinct namespace
- per data source name). We retain this prefix in 'local' and
- 'session' storage for backwards compatibility but elide them from
- "v2" storage, where they're superfluous.
- */
- const kvvfsKeyPrefix = (v)=>kvvfsIsPersistentName(v) ? 'kvvfs-'+v+'-' : '';
-
/**
Internal helper for sqlite3_js_kvvfs_clear() and friends. Its
argument should be one of ('local','session',"") or the name of
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;
: jzClass + '-journal';
s = cache.storagePool[jzClass]
= cache.storagePool[other]
- = Object.assign(Object.create(null),{
- jzClass,
- 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. */,
- storage: new KVVfsStorage,
- keyPrefix: '',
- files: [f]
- });
+ = createStorageObj(jzClass);
+ s.files.push(f);
debug("xOpen installed storage handles [",
jzClass, other,"]", s);
}
}
}/*native method overrides*/
+
+ /**
+ Copies the entire contents of the given transient storage object
+ into a JSON-friendly form. The returned object is structured as
+ follows...
+
+ - "name": the name of the storage. This is 'local' or 'session'
+ for localStorage resp. sessionStorage, and an arbitrary name for
+ transient storage. This propery may be changed before passing
+ this object to sqlite3_js_kvvfs_import_storage() in order to
+ import into a different storage object.
+
+ - "timestamp": the time this function was called, in Unix
+ epoch milliseconds.
+
+ - "size": the unencoded db size.
+
+ - "journal": if includeJournal is true and this db has a
+ journal, it is stored as a string here, otherwise this property
+ is not set.
+
+ - "pages": An array holddig the object holding the raw encoded
+ db pages in their proper order.
+
+ Throws if this db is not opened.
+
+ The encoding of the underlying database is not part of this
+ interface - it is simply passed on as-is. Interested parties
+ are directed to src/os_kv.c in the SQLite source tree.
+
+ Added in version 3.?? (tenatively 3.52).
+ */
+ capi.sqlite3_js_kvvfs_export_storage = function(storageName,includeJournal=true){
+ const store = storageForZClass(storageName);
+ if( !store ){
+ util.toss3(capi.SQLITE_NOTFOUND,
+ "There is no kvvfs storage named",storageName);
+ }
+ debug("store to export=",store);
+ const s = store.storage;
+ const rc = Object.assign(Object.create(null),{
+ name: store.jzClass,
+ timestamp: Date.now(),
+ pages: []
+ });
+ const pages = Object.create(null);
+ const keyPrefix = kvvfsKeyPrefix(rc.name);
+ const rxTail = keyPrefix
+ ? /^kvvfs-[^-]+-(\w+)/ /* X... part of kvvfs-NAME-X... */
+ : undefined;
+ let i = 0, n = s.length;
+ for( ; i < n; ++i ){
+ const k = s.key(i);
+ if( !keyPrefix || k.startsWith(keyPrefix) ){
+ let kk = (keyPrefix ? rxTail.exec(k) : undefined)?.[1] ?? k;
+ switch( kk ){
+ case 'jrnl':
+ if( includeJournal ) rc.journal = s.getItem(k);
+ break;
+ case 'sz':
+ rc.size = +s.getItem(k);
+ break;
+ default:
+ kk = +kk /* coerce to number */;
+ if( !util.isInt32(kk) || kk<=0 ){
+ util.toss3(capi.SQLITE_RANGE, "Malformed kvvfs key: "+k);
+ }
+ pages[kk] = s.getItem(k);
+ break;
+ }
+ }
+ }
+ /* Now sort the page numbers and move them into an array. In JS
+ property keys are always strings, so we have to coerce them to
+ numbers so we can get them sorted properly for the array. */
+ Object.keys(pages).map((v)=>+v).sort().forEach(
+ (v)=>rc.pages.push(pages[v])
+ );
+ return rc;
+ }/* capi.sqlite3_js_kvvfs_export_storage */;
+
+ /**
+ INCOMPLETE. This interface is subject to change.
+
+ The counterpart of sqlite3_js_kvvfs_export_storage(). Its
+ argument must be the result of that function().
+
+ This either replaces the contents of an existing transient
+ storage object or installs one named exp.name, setting
+ the storage's db contents to that of the exp object.
+
+ Throws on error. Error conditions include:
+
+ - The give storage object is currently opened by any db.
+ Performing this page-by-page import would invoke undefined
+ behavior on them.
+
+ - Malformed input object.
+
+ If it throws after starting the import then it clears the
+ storage before returning, to avoid leaving the db in an
+ undefined state. It may throw for any of the above-listed
+ conditions before reaching that step, in which case the db is
+ not modified.
+ */
+ capi.sqlite3_js_kvvfs_import_storage = function(exp, overwrite=false){
+ if( !exp?.timestamp
+ || !exp.name
+ || undefined===exp.size
+ || exp.size<0 || exp.size>=0x7fffffff
+ || !Array.isArray(exp.pages) ){
+ util.toss3(capi.SQLITE_MISUSE, "Malformed export object.");
+ }
+ //warn("importFromObject() is incomplete");
+ let store = storageForZClass(exp.name);
+ if( store ){
+ if( !overwrite ){
+ //warn("Storage exists:",arguments,store);
+ util.toss3(capi.SQLITE_ACCESS,
+ "Storage '"+exp.name+"' already exists and",
+ "overwrite was not specified.");
+ }else if( !store.files || !store.jzClass ){
+ util.toss3(capi.SQLITE_ERROR,
+ "Internal storage object", exp.name,"seems to be malformed.");
+ }else if( store.files.length ){
+ util.toss3(capi.SQLITE_IOERR_ACCESS,
+ "Cannot import db storage while it is in use.");
+ }
+ capi.sqlite3_js_kvvfs_clear(exp.name);
+ }else{
+ if( cache.rxJournalSuffix.test(exp.name) ){
+ /* This isn't actually a problem, but the public API does not
+ specifically expose the '-journal' name of the storage so
+ exporting it "shouldn't happen." */
+ util.toss3(capi.SQLITE_MISUSE,
+ "Cowardly refusing to create storage with a",
+ "'-journal' suffix.");
+ }
+ store = createStorageObj(exp.name);
+ cache.storagePool[exp.name] =
+ cache.storagePool[exp.name+'-journal'] = store;
+ //warn("Installing new storage:",store);
+ }
+ //debug("Importing store",store.cts.files.length, store);
+ //debug("object to import:",exp);
+ const keyPrefix = kvvfsKeyPrefix(exp.name);
+ try{
+ /* Force the native KVVfsFile instances to re-read the db
+ and page size. */;
+ const s = store.storage;
+ s.setItem(keyPrefix+'sz', exp.size);
+ if( exp.journal ) s.setItem(keyPrefix+'jrnl', exp.journal);
+ exp.pages.forEach((v,ndx)=>s.setItem(keyPrefix+(ndx+1), v));
+ s.getItem("")/*kludge: for KVVfsStorage to reset its keys*/;
+ }catch(e){
+ capi.sqlite3_js_kvvfs_clear(exp.name);
+ throw e;
+ }
+ return this;
+ };
+
if(sqlite3?.oo1?.DB){
/**
Functionally equivalent to DB(storageName,'c','kvvfs') except
return jdb.storageSize(this.affirmOpen().filename);
};
- /**
- Copies the entire contents of this db into a JSON-friendly
- form. The returned object is structured as follows...
-
- - "name": the name of the db. This is 'local' or 'session' for
- localStorage resp. sessionStorage, and an arbitrary name for
- transient storage. This propery may be changed before passing
- this object to importFromObject() in order to import into a
- different storage object.
-
- - "timestamp": the time this function was called, in Unix
- epoch milliseconds.
-
- - "size": the unencoded db size.
-
- - "journal": if includeJournal is true and this db has a
- journal, it is stored as a string here, otherwise this property
- is not set.
-
- - "pages": An array holddig the object holding the raw encoded
- db pages in their proper order.
-
- Throws if this db is not opened.
-
- The encoding of the underlying database is not part of this
- interface - it is simply passed on as-is. Interested parties
- are directed to src/os_kv.c in the SQLite source tree.
-
- Trivia: for non-trivial databases, this object's JSON encoding
- will be slightly smaller that the full db, as this
- representation strips out some repetitive parts.
-
- Added in version 3.?? (tenatively 3.52).
- */
- jdb.prototype.exportToObject = function(includeJournal=true){
- this.affirmOpen();
- const store = storageForZClass(this.affirmOpen().filename);
- if( !store ){
- util.toss3(capi.SQLITE_ERROR,"kvvfs db '",
- this.filename,"' has no storage object.");
- }
- debug("store=",store);
- const s = store.storage;
- const rc = Object.assign(Object.create(null),{
- name: this.filename,
- timestamp: Date.now(),
- pages: []
- });
- const pages = Object.create(null);
- const keyPrefix = kvvfsKeyPrefix(rc.name);
- const rxTail = keyPrefix
- ? /^kvvfs-[^-]+-(\w+)/ /* X... part of kvvfs-NAME-X... */
- : undefined;
- let i = 0, n = s.length;
- for( ; i < n; ++i ){
- const k = s.key(i);
- if( !keyPrefix || k.startsWith(keyPrefix) ){
- let kk = (keyPrefix ? rxTail.exec(k) : undefined)?.[1] ?? k;
- switch( kk ){
- case 'jrnl':
- if( includeJournal ) rc.journal = s.getItem(k);
- break;
- case 'sz':
- rc.size = +s.getItem(k);
- break;
- default:
- kk = +kk /* coerce to number */;
- if( !util.isInt32(kk) || kk<=0 ){
- util.toss3(capi.SQLITE_RANGE, "Malformed kvvfs key: "+k);
- }
- pages[kk] = s.getItem(k);
- break;
- }
- }
- }
- /* Now sort the page numbers and move them into an array. In JS
- property keys are always strings, so we have to coerce them to
- numbers so we can get them sorted properly for the array. */
- Object.keys(pages).map((v)=>+v).sort().forEach(
- (v)=>rc.pages.push(pages[v])
- );
- return rc;
- };
-
- /**
- Does not yet work: it imports the db but the handle cannot
- read from the modified-underneath-it storage yet. We have to
- figure out how to get the file to re-read the db size.
-
- The counterpart of exportToObject(). Its argument must be
- the result of exportToObject().
-
- This necessarily wipes out the whole database storage, so
- invoking this while the db is in active use invokes undefined
- behavior.
-
- Returns this object on success. Throws on error. Error
- conditions include:
-
- - This db is closed.
-
- - A transaction is active.
-
- - If any statements are open.
-
- - Malformed input object.
-
- - Other handles to the same storage object are opened.
- Performing this page-by-page import would invoke undefined
- behavior on them.
-
- Those are the error case it can easily cover. The room for
- undefined behavior in wiping a db's storage out from under it
- is a whole other potential minefield.
-
- If it throws after starting the input then it clears the
- storage before returning, to avoid leaving the db in an
- undefined state. It may throw for any of the above-listed
- conditions before reaching that step, in which case the db is
- not modified.
- */
- jdb.prototype.importFromObject = function(exp){
- this.affirmOpen();
- if( !exp?.timestamp
- || !exp.name
- || undefined===exp.size
- || exp.size<0 || exp.size>=0x7fffffff
- || !Array.isArray(exp.pages) ){
- util.toss3(capi.SQLITE_MISUSE, "Malformed export object.");
- }else if( capi.sqlite3_next_stmt(this.pointer, null) ){
- util.toss3(capi.SQLITE_MISUSE,
- "Cannot import when statements are active.");
- }else if( capi.sqlite3_txn_state(this.pointer, null)>0 ){
- util.toss3(capi.SQLITE_MISUSE,
- "Cannot import the db while a transaction is active.");
- }
- //warn("importFromObject() is incomplete");
- const store = kvvfsWhich(this.filename);
- if( !store?.cts ){
- util.toss3(capi.SQLITE_ERROR,
- "Somehow missing a storage object for", this.filename);
- }else if( store.cts.files.length>1 ){
- util.toss3(capi.SQLITE_IOERR_ACCESS,
- "Cannot import a db when multiple handles to it",
- "are opened.");
- }
- //debug("Importing store",store.cts.files.length, store);
- //debug("object to import:",exp);
- const keyPrefix = kvvfsKeyPrefix(this.filename);
- this.clearStorage();
- try{
- /* Force the native KVVfsFile instances to re-read the db
- and page size. */;
- const s = store.cts.storage;
- s.setItem(keyPrefix+'sz', exp.size);
- if( exp.journal ) s.setItem(keyPrefix+'jrnl', exp.journal);
- exp.pages.forEach((v,ndx)=>s.setItem(keyPrefix+(ndx+1), v));
- //debug("imported:",this.exportToObject());
- //this.exec("pragma page_size");
- for( const f of store.cts.files ){
- f.$szDb = exp.size;
- f.$szPage = -1;
- }
- }catch(e){
- this.clearStorage();
- throw e;
- }
- return this;
- };
-
if( sqlite3.__isUnderTest ){
jdb.test = {
kvvfsWhich,