/**
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.
+ of the kvvfs. Storage is a native class and its 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.
/* Start off with mappings for well-known names. */
localThread: {
refc: 3/*never reaches 0*/,
- s: new TransientStorage
+ s: new TransientStorage,
+ files: [/*KVVfsFile instances currently using this storage*/]
}
});
- if( globalThis.localStorage ){
- cache.jzClassToStorage.local =
- {
- refc: 3/*never reaches 0*/,
- s: globalThis.localStorage,
- /* If useFullZClass is true, kvvfs storage keys are in
- the form kvvfs-{zClass}-*, else they lack the "-{zClass}"
- part. local/session storage must use the long form for
- backwards compatibility. */
- useFullZClass: true
- };
+ if( globalThis.localStorage instanceof globalThis.Storage ){
+ cache.jzClassToStorage.local = {
+ refc: 3/*never reaches 0*/,
+ s: 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: []
+ };
}
- if( globalThis.sessionStorage ){
- cache.jzClassToStorage.session =
- {
- refc: 3/*never reaches 0*/,
- s: globalThis.sessionStorage,
- useFullZClass: true
- }
+ if( globalThis.sessionStorage instanceof globalThis.Storage ){
+ cache.jzClassToStorage.session = {
+ refc: 3/*never reaches 0*/,
+ s: globalThis.sessionStorage,
+ keyPrefix: "kvvfs-session-",
+ files: []
+ }
}
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. */
+ their Storage-ish object, named "KEYPREFIXjrnl" (see above
+ re. KEYPREFIX). We always map the db and its journal to the
+ same Storage object. */
const orig = cache.jzClassToStorage[k];
orig.jzClass = k;
cache.jzClassToStorage[k+'-journal'] = orig;
}
+ 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-per-db-name namespace). We retain this prefix in 'local'
+ and 'session' storage for backwards compatibility but elide them
+ from "v2" transient 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
It returns an object in the form:
.prefix = the key prefix for this storage. Typically
- ("kvvfs-"+which) for persistent storage and "kvvfs-" for
- transient. (The former is historical, retained for backwards
- compatibility.)
+ ("kvvfs-"+which) for local/sessionStorage and "" for transient
+ storage. (The former is historical, retained for backwards
+ compatibility.) If which is falsy then the prefix is "kvvfs-" for
+ backwards compatibility (it will match keys for both local- and
+ sessionStorage, but not transient storage).
.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.
+ from the main thread) localStorage and sessionStorage. It will be
+ empty if no mapping is found or those objects are not available
+ in the current environment (e.g. a worker thread).
*/
const kvvfsWhich = function callee(which){
const rc = Object.assign(Object.create(null),{
- prefix: 'kvvfs-' + which,
stores: []
});
if( which ){
const s = cache.jzClassToStorage[which];
if( s ){
//debug("kvvfsWhich",s.jzClass,rc.prefix, s.s);
- if( !s.useFullZClass ){
- rc.prefix = 'kvvfs-';
- }
+ rc.prefix = s.keyPrefix ?? '';
rc.stores.push(s.s);
+ }else{
+ 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);
}
capi.sqlite3_js_kvvfs_clear = function(which=""){
let rc = 0;
const store = kvvfsWhich(which);
+ const keyPrefix = store.prefix;
store.stores.forEach((s)=>{
const toRm = [] /* keys to remove */;
let i, n = s.length;
for( i = 0; i < n; ++i ){
const k = s.key(i);
//debug("kvvfs_clear ?",k);
- if(k.startsWith(store.prefix)) toRm.push(k);
+ if(!keyPrefix || k.startsWith(keyPrefix)) toRm.push(k);
}
toRm.forEach((kk)=>s.removeItem(kk));
rc += toRm.length;
const keyForStorage = (store, zClass, zKey)=>{
//debug("keyForStorage(",store, wasm.cstrToJs(zClass), wasm.cstrToJs(zKey));
return wasm.exports.sqlite3__wasm_kvvfsMakeKeyOnPstack(
- store.useFullZClass ? zClass : null, zKey
+ store.keyPrefix ? zClass : null, zKey
);
};
+ /* We use this for the many small key allocations we need.
+ TODO: prealloc a buffer on demand for this. We know its
+ max size from kvvfsMethods.$nKeySize. */
const pstack = wasm.pstack;
+ /**
+ Returns the storage object mapped to the given C-string
+ zClass.
+ */
const storageForZClass =
(zClass)=>cache.jzClassToStorage[wasm.cstrToJs(zClass)];
- const pFileHandles = new Map(
- /* sqlite3_file pointers => objects, each of which has:
- .s = Storage object
- .f = KVVfsFile instance
- .n = JS-string form of f.$zClass
- */
- );
+ /**
+ sqlite3_file pointers => objects, each of which has:
+
+ .s = Storage object
+ .f = KVVfsFile instance
+ .n = JS-string form of f.$zClass
+ */
+ const pFileHandles = new Map();
if( sqlite3.__isUnderTest ){
sqlite3.kvvfsStuff = {
//debug("xOpen", jzClass, s);
if( s ){
++s.refc;
+ s.files.push(f);
}else{
/* TODO: a url flag which tells it to keep the storage
around forever so that future xOpen()s get the same
will follow soon enough and bump the
refcount. If we start at 2 here, that
pending open will increment it again. */,
- s: new TransientStorage
+ s: new TransientStorage,
+ files: [f]
});
debug("xOpen installed storage handles [",
jzClass, other,"]", s);
If we poke 0 then no problem... except that
xAccess() doesn't report the truth. Same effect
if we move that to the native impl
- os_kv.c:kvvfsAccess(). */
+ os_kv.c's kvvfsAccess(). */
);
}
debug("xAccess", jzName, drc, pResOut, wasm.peek32(pResOut));
pFileHandles.delete(pFile);
const s = cache.jzClassToStorage[h.n];
util.assert(s, "Missing jzClassToStorage["+h.n+"]");
+ util.assert(h.f, "Missing KVVfsFile handle for "+h.n);
+ s.files = s.files.filter((v)=>v!==h.f);
if( 0===--s.refc ){
const other = h.f.$isJournal
? h.n.replace(cache.rxJournalSuffix,'')
debug("cleaning up storage handles [", h.n, other,"]",s);
delete cache.jzClassToStorage[h.n];
delete cache.jzClassToStorage[other];
- delete s.s;
- delete s.refc;
+ if( !sqlite3.__isUnderTest ){
+ delete s.s;
+ delete s.refc;
+ }
}
originalIoMethods(h.f).xClose(pFile);
h.f.dispose();
- "name": the name of the db. This is 'local' or 'session' for
localStorage resp. sessionStorage, and an arbitrary name for
- transient storage.
+ 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.
-
- - "data": An object holding the raw encoded state. It has the
- following properties:
+ epoch milliseconds.
- - "kvvfs[-X]-sz" = the decoded size of the db. Its encoded
- size may vary wildly from that in either direction,
- depending largely on the ratio of empty space to data.
+ - "size": the unencoded db size.
- - "kvvfs[-X]-jrnl" = if includeJournal is true and the db has
- a journal, it is stored in this record. If is has no
- journal, or includeJournal is false, this key is not set.
+ - "journal": if includeJournal is true and this db has a
+ journal, it is stored as a string here, otherwise this property
+ is not set.
- - "kvvfs[-X]-###" = one encoded page of the db, with ###
- corresponding to the page number.
+ - "pages": An array holddig the object holding the raw encoded
+ db pages in their proper order.
- The [-X] parts are only set for localStorage and sessionStorage
- back-ends and the X of each is 'local' or 'session'. That is:
- the keys contain the storage back-end's name because of how the
- underlying native VFS works (each key goes in its own file so
- it must be distinct per storage name). That part is retained
- here for backwards compatibility - transient storage objects
- elide that part.
+ 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.
+ interface - it is simply passed on as-is. Interested parties
+ are directed to src/os_kv.c in the SQLite source tree.
- Throws if this db is not opened.
+ 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=false){
+ jdb.prototype.exportToObject = function(includeJournal=true){
this.affirmOpen();
const store = cache.jzClassToStorage[this.affirmOpen().filename];
- const rx = includeJournal ? undefined : /^kvvfs(-(local|session))?-jrnl$/;
- if( store ){
- const s = store.s;
- const rc = Object.assign(Object.create(null),{
- name: this.filename,
- timestamp: (new Date()).valueOf(),
- data:Object.create(null)
- });
- let i = 0, n = s.length;
- for( ; i < n; ++i ){
- const k = s.key(i);
- if( !rx || !rx.test(k) ){
- rc.data[k] = s.getItem(k);
+ const rxTail = /^kvvfs(-(local|session))?-(\w+)/;
+ if( !store ){
+ util.toss3(capi.SQLITE_ERROR,"kvvfs db '",
+ this.filename,"' has no storage object.");
+ }
+ const s = store.s;
+ const rc = Object.assign(Object.create(null),{
+ name: this.filename,
+ timestamp: (new Date()).valueOf(),
+ pages: []
+ });
+ const pages = Object.create(null);
+ const keyPrefix = kvvfsKeyPrefix(rc.name);
+ let i = 0, n = s.length;
+ for( ; i < n; ++i ){
+ const k = s.key(i);
+ if( !keyPrefix || k.startsWith(keyPrefix) ){
+ const m = rxTail.exec(k);
+ let kk = m[3];
+ 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;
}
}
- return rc;
}
+ /* 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;
+ };
+
+ /**
+ 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.
+
+ Throws on error. Returns this object on success.
+
+ FIXMEs:
+
+ - We need the page size in the export so that we can reset it,
+ if needed, on the import.
+
+ - We need to ensure that the native-size KVVfsFile::szDb and
+ KVVfsFile::szPage get set to -1 for all open instances so that
+ they re-read the db size.
+ */
+ jdb.prototype.importFromObject = function(exp){
+ this.affirmOpen();
+ if( !exp?.timestamp
+ || !exp.name
+ || undefined===exp.size
+ || !Array.isArray(exp.pages) ){
+ util.toss3(capi.SQLITE_MISUSE, "Malformed export object.");
+ }
+ warn("importFromObject() is incomplete");
+ this.clearStorage();
+ const store = kvvfsWhich(this.filename);
+ util.assert(store?.s, "Somehow missing a storage object for",this.filename);
+ const keyPrefix = kvvfsKeyPrefix(this.filename);
+ try{
+ 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)));
+ }catch(e){
+ this.clearStorage();
+ throw e;
+ }
+ return this;
};
if( sqlite3.__isUnderTest ){