aboutsummaryrefslogtreecommitdiff
path: root/ext/wasm/sqlite3-worker1-promiser.js
diff options
context:
space:
mode:
authorstephan <stephan@noemail.net>2022-08-24 05:59:23 +0000
committerstephan <stephan@noemail.net>2022-08-24 05:59:23 +0000
commit9a34509a06ad893ae3ac786363ebf8d29b3e3a7c (patch)
tree45fe8feae7e32d3a5db59c969857486de728d168 /ext/wasm/sqlite3-worker1-promiser.js
parentefeee19a958b905cc8e939e54b2959089bb89108 (diff)
downloadsqlite-9a34509a06ad893ae3ac786363ebf8d29b3e3a7c.tar.gz
sqlite-9a34509a06ad893ae3ac786363ebf8d29b3e3a7c.zip
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
Diffstat (limited to 'ext/wasm/sqlite3-worker1-promiser.js')
-rw-r--r--ext/wasm/sqlite3-worker1-promiser.js232
1 files changed, 232 insertions, 0 deletions
diff --git a/ext/wasm/sqlite3-worker1-promiser.js b/ext/wasm/sqlite3-worker1-promiser.js
new file mode 100644
index 000000000..c01ed9a5c
--- /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
+};