From 9a34509a06ad893ae3ac786363ebf8d29b3e3a7c Mon Sep 17 00:00:00 2001 From: stephan Date: Wed, 24 Aug 2022 05:59:23 +0000 Subject: [PATCH] More work on how to configure the sqlite3 JS API bootstrapping process from higher-level code. Initial version of sqlite3-worker1-promiser, a Promise-based proxy for the Worker API #1. FossilOrigin-Name: b030f321bd5a38cdd5d6f6735f201afa62d30d2b0ba02e67f055b4895553a878 --- ext/wasm/api/sqlite3-api-cleanup.js | 45 ++-- ext/wasm/api/sqlite3-api-prologue.js | 48 +++- ext/wasm/api/sqlite3-api-worker1.js | 37 ++- ext/wasm/sqlite3-worker1-promiser.js | 232 ++++++++++++++++++ ext/wasm/testing-worker-promise.js | 139 ----------- ...ise.html => testing-worker1-promiser.html} | 3 +- ext/wasm/testing-worker1-promiser.js | 214 ++++++++++++++++ manifest | 21 +- manifest.uuid | 2 +- 9 files changed, 537 insertions(+), 204 deletions(-) create mode 100644 ext/wasm/sqlite3-worker1-promiser.js delete mode 100644 ext/wasm/testing-worker-promise.js rename ext/wasm/{testing-worker-promise.html => testing-worker1-promiser.html} (91%) create mode 100644 ext/wasm/testing-worker1-promiser.js 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 @@
- + + diff --git a/ext/wasm/testing-worker1-promiser.js b/ext/wasm/testing-worker1-promiser.js new file mode 100644 index 0000000000..9475df411a --- /dev/null +++ b/ext/wasm/testing-worker1-promiser.js @@ -0,0 +1,214 @@ +/* + 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. + + *********************************************************************** + + Demonstration of the sqlite3 Worker API #1 Promiser: a Promise-based + proxy for for the sqlite3 Worker #1 API. +*/ +'use strict'; +(function(){ + const T = self.SqliteTestUtil; + const eOutput = document.querySelector('#test-output'); + const warn = console.warn.bind(console); + const error = console.error.bind(console); + 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); + }; + + 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"); + }; + + //why is this triggered even when we catch() a Promise? + //window.addEventListener('unhandledrejection', function(event) { + // warn('unhandledrejection',event); + //}); + + const promiserConfig = { + worker: ()=>{ + const w = new Worker("sqlite3-worker1.js"); + w.onerror = (event)=>error("worker.onerror",event); + return w; + }, + //debug: (...args)=>console.debug('worker debug',...args), + onunhandled: function(ev){ + error("Unhandled worker message:",ev.data); + }, + onready: function(){ + self.sqlite3TestModule.setStatus(null)/*hide the HTML-side is-loading spinner*/; + runTests(); + }, + onerror: function(ev){ + error("worker1 error:",ev); + } + }; + const workerPromise = self.sqlite3Worker1Promiser(promiserConfig); + delete self.sqlite3Worker1Promiser; + + const wtest = async function(msgType, msgArgs, callback){ + let p = workerPromise({type: msgType, args:msgArgs}); + 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(); + wtest('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(promiserConfig.dbId === ev.dbId); + }).then(runTests2) + .catch((err)=>error("error response:",err)); + }; + + const runTests2 = async function(){ + const mustNotReach = ()=>toss("This is not supposed to be reached."); + + await wtest('exec',{ + sql: ["create table t(a,b)", + "insert into t(a,b) values(1,2),(3,4),(5,6)" + ].join(';'), + multi: true, + resultRows: [], columnNames: [] + }, function(ev){ + ev = ev.result; + T.assert(0===ev.resultRows.length) + .assert(0===ev.columnNames.length); + }); + + await wtest('exec',{ + sql: 'select a a, b b from t order by a', + resultRows: [], columnNames: [], + }, function(ev){ + ev = ev.result; + T.assert(3===ev.resultRows.length) + .assert(1===ev.resultRows[0][0]) + .assert(6===ev.resultRows[2][1]) + .assert(2===ev.columnNames.length) + .assert('b'===ev.columnNames[1]); + }); + + await wtest('exec',{ + sql: 'select a a, b b from t order by a', + resultRows: [], columnNames: [], + rowMode: 'object' + }, function(ev){ + ev = ev.result; + T.assert(3===ev.resultRows.length) + .assert(1===ev.resultRows[0].a) + .assert(6===ev.resultRows[2].b) + }); + + await wtest( + 'exec', + {sql:'intentional_error'}, + mustNotReach + ).catch((e)=>{ + warn("Intentional error:",e); + // Why does the browser report console.error "Uncaught (in + // promise)" when we catch(), and does so _twice_ if we don't + // catch()? According to all docs, that error must be supressed + // if we explicitly catch(). + }); + + await wtest('exec',{ + sql:'select 1 union all select 3', + resultRows: [], + //rowMode: 'array', // array is the default in the Worker interface + }, function(ev){ + ev = ev.result; + T.assert(2 === ev.resultRows.length) + .assert(1 === ev.resultRows[0][0]) + .assert(3 === ev.resultRows[1][0]); + }); + + const resultRowTest1 = function f(row){ + if(undefined === f.counter) f.counter = 0; + if(row) ++f.counter; + //log("exec() result row:",row); + T.assert(null===row || 'number' === typeof row.b); + }; + await wtest('exec',{ + sql: 'select a a, b b from t order by a', + callback: resultRowTest1, + rowMode: 'object' + }, function(ev){ + T.assert(3===resultRowTest1.counter); + resultRowTest1.counter = 0; + }); + + await wtest('exec',{ + multi: true, + sql:[ + 'pragma foreign_keys=0;', + // ^^^ arbitrary query with no result columns + 'select a, b from t order by a desc; select a from t;' + // multi-exec only honors results from the first + // statement with result columns (regardless of whether) + // it has any rows). + ], + rowMode: 1, + resultRows: [] + },function(ev){ + const rows = ev.result.resultRows; + T.assert(3===rows.length). + assert(6===rows[0]); + }); + + await wtest('exec',{sql: 'delete from t where a>3'}); + + await wtest('exec',{ + sql: 'select count(a) from t', + resultRows: [] + },function(ev){ + ev = ev.result; + T.assert(1===ev.resultRows.length) + .assert(2===ev.resultRows[0][0]); + }); + + /***** close() tests must come last. *****/ + await wtest('close',{unlink:true},function(ev){ + T.assert(!promiserConfig.dbId); + T.assert('string' === typeof ev.result.filename); + }); + + await wtest('close').then((ev)=>{ + T.assert(undefined === ev.result.filename); + log("That's all, folks!"); + }); + }/*runTests2()*/; + + + log("Init complete, but async init bits may still be running."); +})(); diff --git a/manifest b/manifest index d26d33dea6..05095d6b81 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C The\svery\sbasics\sof\sa\sPromise-based\sproxy\sfor\sthe\sWorker\s#1\sAPI.\sStill\srequires\sconsiderable\scleanup,\stesting,\sand\sa\ssolution\sfor\sthe\sexec-callback-via-event-type-name\sproblem. -D 2022-08-24T00:51:39.887 +C More\swork\son\show\sto\sconfigure\sthe\ssqlite3\sJS\sAPI\sbootstrapping\sprocess\sfrom\shigher-level\scode.\sInitial\sversion\sof\ssqlite3-worker1-promiser,\sa\sPromise-based\sproxy\sfor\sthe\sWorker\sAPI\s#1. +D 2022-08-24T05:59:23.851 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -481,12 +481,12 @@ F ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api 1ec3c73e7d66e95529c3c64ac3de F ext/wasm/api/README.md d876597edd2b9542b6ea031adaaff1c042076fde7b670b1dc6d8a87b28a6631b F ext/wasm/api/post-js-footer.js b64319261d920211b8700004d08b956a6c285f3b0bba81456260a713ed04900c F ext/wasm/api/post-js-header.js 0e853b78db83cb1c06b01663549e0e8b4f377f12f5a2d9a4a06cb776c003880b -F ext/wasm/api/sqlite3-api-cleanup.js acf798ce96285c0d52738466a96c9deb9d66647f711a40caecab90b5ce66ac3c +F ext/wasm/api/sqlite3-api-cleanup.js 4c353bdc2452623f0c1c1e55ae1a0589db9cbaed9756760bb15179ef9b58bc98 F ext/wasm/api/sqlite3-api-glue.js 67ca83974410961953eeaa1dfed3518530d68381729ed1d27f95122f5baeabd3 F ext/wasm/api/sqlite3-api-oo1.js f6dcaac3270182471f97efcfda25bd4a4ac1777b8ec52ebd1c6846721160e54c F ext/wasm/api/sqlite3-api-opfs.js 011799db398157cbd254264b6ebae00d7234b93d0e9e810345f213a5774993c0 -F ext/wasm/api/sqlite3-api-prologue.js 6e0e7787ed955ea2b6158e0bb7608f63b54236847700d183e49e1f10d0525b8f -F ext/wasm/api/sqlite3-api-worker1.js c9e4edb89f41a4fa65d136ae180c1bc0beb694eb95f7d9e6936fbb702914c160 +F ext/wasm/api/sqlite3-api-prologue.js 4a279604272851696975837534739597206c0800c8ea78810fe8e211ee101374 +F ext/wasm/api/sqlite3-api-worker1.js 9691e144a77490f482caa2c0f0bd38a8f955c6dc9c10b2f39c6491e817aefd8c F ext/wasm/api/sqlite3-wasi.h 25356084cfe0d40458a902afb465df8c21fc4152c1d0a59b563a3fba59a068f9 F ext/wasm/api/sqlite3-wasm.c 0d81282eaeff2a6e9fc5c28a388c5c5b45cf25a9393992fa511ac009b27df982 F ext/wasm/common/SqliteTestUtil.js eb96275bed43fdb364b7d65bcded0ca5e22aaacff120d593d1385f852f486247 @@ -508,9 +508,10 @@ F ext/wasm/scratchpad-opfs-main.js 69e960e9161f6412fd0c30f355d4112f1894d6609eb43 F ext/wasm/scratchpad-opfs-worker.html 66c1d15d678f3bd306373d76b61c6c8aef988f61f4a8dd40185d452f9c6d2bf5 F ext/wasm/scratchpad-opfs-worker.js 3ec2868c669713145c76eb5877c64a1b20741f741817b87c907a154b676283a9 F ext/wasm/scratchpad-opfs-worker2.js 5f2237427ac537b8580b1c659ff14ad2621d1694043eaaf41ae18dbfef2e48c0 +F ext/wasm/sqlite3-worker1-promiser.js 291f89330bc856e7ef8a321b4891554633c6407b52efc69c9b1d1b3e7c69d4a6 F ext/wasm/sqlite3-worker1.js 0c1e7626304543969c3846573e080c082bf43bcaa47e87d416458af84f340a9e -F ext/wasm/testing-worker-promise.html ba3d5423cfbdc96c332af3632dfcb61527ba8fd7e487b3bf3f07542f890c3e08 -F ext/wasm/testing-worker-promise.js c05c46a3a22b1910f6a1db11f3da6df701259eaa1277ddba085247b7f9059423 +F ext/wasm/testing-worker1-promiser.html 6eaec6e04a56cf24cf4fa8ef49d78ce8905dde1354235c9125dca6885f7ce893 w ext/wasm/testing-worker-promise.html +F ext/wasm/testing-worker1-promiser.js 3c13fda53cc8b5d148ae34f621eba99aff393d66718b216bfd9d3f9075dd83bc w ext/wasm/testing-worker-promise.js F ext/wasm/testing1.html 528001c7e32ee567abc195aa071fd9820cc3c8ffc9c8a39a75e680db05f0c409 F ext/wasm/testing1.js 2def7a86c52ff28b145cb86188d5c7a49d5993f9b78c50d140e1c31551220955 F ext/wasm/testing2.html a66951c38137ff1d687df79466351f3c734fa9c6d9cce71d3cf97c291b2167e3 @@ -2008,8 +2009,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 03b9db9b98cb36faa7de5a8a64d2e13c4aeaadfefb33ac92bb41056f6be3f121 -R 5b16a785d8c414a8eb9d618c8d9d8cea +P 1e447849fb65887e806e3348a8a68f70ea6802bc0a1e56c385a279f27cc0cdda +R 20d6ad983c84af7c7f2e33fe283b134d U stephan -Z fdaa5cf9f1bfd4215c6ebf07223819c1 +Z 996279c2387a16066b296229c9c99a7d # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 1b823c2233..26a4257f52 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -1e447849fb65887e806e3348a8a68f70ea6802bc0a1e56c385a279f27cc0cdda \ No newline at end of file +b030f321bd5a38cdd5d6f6735f201afa62d30d2b0ba02e67f055b4895553a878 \ No newline at end of file