1/*
2  2022-07-22
3
4  The author disclaims copyright to this source code.  In place of a
5  legal notice, here is a blessing:
6
7  *   May you do good and not evil.
8  *   May you find forgiveness for yourself and forgive others.
9  *   May you share freely, never taking more than you give.
10
11  ***********************************************************************
12
13  This file implements the initializer for the sqlite3 "Worker API
14  #1", a very basic DB access API intended to be scripted from a main
15  window thread via Worker-style messages. Because of limitations in
16  that type of communication, this API is minimalistic and only
17  capable of serving relatively basic DB requests (e.g. it cannot
18  process nested query loops concurrently).
19
20  This file requires that the core C-style sqlite3 API and OO API #1
21  have been loaded.
22*/
23
24/**
25  This function implements a Worker-based wrapper around SQLite3 OO
26  API #1, colloquially known as "Worker API #1".
27
28  In order to permit this API to be loaded in worker threads without
29  automatically registering onmessage handlers, initializing the
30  worker API requires calling initWorker1API(). If this function
31  is called from a non-worker thread then it throws an exception.
32
33  When initialized, it installs message listeners to receive Worker
34  messages and then it posts a message in the form:
35
36  ```
37  {type:'sqlite3-api',result:'worker1-ready'}
38  ```
39
40  to let the client know that it has been initialized. Clients may
41  optionally depend on this function not returning until
42  initialization is complete, as the initialization is synchronous.
43  In some contexts, however, listening for the above message is
44  a better fit.
45*/
46self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
47sqlite3.initWorker1API = function(){
48  'use strict';
49  /**
50     UNDER CONSTRUCTION
51
52     We need an API which can proxy the DB API via a Worker message
53     interface. The primary quirky factor in such an API is that we
54     cannot pass callback functions between the window thread and a
55     worker thread, so we have to receive all db results via
56     asynchronous message-passing. That requires an asychronous API
57     with a distinctly different shape than OO API #1.
58
59     TODOs include, but are not necessarily limited to:
60
61     - Support for handling multiple DBs via this interface is under
62     development.
63  */
64  const toss = (...args)=>{throw new Error(args.join(' '))};
65  if('function' !== typeof importScripts){
66    toss("Cannot initalize the sqlite3 worker API in the main thread.");
67  }
68  const self = this.self;
69  const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object.");
70  const SQLite3 = sqlite3.oo1 || toss("Missing this.sqlite3.oo1 OO API.");
71  const DB = SQLite3.DB;
72
73  /**
74     Returns the app-wide unique ID for the given db, creating one if
75     needed.
76  */
77  const getDbId = function(db){
78    let id = wState.idMap.get(db);
79    if(id) return id;
80    id = 'db#'+(++wState.idSeq)+'@'+db.pointer;
81    /** ^^^ can't simply use db.pointer b/c closing/opening may re-use
82        the same address, which could map pending messages to a wrong
83        instance. */
84    wState.idMap.set(db, id);
85    return id;
86  };
87
88  /**
89     Helper for managing Worker-level state.
90  */
91  const wState = {
92    defaultDb: undefined,
93    idSeq: 0,
94    idMap: new WeakMap,
95    open: function(opt){
96      const db = new DB(opt.filename);
97      this.dbs[getDbId(db)] = db;
98      if(!this.defaultDb) this.defaultDb = db;
99      return db;
100    },
101    close: function(db,alsoUnlink){
102      if(db){
103        delete this.dbs[getDbId(db)];
104        const filename = db.fileName();
105        db.close();
106        if(db===this.defaultDb) this.defaultDb = undefined;
107        if(alsoUnlink && filename){
108          sqlite3.capi.sqlite3_wasm_vfs_unlink(filename);
109        }
110      }
111    },
112    post: function(msg,xferList){
113      if(xferList){
114        self.postMessage( msg, xferList );
115        xferList.length = 0;
116      }else{
117        self.postMessage(msg);
118      }
119    },
120    /** Map of DB IDs to DBs. */
121    dbs: Object.create(null),
122    getDb: function(id,require=true){
123      return this.dbs[id]
124        || (require ? toss("Unknown (or closed) DB ID:",id) : undefined);
125    }
126  };
127
128  /** Throws if the given db is falsy or not opened. */
129  const affirmDbOpen = function(db = wState.defaultDb){
130    return (db && db.pointer) ? db : toss("DB is not opened.");
131  };
132
133  /** Extract dbId from the given message payload. */
134  const getMsgDb = function(msgData,affirmExists=true){
135    const db = wState.getDb(msgData.dbId,false) || wState.defaultDb;
136    return affirmExists ? affirmDbOpen(db) : db;
137  };
138
139  const getDefaultDbId = function(){
140    return wState.defaultDb && getDbId(wState.defaultDb);
141  };
142
143  /**
144     A level of "organizational abstraction" for the Worker
145     API. Each method in this object must map directly to a Worker
146     message type key. The onmessage() dispatcher attempts to
147     dispatch all inbound messages to a method of this object,
148     passing it the event.data part of the inbound event object. All
149     methods must return a plain Object containing any response
150     state, which the dispatcher may amend. All methods must throw
151     on error.
152  */
153  const wMsgHandler = {
154    xfer: [/*Temp holder for "transferable" postMessage() state.*/],
155    /**
156       Proxy for the DB constructor. Expects to be passed a single
157       object or a falsy value to use defaults. The object may have a
158       filename property to name the db file (see the DB constructor
159       for peculiarities and transformations). The response is an
160       object:
161
162       {
163         filename: db filename (possibly differing from the input),
164
165         dbId: an opaque ID value which must be passed in the message
166               envelope to other calls in this API to tell them which
167               db to use. If it is not provided to future calls, they
168               will default to operating on the first-opened db.
169
170          persistent: prepend sqlite3.capi.sqlite3_web_persistent_dir()
171                      to the given filename so that it is stored
172                      in persistent storage _if_ the environment supports it.
173                      If persistent storage is not supported, the filename
174                      is used as-is.
175       }
176    */
177    open: function(ev){
178      const oargs = Object.create(null), args = (ev.args || Object.create(null));
179      if(args.simulateError){ // undocumented internal testing option
180        toss("Throwing because of simulateError flag.");
181      }
182      if(args.persistent && args.filename){
183        oargs.filename = sqlite3.capi.sqlite3_web_persistent_dir() + args.filename;
184      }else if('' === args.filename){
185        oargs.filename = args.filename;
186      }else{
187        oargs.filename = args.filename || ':memory:';
188      }
189      const db = wState.open(oargs);
190      return {
191        filename: db.filename,
192        dbId: getDbId(db)
193      };
194    },
195    /**
196       Proxy for DB.close(). ev.args may be elided or an object with
197       an `unlink` property. If that value is truthy then the db file
198       (if the db is currently open) will be unlinked from the virtual
199       filesystem, else it will be kept intact. The result object is:
200
201       {
202         filename: db filename _if_ the db is opened when this
203                   is called, else the undefined value
204         dbId: the ID of the closed b, or undefined if none is closed
205       }
206
207       It does not error if the given db is already closed or no db is
208       provided. It is simply does nothing useful in that case.
209    */
210    close: function(ev){
211      const db = getMsgDb(ev,false);
212      const response = {
213        filename: db && db.filename,
214        dbId: db ? getDbId(db) : undefined
215      };
216      if(db){
217        wState.close(db, ((ev.args && 'object'===typeof ev.args)
218                          ? !!ev.args.unlink : false));
219      }
220      return response;
221    },
222    /**
223       Proxy for DB.exec() which expects a single argument of type
224       string (SQL to execute) or an options object in the form
225       expected by exec(). The notable differences from exec()
226       include:
227
228       - The default value for options.rowMode is 'array' because
229       the normal default cannot cross the window/Worker boundary.
230
231       - A function-type options.callback property cannot cross
232       the window/Worker boundary, so is not useful here. If
233       options.callback is a string then it is assumed to be a
234       message type key, in which case a callback function will be
235       applied which posts each row result via:
236
237       postMessage({type: thatKeyType, row: theRow})
238
239       And, at the end of the result set (whether or not any
240       result rows were produced), it will post an identical
241       message with row:null to alert the caller than the result
242       set is completed.
243
244       The callback proxy must not recurse into this interface, or
245       results are undefined. (It hypothetically cannot recurse
246       because an exec() call will be tying up the Worker thread,
247       causing any recursion attempt to wait until the first
248       exec() is completed.)
249
250       The response is the input options object (or a synthesized
251       one if passed only a string), noting that
252       options.resultRows and options.columnNames may be populated
253       by the call to exec().
254
255       This opens/creates the Worker's db if needed.
256    */
257    exec: function(ev){
258      const opt = (
259        'string'===typeof ev.args
260      ) ? {sql: ev.args} : (ev.args || Object.create(null));
261      if(undefined===opt.rowMode){
262        /* Since the default rowMode of 'stmt' is not useful
263           for the Worker interface, we'll default to
264           something else. */
265        opt.rowMode = 'array';
266      }else if('stmt'===opt.rowMode){
267        toss("Invalid rowMode for exec(): stmt mode",
268             "does not work in the Worker API.");
269      }
270      const db = getMsgDb(ev);
271      if(opt.callback || Array.isArray(opt.resultRows)){
272        // Part of a copy-avoidance optimization for blobs
273        db._blobXfer = this.xfer;
274      }
275      const callbackMsgType = opt.callback;
276      if('string' === typeof callbackMsgType){
277        /* Treat this as a worker message type and post each
278           row as a message of that type. */
279        const that = this;
280        opt.callback =
281          (row)=>wState.post({type: callbackMsgType, row:row}, this.xfer);
282      }
283      try {
284        db.exec(opt);
285        if(opt.callback instanceof Function){
286          opt.callback = callbackMsgType;
287          wState.post({type: callbackMsgType, row: null});
288        }
289      }/*catch(e){
290         console.warn("Worker is propagating:",e);throw e;
291         }*/finally{
292           delete db._blobXfer;
293           if(opt.callback){
294             opt.callback = callbackMsgType;
295           }
296         }
297      return opt;
298    }/*exec()*/,
299    /**
300       TO(RE)DO, once we can abstract away access to the
301       JS environment's virtual filesystem. Currently this
302       always throws.
303
304       Response is (should be) an object:
305
306       {
307         buffer: Uint8Array (db file contents),
308         filename: the current db filename,
309         mimetype: 'application/x-sqlite3'
310       }
311
312       TODO is to determine how/whether this feature can support
313       exports of ":memory:" and "" (temp file) DBs. The latter is
314       ostensibly easy because the file is (potentially) on disk, but
315       the former does not have a structure which maps directly to a
316       db file image. We can VACUUM INTO a :memory:/temp db into a
317       file for that purpose, though.
318    */
319    export: function(ev){
320      toss("export() requires reimplementing for portability reasons.");
321      /**
322         We need to reimplement this to use the Emscripten FS
323         interface. That part used to be in the OO#1 API but that
324         dependency was removed from that level of the API.
325      */
326      /**const db = getMsgDb(ev);
327      const response = {
328        buffer: db.exportBinaryImage(),
329        filename: db.filename,
330        mimetype: 'application/x-sqlite3'
331      };
332      this.xfer.push(response.buffer.buffer);
333      return response;**/
334    }/*export()*/,
335    toss: function(ev){
336      toss("Testing worker exception");
337    }
338  }/*wMsgHandler*/;
339
340  /**
341     UNDER CONSTRUCTION!
342
343     A subset of the DB API is accessible via Worker messages in the
344     form:
345
346     { type: apiCommand,
347       dbId: optional DB ID value (else uses a default db handle),
348       args: apiArguments,
349       messageId: optional client-specific value
350     }
351
352     As a rule, these commands respond with a postMessage() of their
353     own in the form:
354
355     TODO: refactoring is underway.
356
357     The responses always have an object-format `result` part. If the
358     inbound object has a `messageId` property, that property is
359     always mirrored in the result object, for use in client-side
360     dispatching of these asynchronous results. Exceptions thrown
361     during processing result in an `error`-type event with a payload
362     in the form:
363
364     { type: 'error',
365       dbId: DB handle ID,
366       [messageId: if set in the inbound message],
367       result: {
368         operation: "inbound message's 'type' value",
369         message: error string,
370         errorClass: class name of the error type,
371         input: ev.data
372       }
373     }
374
375     The individual APIs are documented in the wMsgHandler object.
376  */
377  self.onmessage = function(ev){
378    ev = ev.data;
379    let result, dbId = ev.dbId, evType = ev.type;
380    const arrivalTime = performance.now();
381    try {
382      if(wMsgHandler.hasOwnProperty(evType) &&
383         wMsgHandler[evType] instanceof Function){
384        result = wMsgHandler[evType](ev);
385      }else{
386        toss("Unknown db worker message type:",ev.type);
387      }
388    }catch(err){
389      evType = 'error';
390      result = {
391        operation: ev.type,
392        message: err.message,
393        errorClass: err.name,
394        input: ev
395      };
396      if(err.stack){
397        result.stack = ('string'===typeof err.stack)
398          ? err.stack.split('\n') : err.stack;
399      }
400      if(0) console.warn("Worker is propagating an exception to main thread.",
401                         "Reporting it _here_ for the stack trace:",err,result);
402    }
403    if(!dbId){
404      dbId = result.dbId/*from 'open' cmd*/
405        || getDefaultDbId();
406    }
407    // Timing info is primarily for use in testing this API. It's not part of
408    // the public API. arrivalTime = when the worker got the message.
409    wState.post({
410      type: evType,
411      dbId: dbId,
412      messageId: ev.messageId,
413      workerReceivedTime: arrivalTime,
414      workerRespondTime: performance.now(),
415      departureTime: ev.departureTime,
416      result: result
417    }, wMsgHandler.xfer);
418  };
419  self.postMessage({type:'sqlite3-api',result:'worker1-ready'});
420}.bind({self, sqlite3});
421});
422
423