| //#ifnot omit-oo1 |
| /* |
| 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 one 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 documented 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 the |
| promiser factory replaces the worker.onmessage property. This |
| config option 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 off work at the earliest |
| opportunity, and the only way to know when the worker module has |
| completed loading. The irony of using a callback for this, instead |
| of returning a promise from sqlite3Worker1Promiser() is not lost on |
| the developers: see sqlite3Worker1Promiser.v2() which uses a |
| Promise instead. |
| |
| - `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 on each call |
| so that dispatching can work. If not defined, a default generator |
| is used (which should be sufficient for most or all cases). |
| |
| - `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 the |
| current database ID if it is _not_ set in the message object. |
| |
| The function throws on error. |
| |
| The function installs a temporary 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 sq3Promiser = sqlite3Worker1Promiser(config); |
| sq3Promiser('open', {filename:"/foo.db"}).then(function(msg){ |
| console.log("open response",msg); // => {type:'open', result: {filename:'/foo.db'}, ...} |
| }); |
| sq3Promiser({type:'close'}).then((msg)=>{ |
| console.log("close response",msg); // => {type:'close', result: {filename:'/foo.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, |
| passed the same worker message format as the worker API emits: |
| |
| {type:typeString, |
| row:VALUE, |
| rowNumber:1-based-#, |
| columnNames: array} |
| |
| Where `typeString` is an internally-synthesized message type string |
| used temporarily for worker message dispatching. It can be ignored |
| by all client code except that which tests this API. The `row` |
| property contains the row result in the form implied by the |
| `rowMode` option (defaulting to `'array'`). The `rowNumber` is a |
| 1-based integer value incremented by 1 on each call into the |
| callback. |
| |
| At the end of the result set, the same event is fired with |
| (row=undefined, rowNumber=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. |
| |
| Notable shortcomings: |
| |
| - This API was not designed with ES6 modules in mind. Neither Firefox |
| nor Safari support, as of March 2023, the {type:"module"} flag to the |
| Worker constructor, so that particular usage is not something we're going |
| to target for the time being: |
| |
| https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker |
| */ |
| globalThis.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){ |
| // Inspired by: https://stackoverflow.com/a/52439530 |
| if(1===arguments.length && 'function'===typeof arguments[0]){ |
| const f = config; |
| config = Object.assign(Object.create(null), callee.defaultConfig); |
| config.onready = f; |
| }else{ |
| config = Object.assign(Object.create(null), callee.defaultConfig, config); |
| } |
| const handlerMap = Object.create(null); |
| const noop = function(){}; |
| const err = config.onerror |
| || noop /* config.onerror is intentionally undocumented |
| pending finding a less ambiguous name */; |
| const debug = config.debug || noop; |
| const idTypeMap = config.generateMessageId ? undefined : Object.create(null); |
| const genMsgId = config.generateMessageId || function(msg){ |
| return msg.type+'#'+(idTypeMap[msg.type] = (idTypeMap[msg.type]||0) + 1); |
| }; |
| const toss = (...args)=>{throw new Error(args.join(' '))}; |
| if(!config.worker) config.worker = callee.defaultConfig.worker; |
| if('function'===typeof config.worker) config.worker = config.worker(); |
| let dbId; |
| let promiserFunc; |
| 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(promiserFunc); |
| return; |
| } |
| msgHandler = handlerMap[ev.type] /* check for exec per-row callback */; |
| if(msgHandler && msgHandler.onrow){ |
| msgHandler.onrow(ev); |
| 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(!dbId) dbId = ev.dbId; |
| break; |
| case 'close': |
| if(ev.dbId===dbId) dbId = undefined; |
| break; |
| default: |
| break; |
| } |
| try {msgHandler.resolve(ev)} |
| catch(e){msgHandler.reject(e)} |
| }/*worker.onmessage()*/; |
| return promiserFunc = function(/*(msgType, msgArgs) || (msgEnvelope)*/){ |
| let msg; |
| if(1===arguments.length){ |
| msg = arguments[0]; |
| }else if(2===arguments.length){ |
| msg = Object.create(null); |
| msg.type = arguments[0]; |
| msg.args = arguments[1]; |
| msg.dbId = msg.args.dbId; |
| }else{ |
| toss("Invalid arguments for sqlite3Worker1Promiser()-created factory."); |
| } |
| if(!msg.dbId && msg.type!=='open') msg.dbId = dbId; |
| msg.messageId = genMsgId(msg); |
| msg.departureTime = performance.now(); |
| const proxy = Object.create(null); |
| proxy.message = msg; |
| let rowCallbackId /* message handler ID for exec on-row callback proxy */; |
| if('exec'===msg.type && msg.args){ |
| if('function'===typeof msg.args.callback){ |
| rowCallbackId = msg.messageId+':row'; |
| proxy.onrow = msg.args.callback; |
| msg.args.callback = rowCallbackId; |
| handlerMap[rowCallbackId] = proxy; |
| }else if('string' === typeof msg.args.callback){ |
| toss("exec callback may not be a string when using the Promise interface."); |
| /** |
| Design note: the reason for this limitation is that this |
| API takes over worker.onmessage() and the client has no way |
| of adding their own message-type handlers to it. Per-row |
| callbacks are implemented as short-lived message.type |
| mappings for worker.onmessage(). |
| |
| We "could" work around this by providing a new |
| config.fallbackMessageHandler (or some such) which contains |
| a map of event type names to callbacks. Seems like overkill |
| for now, seeing as the client can pass callback functions |
| to this interface (whereas the string-form "callback" is |
| needed for the over-the-Worker interface). |
| */ |
| } |
| } |
| //debug("requestWork", msg); |
| let p = new Promise(function(resolve, reject){ |
| proxy.resolve = resolve; |
| proxy.reject = reject; |
| handlerMap[msg.messageId] = proxy; |
| debug("Posting",msg.type,"message to Worker dbId="+(dbId||'default')+':',msg); |
| config.worker.postMessage(msg); |
| }); |
| if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]); |
| return p; |
| }; |
| }/*sqlite3Worker1Promiser()*/; |
| |
| globalThis.sqlite3Worker1Promiser.defaultConfig = { |
| worker: function(){ |
| //#if target=es6-module |
| return new Worker(new URL("sqlite3-worker1-bundler-friendly.mjs", import.meta.url),{ |
| type: 'module' |
| }); |
| //#else |
| let theJs = "sqlite3-worker1.js"; |
| if(this.currentScript){ |
| const src = this.currentScript.src.split('/'); |
| src.pop(); |
| theJs = src.join('/')+'/' + theJs; |
| //sqlite3.config.warn("promiser currentScript, theJs =",this.currentScript,theJs); |
| }else if(globalThis.location){ |
| //sqlite3.config.warn("promiser globalThis.location =",globalThis.location); |
| const urlParams = new URL(globalThis.location.href).searchParams; |
| if(urlParams.has('sqlite3.dir')){ |
| theJs = urlParams.get('sqlite3.dir') + '/' + theJs; |
| } |
| } |
| return new Worker(theJs + globalThis.location.search); |
| //#endif |
| } |
| //#ifnot target=es6-module |
| .bind({ |
| currentScript: globalThis?.document?.currentScript |
| }) |
| //#endif |
| , |
| onerror: (...args)=>console.error('worker1 promiser error',...args) |
| }/*defaultConfig*/; |
| |
| /** |
| sqlite3Worker1Promiser.v2(), added in 3.46, works identically to |
| sqlite3Worker1Promiser() except that it returns a Promise instead |
| of relying an an onready callback in the config object. The Promise |
| resolves to the same factory function which |
| sqlite3Worker1Promiser() returns. |
| |
| If config is-a function or is an object which contains an onready |
| function, that function is replaced by a proxy which will resolve |
| after calling the original function and will reject if that |
| function throws. |
| */ |
| sqlite3Worker1Promiser.v2 = function(config){ |
| let oldFunc; |
| if( 'function' == typeof config ){ |
| oldFunc = config; |
| config = {}; |
| }else if('function'===typeof config?.onready){ |
| oldFunc = config.onready; |
| delete config.onready; |
| } |
| const promiseProxy = Object.create(null); |
| config = Object.assign((config || Object.create(null)),{ |
| onready: async function(func){ |
| try { |
| if( oldFunc ) await oldFunc(func); |
| promiseProxy.resolve(func); |
| } |
| catch(e){promiseProxy.reject(e)} |
| } |
| }); |
| const p = new Promise(function(resolve,reject){ |
| promiseProxy.resolve = resolve; |
| promiseProxy.reject = reject; |
| }); |
| try{ |
| this.original(config); |
| }catch(e){ |
| promiseProxy.reject(e); |
| } |
| return p; |
| }.bind({ |
| /* We do this because clients are |
| recommended to delete globalThis.sqlite3Worker1Promiser. */ |
| original: sqlite3Worker1Promiser |
| }); |
| |
| //#if target=es6-module |
| /** |
| When built as a module, we export sqlite3Worker1Promiser.v2() |
| instead of sqlite3Worker1Promise() because (A) its interface is more |
| conventional for ESM usage and (B) the ESM option export option for |
| this API did not exist until v2 was created, so there's no backwards |
| incompatibility. |
| */ |
| export default sqlite3Worker1Promiser.v2; |
| //#endif /* target=es6-module */ |
| //#else |
| /* Built with the omit-oo1 flag. */ |
| //#endif ifnot omit-oo1 |