5 The author disclaims copyright to this source code. In place of a
6 legal notice, here is a blessing:
8 * May you do good and not evil.
9 * May you find forgiveness for yourself and forgive others.
10 * May you share freely, never taking more than you give.
12 ***********************************************************************
14 This file implements a Promise-based proxy for the sqlite3 Worker
15 API #1. It is intended to be included either from the main thread or
16 a Worker, but only if (A) the environment supports nested Workers
17 and (B) it's _not_ a Worker which loads the sqlite3 WASM/JS
18 module. This file's features will load that module and provide a
19 slightly simpler client-side interface than the slightly-lower-level
22 This script necessarily exposes one global symbol, but clients may
23 freely `delete` that symbol after calling it.
27 Configures an sqlite3 Worker API #1 Worker such that it can be
28 manipulated via a Promise-based interface and returns a factory
29 function which returns Promises for communicating with the worker.
30 This proxy has an _almost_ identical interface to the normal
31 worker API, with any exceptions documented below.
33 It requires a configuration object with the following properties:
35 - `worker` (required): a Worker instance which loads
36 `sqlite3-worker1.js` or a functional equivalent. Note that the
37 promiser factory replaces the worker.onmessage property. This
38 config option may alternately be a function, in which case this
39 function re-assigns this property with the result of calling that
40 function, enabling delayed instantiation of a Worker.
42 - `onready` (optional, but...): this callback is called with no
43 arguments when the worker fires its initial
44 'sqlite3-api'/'worker1-ready' message, which it does when
45 sqlite3.initWorker1API() completes its initialization. This is the
46 simplest way to tell the worker to kick off work at the earliest
47 opportunity, and the only way to know when the worker module has
48 completed loading. The irony of using a callback for this, instead
49 of returning a promise from sqlite3Worker1Promiser() is not lost on
50 the developers: see sqlite3Worker1Promiser.v2() which uses a
53 - `onunhandled` (optional): a callback which gets passed the
54 message event object for any worker.onmessage() events which
55 are not handled by this proxy. Ideally that "should" never
56 happen, as this proxy aims to handle all known message types.
58 - `generateMessageId` (optional): a function which, when passed an
59 about-to-be-posted message object, generates a _unique_ message ID
60 for the message, which this API then assigns as the messageId
61 property of the message. It _must_ generate unique IDs on each call
62 so that dispatching can work. If not defined, a default generator
63 is used (which should be sufficient for most or all cases).
65 - `debug` (optional): a console.debug()-style function for logging
66 information about messages.
68 This function returns a stateful factory function with the
71 - Promise function(messageType, messageArgs)
72 - Promise function({message object})
74 The first form expects the "type" and "args" values for a Worker
75 message. The second expects an object in the form {type:...,
76 args:...} plus any other properties the client cares to set. This
77 function will always set the `messageId` property on the object,
78 even if it's already set, and will set the `dbId` property to the
79 current database ID if it is _not_ set in the message object.
81 The function throws on error.
83 The function installs a temporary message listener, posts a
84 message to the configured Worker, and handles the message's
85 response via the temporary message listener. The then() callback
86 of the returned Promise is passed the `message.data` property from
87 the resulting message, i.e. the payload from the worker, stripped
88 of the lower-level event state which the onmessage() handler
95 const sq3Promiser = sqlite3Worker1Promiser(config);
96 sq3Promiser('open', {filename:"/foo.db"}).then(function(msg){
97 console.log("open response",msg); // => {type:'open', result: {filename:'/foo.db'}, ...}
99 sq3Promiser({type:'close'}).then((msg)=>{
100 console.log("close response",msg); // => {type:'close', result: {filename:'/foo.db'}, ...}
104 Differences from Worker API #1:
106 - exec's {callback: STRING} option does not work via this
107 interface (it triggers an exception), but {callback: function}
108 does and works exactly like the STRING form does in the Worker:
109 the callback is called one time for each row of the result set,
110 passed the same worker message format as the worker API emits:
117 Where `typeString` is an internally-synthesized message type string
118 used temporarily for worker message dispatching. It can be ignored
119 by all client code except that which tests this API. The `row`
120 property contains the row result in the form implied by the
121 `rowMode` option (defaulting to `'array'`). The `rowNumber` is a
122 1-based integer value incremented by 1 on each call into the
125 At the end of the result set, the same event is fired with
126 (row=undefined, rowNumber=null) to indicate that
127 the end of the result set has been reached. Note that the rows
128 arrive via worker-posted messages, with all the implications
131 Notable shortcomings:
133 - This API was not designed with ES6 modules in mind. Neither Firefox
134 nor Safari support, as of March 2023, the {type:"module"} flag to the
135 Worker constructor, so that particular usage is not something we're going
136 to target for the time being:
138 https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker
140 globalThis.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){
141 // Inspired by: https://stackoverflow.com/a/52439530
142 if(1===arguments.length && 'function'===typeof arguments[0]){
144 config = Object.assign(Object.create(null), callee.defaultConfig);
147 config = Object.assign(Object.create(null), callee.defaultConfig, config);
149 const handlerMap = Object.create(null);
150 const noop = function(){};
151 const err = config.onerror
152 || noop /* config.onerror is intentionally undocumented
153 pending finding a less ambiguous name */;
154 const debug = config.debug || noop;
155 const idTypeMap = config.generateMessageId ? undefined : Object.create(null);
156 const genMsgId = config.generateMessageId || function(msg){
157 return msg.type+'#'+(idTypeMap[msg.type] = (idTypeMap[msg.type]||0) + 1);
159 const toss = (...args)=>{throw new Error(args.join(' '))};
160 if(!config.worker) config.worker = callee.defaultConfig.worker;
161 if('function'===typeof config.worker) config.worker = config.worker();
164 config.worker.onmessage = function(ev){
166 debug('worker1.onmessage',ev);
167 let msgHandler = handlerMap[ev.messageId];
169 if(ev && 'sqlite3-api'===ev.type && 'worker1-ready'===ev.result) {
170 /*fired one time when the Worker1 API initializes*/
171 if(config.onready) config.onready(promiserFunc);
174 msgHandler = handlerMap[ev.type] /* check for exec per-row callback */;
175 if(msgHandler && msgHandler.onrow){
176 msgHandler.onrow(ev);
179 if(config.onunhandled) config.onunhandled(arguments[0]);
180 else err("sqlite3Worker1Promiser() unhandled worker message:",ev);
183 delete handlerMap[ev.messageId];
186 msgHandler.reject(ev);
189 if(!dbId) dbId = ev.dbId;
192 if(ev.dbId===dbId) dbId = undefined;
197 try {msgHandler.resolve(ev)}
198 catch(e){msgHandler.reject(e)}
199 }/*worker.onmessage()*/;
200 return promiserFunc = function(/*(msgType, msgArgs) || (msgEnvelope)*/){
202 if(1===arguments.length){
204 }else if(2===arguments.length){
205 msg = Object.create(null);
206 msg.type = arguments[0];
207 msg.args = arguments[1];
208 msg.dbId = msg.args.dbId;
210 toss("Invalid arguments for sqlite3Worker1Promiser()-created factory.");
212 if(!msg.dbId && msg.type!=='open') msg.dbId = dbId;
213 msg.messageId = genMsgId(msg);
214 msg.departureTime = performance.now();
215 const proxy = Object.create(null);
217 let rowCallbackId /* message handler ID for exec on-row callback proxy */;
218 if('exec'===msg.type && msg.args){
219 if('function'===typeof msg.args.callback){
220 rowCallbackId = msg.messageId+':row';
221 proxy.onrow = msg.args.callback;
222 msg.args.callback = rowCallbackId;
223 handlerMap[rowCallbackId] = proxy;
224 }else if('string' === typeof msg.args.callback){
225 toss("exec callback may not be a string when using the Promise interface.");
227 Design note: the reason for this limitation is that this
228 API takes over worker.onmessage() and the client has no way
229 of adding their own message-type handlers to it. Per-row
230 callbacks are implemented as short-lived message.type
231 mappings for worker.onmessage().
233 We "could" work around this by providing a new
234 config.fallbackMessageHandler (or some such) which contains
235 a map of event type names to callbacks. Seems like overkill
236 for now, seeing as the client can pass callback functions
237 to this interface (whereas the string-form "callback" is
238 needed for the over-the-Worker interface).
242 //debug("requestWork", msg);
243 let p = new Promise(function(resolve, reject){
244 proxy.resolve = resolve;
245 proxy.reject = reject;
246 handlerMap[msg.messageId] = proxy;
247 debug("Posting",msg.type,"message to Worker dbId="+(dbId||'default')+':',msg);
248 config.worker.postMessage(msg);
250 if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]);
253 }/*sqlite3Worker1Promiser()*/;
255 globalThis.sqlite3Worker1Promiser.defaultConfig = {
257 //#if target=es6-module
258 return new Worker(new URL("sqlite3-worker1-bundler-friendly.mjs", import.meta.url),{
262 let theJs = "sqlite3-worker1.js";
263 if(this.currentScript){
264 const src = this.currentScript.src.split('/');
266 theJs = src.join('/')+'/' + theJs;
267 //sqlite3.config.warn("promiser currentScript, theJs =",this.currentScript,theJs);
268 }else if(globalThis.location){
269 //sqlite3.config.warn("promiser globalThis.location =",globalThis.location);
270 const urlParams = new URL(globalThis.location.href).searchParams;
271 if(urlParams.has('sqlite3.dir')){
272 theJs = urlParams.get('sqlite3.dir') + '/' + theJs;
275 return new Worker(theJs + globalThis.location.search);
278 //#ifnot target=es6-module
280 currentScript: globalThis?.document?.currentScript
284 onerror: (...args)=>console.error('worker1 promiser error',...args)
288 sqlite3Worker1Promiser.v2(), added in 3.46, works identically to
289 sqlite3Worker1Promiser() except that it returns a Promise instead
290 of relying an an onready callback in the config object. The Promise
291 resolves to the same factory function which
292 sqlite3Worker1Promiser() returns.
294 If config is-a function or is an object which contains an onready
295 function, that function is replaced by a proxy which will resolve
296 after calling the original function and will reject if that
299 sqlite3Worker1Promiser.v2 = function(config){
301 if( 'function' == typeof config ){
304 }else if('function'===typeof config?.onready){
305 oldFunc = config.onready;
306 delete config.onready;
308 const promiseProxy = Object.create(null);
309 config = Object.assign((config || Object.create(null)),{
310 onready: async function(func){
312 if( oldFunc ) await oldFunc(func);
313 promiseProxy.resolve(func);
315 catch(e){promiseProxy.reject(e)}
318 const p = new Promise(function(resolve,reject){
319 promiseProxy.resolve = resolve;
320 promiseProxy.reject = reject;
323 this.original(config);
325 promiseProxy.reject(e);
329 /* We do this because clients are
330 recommended to delete globalThis.sqlite3Worker1Promiser. */
331 original: sqlite3Worker1Promiser
334 //#if target=es6-module
336 When built as a module, we export sqlite3Worker1Promiser.v2()
337 instead of sqlite3Worker1Promise() because (A) its interface is more
338 conventional for ESM usage and (B) the ESM option export option for
339 this API did not exist until v2 was created, so there's no backwards
342 export default sqlite3Worker1Promiser.v2;
343 //#endif /* target=es6-module */
345 /* Built with the omit-oo1 flag. */
346 //#endif ifnot omit-oo1