Fixes default log output to console for macOS
[sqlcipher.git] / ext / wasm / api / sqlite3-worker1-promiser.c-pp.js
blob55e497ead55c3592e340abf412c679512808cb35
1 //#ifnot omit-oo1
2 /*
3   2022-08-24
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
20   Worker API does.
22   This script necessarily exposes one global symbol, but clients may
23   freely `delete` that symbol after calling it.
25 'use strict';
26 /**
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
51    Promise instead.
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
69    following interfaces:
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
89    receives.
91    Example usage:
93    ```
94    const config = {...};
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'}, ...}
98    });
99    sq3Promiser({type:'close'}).then((msg)=>{
100      console.log("close response",msg); // => {type:'close', result: {filename:'/foo.db'}, ...}
101    });
102    ```
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:
112      {type:typeString,
113       row:VALUE,
114       rowNumber:1-based-#,
115       columnNames: array}
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
123    callback.
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
129    of that.
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]){
143     const f = config;
144     config = Object.assign(Object.create(null), callee.defaultConfig);
145     config.onready = f;
146   }else{
147     config = Object.assign(Object.create(null), callee.defaultConfig, config);
148   }
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);
158   };
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();
162   let dbId;
163   let promiserFunc;
164   config.worker.onmessage = function(ev){
165     ev = ev.data;
166     debug('worker1.onmessage',ev);
167     let msgHandler = handlerMap[ev.messageId];
168     if(!msgHandler){
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);
172         return;
173       }
174       msgHandler = handlerMap[ev.type] /* check for exec per-row callback */;
175       if(msgHandler && msgHandler.onrow){
176         msgHandler.onrow(ev);
177         return;
178       }
179       if(config.onunhandled) config.onunhandled(arguments[0]);
180       else err("sqlite3Worker1Promiser() unhandled worker message:",ev);
181       return;
182     }
183     delete handlerMap[ev.messageId];
184     switch(ev.type){
185         case 'error':
186           msgHandler.reject(ev);
187           return;
188         case 'open':
189           if(!dbId) dbId = ev.dbId;
190           break;
191         case 'close':
192           if(ev.dbId===dbId) dbId = undefined;
193           break;
194         default:
195           break;
196     }
197     try {msgHandler.resolve(ev)}
198     catch(e){msgHandler.reject(e)}
199   }/*worker.onmessage()*/;
200   return promiserFunc = function(/*(msgType, msgArgs) || (msgEnvelope)*/){
201     let msg;
202     if(1===arguments.length){
203       msg = arguments[0];
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;
209     }else{
210       toss("Invalid arguments for sqlite3Worker1Promiser()-created factory.");
211     }
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);
216     proxy.message = msg;
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.");
226         /**
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).
239         */
240       }
241     }
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);
249     });
250     if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]);
251     return p;
252   };
253 }/*sqlite3Worker1Promiser()*/;
255 globalThis.sqlite3Worker1Promiser.defaultConfig = {
256   worker: function(){
257 //#if target=es6-module
258     return new Worker(new URL("sqlite3-worker1-bundler-friendly.mjs", import.meta.url),{
259       type: 'module'
260     });
261 //#else
262     let theJs = "sqlite3-worker1.js";
263     if(this.currentScript){
264       const src = this.currentScript.src.split('/');
265       src.pop();
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;
273       }
274     }
275     return new Worker(theJs + globalThis.location.search);
276 //#endif
277   }
278 //#ifnot target=es6-module
279   .bind({
280     currentScript: globalThis?.document?.currentScript
281   })
282 //#endif
283   ,
284   onerror: (...args)=>console.error('worker1 promiser error',...args)
285 }/*defaultConfig*/;
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
297    function throws.
299 sqlite3Worker1Promiser.v2 = function(config){
300   let oldFunc;
301   if( 'function' == typeof config ){
302     oldFunc = config;
303     config = {};
304   }else if('function'===typeof config?.onready){
305     oldFunc = config.onready;
306     delete config.onready;
307   }
308   const promiseProxy = Object.create(null);
309   config = Object.assign((config || Object.create(null)),{
310     onready: async function(func){
311       try {
312         if( oldFunc ) await oldFunc(func);
313         promiseProxy.resolve(func);
314       }
315       catch(e){promiseProxy.reject(e)}
316     }
317   });
318   const p = new Promise(function(resolve,reject){
319     promiseProxy.resolve = resolve;
320     promiseProxy.reject = reject;
321   });
322   try{
323     this.original(config);
324   }catch(e){
325     promiseProxy.reject(e);
326   }
327   return p;
328 }.bind({
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
340   incompatibility.
342 export default sqlite3Worker1Promiser.v2;
343 //#endif /* target=es6-module */
344 //#else
345 /* Built with the omit-oo1 flag. */
346 //#endif ifnot omit-oo1