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