190034d268
FossilOrigin-Name: a396d7c54c5f251daaaac1e68321c2a54d3b6969599027c29b7ca7abe7fb8c4e
2099 lines
73 KiB
JavaScript
2099 lines
73 KiB
JavaScript
/*
|
|
2025-11-21
|
|
|
|
The author disclaims copyright to this source code. In place of a
|
|
legal notice, here is a blessing:
|
|
|
|
* May you do good and not evil.
|
|
* May you find forgiveness for yourself and forgive others.
|
|
* May you share freely, never taking more than you give.
|
|
|
|
***********************************************************************
|
|
|
|
This file houses the "kvvfs" pieces of the SQLite3 JS API. Most of
|
|
kvvfs is implemented in src/os_kv.c and exposed/extended for use
|
|
here via sqlite3-wasm.c.
|
|
|
|
Main project home page: https://sqlite.org
|
|
|
|
Documentation home page: https://sqlite.org/wasm
|
|
*/
|
|
//#if omit-kvvfs
|
|
globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
|
/* These are JS plumbing, not part of the public API */
|
|
delete sqlite3.capi.sqlite3_kvvfs_methods;
|
|
delete sqlite3.capi.KVVfsFile;
|
|
}
|
|
//#else
|
|
//#@ policy error
|
|
//#savepoint begin
|
|
//#define kvvfs-v2-added-in "3.52.0"
|
|
|
|
/**
|
|
kvvfs - the Key/Value VFS - is an SQLite3 VFS which delegates
|
|
storage of its pages and metadata to a key-value store.
|
|
|
|
It was conceived in order to support JS's localStorage and
|
|
sessionStorage objects. Its native implementation uses files as
|
|
key/value storage (one file per record) but the JS implementation
|
|
replaces a few methods so that it can use the aforementioned
|
|
objects as storage.
|
|
|
|
It uses a bespoke ASCII encoding to store each db page as a
|
|
separate record and stores some metadata, like the db's unencoded
|
|
size and its journal, as individual records.
|
|
|
|
kvvfs is significantly less efficient than a plain in-memory db but
|
|
it also, as a side effect of its design, offers a JSON-friendly
|
|
interchange format for exporting and importing databases.
|
|
|
|
kvvfs is _not_ designed for heavy db loads. It is relatively
|
|
malloc()-heavy, having to de/allocate frequently, and it
|
|
spends much of its time converting the raw db pages into and out of
|
|
an ASCII encoding.
|
|
|
|
But it _does_ work and is "performant enough" for db work of the
|
|
scale of a db which will fit within sessionStorage or localStorage
|
|
(just 2-3mb).
|
|
|
|
"Version 2" extends it to support using Storage-like objects as
|
|
backing storage, Storage being the JS class which localStorage and
|
|
sessionStorage both derive from. This essentially moves the backing
|
|
store from whatever localStorage and sessionStorage use to an
|
|
in-memory object.
|
|
|
|
This effort is primarily a stepping stone towards eliminating, if
|
|
it proves possible, the POSIX I/O API dependencies in SQLite's WASM
|
|
builds. That is: if this VFS works properly, it can be set as the
|
|
default VFS and we can eliminate the "unix" VFS from the JS/WASM
|
|
builds (as opposed to server-wise/WASI builds). That still, as of
|
|
2025-11-23, a ways away, but it's the main driver for version 2 of
|
|
kvvfs.
|
|
|
|
Version 2 remains compatible with version 1 databases and always
|
|
writes localStorage/sessionStorage metadata in the v1 format, so
|
|
such dbs can be manipulated freely by either version. For transient
|
|
storage objects (new in version 2), the format of its record keys
|
|
is simpified, requiring less space than v1 keys by eliding
|
|
redundant (in this context) info from the keys.
|
|
|
|
Another benefit of v2 is its ability to export dbs into a
|
|
JSON-friendly (but not human-friendly) format.
|
|
|
|
A potential, as-yet-unproven, benefit, would be the ability to plug
|
|
arbitrary Storage-compatible objects in so that clients could,
|
|
e.g. asynchronously post updates to db pages to some back-end for
|
|
backups.
|
|
*/
|
|
globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
|
if( sqlite3.config.disable?.vfs?.kvvfs ){
|
|
return;
|
|
}
|
|
'use strict';
|
|
const capi = sqlite3.capi,
|
|
sqlite3_kvvfs_methods = capi.sqlite3_kvvfs_methods,
|
|
KVVfsFile = capi.KVVfsFile,
|
|
pKvvfs = sqlite3.capi.sqlite3_vfs_find("kvvfs")
|
|
|
|
/* These are JS plumbing, not part of the public API */
|
|
delete capi.sqlite3_kvvfs_methods;
|
|
delete capi.KVVfsFile;
|
|
|
|
if( !pKvvfs ) return /* nothing to do */;
|
|
if( 0 ){
|
|
/* This working would be our proverbial holy grail, in that it
|
|
would allow us to eliminate the current default VFS, which
|
|
relies on POSIX I/O APIs. Eliminating that dependency would get
|
|
us one giant step closer to creating wasi-sdk builds. */
|
|
capi.sqlite3_vfs_register(pKvvfs, 1);
|
|
}
|
|
|
|
const util = sqlite3.util,
|
|
wasm = sqlite3.wasm,
|
|
toss3 = util.toss3,
|
|
hop = (o,k)=>Object.prototype.hasOwnProperty.call(o,k);
|
|
|
|
const kvvfsMethods = new sqlite3_kvvfs_methods(
|
|
/* Wraps the static sqlite3_api_methods singleton */
|
|
wasm.exports.sqlite3__wasm_kvvfs_methods()
|
|
);
|
|
util.assert( 32<=kvvfsMethods.$nKeySize, "unexpected kvvfsMethods.$nKeySize: "+kvvfsMethods.$nKeySize);
|
|
|
|
/**
|
|
Most of the VFS-internal state.
|
|
*/
|
|
const cache = Object.assign(Object.create(null),{
|
|
/** Regex matching journal file names. */
|
|
rxJournalSuffix: /-journal$/,
|
|
/** Frequently-used C-string. */
|
|
zKeyJrnl: wasm.allocCString("jrnl"),
|
|
/** Frequently-used C-string. */
|
|
zKeySz: wasm.allocCString("sz"),
|
|
/**
|
|
The maximum size of a kvvfs record key. It is historically only
|
|
32, a limitation currently retained only because it's convenient to
|
|
do so (the underlying code has outgrown the need for the artifically
|
|
low limit).
|
|
|
|
We cache this value here because the end of this init code will
|
|
dispose of kvvfsMethods, invalidating it.
|
|
*/
|
|
keySize: kvvfsMethods.$nKeySize,
|
|
/**
|
|
WASM heap memory buffers to optimize out some frequent
|
|
allocations.
|
|
*/
|
|
buffer: Object.assign(Object.create(null),{
|
|
/**
|
|
The size of each buffer in this.pool.
|
|
|
|
kvvfsMethods.$nBufferSize is slightly larger than the output
|
|
space needed for a kvvfs-encoded 64kb db page in a worse-cast
|
|
encoding (128kb). It is not suitable for arbitrary buffer
|
|
use, only page de/encoding.
|
|
*/
|
|
n: kvvfsMethods.$nBufferSize,
|
|
/**
|
|
Map of buffer ids to wasm.alloc()'d pointers of size
|
|
this.n. (Re)used by various internals.
|
|
|
|
Buffer ids 0 and 1 are used in the API internals. Other
|
|
names are used in higher-level APIs.
|
|
|
|
See memBuffer() and memBufferFree().
|
|
*/
|
|
pool: Object.create(null)
|
|
})
|
|
});
|
|
|
|
/**
|
|
Returns a (cached) wasm.alloc()'d buffer of cache.buffer.n size,
|
|
throwing on OOM.
|
|
|
|
We leak this one-time alloc because we've no better option.
|
|
sqlite3_vfs does not have a finalizer, so we've no place to hook
|
|
in the cleanup. We "could" extend sqlite3_shutdown() to have a
|
|
cleanup list for stuff like this but that function is never
|
|
used in JS, so it's hardly worth it.
|
|
*/
|
|
cache.memBuffer = (id=0)=>cache.buffer.pool[id] ??= wasm.alloc(cache.buffer.n);
|
|
|
|
/** Frees the buffer with the given id. */
|
|
cache.memBufferFree = (id)=>{
|
|
const b = cache.buffer.pool[id];
|
|
if( b ){
|
|
wasm.dealloc(b);
|
|
delete cache.buffer.pool[id];
|
|
}
|
|
};
|
|
|
|
const noop = ()=>{};
|
|
const debug = sqlite3.__isUnderTest
|
|
? (...args)=>sqlite3.config.debug?.("kvvfs:", ...args)
|
|
: noop;
|
|
const warn = (...args)=>sqlite3.config.warn?.("kvvfs:", ...args);
|
|
const error = (...args)=>sqlite3.config.error?.("kvvfs:", ...args);
|
|
|
|
/**
|
|
Implementation of JS's Storage interface for use as backing store
|
|
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 class implements (only) the
|
|
Storage interface, to make it a drop-in replacement for
|
|
localStorage/sessionStorage. (Any behavioral discrepancies are to
|
|
be considered bugs.)
|
|
|
|
This impl simply proxies a plain, prototype-less Object, suitable
|
|
for JSON-ing.
|
|
|
|
Design note: Storage has a bit of an odd iteration-related
|
|
interface as does not (AFAIK) specify specific behavior regarding
|
|
modification during traversal. Because of that, this class does
|
|
some seemingly unnecessary things with its #keys member, deleting
|
|
and recreating it whenever a property index might be invalidated.
|
|
*/
|
|
class KVVfsStorage {
|
|
#map = Object.create(null);
|
|
#keys = null;
|
|
#size = 0;
|
|
|
|
constructor(){
|
|
this.clear();
|
|
}
|
|
|
|
#getKeys(){
|
|
return this.#keys ??= Object.keys(this.#map);
|
|
}
|
|
|
|
key(n){
|
|
if(n < 0 || n >= this.#size) return null;
|
|
return this.#getKeys()[n];
|
|
}
|
|
|
|
getItem(k){
|
|
return this.#map[k] ?? null;
|
|
}
|
|
|
|
setItem(k,v){
|
|
if( !(k in this.#map) ){
|
|
++this.#size;
|
|
this.#keys = null;
|
|
}
|
|
this.#map[k] = ''+v;
|
|
}
|
|
|
|
removeItem(k){
|
|
if( k in this.#map ){
|
|
delete this.#map[k];
|
|
--this.#size;
|
|
this.#keys = null;
|
|
}
|
|
}
|
|
|
|
clear(){
|
|
this.#map = Object.create(null);
|
|
this.#keys = null;
|
|
this.#size = 0;
|
|
}
|
|
|
|
get length() {
|
|
return this.#size;
|
|
}
|
|
}/*KVVfsStorage*/;
|
|
|
|
/** True if v is the name of 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 and so that they
|
|
can co-exist with client data in their storage, but we elide them
|
|
from "v2" storage, where they're superfluous.
|
|
*/
|
|
const kvvfsKeyPrefix = (v)=>kvvfsIsPersistentName(v) ? 'kvvfs-'+v+'-' : '';
|
|
|
|
/**
|
|
Throws if storage name n (JS string) is not valid for use as a
|
|
storage name. Much of this goes back to kvvfs having a fixed
|
|
buffer size for its keys, and the storage name needing to be
|
|
encoded in the keys for local/session storage.
|
|
|
|
The second argument must only be true when called from xOpen() -
|
|
it makes names with a "-journal" suffix legal.
|
|
*/
|
|
const validateStorageName = function(n,mayBeJournal=false){
|
|
if( kvvfsIsPersistentName(n) ) return;
|
|
const len = (new Blob([n])).size/*byte length*/;
|
|
if( !len ) toss3(capi.SQLITE_MISUSE, "Empty name is not permitted.");
|
|
let maxLen = cache.keySize - 1;
|
|
if( cache.rxJournalSuffix.test(n) ){
|
|
if( !mayBeJournal ){
|
|
toss3(capi.SQLITE_MISUSE,
|
|
"Storage names may not have a '-journal' suffix.");
|
|
}
|
|
}else if( ['-wal','-shm'].filter(v=>n.endsWith(v)).length ){
|
|
toss3(capi.SQLITE_MISUSE,
|
|
"Storage names may not have a -wal or -shm suffix.");
|
|
}else{
|
|
maxLen -= 8 /* so we have space for a matching "-journal" suffix */;
|
|
}
|
|
if( len > maxLen ){
|
|
toss3(capi.SQLITE_RANGE, "Storage name is too long. Limit =", maxLen);
|
|
}
|
|
let i;
|
|
for( i = 0; i < len; ++i ){
|
|
const ch = n.codePointAt(i);
|
|
if( ch<32 ){
|
|
toss3(capi.SQLITE_RANGE,
|
|
"Illegal character ("+ch+"d) in storage name:",n);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
Create a new instance of the objects which go into
|
|
cache.storagePool, with a refcount of 1. If passed a Storage-like
|
|
object as its second argument, it is used for the storage,
|
|
otherwise it creates a new KVVfsStorage object.
|
|
*/
|
|
const newStorageObj = (name,storage=undefined)=>Object.assign(Object.create(null),{
|
|
/**
|
|
JS string value of this KVVfsFile::$zClass. i.e. the storage's
|
|
name.
|
|
*/
|
|
jzClass: name,
|
|
/**
|
|
Refcount. This keeps dbs and journals pointing to the same
|
|
storage for the life of both and enables kvvfs to behave more
|
|
like a conventional filesystem (a stepping stone towards
|
|
downstream API goals). Managed by xOpen() and xClose().
|
|
*/
|
|
refc: 1,
|
|
/**
|
|
If true, this storage will be removed by xClose() or
|
|
sqlite3_js_kvvfs_unlink() when refc reaches 0. The others will
|
|
persist when refc==0, to give the illusion of real back-end
|
|
storage. Managed by xOpen() and sqlite3_js_kvvfs_reserve(). By
|
|
default this is false but the delete-on-close=1 flag can be
|
|
used to set this to true.
|
|
*/
|
|
deleteAtRefc0: false,
|
|
/**
|
|
The backing store. Must implement the Storage interface.
|
|
*/
|
|
storage: storage || new KVVfsStorage,
|
|
/**
|
|
The storage prefix used for kvvfs keys. It is
|
|
"kvvfs-STORAGENAME-" for local/session storage and an empty
|
|
string for other 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),
|
|
/**
|
|
KVVfsFile instances currently using this storage. Managed by
|
|
xOpen() and xClose().
|
|
*/
|
|
files: [],
|
|
/**
|
|
If set, it's an array of objects with various event
|
|
callbacks. See sqlite3_js_kvvfs_listen(). When there are no
|
|
listeners, this member is set to undefined (instead of an empty
|
|
array) to allow us to more easily optimize out calls to
|
|
notifyListeners() for the common case of no listeners.
|
|
*/
|
|
listeners: undefined
|
|
});
|
|
|
|
/**
|
|
Public interface for kvvfs v2. The capi.sqlite3_js_kvvfs_...()
|
|
routines remain in place for v1. Some members of this class proxy
|
|
to those functions but use different default argument values in
|
|
some cases.
|
|
*/
|
|
const kvvfs = sqlite3.kvvfs = Object.create(null);
|
|
if( sqlite3.__isUnderTest ){
|
|
/* For inspection via the dev tools console. */
|
|
kvvfs.log = Object.assign(Object.create(null),{
|
|
xOpen: false,
|
|
xClose: false,
|
|
xWrite: false,
|
|
xRead: false,
|
|
xSync: false,
|
|
xAccess: false,
|
|
xFileControl: false,
|
|
xRcrdRead: false,
|
|
xRcrdWrite: false,
|
|
xRcrdDelete: false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
Deletes the cache.storagePool entries for store (a
|
|
cache.storagePool entry) and its db/journal counterpart.
|
|
*/
|
|
const deleteStorage = function(store){
|
|
const other = cache.rxJournalSuffix.test(store.jzClass)
|
|
? store.jzClass.replace(cache.rxJournalSuffix,'')
|
|
: store.jzClass+'-journal';
|
|
kvvfs?.log?.xClose
|
|
&& debug("cleaning up storage handles [", store.jzClass, other,"]",store);
|
|
delete cache.storagePool[store.jzClass];
|
|
delete cache.storagePool[other];
|
|
if( !sqlite3.__isUnderTest ){
|
|
/* In test runs, leave these for inspection. If we delete them here,
|
|
any prior dumps of them emitted via the console get cleared out
|
|
because the console shows live objects instead of call-time
|
|
static dumps. */
|
|
delete store.storage;
|
|
delete store.refc;
|
|
}
|
|
};
|
|
|
|
/**
|
|
Add both store.jzClass and store.jzClass+"-journal"
|
|
to cache,storagePool.
|
|
*/
|
|
const installStorageAndJournal = (store)=>
|
|
cache.storagePool[store.jzClass] =
|
|
cache.storagePool[store.jzClass+'-journal'] = store;
|
|
|
|
/**
|
|
The public name of the current thread's transient storage
|
|
object. A storage object with this name gets preinstalled.
|
|
*/
|
|
const nameOfThisThreadStorage = '.';
|
|
|
|
/**
|
|
Map of JS-stringified KVVfsFile::zClass names to
|
|
reference-counted Storage objects. These objects are created 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.
|
|
*/
|
|
cache.storagePool = Object.assign(Object.create(null),{
|
|
/* Start off with mappings for well-known names. */
|
|
[nameOfThisThreadStorage]: newStorageObj(nameOfThisThreadStorage)
|
|
});
|
|
|
|
if( globalThis.Storage ){
|
|
/* If available, install local/session storage. */
|
|
if( globalThis.localStorage instanceof globalThis.Storage ){
|
|
cache.storagePool.local = newStorageObj('local', globalThis.localStorage);
|
|
}
|
|
if( globalThis.sessionStorage instanceof globalThis.Storage ){
|
|
cache.storagePool.session = newStorageObj('session', globalThis.sessionStorage);
|
|
}
|
|
}
|
|
|
|
cache.builtinStorageNames = Object.keys(cache.storagePool);
|
|
|
|
const isBuiltinName = (n)=>cache.builtinStorageNames.indexOf(n)>-1;
|
|
|
|
/* Add "-journal" twins for each cache.storagePool entry... */
|
|
for(const k of Object.keys(cache.storagePool)){
|
|
/* Journals in kvvfs are are stored as individual records within
|
|
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];
|
|
cache.storagePool[k+'-journal'] = orig;
|
|
}
|
|
|
|
cache.setError = (e=undefined, dfltErrCode=capi.SQLITE_ERROR)=>{
|
|
if( e ){
|
|
cache.lastError = e;
|
|
return (e.resultCode | 0) || dfltErrCode;
|
|
}
|
|
delete cache.lastError;
|
|
return 0;
|
|
};
|
|
|
|
cache.popError = ()=>{
|
|
const e = cache.lastError;
|
|
delete cache.lastError;
|
|
return e;
|
|
};
|
|
|
|
/** Exception handler for notifyListeners(). */
|
|
const catchForNotify = (e)=>{
|
|
warn("kvvfs.listener handler threw:",e);
|
|
};
|
|
|
|
const kvvfsDecode = wasm.exports.sqlite3__wasm_kvvfs_decode;
|
|
const kvvfsEncode = wasm.exports.sqlite3__wasm_kvvfs_encode;
|
|
|
|
/**
|
|
Listener events and their argument(s) (via the callback(ev)
|
|
ev.data member):
|
|
|
|
'open': number of opened handles on this storage.
|
|
|
|
'close': number of opened handles on this storage.
|
|
|
|
'write': key, value
|
|
|
|
'delete': key
|
|
|
|
'sync': true if it's from xSync(), false if it's from
|
|
xFileControl().
|
|
|
|
For efficiency's sake, all calls to this function should
|
|
be in the form:
|
|
|
|
store.listeners && notifyListeners(...);
|
|
|
|
Failing to do so will trigger an exceptin in this function (which
|
|
will be ignored but may produce a console warning).
|
|
*/
|
|
const notifyListeners = async function(eventName,store,...args){
|
|
try{
|
|
//cache.rxPageNoSuffix ??= /(\d+)$/;
|
|
if( store.keyPrefix && args[0] ){
|
|
args[0] = args[0].replace(store.keyPrefix,'');
|
|
}
|
|
let u8enc, z0, z1, wcache;
|
|
for(const ear of store.listeners){
|
|
const ev = Object.create(null);
|
|
ev.storageName = store.jzClass;
|
|
ev.type = eventName;
|
|
const decodePages = ear.decodePages;
|
|
const f = ear.events[eventName];
|
|
if( f ){
|
|
if( !ear.includeJournal && args[0]==='jrnl' ){
|
|
continue;
|
|
}
|
|
if( 'write'===eventName && ear.decodePages && +args[0]>0 ){
|
|
/* Decode pages to Uint8Array, caching the result in
|
|
wcache in case we have more listeners. */
|
|
ev.data = [args[0]];
|
|
if( wcache?.[args[0]] ){
|
|
ev.data[1] = wcache[args[0]];
|
|
continue;
|
|
}
|
|
u8enc ??= new TextEncoder('utf-8');
|
|
z0 ??= cache.memBuffer(10);
|
|
z1 ??= cache.memBuffer(11);
|
|
const u = u8enc.encode(args[1]);
|
|
const heap = wasm.heap8u();
|
|
heap.set(u, Number(z0));
|
|
heap[wasm.ptr.addn(z0, u.length)] = 0;
|
|
const rc = kvvfsDecode(z0, z1, cache.buffer.n);
|
|
if( rc>0 ){
|
|
wcache ??= Object.create(null);
|
|
wcache[args[0]]
|
|
= ev.data[1]
|
|
= heap.slice(Number(z1), wasm.ptr.addn(z1,rc));
|
|
}else{
|
|
continue;
|
|
}
|
|
}else{
|
|
ev.data = args.length
|
|
? ((args.length===1) ? args[0] : args)
|
|
: undefined;
|
|
}
|
|
try{f(ev)?.catch?.(catchForNotify)}
|
|
catch(e){
|
|
warn("notifyListeners [",store.jzClass,"]",eventName,e);
|
|
}
|
|
}
|
|
}
|
|
}catch(e){
|
|
catchForNotify(e);
|
|
}
|
|
}/*notifyListeners()*/;
|
|
|
|
/**
|
|
Returns the storage object mapped to the given string zClass
|
|
(C-string pointer or JS string).
|
|
*/
|
|
const storageForZClass = (zClass)=>
|
|
'string'===typeof zClass
|
|
? cache.storagePool[zClass]
|
|
: cache.storagePool[wasm.cstrToJs(zClass)];
|
|
|
|
//#if 0
|
|
// fileForDb() works but we don't have a current need for it.
|
|
/**
|
|
Expects an (sqlite3*). Uses sqlite3_file_control() to extract its
|
|
(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). 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 = wasm.pstack.pointer;
|
|
try{
|
|
const pOut = wasm.pstack.allocPtr();
|
|
return wasm.exports.sqlite3_file_control(
|
|
pDb, wasm.ptr.null, capi.SQLITE_FCNTL_FILE_POINTER, pOut
|
|
)
|
|
? null
|
|
: new KVVfsFile(wasm.peekPtr(pOut));
|
|
}finally{
|
|
wasm.pstack.restore(stack);
|
|
}
|
|
};
|
|
|
|
/**
|
|
Expects an object from the storagePool map. The $szPage and
|
|
$szDb members of each store.files entry is set to -1 in an attempt
|
|
to trigger those values to reload.
|
|
*/
|
|
const alertFilesToReload = (store)=>{
|
|
try{
|
|
for( const f of store.files ){
|
|
// FIXME: we need to use one of the C APIs for this, maybe an
|
|
// fcntl.
|
|
f.$szPage = -1;
|
|
f.$szDb = -1n
|
|
}
|
|
}catch(e){
|
|
error("alertFilesToReload()",store,e);
|
|
throw e;
|
|
}
|
|
};
|
|
//#/if
|
|
|
|
const kvvfsMakeKey = wasm.exports.sqlite3__wasm_kvvfsMakeKey;
|
|
/**
|
|
Returns a C string from kvvfsMakeKey() OR returns zKey. In the
|
|
former case the memory is static, so must be copied before a
|
|
second call. zKey MUST be a pointer passed to a VFS/file method,
|
|
to allow us to avoid an alloc and/or an snprintf(). It requires
|
|
C-string arguments for zClass and zKey. zClass may be NULL but
|
|
zKey may not.
|
|
*/
|
|
const zKeyForStorage = (store, zClass, zKey)=>{
|
|
//debug("zKeyForStorage(",store, wasm.cstrToJs(zClass), wasm.cstrToJs(zKey));
|
|
return (zClass && store.keyPrefix) ? kvvfsMakeKey(zClass, zKey) : zKey;
|
|
};
|
|
|
|
const jsKeyForStorage = (store,zClass,zKey)=>
|
|
wasm.cstrToJs(zKeyForStorage(store, zClass, zKey));
|
|
|
|
const storageGetDbSize = (store)=>+store.storage.getItem(store.keyPrefix + "sz");
|
|
|
|
/**
|
|
sqlite3_file pointers => objects, each of which has:
|
|
|
|
.file = KVVfsFile instance
|
|
|
|
.jzClass = JS-string form of f.$zClass
|
|
|
|
.storage = Storage object. It is shared between a db and its
|
|
journal.
|
|
*/
|
|
const pFileHandles = new Map();
|
|
|
|
/**
|
|
Original WASM functions for methods we partially override.
|
|
*/
|
|
const originalMethods = {
|
|
vfs: Object.create(null),
|
|
ioDb: Object.create(null),
|
|
ioJrnl: Object.create(null)
|
|
};
|
|
|
|
/** Returns the appropriate originalMethods[X] instance for the
|
|
given a KVVfsFile instance. */
|
|
const originalIoMethods = (kvvfsFile)=>
|
|
originalMethods[kvvfsFile.$isJournal ? 'ioJrnl' : 'ioDb'];
|
|
|
|
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);
|
|
const recordHandler =
|
|
Object.create(null)/** helper for some vfs
|
|
routines. Populated later. */;
|
|
const kvvfsInternal = Object.assign(Object.create(null),{
|
|
pFileHandles,
|
|
cache,
|
|
storageForZClass,
|
|
KVVfsStorage,
|
|
/**
|
|
BUG: changing to a page size other than the default,
|
|
then vacuuming, corrupts the db. As a workaround,
|
|
until this is resolved, we forcibly disable
|
|
(pragma page_size=...) changes.
|
|
*/
|
|
disablePageSizeChange: true
|
|
});
|
|
if( kvvfs.log ){
|
|
// this is a test build
|
|
kvvfs.internal = kvvfsInternal;
|
|
}
|
|
|
|
/**
|
|
Implementations for members of the object referred to by
|
|
sqlite3__wasm_kvvfs_methods(). We swap out some native
|
|
implementations with these so that we can use JS Storage for
|
|
their backing store.
|
|
*/
|
|
const methodOverrides = {
|
|
|
|
/**
|
|
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.
|
|
|
|
A db's size is stored in a record named kvvfs[-storagename]-sz
|
|
and the journal is stored in kvvfs[-storagename]-jrnl. The
|
|
[-storagename] part is a remnant of the native impl (so that
|
|
it has unique filenames per db) and is only used for
|
|
localStorage and sessionStorage. We elide that part (to save
|
|
space) from other storage objects but retain it on those two
|
|
to avoid invalidating pre-version-2 session/localStorage dbs.
|
|
|
|
The interface docs for these methods are in src/os_kv.c's
|
|
kvrecordRead(), kvrecordWrite(), and kvrecordDelete().
|
|
*/
|
|
recordHandler: {
|
|
xRcrdRead: (zClass, zKey, zBuf, nBuf)=>{
|
|
try{
|
|
const jzClass = wasm.cstrToJs(zClass);
|
|
const store = storageForZClass(jzClass);
|
|
if( !store ) return -1;
|
|
const jXKey = jsKeyForStorage(store, zClass, zKey);
|
|
kvvfs?.log?.xRcrdRead && warn("xRcrdRead", jzClass, jXKey, nBuf, store );
|
|
const jV = store.storage.getItem(jXKey);
|
|
if(null===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. */;
|
|
if( 0 ){
|
|
debug("xRcrdRead", jXKey, store, jV);
|
|
}
|
|
if(nBuf<=0) return nV;
|
|
else if(1===nBuf){
|
|
wasm.poke(zBuf, 0);
|
|
return nV;
|
|
}
|
|
if( nBuf+1<nV ){
|
|
toss3(capi.SQLITE_RANGE,
|
|
"xRcrdRead()",jzClass,jXKey,
|
|
"input buffer is too small: need",
|
|
nV,"but have",nBuf);
|
|
}
|
|
if( 0 ){
|
|
debug("xRcrdRead", nBuf, zClass, wasm.cstrToJs(zClass),
|
|
wasm.cstrToJs(zKey), nV, jV, store);
|
|
}
|
|
const zV = cache.memBuffer(0);
|
|
//if( !zV ) return -3 /*OOM*/;
|
|
const heap = wasm.heap8();
|
|
let i;
|
|
for(i = 0; i < nV; ++i){
|
|
heap[wasm.ptr.add(zV,i)] = jV.codePointAt(i) & 0xFF;
|
|
}
|
|
heap.copyWithin(
|
|
Number(zBuf), Number(zV), wasm.ptr.addn(zV, i)
|
|
);
|
|
heap[wasm.ptr.add(zBuf, nV)] = 0;
|
|
return nBuf;
|
|
}catch(e){
|
|
error("kvrecordRead()",e);
|
|
cache.setError(e);
|
|
return -2;
|
|
}
|
|
},
|
|
|
|
xRcrdWrite: (zClass, zKey, zData)=>{
|
|
try {
|
|
const store = storageForZClass(zClass);
|
|
const jxKey = jsKeyForStorage(store, zClass, zKey);
|
|
const jData = wasm.cstrToJs(zData);
|
|
kvvfs?.log?.xRcrdWrite && warn("xRcrdWrite",jxKey, store);
|
|
store.storage.setItem(jxKey, jData);
|
|
store.listeners && notifyListeners('write', store, jxKey, jData);
|
|
return 0;
|
|
}catch(e){
|
|
error("kvrecordWrite()",e);
|
|
return cache.setError(e, capi.SQLITE_IOERR);
|
|
}
|
|
},
|
|
|
|
xRcrdDelete: (zClass, zKey)=>{
|
|
try {
|
|
const store = storageForZClass(zClass);
|
|
const jxKey = jsKeyForStorage(store, zClass, zKey);
|
|
kvvfs?.log?.xRcrdDelete && warn("xRcrdDelete",jxKey, store);
|
|
store.storage.removeItem(jxKey);
|
|
store.listeners && notifyListeners('delete', store, jxKey);
|
|
return 0;
|
|
}catch(e){
|
|
error("kvrecordDelete()",e);
|
|
return cache.setError(e, capi.SQLITE_IOERR);
|
|
}
|
|
}
|
|
}/*recordHandler*/,
|
|
|
|
/**
|
|
Override certain operations of the underlying sqlite3_vfs and
|
|
the two sqlite3_io_methods instances so that we can tie
|
|
Storage objects to db names.
|
|
*/
|
|
vfs:{
|
|
/* sqlite3_kvvfs_methods::pVfs's methods */
|
|
xOpen: function(pProtoVfs,zName,pProtoFile,flags,pOutFlags){
|
|
cache.popError();
|
|
let zToFree /* alloc()'d memory for temp db name */;
|
|
try{
|
|
if( !zName ){
|
|
zToFree = wasm.allocCString(""+pProtoFile+"."
|
|
+(Math.random() * 100000 | 0));
|
|
zName = zToFree;
|
|
}
|
|
const jzClass = wasm.cstrToJs(zName);
|
|
kvvfs?.log?.xOpen && debug("xOpen",jzClass,"flags =",flags);
|
|
validateStorageName(jzClass, true);
|
|
if( (flags & (capi.SQLITE_OPEN_MAIN_DB
|
|
| capi.SQLITE_OPEN_TEMP_DB
|
|
| capi.SQLITE_OPEN_TRANSIENT_DB))
|
|
&& cache.rxJournalSuffix.test(jzClass) ){
|
|
toss3(capi.SQLITE_ERROR,
|
|
"DB files may not have a '-journal' suffix.");
|
|
}
|
|
let s = storageForZClass(jzClass);
|
|
if( !s && !(flags & capi.SQLITE_OPEN_CREATE) ){
|
|
toss3(capi.SQLITE_ERROR, "Storage not found:", jzClass);
|
|
}
|
|
const rc = originalMethods.vfs.xOpen(pProtoVfs, zName, pProtoFile,
|
|
flags, pOutFlags);
|
|
if( rc ) return rc;
|
|
let deleteAt0 = !!(capi.SQLITE_OPEN_DELETEONCLOSE & flags);
|
|
if(wasm.isPtr(arguments[1]/*original zName*/)){
|
|
if(capi.sqlite3_uri_boolean(zName, "delete-on-close", 0)){
|
|
deleteAt0 = true;
|
|
}
|
|
}
|
|
const f = new KVVfsFile(pProtoFile);
|
|
util.assert(f.$zClass, "Missing f.$zClass");
|
|
f.addOnDispose(zToFree);
|
|
zToFree = undefined;
|
|
//debug("xOpen", jzClass, s);
|
|
if( s ){
|
|
++s.refc;
|
|
//no if( true===deleteAt0 ) s.deleteAtRefc0 = true;
|
|
s.files.push(f);
|
|
wasm.poke32(pOutFlags, flags);
|
|
}else{
|
|
wasm.poke32(pOutFlags, flags | capi.SQLITE_OPEN_CREATE);
|
|
util.assert( !f.$isJournal, "Opening a journal before its db? "+jzClass );
|
|
/* Map both zName and zName-journal to the same storage. */
|
|
const nm = jzClass.replace(cache.rxJournalSuffix,'');
|
|
s = newStorageObj(nm);
|
|
installStorageAndJournal(s);
|
|
s.files.push(f);
|
|
s.deleteAtRefc0 = deleteAt0;
|
|
kvvfs?.log?.xOpen
|
|
&& debug("xOpen installed storage handle [",nm, nm+"-journal","]", s);
|
|
}
|
|
pFileHandles.set(pProtoFile, {store: s, file: f, jzClass});
|
|
s.listeners && notifyListeners('open', s, s.files.length);
|
|
return 0;
|
|
}catch(e){
|
|
warn("xOpen:",e);
|
|
return cache.setError(e);
|
|
}finally{
|
|
zToFree && wasm.dealloc(zToFree);
|
|
}
|
|
}/*xOpen()*/,
|
|
|
|
xDelete: function(pVfs, zName, iSyncFlag){
|
|
cache.popError();
|
|
try{
|
|
const jzName = wasm.cstrToJs(zName);
|
|
if( cache.rxJournalSuffix.test(jzName) ){
|
|
recordHandler.xRcrdDelete(zName, cache.zKeyJrnl);
|
|
}/*
|
|
else: historically not done, but maybe otherwise delete
|
|
all db pages from storageForZClass(zName)?
|
|
*/
|
|
return 0;
|
|
}catch(e){
|
|
warn("xDelete",e);
|
|
return cache.setError(e);
|
|
}
|
|
},
|
|
|
|
xAccess: function(pProtoVfs, zPath, flags, pResOut){
|
|
cache.popError();
|
|
try{
|
|
const s = storageForZClass(zPath);
|
|
const jzPath = s?.jzClass || wasm.cstrToJs(zPath);
|
|
if( kvvfs?.log?.xAccess ){
|
|
debug("xAccess",jzPath,"flags =",
|
|
flags,"*pResOut =",wasm.peek32(pResOut),
|
|
"store =",s);
|
|
}
|
|
if( !s ){
|
|
// From the API docs:
|
|
/** The xAccess method returns [SQLITE_OK] on success or some
|
|
** non-zero error code if there is an I/O error or if the name of
|
|
** the file given in the second argument is illegal.
|
|
*/
|
|
// However, returning non-0 from here is fatal, so we don't do that.
|
|
try{validateStorageName(jzPath)}
|
|
catch(e){
|
|
//warn("xAccess is ignoring name validation failure:",e);
|
|
wasm.poke32(pResOut, 0);
|
|
return 0;
|
|
}
|
|
}
|
|
if( s ){
|
|
const key = s.keyPrefix+
|
|
(cache.rxJournalSuffix.test(jzPath) ? "jrnl" : "1");
|
|
const res = s.storage.getItem(key) ? 0 : 1;
|
|
/* This res value looks completely backwards to me, and
|
|
is the opposite of the native kvvfs's impl, but it's
|
|
working, whereas reimplementing the native one
|
|
faithfully does not. Read the lib-level code of where
|
|
this is invoked, my expectation is that we set res to 0
|
|
for not-exists. */
|
|
//warn("access res",jzPath,res);
|
|
wasm.poke32(pResOut, res);
|
|
}else{
|
|
wasm.poke32(pResOut, 0);
|
|
}
|
|
return 0;
|
|
}catch(e){
|
|
error('xAccess',e);
|
|
return cache.setError(e);
|
|
}
|
|
},
|
|
|
|
xRandomness: function(pVfs, nOut, pOut){
|
|
const heap = wasm.heap8u();
|
|
let i = 0;
|
|
const npOut = Number(pOut);
|
|
for(; i < nOut; ++i) heap[npOut + i] = (Math.random()*255000) & 0xFF;
|
|
return nOut;
|
|
},
|
|
|
|
xGetLastError: function(pVfs,nOut,pOut){
|
|
const e = cache.popError();
|
|
debug('xGetLastError',e);
|
|
if(e){
|
|
const scope = wasm.scopedAllocPush();
|
|
try{
|
|
const [cMsg, n] = wasm.scopedAllocCString(e.message, true);
|
|
wasm.cstrncpy(pOut, cMsg, nOut);
|
|
if(n > nOut) wasm.poke8(wasm.ptr.add(pOut,nOut,-1), 0);
|
|
debug("set xGetLastError",e.message);
|
|
return (e.resultCode | 0) || capi.SQLITE_IOERR;
|
|
}catch(e){
|
|
return capi.SQLITE_NOMEM;
|
|
}finally{
|
|
wasm.scopedAllocPop(scope);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//#if 0
|
|
// these impls work but there's currently no pressing need _not_ use
|
|
// the native impls.
|
|
xCurrentTime: function(pVfs,pOut){
|
|
wasm.poke64f(pOut, 2440587.5 + (Date.now()/86400000));
|
|
return 0;
|
|
},
|
|
|
|
xCurrentTimeInt64: function(pVfs,pOut){
|
|
wasm.poke64(pOut, (2440587.5 * 86400000) + Date.now());
|
|
return 0;
|
|
}
|
|
//#/if
|
|
}/*.vfs*/,
|
|
|
|
/**
|
|
kvvfs has separate sqlite3_api_methods impls for some of the
|
|
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.
|
|
*/
|
|
ioDb:{
|
|
/* sqlite3_kvvfs_methods::pIoDb's methods */
|
|
xClose: function(pFile){
|
|
cache.popError();
|
|
try{
|
|
const h = pFileHandles.get(pFile);
|
|
kvvfs?.log?.xClose && debug("xClose", pFile, h);
|
|
if( h ){
|
|
pFileHandles.delete(pFile);
|
|
const s = h.store;//storageForZClass(h.jzClass);
|
|
s.files = s.files.filter((v)=>v!==h.file);
|
|
if( --s.refc<=0 && s.deleteAtRefc0 ){
|
|
deleteStorage(s);
|
|
}
|
|
originalMethods.ioDb/*same for journals*/.xClose(pFile);
|
|
h.file.dispose();
|
|
s.listeners && notifyListeners('close', s, s.files.length);
|
|
}else{
|
|
/* Can happen if xOpen fails */
|
|
}
|
|
return 0;
|
|
}catch(e){
|
|
error("xClose",e);
|
|
return cache.setError(e);
|
|
}
|
|
},
|
|
|
|
xFileControl: function(pFile, opId, pArg){
|
|
cache.popError();
|
|
try{
|
|
const h = pFileHandles.get(pFile);
|
|
util.assert(h, "Missing KVVfsFile handle");
|
|
kvvfs?.log?.xFileControl && debug("xFileControl",h,'op =',opId);
|
|
if( opId===capi.SQLITE_FCNTL_PRAGMA
|
|
&& kvvfsInternal.disablePageSizeChange ){
|
|
/* pArg== length-3 (char**) */
|
|
//const argv = wasm.cArgvToJs(3, pArg); // the easy way
|
|
const zName = wasm.peekPtr(wasm.ptr.add(pArg, wasm.ptr.size));
|
|
if( "page_size"===wasm.cstrToJs(zName) ){
|
|
kvvfs?.log?.xFileControl
|
|
&& debug("xFileControl pragma",wasm.cstrToJs(zName));
|
|
const zVal = wasm.peekPtr(wasm.ptr.add(pArg, 2*wasm.ptr.size));
|
|
if( zVal ){
|
|
/* Without this, pragma page_size=N; followed by a
|
|
vacuum breaks the db. With this, it continues
|
|
working but does not actually change the page
|
|
size. */
|
|
kvvfs?.log?.xFileControl
|
|
&& warn("xFileControl pragma", h,
|
|
"NOT setting page size to", wasm.cstrToJs(zVal));
|
|
h.file.$szPage = -1;
|
|
return 0/*corrupts: capi.SQLITE_NOTFOUND*/;
|
|
}else if( h.file.$szPage>0 ){
|
|
kvvfs?.log?.xFileControl &&
|
|
warn("xFileControl", h, "getting page size",h.file.$szPage);
|
|
wasm.pokePtr(pArg, wasm.allocCString(""+h.file.$szPage)
|
|
/* memory now owned by the library */);
|
|
return 0;//capi.SQLITE_NOTFOUND;
|
|
}
|
|
}
|
|
}
|
|
const rc = originalMethods.ioDb.xFileControl(pFile, opId, pArg);
|
|
if( 0==rc && capi.SQLITE_FCNTL_SYNC===opId ){
|
|
h.store.listeners && notifyListeners('sync', h.store, false);
|
|
}
|
|
return rc;
|
|
}catch(e){
|
|
error("xFileControl",e);
|
|
return cache.setError(e);
|
|
}
|
|
},
|
|
|
|
xSync: function(pFile,flags){
|
|
cache.popError();
|
|
try{
|
|
const h = pFileHandles.get(pFile);
|
|
kvvfs?.log?.xSync && debug("xSync", h);
|
|
util.assert(h, "Missing KVVfsFile handle");
|
|
const rc = originalMethods.ioDb.xSync(pFile, flags);
|
|
if( 0==rc && h.store.listeners ) notifyListeners('sync', h.store, true);
|
|
return rc;
|
|
}catch(e){
|
|
error("xSync",e);
|
|
return cache.setError(e);
|
|
}
|
|
},
|
|
|
|
//#if 0
|
|
// We override xRead/xWrite only for logging/debugging. They
|
|
// should otherwise be disabled (it's faster that way).
|
|
xRead: function(pFile,pTgt,n,iOff64){
|
|
cache.popError();
|
|
try{
|
|
if( kvvfs?.log?.xRead ){
|
|
const h = pFileHandles.get(pFile);
|
|
util.assert(h, "Missing KVVfsFile handle");
|
|
debug("xRead", n, iOff64, h);
|
|
}
|
|
return originalMethods.ioDb.xRead(pFile, pTgt, n, iOff64);
|
|
}catch(e){
|
|
error("xRead",e);
|
|
return cache.setError(e);
|
|
}
|
|
},
|
|
xWrite: function(pFile,pSrc,n,iOff64){
|
|
cache.popError();
|
|
try{
|
|
if( kvvfs?.log?.xWrite ){
|
|
const h = pFileHandles.get(pFile);
|
|
util.assert(h, "Missing KVVfsFile handle");
|
|
debug("xWrite", n, iOff64, h);
|
|
}
|
|
return originalMethods.ioDb.xWrite(pFile, pSrc, n, iOff64);
|
|
}catch(e){
|
|
error("xWrite",e);
|
|
return cache.setError(e);
|
|
}
|
|
},
|
|
//#/if
|
|
|
|
//#if 0
|
|
xTruncate: function(pFile,i64){},
|
|
xFileSize: function(pFile,pi64Out){},
|
|
xLock: function(pFile,iLock){},
|
|
xUnlock: function(pFile,iLock){},
|
|
xCheckReservedLock: function(pFile,piOut){},
|
|
xSectorSize: function(pFile){},
|
|
xDeviceCharacteristics: function(pFile){}
|
|
//#/if
|
|
}/*.ioDb*/,
|
|
|
|
ioJrnl:{
|
|
/* sqlite3_kvvfs_methods::pIoJrnl's methods. Those set to true
|
|
are copied as-is from the ioDb objects. Others are specific
|
|
to journal files. */
|
|
xClose: true,
|
|
//#if 0
|
|
xRead: function(pFile,pTgt,n,iOff64){},
|
|
xWrite: function(pFile,pSrc,n,iOff64){},
|
|
xTruncate: function(pFile,i64){},
|
|
xSync: function(pFile,flags){},
|
|
xFileControl: function(pFile, opId, pArg){},
|
|
xFileSize: function(pFile,pi64Out){},
|
|
xLock: true,
|
|
xUnlock: true,
|
|
xCheckReservedLock: true,
|
|
xSectorSize: true,
|
|
xDeviceCharacteristics: true
|
|
//#/if
|
|
}/*.ioJrnl*/
|
|
}/*methodOverrides*/;
|
|
|
|
//#if 0
|
|
debug("pVfs and friends", pVfs, pIoDb, pIoJrnl,
|
|
kvvfsMethods, capi.sqlite3_file.structInfo,
|
|
KVVfsFile.structInfo);
|
|
//#/if
|
|
|
|
try {
|
|
util.assert( cache.buffer.n>1024*129, "Heap buffer is not large enough"
|
|
/* Native is SQLITE_KVOS_SZ is 133073 as of this writing */ );
|
|
for(const e of Object.entries(methodOverrides.recordHandler)){
|
|
// Overwrite kvvfsMethods's callbacks
|
|
const k = e[0], f = e[1];
|
|
recordHandler[k] = f;
|
|
if( 0 ){
|
|
// bug: this should work
|
|
kvvfsMethods.installMethod(k, f);
|
|
}else{
|
|
kvvfsMethods[kvvfsMethods.memberKey(k)] =
|
|
wasm.installFunction(kvvfsMethods.memberSignature(k), f);
|
|
}
|
|
}
|
|
for(const e of Object.entries(methodOverrides.vfs)){
|
|
// Overwrite some pVfs entries and stash the original impls
|
|
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]);
|
|
pVfs[km] = wasm.installFunction(member.signature, f);
|
|
}
|
|
for(const e of Object.entries(methodOverrides.ioDb)){
|
|
// Similar treatment for pVfs.$pIoDb a.k.a. pIoDb...
|
|
const k = e[0], f = e[1], km = pIoDb.memberKey(k);
|
|
originalMethods.ioDb[k] = wasm.functionEntry(pIoDb[km])
|
|
|| util.toss("Missing native pIoDb[",km,"]");
|
|
pIoDb[km] = wasm.installFunction(pIoDb.memberSignature(k), f);
|
|
}
|
|
for(const e of Object.entries(methodOverrides.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])
|
|
|| util.toss("Missing native pIoJrnl[",km,"]");
|
|
if( true===f ){
|
|
/* use pIoDb's copy */
|
|
pIoJrnl[km] = pIoDb[km] || util.toss("Missing copied pIoDb[",km,"]");
|
|
}else{
|
|
pIoJrnl[km] = wasm.installFunction(pIoJrnl.memberSignature(k), f);
|
|
}
|
|
}
|
|
}finally{
|
|
kvvfsMethods.dispose();
|
|
pVfs.dispose();
|
|
pIoDb.dispose();
|
|
pIoJrnl.dispose();
|
|
}
|
|
|
|
/*
|
|
That gets all of the low-level bits out of the way. What follows
|
|
are the public API additions.
|
|
*/
|
|
|
|
/**
|
|
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'
|
|
- '' - see below.
|
|
- A transient kvvfs storage object name.
|
|
|
|
In the first two cases, only sessionStorage resp. localStorage is
|
|
cleared. An empty string resolves to both 'local' and 'session'
|
|
storage.
|
|
|
|
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','').
|
|
|
|
- It throws if a db currently has the storage opened UNLESS the
|
|
storage object is localStorage or sessionStorage. That version 1
|
|
did not throw for this case was due to an architectural
|
|
limitation which has since been overcome, but removal of
|
|
JsStorageDb.prototype.clearStorage() would be a backwards compatibility
|
|
break, so this function permits wiping the storage for those two
|
|
cases even if they are opened. Use with care.
|
|
*/
|
|
const sqlite3_js_kvvfs_clear = function callee(which){
|
|
if( ''===which ){
|
|
return callee('local') + callee('session');
|
|
}
|
|
const store = storageForZClass(which);
|
|
if( !store ) return 0;
|
|
if( store.files.length ){
|
|
if( globalThis.localStorage===store.storage
|
|
|| globalThis.sessionStorage===store.storage ){
|
|
/* backwards compatibility: allow these to be cleared
|
|
while opened. */
|
|
}else{
|
|
/* Interestingly, kvvfs recovers just fine when the storage is
|
|
wiped, so long as the db is not in use and its schema is
|
|
recreated before it's used, but client apps should not have
|
|
to be faced with that eventuality mid-query (where it
|
|
_will_ cause failures). Therefore we disallow it when
|
|
storage handles are opened. Kvvfs version 1 could not
|
|
detect this case - see the if() block above.
|
|
*/
|
|
toss3(capi.SQLITE_ACCESS,
|
|
"Cannot clear in-use database storage.");
|
|
}
|
|
}
|
|
const s = store.storage;
|
|
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(!store.keyPrefix || k.startsWith(store.keyPrefix)) toRm.push(k);
|
|
}
|
|
toRm.forEach((kk)=>s.removeItem(kk));
|
|
//alertFilesToReload(store);
|
|
return toRm.length;
|
|
};
|
|
|
|
/**
|
|
This routine estimates the approximate amount of
|
|
storage used by the given kvvfs back-end.
|
|
|
|
Its arguments are 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.
|
|
|
|
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.
|
|
*/
|
|
const sqlite3_js_kvvfs_size = function callee(which){
|
|
if( ''===which ){
|
|
return callee('local') + callee('session');
|
|
}
|
|
const store = storageForZClass(which);
|
|
if( !store ) return 0;
|
|
const s = store.storage;
|
|
let i, sz = 0;
|
|
for(i = 0; i < s.length; ++i){
|
|
const k = s.key(i);
|
|
if(!store.keyPrefix || k.startsWith(store.keyPrefix)){
|
|
sz += k.length;
|
|
sz += s.getItem(k).length;
|
|
}
|
|
}
|
|
return sz * 2 /* because JS uses 2-byte char encoding */;
|
|
};
|
|
|
|
/**
|
|
Exports a kvvfs storage object to an object, optionally
|
|
JSON-friendly.
|
|
|
|
Usages:
|
|
|
|
thisfunc(storageName);
|
|
thisfunc(options);
|
|
|
|
In the latter case, the options object must be an object with
|
|
the following properties:
|
|
|
|
- "name" (string) required. The storage to export.
|
|
|
|
- "decodePages" (bool=false). If true, the .pages result property
|
|
holdes Uint8Array objects holding the raw binary-format db
|
|
pages. The default is to use kvvfs-encoded string pages
|
|
(JSON-friendly).
|
|
|
|
- "includeJournal" (bool=false). If true and the db has a current
|
|
journal, it is exported as well. (Kvvfs journals are stored as a
|
|
single record within the db's storage object.)
|
|
|
|
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() 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 options.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 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, with the
|
|
caveat that that code also does not offer a public interface.
|
|
i.e. the encoding is a private implementation detail of kvvfs.
|
|
The format may be changed in the future but kvvfs will continue
|
|
to support the current form.
|
|
|
|
Added in version @kvvfs-v2-added-in@.
|
|
*/
|
|
const sqlite3_js_kvvfs_export = function callee(...args){
|
|
let opt;
|
|
if( 1===args.length && 'object'===typeof args[0] ){
|
|
opt = args[0];
|
|
}else if(args.length){
|
|
opt = Object.assign(Object.create(null),{
|
|
name: args[0],
|
|
//decodePages: true
|
|
});
|
|
}
|
|
const store = opt ? storageForZClass(opt.name) : null;
|
|
if( !store ){
|
|
toss3(capi.SQLITE_NOTFOUND,
|
|
"There is no kvvfs storage named",opt?.name);
|
|
}
|
|
//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);
|
|
let xpages;
|
|
const keyPrefix = store.keyPrefix;
|
|
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( opt.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 ){
|
|
toss3(capi.SQLITE_RANGE, "Malformed kvvfs key: "+k);
|
|
}
|
|
if( opt.decodePages ){
|
|
const spg = s.getItem(k),
|
|
n = spg.length,
|
|
z = cache.memBuffer(0),
|
|
zDec = cache.memBuffer(1),
|
|
heap = wasm.heap8u()/* MUST be inited last*/;
|
|
let i = 0;
|
|
for( ; i < n; ++i ){
|
|
heap[wasm.ptr.add(z, i)] = spg.codePointAt(i) & 0xff;
|
|
}
|
|
heap[wasm.ptr.add(z, i)] = 0;
|
|
//debug("Decoding",i,"page bytes");
|
|
const nDec = kvvfsDecode(
|
|
z, zDec, cache.buffer.n
|
|
);
|
|
//debug("Decoded",nDec,"page bytes");
|
|
pages[kk] = heap.slice(Number(zDec), wasm.ptr.addn(zDec, nDec));
|
|
}else{
|
|
pages[kk] = s.getItem(k);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if( opt.decodePages ) cache.memBufferFree(1);
|
|
/* 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;
|
|
}/* sqlite3_js_kvvfs_export */;
|
|
|
|
/**
|
|
The counterpart of sqlite3_js_kvvfs_export(). Its
|
|
argument must be the result of that function() or
|
|
a compatible one.
|
|
|
|
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 given 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. If
|
|
exp.name refers to a new storage name then if it throws, the name
|
|
does not get installed.
|
|
|
|
Added in version @kvvfs-v2-added-in@.
|
|
*/
|
|
const sqlite3_js_kvvfs_import = function(exp, overwrite=false){
|
|
if( !exp?.timestamp
|
|
|| !exp.name
|
|
|| undefined===exp.size
|
|
|| !Array.isArray(exp.pages) ){
|
|
toss3(capi.SQLITE_MISUSE, "Malformed export object.");
|
|
}else if( !exp.size
|
|
|| (exp.size !== (exp.size | 0))
|
|
//|| (exp.size % cache.fixedPageSize)
|
|
|| exp.size>=0x7fffffff ){
|
|
toss3(capi.SQLITE_RANGE, "Invalid db size: "+exp.size);
|
|
}
|
|
|
|
validateStorageName(exp.name);
|
|
let store = storageForZClass(exp.name);
|
|
const isNew = !store;
|
|
if( store ){
|
|
if( !overwrite ){
|
|
//warn("Storage exists:",arguments,store);
|
|
toss3(capi.SQLITE_ACCESS,
|
|
"Storage '"+exp.name+"' already exists and",
|
|
"overwrite was not specified.");
|
|
}else if( !store.files || !store.jzClass ){
|
|
toss3(capi.SQLITE_ERROR,
|
|
"Internal storage object", exp.name,"seems to be malformed.");
|
|
}else if( store.files.length ){
|
|
toss3(capi.SQLITE_IOERR_ACCESS,
|
|
"Cannot import db storage while it is in use.");
|
|
}
|
|
sqlite3_js_kvvfs_clear(exp.name);
|
|
}else{
|
|
store = newStorageObj(exp.name);
|
|
//warn("Installing new storage:",store);
|
|
}
|
|
//debug("Importing store",store.poolEntry.files.length, store);
|
|
//debug("object to import:",exp);
|
|
const keyPrefix = kvvfsKeyPrefix(exp.name);
|
|
let zEnc;
|
|
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);
|
|
if( exp.pages[0] instanceof Uint8Array ){
|
|
/* raw binary pages */
|
|
//debug("pages",exp.pages);
|
|
exp.pages.forEach((u,ndx)=>{
|
|
const n = u.length;
|
|
if( 0 && cache.fixedPageSize !== n ){
|
|
util.toss3(capi.SQLITE_RANGE,"Unexpected page size:", n);
|
|
}
|
|
zEnc ??= cache.memBuffer(1);
|
|
const zBin = cache.memBuffer(0),
|
|
heap = wasm.heap8u()/*MUST be inited last*/;
|
|
/* Copy u to the heap and encode the heap copy via C. This
|
|
is _presumably_ faster than porting the encoding algo to
|
|
JS. */
|
|
heap.set(u, Number(zBin));
|
|
heap[wasm.ptr.addn(zBin,n)] = 0;
|
|
const rc = kvvfsEncode(zBin, n, zEnc);
|
|
util.assert( rc < cache.buffer.n,
|
|
"Impossibly long output - possibly smashed the heap" );
|
|
util.assert( 0===wasm.peek8(wasm.ptr.add(zEnc,rc)),
|
|
"Expecting NUL-terminated encoded output" );
|
|
const jenc = wasm.cstrToJs(zEnc);
|
|
//debug("(un)encoded page:",u,jenc);
|
|
s.setItem(keyPrefix+(ndx+1), jenc);
|
|
});
|
|
}else if( exp.pages[0] ){
|
|
/* kvvfs-encoded pages */
|
|
exp.pages.forEach((v,ndx)=>s.setItem(keyPrefix+(ndx+1), v));
|
|
}
|
|
if( isNew ) installStorageAndJournal(store);
|
|
}catch{
|
|
if( !isNew ){
|
|
try{sqlite3_js_kvvfs_clear(exp.name);}catch(ee){/*ignored*/}
|
|
}
|
|
}finally{
|
|
if( zEnc ) cache.memBufferFree(1);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
If no kvvfs storage exists with the given name, one is
|
|
installed. If one exists, its reference count is increased so
|
|
that it won't be freed by the closing of a database or journal
|
|
file.
|
|
|
|
Throws if the name is not valid for a new storage object.
|
|
|
|
Added in version @kvvfs-v2-added-in@.
|
|
*/
|
|
const sqlite3_js_kvvfs_reserve = function(name){
|
|
let store = storageForZClass(name);
|
|
if( store ){
|
|
++store.refc;
|
|
return;
|
|
}
|
|
validateStorageName(name);
|
|
installStorageAndJournal(newStorageObj(name));
|
|
};
|
|
|
|
/**
|
|
Conditionally "unlinks" a kvvfs storage object, reducing its
|
|
reference count by 1.
|
|
|
|
This is a no-op if name ends in "-journal" or refers to a
|
|
built-in storage object.
|
|
|
|
It will not lower the refcount below the number of
|
|
currently-opened db/journal files for the storage (so that it
|
|
cannot delete it out from under them).
|
|
|
|
If the refcount reaches 0 then the storage object is
|
|
removed.
|
|
|
|
Returns true if it reduces the refcount, else false. A result of
|
|
true does not necessarily mean that the storage unit was removed,
|
|
just that its refcount was lowered. Similarly, a result of false
|
|
does not mean that the storage is removed - it may still have
|
|
opened handles.
|
|
|
|
Added in version @kvvfs-v2-added-in@.
|
|
*/
|
|
const sqlite3_js_kvvfs_unlink = function(name){
|
|
const store = storageForZClass(name);
|
|
if( !store
|
|
|| kvvfsIsPersistentName(store.jzClass)
|
|
|| isBuiltinName(store.jzClass)
|
|
|| cache.rxJournalSuffix.test(name) ) return false;
|
|
if( store.refc > store.files.length || 0===store.files.length ){
|
|
if( --store.refc<=0 ){
|
|
/* Ignoring deleteAtRefc0 for an explicit unlink */
|
|
deleteStorage(store);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
Adds an event listener to a kvvfs storage object. The idea is
|
|
that this can be used to asynchronously back up one kvvfs storage
|
|
object to another or another channel entirely. (The caveat in the
|
|
latter case is that kvvfs's format is not readily consumable by
|
|
downstream code.)
|
|
|
|
Its argument must be an object with the following properties:
|
|
|
|
- storage: the name of the kvvfs storage object.
|
|
|
|
- reserve [=false]: if true, sqlite3_js_kvvfs_reserve() is used
|
|
to ensure that the storage exists if it does not already.
|
|
If this is false and the storage does not exist then an
|
|
exception is thrown.
|
|
|
|
- events: an object which may have any of the following
|
|
callback function properties: open, close, write, delete.
|
|
|
|
- decodePages [=false]: if true, write events will receive each
|
|
db page write in the form of a Uint8Array holding the raw binary
|
|
db page. The default is to emit the kvvfs-format page because it
|
|
requires no extra work, we already have it in hand, and it's
|
|
often smaller. It's not great for interchange, though.
|
|
|
|
- includeJournal [=false]: if true, writes and deletes of
|
|
"jrnl" records are included. If false, no events are sent
|
|
for journal updates.
|
|
|
|
Passing the same object to sqlite3_js_kvvfs_unlisten() will
|
|
remove the listener.
|
|
|
|
Each one of the events callbacks will be called asynchronously
|
|
when the given storage performs those operations. They may be
|
|
asynchronous functions but are not required to be (the events are
|
|
fired async either way, but making the event callbacks async may
|
|
be advantageous when multiple listeners are involved). All
|
|
exceptions, including those via Promises, are ignored but may (or
|
|
may not) trigger warning output on the console.
|
|
|
|
Each callback gets passed a single object with the following
|
|
properties:
|
|
|
|
.type = the same as the name of the callback
|
|
|
|
.storageName = the name of the storage object
|
|
|
|
.data = callback-dependent:
|
|
|
|
- 'open' and 'close' get an integer, the number of
|
|
currently-opened handles on the storage.
|
|
|
|
- 'write' gets a length-two array holding the key and value which
|
|
were written. The key is always a string, even if it's a db page
|
|
number. For db-page records, the value's type depends on
|
|
opt.decodePages. All others, including the journal, are strings.
|
|
(The journal, being a kvvfs-specific format, is delivered in
|
|
that same JSON-friendly format.) More details below.
|
|
|
|
- 'delete' gets the string-type key of the deleted record.
|
|
|
|
- 'sync' gets a boolean value: true if it was triggered by db
|
|
file's xSync(), false if it was triggered by xFileControl(). The
|
|
latter triggers before the xSync() and also triggers if the DB
|
|
has PRAGMA SYNCHRONOUS=OFF (in which case xSync() is not
|
|
triggered).
|
|
|
|
The key/value arguments to 'write', and key argument to 'delete',
|
|
are in one of the following forms:
|
|
|
|
- 'sz' = the unencoded db size as a string. This specific key is
|
|
key is never deleted, so is only ever passed to 'write' events.
|
|
|
|
- 'jrnl' = the current db journal as a kvvfs-encoded string. This
|
|
journal format is not useful anywhere except in the kvvfs
|
|
internals. These events are not fired if opt.includeJournal is
|
|
false.
|
|
|
|
- '[1-9][0-9]*' (a db page number) = Its type depends on
|
|
opt.decodePages. These may be written and deleted in arbitrary
|
|
order.
|
|
|
|
Design note: JS has StorageEvents but only in the main thread,
|
|
which is why the listeners are not based on that.
|
|
|
|
Added in version @kvvfs-v2-added-in@.
|
|
*/
|
|
const sqlite3_js_kvvfs_listen = function(opt){
|
|
if( !opt || 'object'!==typeof opt ){
|
|
toss3(capi.SQLITE_MISUSE, "Expecting a listener object.");
|
|
}
|
|
let store = storageForZClass(opt.storage);
|
|
if( !store ){
|
|
if( opt.storage && opt.reserve ){
|
|
sqlite3_js_kvvfs_reserve(opt.storage);
|
|
store = storageForZClass(opt.storage);
|
|
util.assert(store,
|
|
"Unexpectedly cannot fetch reserved storage "
|
|
+opt.storage);
|
|
}else{
|
|
toss3(capi.SQLITE_NOTFOUND,"No such storage:",opt.storage);
|
|
}
|
|
}
|
|
if( opt.events ){
|
|
(store.listeners ??= []).push(opt);
|
|
}
|
|
};
|
|
|
|
/**
|
|
Removes the kvvfs event listeners for the given options
|
|
object. It must be passed the same object instance which was
|
|
passed to sqlite3_js_kvvfs_listen().
|
|
|
|
This has no side effects if opt is invalid or is not a match for
|
|
any listeners.
|
|
|
|
Return true if it unregisters its argument, else false.
|
|
|
|
Added in version @kvvfs-v2-added-in@.
|
|
*/
|
|
const sqlite3_js_kvvfs_unlisten = function(opt){
|
|
const store = storageForZClass(opt?.storage);
|
|
if( store?.listeners && opt.events ){
|
|
const n = store.listeners.length;
|
|
store.listeners = store.listeners.filter((v)=>v!==opt);
|
|
const rc = n>store.listeners.length;
|
|
if( !store.listeners.length ){
|
|
// to speed up downstream checks for listeners
|
|
store.listeners = undefined;
|
|
}
|
|
return rc;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
sqlite3.kvvfs.reserve = sqlite3_js_kvvfs_reserve;
|
|
sqlite3.kvvfs.import = sqlite3_js_kvvfs_import;
|
|
sqlite3.kvvfs.export = sqlite3_js_kvvfs_export;
|
|
sqlite3.kvvfs.unlink = sqlite3_js_kvvfs_unlink;
|
|
sqlite3.kvvfs.listen = sqlite3_js_kvvfs_listen;
|
|
sqlite3.kvvfs.unlisten = sqlite3_js_kvvfs_unlisten;
|
|
sqlite3.kvvfs.exists = (name)=>!!storageForZClass(name);
|
|
sqlite3.kvvfs.estimateSize = sqlite3_js_kvvfs_size;
|
|
sqlite3.kvvfs.clear = sqlite3_js_kvvfs_clear;
|
|
|
|
|
|
if( globalThis.Storage ){
|
|
/**
|
|
Prior to version 2, kvvfs was only available in the main
|
|
thread. We retain that for the v1 APIs, exposing them only in
|
|
the main UI thread. As of version 2, kvvfs is available in all
|
|
threads but only via its v2 interface (sqlite3.kvvfs).
|
|
|
|
These versions have a default argument value of "" which the v2
|
|
versions lack.
|
|
*/
|
|
capi.sqlite3_js_kvvfs_size = (which="")=>sqlite3_js_kvvfs_size(which);
|
|
capi.sqlite3_js_kvvfs_clear = (which="")=>sqlite3_js_kvvfs_clear(which);
|
|
}
|
|
|
|
//#if not omit-oo1
|
|
if(sqlite3.oo1?.DB){
|
|
/**
|
|
Functionally equivalent to DB(storageName,'c','kvvfs') except
|
|
that it throws if the given storage name is not one of 'local'
|
|
or 'session'.
|
|
|
|
As of version 3.46, the argument may optionally be an options
|
|
object in the form:
|
|
|
|
{
|
|
filename: 'session'|'local',
|
|
... etc. (all options supported by the DB ctor)
|
|
}
|
|
|
|
noting that the 'vfs' option supported by main DB
|
|
constructor is ignored here: the vfs is always 'kvvfs'.
|
|
*/
|
|
const DB = sqlite3.oo1.DB;
|
|
sqlite3.oo1.JsStorageDb = function(
|
|
storageName = sqlite3.oo1.JsStorageDb.defaultStorageName
|
|
){
|
|
const opt = DB.dbCtorHelper.normalizeArgs(...arguments);
|
|
opt.vfs = 'kvvfs';
|
|
if( 0 ){
|
|
// Current tests rely on these, but that's arguably a bug
|
|
if( opt.flags ) opt.flags = 'cw'+opt.flags;
|
|
else opt.flags = 'cw';
|
|
}
|
|
switch( opt.filename ){
|
|
/* sqlite3_open(), in these builds, recognizes the names
|
|
below and performs some magic which we want to bypass
|
|
here for sanity's sake. */
|
|
case ":sessionStorage:": opt.filename = 'session'; break;
|
|
case ":localStorage:": opt.filename = 'local'; break;
|
|
}
|
|
const m = /(file:(\/\/)?)([^?]+)/.exec(opt.filename);
|
|
validateStorageName( m ? m[3] : opt.filename);
|
|
DB.dbCtorHelper.call(this, opt);
|
|
};
|
|
sqlite3.oo1.JsStorageDb.defaultStorageName
|
|
= cache.storagePool.session ? 'session' : nameOfThisThreadStorage;
|
|
const jdb = sqlite3.oo1.JsStorageDb;
|
|
jdb.prototype = Object.create(DB.prototype);
|
|
jdb.clearStorage = sqlite3_js_kvvfs_clear;
|
|
/**
|
|
DEPRECATED: the inherited method of this name (as opposed to
|
|
the "static" class method) is deprecated with version 2 of
|
|
kvvfs. This function will, for backwards comaptibility,
|
|
continue to work with localStorage and sessionStorage, but will
|
|
throw for all other storage because they are opened. Version 1
|
|
was not capable of recognizing that the storage was opened so
|
|
permitted wiping it out at any time, but that was arguably a
|
|
bug.
|
|
|
|
Clears this database instance's storage or throws if this
|
|
instance has been closed. Returns the number of
|
|
database pages which were cleaned up.
|
|
*/
|
|
jdb.prototype.clearStorage = function(){
|
|
return jdb.clearStorage(this.affirmOpen().dbFilename(), true);
|
|
};
|
|
/** Equivalent to sqlite3_js_kvvfs_size(). */
|
|
jdb.storageSize = sqlite3_js_kvvfs_size;
|
|
/**
|
|
Returns the _approximate_ number of bytes this database takes
|
|
up in its storage or throws if this instance has been closed.
|
|
*/
|
|
jdb.prototype.storageSize = function(){
|
|
return jdb.storageSize(this.affirmOpen().dbFilename(), true);
|
|
};
|
|
}/*sqlite3.oo1.JsStorageDb*/
|
|
//#/if not omit-oo1
|
|
|
|
if( sqlite3.__isUnderTest && sqlite3.vtab ){
|
|
/**
|
|
An eponymous vtab for inspecting the kvvfs state. This is only
|
|
intended for use in testing and development, not part of the
|
|
public API.
|
|
*/
|
|
const cols = Object.assign(Object.create(null),{
|
|
rowid: {type: 'INTEGER'},
|
|
name: {type: 'TEXT'},
|
|
nRef: {type: 'INTEGER'},
|
|
nOpen: {type: 'INTEGER'},
|
|
isTransient: {type: 'INTEGER'},
|
|
dbSize: {type: 'INTEGER'}
|
|
});
|
|
Object.keys(cols).forEach((v,i)=>cols[v].colId = i);
|
|
|
|
const VT = sqlite3.vtab;
|
|
const ProtoCursor = Object.assign(Object.create(null),{
|
|
row: function(){
|
|
return cache.storagePool[this.names[this.rowid]];
|
|
}
|
|
});
|
|
Object.assign(Object.create(ProtoCursor),{
|
|
rowid: 0,
|
|
names: Object.keys(cache.storagePool)
|
|
.filter(v=>!cache.rxJournalSuffix.test(v))
|
|
});
|
|
const cursorState = function(cursor, reset){
|
|
const o = (cursor instanceof capi.sqlite3_vtab_cursor)
|
|
? cursor
|
|
: VT.xCursor.get(cursor);
|
|
if( reset || !o.vTabState ){
|
|
o.vTabState = Object.assign(Object.create(ProtoCursor),{
|
|
rowid: 0,
|
|
names: Object.keys(cache.storagePool)
|
|
.filter(v=>!cache.rxJournalSuffix.test(v))
|
|
});
|
|
}
|
|
return o.vTabState;
|
|
};
|
|
|
|
const dbg = 1 ? ()=>{} : (...args)=>debug("vtab",...args);
|
|
|
|
const theModule = function f(){
|
|
return f.mod ??= new sqlite3.capi.sqlite3_module().setupModule({
|
|
catchExceptions: true,
|
|
methods: {
|
|
xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){
|
|
dbg("xConnect");
|
|
try{
|
|
const xcol = [];
|
|
Object.keys(cols).forEach((k)=>{
|
|
xcol.push(k+" "+cols[k].type);
|
|
});
|
|
const rc = capi.sqlite3_declare_vtab(
|
|
pDb, "CREATE TABLE ignored("+xcol.join(',')+")"
|
|
);
|
|
if(0===rc){
|
|
const t = VT.xVtab.create(ppVtab);
|
|
util.assert(
|
|
(t === VT.xVtab.get(wasm.peekPtr(ppVtab))),
|
|
"output pointer check failed"
|
|
);
|
|
}
|
|
return rc;
|
|
}catch(e){
|
|
return VT.xError('xConnect', e, capi.SQLITE_ERROR);
|
|
}
|
|
},
|
|
xCreate: wasm.ptr.null, // eponymous only
|
|
//xCreate: true, // copy xConnect, i.e. also eponymous only
|
|
xDisconnect: function(pVtab){
|
|
dbg("xDisconnect",...arguments);
|
|
VT.xVtab.dispose(pVtab);
|
|
return 0;
|
|
},
|
|
xOpen: function(pVtab, ppCursor){
|
|
dbg("xOpen",...arguments);
|
|
VT.xCursor.create(ppCursor);
|
|
return 0;
|
|
},
|
|
xClose: function(pCursor){
|
|
dbg("xClose",...arguments);
|
|
const c = VT.xCursor.unget(pCursor);
|
|
delete c.vTabState;
|
|
c.dispose();
|
|
return 0;
|
|
},
|
|
xNext: function(pCursor){
|
|
dbg("xNext",...arguments);
|
|
const c = VT.xCursor.get(pCursor);
|
|
++cursorState(c).rowid;
|
|
return 0;
|
|
},
|
|
xColumn: function(pCursor, pCtx, iCol){
|
|
dbg("xColumn",...arguments);
|
|
//const c = VT.xCursor.get(pCursor);
|
|
const st = cursorState(pCursor);
|
|
const store = st.row();
|
|
util.assert(store, "Unexpected xColumn call");
|
|
switch(iCol){
|
|
case cols.rowid.colId:
|
|
capi.sqlite3_result_int(pCtx, st.rowid);
|
|
break;
|
|
case cols.name.colId:
|
|
capi.sqlite3_result_text(pCtx, store.jzClass, -1, capi.SQLITE_TRANSIENT);
|
|
break;
|
|
case cols.nRef.colId:
|
|
capi.sqlite3_result_int(pCtx, store.refc);
|
|
break;
|
|
case cols.nOpen.colId:
|
|
capi.sqlite3_result_int(pCtx, store.files.length);
|
|
break;
|
|
case cols.isTransient.colId:
|
|
capi.sqlite3_result_int(pCtx, !!store.deleteAtRefc0);
|
|
break;
|
|
case cols.dbSize.colId:
|
|
capi.sqlite3_result_int(pCtx, storageGetDbSize(store));
|
|
break;
|
|
default:
|
|
capi.sqlite3_result_error(pCtx, "Invalid column id: "+iCol);
|
|
return capi.SQLITE_RANGE;
|
|
}
|
|
return 0;
|
|
},
|
|
xRowid: function(pCursor, ppRowid64){
|
|
dbg("xRowid",...arguments);
|
|
const st = cursorState(pCursor);
|
|
VT.xRowid(ppRowid64, st.rowid);
|
|
return 0;
|
|
},
|
|
xEof: function(pCursor){
|
|
const st = cursorState(pCursor);
|
|
dbg("xEof?="+(!st.row()),...arguments);
|
|
return !st.row();
|
|
},
|
|
xFilter: function(pCursor, idxNum, idxCStr,
|
|
argc, argv/* [sqlite3_value* ...] */){
|
|
dbg("xFilter",...arguments);
|
|
const st = cursorState(pCursor, true);
|
|
return 0;
|
|
},
|
|
xBestIndex: function(pVtab, pIdxInfo){
|
|
dbg("xBestIndex",...arguments);
|
|
//const t = VT.xVtab.get(pVtab);
|
|
const pii = new capi.sqlite3_index_info(pIdxInfo);
|
|
pii.$estimatedRows = cache.storagePool.size;
|
|
pii.$estimatedCost = 1.0;
|
|
pii.dispose();
|
|
return 0;
|
|
}
|
|
}
|
|
})/*setupModule*/;
|
|
}/*theModule()*/;
|
|
|
|
sqlite3.kvvfs.create_module = function(pDb, name="sqlite_kvvfs"){
|
|
return capi.sqlite3_create_module(pDb, name, theModule(),
|
|
wasm.ptr.null);
|
|
};
|
|
|
|
}/* virtual table */
|
|
|
|
//#if 0
|
|
/**
|
|
The idea here is a simpler wrapper for listening to kvvfs
|
|
changes. Clients would override its onXyz() event methods
|
|
instead of providing callbacks for sqlite3.kvvfs.listen(), the
|
|
main (only?) benefit of which is that this class would do the
|
|
sorting-out and validation of event state before calling the
|
|
overloaded callbacks.
|
|
*/
|
|
kvvfs.Listener = class KvvfsListener {
|
|
#store;
|
|
#listener;
|
|
|
|
constructor(opt){
|
|
this.#listenTo(opt);
|
|
}
|
|
|
|
#event(ev){
|
|
switch(ev.type){
|
|
case 'open': this.onOpen(ev.data); break;
|
|
case 'close': this.onClose(ev.data); break;
|
|
case 'sync': this.onSync(ev.data); break;
|
|
case 'delete':
|
|
switch(ev.data){
|
|
case 'jrnl': break;
|
|
default:{
|
|
const n = +ev.data;
|
|
util.assert( n>0, "Expecting positive db page number" );
|
|
this.onPageChange(n, null);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 'write':{
|
|
const key = ev.data[0], val = ev.data[1];
|
|
switch( key ){
|
|
case 'jrnl': break;
|
|
case 'sz':{
|
|
const sz = +val;
|
|
util.assert( sz>0, "Expecting a db page number" );
|
|
this.onSizeChange(sz);
|
|
break;
|
|
}
|
|
default:
|
|
T.assert( +key>0, "Expecting a positive db page number" );
|
|
this.onPageChange(+key, val);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#listenTo(opt){
|
|
if(this.#listener){
|
|
sqlite3_js_kvvfs_unlisten(this.#listener);
|
|
this.#listener = undefined;
|
|
}
|
|
const eventHandler = async function(ev){this.event(ev)}.bind(this);
|
|
const li = Object.assign(
|
|
{ /* Defaults */
|
|
reserve: false,
|
|
includeJournal: false,
|
|
decodePages: false,
|
|
storage: null
|
|
},
|
|
(/*client options*/opt||{}),
|
|
{/*hard-coded options*/
|
|
events: Object.assign(Object.create(null),{
|
|
'open': eventHandler,
|
|
'close': eventHandler,
|
|
'write': eventHandler,
|
|
'delete': eventHandler,
|
|
'sync': eventHandler
|
|
})
|
|
}
|
|
);
|
|
sqlite3_js_kvvfs_listen(li);
|
|
this.#listener = li;
|
|
}
|
|
|
|
async onSizeChange(sz){}
|
|
async onPageChange(pgNo,content/*null for delete*/){}
|
|
async onSync(mode/*true=xSync, false=xFileControl*/){}
|
|
async onOpen(count){}
|
|
async onClose(count){}
|
|
}/*KvvfsListener*/;
|
|
//#/if nope
|
|
|
|
})/*globalThis.sqlite3ApiBootstrap.initializers*/;
|
|
//#savepoint rollback
|
|
//#/if not omit-kvvfs
|