Snapshot of upstream SQLite 3.46.1
[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'}, ...}
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);
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();
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;
174 msgHandler = handlerMap[ev.type] /* check for exec per-row callback */;
175 if(msgHandler && msgHandler.onrow){
176 msgHandler.onrow(ev);
177 return;
179 if(config.onunhandled) config.onunhandled(arguments[0]);
180 else err("sqlite3Worker1Promiser() unhandled worker message:",ev);
181 return;
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;
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.");
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.");
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]);
251 return p;
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'
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;
275 return new Worker(theJs + globalThis.location.search);
276 //#endif
278 //#ifnot target=es6-module
279 .bind({
280 currentScript: globalThis?.document?.currentScript
282 //#endif
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;
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);
315 catch(e){promiseProxy.reject(e)}
318 const p = new Promise(function(resolve,reject){
319 promiseProxy.resolve = resolve;
320 promiseProxy.reject = reject;
322 try{
323 this.original(config);
324 }catch(e){
325 promiseProxy.reject(e);
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