1cd0df83cSstephan/* 2cd0df83cSstephan 2022-08-24 3cd0df83cSstephan 4cd0df83cSstephan The author disclaims copyright to this source code. In place of a 5cd0df83cSstephan legal notice, here is a blessing: 6cd0df83cSstephan 7cd0df83cSstephan * May you do good and not evil. 8cd0df83cSstephan * May you find forgiveness for yourself and forgive others. 9cd0df83cSstephan * May you share freely, never taking more than you give. 10cd0df83cSstephan 11cd0df83cSstephan *********************************************************************** 12cd0df83cSstephan 13cd0df83cSstephan This file implements a Promise-based proxy for the sqlite3 Worker 14cd0df83cSstephan API #1. It is intended to be included either from the main thread or 15cd0df83cSstephan a Worker, but only if (A) the environment supports nested Workers 16cd0df83cSstephan and (B) it's _not_ a Worker which loads the sqlite3 WASM/JS 17cd0df83cSstephan module. This file's features will load that module and provide a 18cd0df83cSstephan slightly simpler client-side interface than the slightly-lower-level 19cd0df83cSstephan Worker API does. 20cd0df83cSstephan 21cd0df83cSstephan This script necessarily exposes one global symbol, but clients may 22cd0df83cSstephan freely `delete` that symbol after calling it. 23cd0df83cSstephan*/ 24cd0df83cSstephan'use strict'; 25cd0df83cSstephan/** 26cd0df83cSstephan Configures an sqlite3 Worker API #1 Worker such that it can be 27cd0df83cSstephan manipulated via a Promise-based interface and returns a factory 28cd0df83cSstephan function which returns Promises for communicating with the worker. 29cd0df83cSstephan This proxy has an _almost_ identical interface to the normal 30cd0df83cSstephan worker API, with any exceptions documented below. 31cd0df83cSstephan 32cd0df83cSstephan It requires a configuration object with the following properties: 33cd0df83cSstephan 34cd0df83cSstephan - `worker` (required): a Worker instance which loads 35cd0df83cSstephan `sqlite3-worker1.js` or a functional equivalent. Note that the 36cd0df83cSstephan promiser factory replaces the worker.onmessage property. This 37cd0df83cSstephan config option may alternately be a function, in which case this 38cd0df83cSstephan function re-assigns this property with the result of calling that 39cd0df83cSstephan function, enabling delayed instantiation of a Worker. 40cd0df83cSstephan 41cd0df83cSstephan - `onready` (optional, but...): this callback is called with no 42cd0df83cSstephan arguments when the worker fires its initial 43cd0df83cSstephan 'sqlite3-api'/'worker1-ready' message, which it does when 44cd0df83cSstephan sqlite3.initWorker1API() completes its initialization. This is 45cd0df83cSstephan the simplest way to tell the worker to kick off work at the 46cd0df83cSstephan earliest opportunity. 47cd0df83cSstephan 48cd0df83cSstephan - `onunhandled` (optional): a callback which gets passed the 49cd0df83cSstephan message event object for any worker.onmessage() events which 50cd0df83cSstephan are not handled by this proxy. Ideally that "should" never 51cd0df83cSstephan happen, as this proxy aims to handle all known message types. 52cd0df83cSstephan 53cd0df83cSstephan - `generateMessageId` (optional): a function which, when passed an 54cd0df83cSstephan about-to-be-posted message object, generates a _unique_ message ID 55cd0df83cSstephan for the message, which this API then assigns as the messageId 56cd0df83cSstephan property of the message. It _must_ generate unique IDs on each call 57cd0df83cSstephan so that dispatching can work. If not defined, a default generator 58cd0df83cSstephan is used (which should be sufficient for most or all cases). 59cd0df83cSstephan 60cd0df83cSstephan - `debug` (optional): a console.debug()-style function for logging 61cd0df83cSstephan information about messages. 62cd0df83cSstephan 63cd0df83cSstephan This function returns a stateful factory function with the 64cd0df83cSstephan following interfaces: 65cd0df83cSstephan 66cd0df83cSstephan - Promise function(messageType, messageArgs) 67cd0df83cSstephan - Promise function({message object}) 68cd0df83cSstephan 69cd0df83cSstephan The first form expects the "type" and "args" values for a Worker 70cd0df83cSstephan message. The second expects an object in the form {type:..., 71cd0df83cSstephan args:...} plus any other properties the client cares to set. This 72cd0df83cSstephan function will always set the `messageId` property on the object, 73*fd31ae3bSstephan even if it's already set, and will set the `dbId` property to the 74*fd31ae3bSstephan current database ID if it is _not_ set in the message object. 75cd0df83cSstephan 76cd0df83cSstephan The function throws on error. 77cd0df83cSstephan 78cd0df83cSstephan The function installs a temporary message listener, posts a 79cd0df83cSstephan message to the configured Worker, and handles the message's 80cd0df83cSstephan response via the temporary message listener. The then() callback 81cd0df83cSstephan of the returned Promise is passed the `message.data` property from 82cd0df83cSstephan the resulting message, i.e. the payload from the worker, stripped 83cd0df83cSstephan of the lower-level event state which the onmessage() handler 84cd0df83cSstephan receives. 85cd0df83cSstephan 86cd0df83cSstephan Example usage: 87cd0df83cSstephan 88cd0df83cSstephan ``` 89cd0df83cSstephan const config = {...}; 90cd0df83cSstephan const sq3Promiser = sqlite3Worker1Promiser(config); 91cd0df83cSstephan sq3Promiser('open', {filename:"/foo.db"}).then(function(msg){ 92cd0df83cSstephan console.log("open response",msg); // => {type:'open', result: {filename:'/foo.db'}, ...} 93cd0df83cSstephan }); 94cd0df83cSstephan sq3Promiser({type:'close'}).then((msg)=>{ 95cd0df83cSstephan console.log("close response",msg); // => {type:'close', result: {filename:'/foo.db'}, ...} 96cd0df83cSstephan }); 97cd0df83cSstephan ``` 98cd0df83cSstephan 99cd0df83cSstephan Differences from Worker API #1: 100cd0df83cSstephan 101cd0df83cSstephan - exec's {callback: STRING} option does not work via this 102cd0df83cSstephan interface (it triggers an exception), but {callback: function} 103cd0df83cSstephan does and works exactly like the STRING form does in the Worker: 104cd0df83cSstephan the callback is called one time for each row of the result set, 105cd0df83cSstephan passed the same worker message format as the worker API emits: 106cd0df83cSstephan 107cd0df83cSstephan {type:typeString, 108cd0df83cSstephan row:VALUE, 109cd0df83cSstephan rowNumber:1-based-#, 110cd0df83cSstephan columnNames: array} 111cd0df83cSstephan 112cd0df83cSstephan Where `typeString` is an internally-synthesized message type string 113cd0df83cSstephan used temporarily for worker message dispatching. It can be ignored 114cd0df83cSstephan by all client code except that which tests this API. The `row` 115cd0df83cSstephan property contains the row result in the form implied by the 116cd0df83cSstephan `rowMode` option (defaulting to `'array'`). The `rowNumber` is a 117cd0df83cSstephan 1-based integer value incremented by 1 on each call into th 118cd0df83cSstephan callback. 119cd0df83cSstephan 120cd0df83cSstephan At the end of the result set, the same event is fired with 121cd0df83cSstephan (row=undefined, rowNumber=null) to indicate that 122cd0df83cSstephan the end of the result set has been reached. Note that the rows 123cd0df83cSstephan arrive via worker-posted messages, with all the implications 124cd0df83cSstephan of that. 125cd0df83cSstephan*/ 126cd0df83cSstephanself.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){ 127cd0df83cSstephan // Inspired by: https://stackoverflow.com/a/52439530 128cd0df83cSstephan if(1===arguments.length && 'function'===typeof arguments[0]){ 129cd0df83cSstephan const f = config; 130cd0df83cSstephan config = Object.assign(Object.create(null), callee.defaultConfig); 131cd0df83cSstephan config.onready = f; 132cd0df83cSstephan }else{ 133cd0df83cSstephan config = Object.assign(Object.create(null), callee.defaultConfig, config); 134cd0df83cSstephan } 135cd0df83cSstephan const handlerMap = Object.create(null); 136cd0df83cSstephan const noop = function(){}; 137cd0df83cSstephan const err = config.onerror 138cd0df83cSstephan || noop /* config.onerror is intentionally undocumented 139cd0df83cSstephan pending finding a less ambiguous name */; 140cd0df83cSstephan const debug = config.debug || noop; 141cd0df83cSstephan const idTypeMap = config.generateMessageId ? undefined : Object.create(null); 142cd0df83cSstephan const genMsgId = config.generateMessageId || function(msg){ 143cd0df83cSstephan return msg.type+'#'+(idTypeMap[msg.type] = (idTypeMap[msg.type]||0) + 1); 144cd0df83cSstephan }; 145cd0df83cSstephan const toss = (...args)=>{throw new Error(args.join(' '))}; 146cd0df83cSstephan if(!config.worker) config.worker = callee.defaultConfig.worker; 147cd0df83cSstephan if('function'===typeof config.worker) config.worker = config.worker(); 148cd0df83cSstephan let dbId; 149cd0df83cSstephan config.worker.onmessage = function(ev){ 150cd0df83cSstephan ev = ev.data; 151cd0df83cSstephan debug('worker1.onmessage',ev); 152cd0df83cSstephan let msgHandler = handlerMap[ev.messageId]; 153cd0df83cSstephan if(!msgHandler){ 154cd0df83cSstephan if(ev && 'sqlite3-api'===ev.type && 'worker1-ready'===ev.result) { 155cd0df83cSstephan /*fired one time when the Worker1 API initializes*/ 156cd0df83cSstephan if(config.onready) config.onready(); 157cd0df83cSstephan return; 158cd0df83cSstephan } 159cd0df83cSstephan msgHandler = handlerMap[ev.type] /* check for exec per-row callback */; 160cd0df83cSstephan if(msgHandler && msgHandler.onrow){ 161cd0df83cSstephan msgHandler.onrow(ev); 162cd0df83cSstephan return; 163cd0df83cSstephan } 164cd0df83cSstephan if(config.onunhandled) config.onunhandled(arguments[0]); 165cd0df83cSstephan else err("sqlite3Worker1Promiser() unhandled worker message:",ev); 166cd0df83cSstephan return; 167cd0df83cSstephan } 168cd0df83cSstephan delete handlerMap[ev.messageId]; 169cd0df83cSstephan switch(ev.type){ 170cd0df83cSstephan case 'error': 171cd0df83cSstephan msgHandler.reject(ev); 172cd0df83cSstephan return; 173cd0df83cSstephan case 'open': 174cd0df83cSstephan if(!dbId) dbId = ev.dbId; 175cd0df83cSstephan break; 176cd0df83cSstephan case 'close': 177cd0df83cSstephan if(ev.dbId===dbId) dbId = undefined; 178cd0df83cSstephan break; 179cd0df83cSstephan default: 180cd0df83cSstephan break; 181cd0df83cSstephan } 182cd0df83cSstephan try {msgHandler.resolve(ev)} 183cd0df83cSstephan catch(e){msgHandler.reject(e)} 184cd0df83cSstephan }/*worker.onmessage()*/; 185cd0df83cSstephan return function(/*(msgType, msgArgs) || (msgEnvelope)*/){ 186cd0df83cSstephan let msg; 187cd0df83cSstephan if(1===arguments.length){ 188cd0df83cSstephan msg = arguments[0]; 189cd0df83cSstephan }else if(2===arguments.length){ 190cd0df83cSstephan msg = { 191cd0df83cSstephan type: arguments[0], 192cd0df83cSstephan args: arguments[1] 193cd0df83cSstephan }; 194cd0df83cSstephan }else{ 195cd0df83cSstephan toss("Invalid arugments for sqlite3Worker1Promiser()-created factory."); 196cd0df83cSstephan } 197cd0df83cSstephan if(!msg.dbId) msg.dbId = dbId; 198cd0df83cSstephan msg.messageId = genMsgId(msg); 199cd0df83cSstephan msg.departureTime = performance.now(); 200cd0df83cSstephan const proxy = Object.create(null); 201cd0df83cSstephan proxy.message = msg; 202cd0df83cSstephan let rowCallbackId /* message handler ID for exec on-row callback proxy */; 203cd0df83cSstephan if('exec'===msg.type && msg.args){ 204cd0df83cSstephan if('function'===typeof msg.args.callback){ 205cd0df83cSstephan rowCallbackId = msg.messageId+':row'; 206cd0df83cSstephan proxy.onrow = msg.args.callback; 207cd0df83cSstephan msg.args.callback = rowCallbackId; 208cd0df83cSstephan handlerMap[rowCallbackId] = proxy; 209cd0df83cSstephan }else if('string' === typeof msg.args.callback){ 210cd0df83cSstephan toss("exec callback may not be a string when using the Promise interface."); 211cd0df83cSstephan /** 212cd0df83cSstephan Design note: the reason for this limitation is that this 213cd0df83cSstephan API takes over worker.onmessage() and the client has no way 214cd0df83cSstephan of adding their own message-type handlers to it. Per-row 215cd0df83cSstephan callbacks are implemented as short-lived message.type 216cd0df83cSstephan mappings for worker.onmessage(). 217cd0df83cSstephan 218cd0df83cSstephan We "could" work around this by providing a new 219cd0df83cSstephan config.fallbackMessageHandler (or some such) which contains 220cd0df83cSstephan a map of event type names to callbacks. Seems like overkill 221cd0df83cSstephan for now, seeing as the client can pass callback functions 222cd0df83cSstephan to this interface (whereas the string-form "callback" is 223cd0df83cSstephan needed for the over-the-Worker interface). 224cd0df83cSstephan */ 225cd0df83cSstephan } 226cd0df83cSstephan } 227cd0df83cSstephan //debug("requestWork", msg); 228cd0df83cSstephan let p = new Promise(function(resolve, reject){ 229cd0df83cSstephan proxy.resolve = resolve; 230cd0df83cSstephan proxy.reject = reject; 231cd0df83cSstephan handlerMap[msg.messageId] = proxy; 232cd0df83cSstephan debug("Posting",msg.type,"message to Worker dbId="+(dbId||'default')+':',msg); 233cd0df83cSstephan config.worker.postMessage(msg); 234cd0df83cSstephan }); 235cd0df83cSstephan if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]); 236cd0df83cSstephan return p; 237cd0df83cSstephan }; 238cd0df83cSstephan}/*sqlite3Worker1Promiser()*/; 239cd0df83cSstephanself.sqlite3Worker1Promiser.defaultConfig = { 240cd0df83cSstephan worker: function(){ 241cd0df83cSstephan let theJs = "sqlite3-worker1.js"; 242cd0df83cSstephan if(this.currentScript){ 243cd0df83cSstephan const src = this.currentScript.src.split('/'); 244cd0df83cSstephan src.pop(); 245cd0df83cSstephan theJs = src.join('/')+'/' + theJs; 246cd0df83cSstephan //console.warn("promiser currentScript, theJs =",this.currentScript,theJs); 247cd0df83cSstephan }else{ 248cd0df83cSstephan //console.warn("promiser self.location =",self.location); 249cd0df83cSstephan const urlParams = new URL(self.location.href).searchParams; 250cd0df83cSstephan if(urlParams.has('sqlite3.dir')){ 251cd0df83cSstephan theJs = urlParams.get('sqlite3.dir') + '/' + theJs; 252cd0df83cSstephan } 253cd0df83cSstephan } 254cd0df83cSstephan return new Worker(theJs + self.location.search); 255cd0df83cSstephan }.bind({ 256cd0df83cSstephan currentScript: self?.document?.currentScript 257cd0df83cSstephan }), 258cd0df83cSstephan onerror: (...args)=>console.error('worker1 promiser error',...args) 259cd0df83cSstephan}; 260