diff --git a/ext/wasm/api/sqlite3-api-cleanup.js b/ext/wasm/api/sqlite3-api-cleanup.js index d989faccaf..ed6b8c40ea 100644 --- a/ext/wasm/api/sqlite3-api-cleanup.js +++ b/ext/wasm/api/sqlite3-api-cleanup.js @@ -18,44 +18,24 @@ 'use strict'; if('undefined' !== typeof Module){ // presumably an Emscripten build /** - Replace sqlite3ApiBootstrap() with a variant which plugs in the - Emscripten-based config for all config options which the client - does not provide. + Install a suitable default configuration for sqlite3ApiBootstrap(). */ - const SAB = self.sqlite3ApiBootstrap; - self.sqlite3ApiBootstrap = function(apiConfig){ - apiConfig = apiConfig || {}; - const configDefaults = { - Module: Module /* ==> Emscripten-style Module object. Currently - needs to be exposed here for test code. NOT part - of the public API. */, - exports: Module['asm'], - memory: Module.wasmMemory /* gets set if built with -sIMPORT_MEMORY */ - }; - const config = {}; - Object.keys(configDefaults).forEach(function(k){ - config[k] = Object.getOwnPropertyDescriptor(apiConfig, k) - ? apiConfig[k] : configDefaults[k]; - }); - // Copy over any properties apiConfig defines but configDefaults does not... - Object.keys(apiConfig).forEach(function(k){ - if(!Object.getOwnPropertyDescriptor(config, k)){ - config[k] = apiConfig[k]; - } - }); - return SAB(config); - }; + const SABC = self.sqlite3ApiBootstrap.defaultConfig; + SABC.Module = Module /* ==> Current needs to be exposed here for test code. NOT part + of the public API. */; + SABC.exports = Module['asm']; + SABC.memory = Module.wasmMemory /* gets set if built with -sIMPORT_MEMORY */; /** For current (2022-08-22) purposes, automatically call sqlite3ApiBootstrap(). That decision will be revisited at some point, as we really want client code to be able to call this to - configure certain parts. If the global sqliteApiConfig property - is available, it is assumed to be a config object for - sqlite3ApiBootstrap(). + configure certain parts. Clients may modify + self.sqlite3ApiBootstrap.defaultConfig to tweak the default + configuration used by a no-args call to sqlite3ApiBootstrap(). */ //console.warn("self.sqlite3ApiConfig = ",self.sqlite3ApiConfig); - const sqlite3 = self.sqlite3ApiBootstrap(self.sqlite3ApiConfig || Object.create(null)); + const sqlite3 = self.sqlite3ApiBootstrap(); delete self.sqlite3ApiBootstrap; if(self.location && +self.location.port > 1024){ @@ -67,4 +47,9 @@ if('undefined' !== typeof Module){ // presumably an Emscripten build delete sqlite3.capi.util /* arguable, but these are (currently) internal-use APIs */; //console.warn("Module.sqlite3 =",Module.sqlite3); Module.sqlite3 = sqlite3 /* Currently needed by test code and sqlite3-worker1.js */; +}else{ + console.warn("This is not running in an Emscripten module context, so", + "self.sqlite3ApiBootstrap() is _not_ being called due to lack", + "of config info for the WASM environment.", + "It must be called manually."); } diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index 1a38f0343a..d92b948fce 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -93,16 +93,16 @@ The config object properties include: - - `Module`: Emscripten-style module object. Currently only required + - `Module`[^1]: Emscripten-style module object. Currently only required by certain test code and is _not_ part of the public interface. (TODO: rename this to EmscriptenModule to be more explicit.) - - `exports`: the "exports" object for the current WASM + - `exports`[^1]: the "exports" object for the current WASM environment. In an Emscripten build, this should be set to `Module['asm']`. - - `memory`: optional WebAssembly.Memory object, defaulting to - `exports.memory`. In Emscripten environments this should be set + - `memory`[^1]: optional WebAssembly.Memory object, defaulting to + `exports.memory`. In Emscripten environments this should be set to `Module.wasmMemory` if the build uses `-sIMPORT_MEMORY`, or be left undefined/falsy to default to `exports.memory` when using WASM-exported memory. @@ -120,20 +120,26 @@ the `free(3)`-compatible routine for the WASM environment. Defaults to `"free"`. - - `persistentDirName`: if the environment supports persistent storage, this + - `persistentDirName`[^1]: if the environment supports persistent storage, this directory names the "mount point" for that directory. It must be prefixed by `/` and may currently contain only a single directory-name part. Using the root directory name is not supported by any current persistent backend. + + + [^1] = This property may optionally be a function, in which case this + function re-assigns it to the value returned from that function, + enabling delayed evaluation. + */ -self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(apiConfig){ - 'use strict'; - +'use strict'; +self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( + apiConfig = (sqlite3ApiBootstrap.defaultConfig || self.sqlite3ApiConfig) +){ if(sqlite3ApiBootstrap.sqlite3){ /* already initalized */ console.warn("sqlite3ApiBootstrap() called multiple times.", "Config and external initializers are ignored on calls after the first."); return sqlite3ApiBootstrap.sqlite3; } - apiConfig = apiConfig || {}; const config = Object.create(null); { @@ -158,6 +164,16 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(apiConfig){ }); } + [ + // If any of these config options are functions, replace them with + // the result of calling that function... + 'Module', 'exports', 'memory', 'persistentDirName' + ].forEach((k)=>{ + if('function' === typeof config[k]){ + config[k] = config[k](); + } + }); + /** Throws a new Error, the message of which is the concatenation all args with a space between each. */ const toss = (...args)=>{throw new Error(args.join(' '))}; @@ -750,4 +766,16 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(apiConfig){ this array is deleted. */ self.sqlite3ApiBootstrap.initializers = []; -self.sqlite3ApiBootstrap.sqlite3 = undefined /* installed at first call */; +/** + Client code may assign sqlite3ApiBootstrap.defaultConfig an + object-type value before calling sqlite3ApiBootstrap() (without + arguments) in order to tell that call to use this object as its + default config value. The intention of this is to provide + downstream clients with a reasonably flexible approach for plugging in + an environment-suitable configuration without having to define a new + global-scope symbol. +*/ +self.sqlite3ApiBootstrap.defaultConfig = Object.create(null); +/** Placeholder: gets installed by the first call to + self.sqlite3ApiBootstrap(). */ +self.sqlite3ApiBootstrap.sqlite3 = undefined; diff --git a/ext/wasm/api/sqlite3-api-worker1.js b/ext/wasm/api/sqlite3-api-worker1.js index 2e8d5a25d0..39263a4abe 100644 --- a/ext/wasm/api/sqlite3-api-worker1.js +++ b/ext/wasm/api/sqlite3-api-worker1.js @@ -92,11 +92,8 @@ sqlite3.initWorker1API = function(){ defaultDb: undefined, idSeq: 0, idMap: new WeakMap, - open: function(arg){ - // TODO? if arg is a filename, look for a db in this.dbs with the - // same filename and close/reopen it (or just pass it back as is?). - if(!arg && this.defaultDb) return this.defaultDb; - const db = (Array.isArray(arg) ? new DB(...arg) : new DB(arg)); + open: function(opt){ + const db = new DB(opt.filename); this.dbs[getDbId(db)] = db; if(!this.defaultDb) this.defaultDb = db; return db; @@ -169,14 +166,26 @@ sqlite3.initWorker1API = function(){ envelope to other calls in this API to tell them which db to use. If it is not provided to future calls, they will default to operating on the first-opened db. + + persistent: prepend sqlite3.capi.sqlite3_web_persistent_dir() + to the given filename so that it is stored + in persistent storage _if_ the environment supports it. + If persistent storage is not supported, the filename + is used as-is. } */ open: function(ev){ - const oargs = [], args = (ev.args || {}); + const oargs = Object.create(null), args = (ev.args || Object.create(null)); if(args.simulateError){ // undocumented internal testing option toss("Throwing because of simulateError flag."); } - if(args.filename) oargs.push(args.filename); + if(args.persistent && args.filename){ + oargs.filaname = sqlite3.capi.sqlite3_web_persistent_dir() + args.filename; + }else if('' === args.filename){ + oargs.filename = args.filename; + }else{ + oargs.filename = args.filename || ':memory:'; + } const db = wState.open(oargs); return { filename: db.filename, @@ -184,15 +193,15 @@ sqlite3.initWorker1API = function(){ }; }, /** - Proxy for DB.close(). ev.args may either be a boolean or an - object with an `unlink` property. If that value is truthy then - the db file (if the db is currently open) will be unlinked from - the virtual filesystem, else it will be kept intact. The - result object is: + Proxy for DB.close(). ev.args may be elided or an object with + an `unlink` property. If that value is truthy then the db file + (if the db is currently open) will be unlinked from the virtual + filesystem, else it will be kept intact. The result object is: { filename: db filename _if_ the db is opened when this is called, else the undefined value + dbId: the ID of the closed b, or undefined if none is closed } It does not error if the given db is already closed or no db is @@ -356,6 +365,7 @@ sqlite3.initWorker1API = function(){ dbId: DB handle ID, [messageId: if set in the inbound message], result: { + operation: "inbound message's 'type' value", message: error string, errorClass: class name of the error type, input: ev.data @@ -378,6 +388,7 @@ sqlite3.initWorker1API = function(){ }catch(err){ evType = 'error'; result = { + operation: ev.type, message: err.message, errorClass: err.name, input: ev @@ -405,7 +416,7 @@ sqlite3.initWorker1API = function(){ result: result }, wMsgHandler.xfer); }; - setTimeout(()=>self.postMessage({type:'sqlite3-api',result:'worker1-ready'}), 0); + self.postMessage({type:'sqlite3-api',result:'worker1-ready'}); }.bind({self, sqlite3}); }); diff --git a/ext/wasm/sqlite3-worker1-promiser.js b/ext/wasm/sqlite3-worker1-promiser.js new file mode 100644 index 0000000000..c01ed9a5c6 --- /dev/null +++ b/ext/wasm/sqlite3-worker1-promiser.js @@ -0,0 +1,232 @@ +/* + 2022-08-24 + + 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 implements a Promise-based proxy for the sqlite3 Worker + API #1. It is intended to be included either from the main thread or + a Worker, but only if (A) the environment supports nested Workers + and (B) it's _not_ a Worker which loads the sqlite3 WASM/JS + module. This file's features will load that module and provide a + slightly simpler client-side interface than the slightly-lower-level + Worker API does. + + This script necessarily exposes on global symbol, but clients may + freely `delete` that symbol after calling it. +*/ +'use strict'; +/** + Configures an sqlite3 Worker API #1 Worker such that it can be + manipulated via a Promise-based interface and returns a factory + function which returns Promises for communicating with the worker. + This proxy has an _almost_ identical interface to the normal + worker API, with any exceptions noted below. + + It requires a configuration object with the following properties: + + - `worker` (required): a Worker instance which loads + `sqlite3-worker1.js` or a functional equivalent. Note that this + function replaces the worker.onmessage property. This property + may alternately be a function, in which case this function + re-assigns this property with the result of calling that + function, enabling delayed instantiation of a Worker. + + - `onready` (optional, but...): this callback is called with no + arguments when the worker fires its initial + 'sqlite3-api'/'worker1-ready' message, which it does when + sqlite3.initWorker1API() completes its initialization. This is + the simplest way to tell the worker to kick of work at the + earliest opportunity. + + - `onerror` (optional): a callback to pass error-type events from + the worker. The object passed to it will be the error message + payload from the worker. This is _not_ the same as the + worker.onerror property! + + - `onunhandled` (optional): a callback which gets passed the + message event object for any worker.onmessage() events which + are not handled by this proxy. Ideally that "should" never + happen, as this proxy aims to handle all known message types. + + - `generateMessageId` (optional): a function which, when passed + an about-to-be-posted message object, generates a _unique_ + message ID for the message, which this API then assigns as the + messageId property of the message. It _must_ generate unique + IDs so that dispatching can work. If not defined, a default + generator is used. + + - `dbId` (optional): is the database ID to be used by the + worker. This must initially be unset or a falsy value. The + first `open` message sent to the worker will cause this config + entry to be assigned to the ID of the opened database. That ID + "should" be set as the `dbId` property of the message sent in + future requests, so that the worker uses that database. + However, if the worker is not given an explicit dbId, it will + use the first-opened database by default. If client code needs + to work with multiple database IDs, the client-level code will + need to juggle those themselves. A `close` message will clear + this property if it matches the ID of the closed db. Potential + TODO: add a config callback specifically for reporting `open` + and `close` message results, so that clients may track those + values. + + - `debug` (optional): a console.debug()-style function for logging + information about messages. + + + This function returns a stateful factory function with the following + interfaces: + + - Promise function(messageType, messageArgs) + - Promise function({message object}) + + The first form expects the "type" and "args" values for a Worker + message. The second expects an object in the form {type:..., + args:...} plus any other properties the client cares to set. This + function will always set the messageId property on the object, + even if it's already set, and will set the dbId property to + config.dbId if it is _not_ set in the message object. + + The function throws on error. + + The function installs a temporarily message listener, posts a + message to the configured Worker, and handles the message's + response via the temporary message listener. The then() callback + of the returned Promise is passed the `message.data` property from + the resulting message, i.e. the payload from the worker, stripped + of the lower-level event state which the onmessage() handler + receives. + + Example usage: + + ``` + const config = {...}; + const eventPromiser = sqlite3Worker1Promiser(config); + eventPromiser('open', {filename:"/foo.db"}).then(function(msg){ + console.log("open response",msg); // => {type:'open', result: {filename:'/foo.db'}, ...} + // Recall that config.dbId will be set for the first 'open' + // call and cleared for a matching 'close' call. + }); + eventPromiser({type:'close'}).then((msg)=>{ + console.log("open response",msg); // => {type:'open', result: {filename:'/foo.db'}, ...} + // Recall that config.dbId will be used by default for the message's dbId if + // none is explicitly provided, and a 'close' op will clear config.dbId if it + // closes that exact db. + }); + ``` + + Differences from Worker API #1: + + - exec's {callback: STRING} option does not work via this + interface (it triggers an exception), but {callback: function} + does and works exactly like the STRING form does in the Worker: + the callback is called one time for each row of the result set + and once more, at the end, passed only `null`, to indicate that + the end of the result set has been reached. Note that the rows + arrive via worker-posted messages, with all the implications + of that. + + + TODO?: a config option which causes it to queue up events to fire + one at a time and flush the event queue on the first error. The + main use for this is test runs which must fail at the first error. +*/ +self.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){ + // Inspired by: https://stackoverflow.com/a/52439530 + let idNumber = 0; + const handlerMap = Object.create(null); + const noop = function(){}; + const err = config.onerror || noop; + const debug = config.debug || noop; + const genMsgId = config.generateMessageId || function(msg){ + return msg.type+'#'+(++idNumber); + }; + const toss = (...args)=>{throw new Error(args.join(' '))}; + if('function'===typeof config.worker) config.worker = config.worker(); + config.worker.onmessage = function(ev){ + ev = ev.data; + debug('worker1.onmessage',ev); + let msgHandler = handlerMap[ev.messageId]; + if(!msgHandler){ + if(ev && 'sqlite3-api'===ev.type && 'worker1-ready'===ev.result) { + /*fired one time when the Worker1 API initializes*/ + if(config.onready) config.onready(); + return; + } + msgHandler = handlerMap[ev.type] /* check for exec per-row callback */; + if(msgHandler && msgHandler.onrow){ + msgHandler.onrow(ev.row); + return; + } + if(config.onunhandled) config.onunhandled(arguments[0]); + else err("sqlite3Worker1Promiser() unhandled worker message:",ev); + return; + } + delete handlerMap[ev.messageId]; + switch(ev.type){ + case 'error': + msgHandler.reject(ev); + return; + case 'open': + if(!config.dbId) config.dbId = ev.dbId; + break; + case 'close': + if(config.dbId === ev.dbId) config.dbId = undefined; + break; + default: + break; + } + msgHandler.resolve(ev); + }/*worker.onmessage()*/; + return function(/*(msgType, msgArgs) || (msg)*/){ + let msg; + if(1===arguments.length){ + msg = arguments[0]; + }else if(2===arguments.length){ + msg = { + type: arguments[0], + args: arguments[1] + }; + }else{ + toss("Invalid arugments for sqlite3Worker1Promiser()-created factory."); + } + if(!msg.dbId) msg.dbId = config.dbId; + msg.messageId = genMsgId(msg); + msg.departureTime = performance.now(); + const proxy = Object.create(null); + proxy.message = msg; + let cbId /* message handler ID for exec on-row callback proxy */; + if('exec'===msg.type && msg.args){ + if('function'===typeof msg.args.callback){ + cbId = genMsgId(msg)+':row'; + proxy.onrow = msg.args.callback; + msg.args.callback = cbId; + handlerMap[cbId] = proxy; + }else if('string' === typeof msg.args.callback){ + toss("exec callback may not be a string when using the Promise interface."); + } + } + //debug("requestWork", msg); + const p = new Promise(function(resolve, reject){ + proxy.resolve = resolve; + proxy.reject = reject; + handlerMap[msg.messageId] = proxy; + debug("Posting",msg.type,"message to Worker dbId="+(config.dbId||'default')+':',msg); + config.worker.postMessage(msg); + }); + if(cbId) p.finally(()=>delete handlerMap[cbId]); + return p; + }; +}/*sqlite3Worker1Promiser()*/; +self.sqlite3Worker1Promiser.defaultConfig = { + worker: ()=>new Worker('sqlite3-worker1.js'), + onerror: console.error.bind(console), + dbId: undefined +}; diff --git a/ext/wasm/testing-worker-promise.js b/ext/wasm/testing-worker-promise.js deleted file mode 100644 index c60c6c2882..0000000000 --- a/ext/wasm/testing-worker-promise.js +++ /dev/null @@ -1,139 +0,0 @@ -/* - 2022-08-23 - - 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. - - *********************************************************************** - - - UNDER CONSTRUCTION: a Promise-based proxy for for the sqlite3 Worker - #1 API. -*/ -'use strict'; -(function(){ - const T = self.SqliteTestUtil; - const DbState = { - id: undefined - }; - const eOutput = document.querySelector('#test-output'); - const log = console.log.bind(console); - const logHtml = async function(cssClass,...args){ - log.apply(this, args); - const ln = document.createElement('div'); - if(cssClass) ln.classList.add(cssClass); - ln.append(document.createTextNode(args.join(' '))); - eOutput.append(ln); - }; - const warn = console.warn.bind(console); - const error = console.error.bind(console); - - let startTime; - const logEventResult = async function(evd){ - logHtml(evd.errorClass ? 'error' : '', - "response to",evd.messageId,"Worker time =", - (evd.workerRespondTime - evd.workerReceivedTime),"ms.", - "Round-trip event time =", - (performance.now() - evd.departureTime),"ms.", - (evd.errorClass ? evd.message : "") - ); - }; - - const testCount = async ()=>{ - logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms"); - }; - - // Inspiration: https://stackoverflow.com/a/52439530 - const worker = new Worker("sqlite3-worker1.js"); - worker.onerror = function(event){ - error("worker.onerror",event); - }; - const WorkerPromiseHandler = Object.create(null); - WorkerPromiseHandler.nextId = function f(){ - return 'msg#'+(f._ = (f._ || 0) + 1); - }; - - /** Posts a worker message as {type:eventType, data:eventData}. */ - const requestWork = async function(eventType, eventData){ - //log("requestWork", eventType, eventData); - T.assert(eventData && 'object'===typeof eventData); - /* ^^^ that is for the testing and messageId-related code, not - a hard requirement of all of the Worker-exposed APIs. */ - const wph = WorkerPromiseHandler; - const msgId = wph.nextId(); - const proxy = wph[msgId] = Object.create(null); - proxy.promise = new Promise(function(resolve, reject){ - proxy.resolve = resolve; - proxy.reject = reject; - const msg = { - type: eventType, - args: eventData, - dbId: DbState.id, - messageId: msgId, - departureTime: performance.now() - }; - log("Posting",eventType,"message to worker dbId="+(DbState.id||'default')+':',msg); - worker.postMessage(msg); - }); - log("Set up promise",proxy); - return proxy.promise; - }; - - - const runOneTest = async function(eventType, eventData, callback){ - T.assert(eventData && 'object'===typeof eventData); - /* ^^^ that is for the testing and messageId-related code, not - a hard requirement of all of the Worker-exposed APIs. */ - let p = requestWork(eventType, eventData); - if(callback) p.then(callback).finally(testCount); - return p; - }; - - const runTests = async function(){ - logHtml('', - "Sending 'open' message and waiting for its response before continuing."); - startTime = performance.now(); - runOneTest('open', { - filename:'testing2.sqlite3', - simulateError: 0 /* if true, fail the 'open' */ - }, function(ev){ - log("then open result",ev); - T.assert('testing2.sqlite3'===ev.result.filename) - .assert(ev.dbId) - .assert(ev.messageId) - .assert(DbState.id === ev.dbId); - }).catch((err)=>error("error response:",err)); - }; - - worker.onmessage = function(ev){ - ev = ev.data; - (('error'===ev.type) ? error : log)('worker.onmessage',ev); - const msgHandler = WorkerPromiseHandler[ev.messageId]; - if(!msgHandler){ - if('worker1-ready'===ev.result) { - /*sqlite3-api/worker1-ready is fired when the Worker1 API initializes*/ - self.sqlite3TestModule.setStatus(null)/*hide the HTML-side is-loading spinner*/; - runTests(); - return; - } - error("Unhandled worker message:",ev); - return; - } - logEventResult(ev); - delete WorkerPromiseHandler[ev.messageId]; - if('error'===ev.type){ - msgHandler.reject(ev); - } - else{ - if(!DbState.id && ev.dbId) DbState.id = ev.dbId; - msgHandler.resolve(ev); // async, so testCount() results on next line are out of order - //testCount(); - } - }; - - log("Init complete, but async init bits may still be running."); -})(); diff --git a/ext/wasm/testing-worker-promise.html b/ext/wasm/testing-worker1-promiser.html similarity index 91% rename from ext/wasm/testing-worker-promise.html rename to ext/wasm/testing-worker1-promiser.html index eab30c301f..9af809d9ed 100644 --- a/ext/wasm/testing-worker-promise.html +++ b/ext/wasm/testing-worker1-promiser.html @@ -28,6 +28,7 @@