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
46  Note that the worker-based interface can be slightly quirky because
47  of its async nature. In particular, any number of messages may be posted
48  to the worker before it starts handling any of them. If, e.g., an
49  "open" operation fails, any subsequent messages will fail. The
50  Promise-based wrapper for this API (`sqlite3-worker1-promiser.js`)
51  is more comfortable to use in that regard.
52
53
54  TODO: hoist the message API docs from deep in this code to here.
55
56*/
57self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
58sqlite3.initWorker1API = function(){
59  'use strict';
60  const toss = (...args)=>{throw new Error(args.join(' '))};
61  if('function' !== typeof importScripts){
62    toss("Cannot initalize the sqlite3 worker API in the main thread.");
63  }
64  const self = this.self;
65  const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object.");
66  const SQLite3 = sqlite3.oo1 || toss("Missing this.sqlite3.oo1 OO API.");
67  const DB = SQLite3.DB;
68
69  /**
70     Returns the app-wide unique ID for the given db, creating one if
71     needed.
72  */
73  const getDbId = function(db){
74    let id = wState.idMap.get(db);
75    if(id) return id;
76    id = 'db#'+(++wState.idSeq)+'@'+db.pointer;
77    /** ^^^ can't simply use db.pointer b/c closing/opening may re-use
78        the same address, which could map pending messages to a wrong
79        instance. */
80    wState.idMap.set(db, id);
81    return id;
82  };
83
84  /**
85     Internal helper for managing Worker-level state.
86  */
87  const wState = {
88    defaultDb: undefined,
89    idSeq: 0,
90    idMap: new WeakMap,
91    xfer: [/*Temp holder for "transferable" postMessage() state.*/],
92    open: function(opt){
93      const db = new DB(opt.filename);
94      this.dbs[getDbId(db)] = db;
95      if(!this.defaultDb) this.defaultDb = db;
96      return db;
97    },
98    close: function(db,alsoUnlink){
99      if(db){
100        delete this.dbs[getDbId(db)];
101        const filename = db.fileName();
102        db.close();
103        if(db===this.defaultDb) this.defaultDb = undefined;
104        if(alsoUnlink && filename){
105          sqlite3.capi.sqlite3_wasm_vfs_unlink(filename);
106        }
107      }
108    },
109    /**
110       Posts the given worker message value. If xferList is provided,
111       it must be an array, in which case a copy of it passed as
112       postMessage()'s second argument and xferList.length is set to
113       0.
114    */
115    post: function(msg,xferList){
116      if(xferList && xferList.length){
117        self.postMessage( msg, Array.from(xferList) );
118        xferList.length = 0;
119      }else{
120        self.postMessage(msg);
121      }
122    },
123    /** Map of DB IDs to DBs. */
124    dbs: Object.create(null),
125    getDb: function(id,require=true){
126      return this.dbs[id]
127        || (require ? toss("Unknown (or closed) DB ID:",id) : undefined);
128    }
129  };
130
131  /** Throws if the given db is falsy or not opened. */
132  const affirmDbOpen = function(db = wState.defaultDb){
133    return (db && db.pointer) ? db : toss("DB is not opened.");
134  };
135
136  /** Extract dbId from the given message payload. */
137  const getMsgDb = function(msgData,affirmExists=true){
138    const db = wState.getDb(msgData.dbId,false) || wState.defaultDb;
139    return affirmExists ? affirmDbOpen(db) : db;
140  };
141
142  const getDefaultDbId = function(){
143    return wState.defaultDb && getDbId(wState.defaultDb);
144  };
145
146  /**
147     A level of "organizational abstraction" for the Worker
148     API. Each method in this object must map directly to a Worker
149     message type key. The onmessage() dispatcher attempts to
150     dispatch all inbound messages to a method of this object,
151     passing it the event.data part of the inbound event object. All
152     methods must return a plain Object containing any result
153     state, which the dispatcher may amend. All methods must throw
154     on error.
155  */
156  const wMsgHandler = {
157    /**
158       Proxy for the DB constructor. Expects to be passed a single
159       object or a falsy value to use defaults:
160
161       {
162         filename [=":memory:" or "" (unspecified)]: the db filename.
163         See the sqlite3.oo1.DB constructor for peculiarities and transformations,
164
165         persistent [=false]: if true and filename is not one of ("",
166         ":memory:"), prepend
167         sqlite3.capi.sqlite3_web_persistent_dir() to the given
168         filename so that it is stored in persistent storage _if_ the
169         environment supports it.  If persistent storage is not
170         supported, the filename is used as-is.
171       }
172
173       The response object looks like:
174
175       {
176         filename: db filename, possibly differing from the input.
177
178         dbId: an opaque ID value which must be passed in the message
179         envelope to other calls in this API to tell them which db to
180         use. If it is not provided to future calls, they will default
181         to operating on the first-opened db.
182
183         persistent: true if the given filename resides in the
184         known-persistent storage, else false. This determination is
185         independent of the `persistent` input argument.
186       }
187    */
188    open: function(ev){
189      const oargs = Object.create(null), args = (ev.args || Object.create(null));
190      if(args.simulateError){ // undocumented internal testing option
191        toss("Throwing because of simulateError flag.");
192      }
193      const rc = Object.create(null);
194      const pDir = sqlite3.capi.sqlite3_web_persistent_dir();
195      if(!args.filename || ':memory:'===args.filename){
196        oargs.filename = args.filename || '';
197      }else if(pDir){
198        oargs.filename = pDir + ('/'===args.filename[0] ? args.filename : ('/'+args.filename));
199      }else{
200        oargs.filename = args.filename;
201      }
202      const db = wState.open(oargs);
203      rc.filename = db.filename;
204      rc.persistent = !!pDir && db.filename.startsWith(pDir);
205      rc.dbId = getDbId(db);
206      return rc;
207    },
208    /**
209       Proxy for DB.close(). ev.args may be elided or an object with
210       an `unlink` property. If that value is truthy then the db file
211       (if the db is currently open) will be unlinked from the virtual
212       filesystem, else it will be kept intact, noting that unlink
213       failure is ignored. The result object is:
214
215       {
216         filename: db filename _if_ the db is opened when this
217         is called, else the undefined value
218
219         dbId: the ID of the closed b, or undefined if none is closed
220       }
221
222       It does not error if the given db is already closed or no db is
223       provided. It is simply does nothing useful in that case.
224    */
225    close: function(ev){
226      const db = getMsgDb(ev,false);
227      const response = {
228        filename: db && db.filename,
229        dbId: db && getDbId(db)
230      };
231      if(db){
232        wState.close(db, ((ev.args && 'object'===typeof ev.args)
233                          ? !!ev.args.unlink : false));
234      }
235      return response;
236    },
237    /**
238       Proxy for oo1.DB.exec() which expects a single argument of type
239       string (SQL to execute) or an options object in the form
240       expected by exec(). The notable differences from exec()
241       include:
242
243       - The default value for options.rowMode is 'array' because
244       the normal default cannot cross the window/Worker boundary.
245
246       - A function-type options.callback property cannot cross
247       the window/Worker boundary, so is not useful here. If
248       options.callback is a string then it is assumed to be a
249       message type key, in which case a callback function will be
250       applied which posts each row result via:
251
252       postMessage({type: thatKeyType, rowNumber: 1-based-#, row: theRow})
253
254       And, at the end of the result set (whether or not any result
255       rows were produced), it will post an identical message with
256       (row=undefined, rowNumber=null) to alert the caller than the
257       result set is completed. Note that a row value of `null` is
258       a legal row result for certain `rowMode` values.
259
260       (Design note: we don't use (row=undefined, rowNumber=undefined)
261       to indicate end-of-results because fetching those would be
262       indistinguishable from fetching from an empty object unless the
263       client used hasOwnProperty() (or similar) to distinguish
264       "missing property" from "property with the undefined value".
265       Similarly, `null` is a legal value for `row` in some case ,
266       whereas the db layer won't emit a result value of `undefined`.)
267
268       The callback proxy must not recurse into this interface, or
269       results are undefined. (It hypothetically cannot recurse
270       because an exec() call will be tying up the Worker thread,
271       causing any recursion attempt to wait until the first
272       exec() is completed.)
273
274       The response is the input options object (or a synthesized
275       one if passed only a string), noting that
276       options.resultRows and options.columnNames may be populated
277       by the call to db.exec().
278    */
279    exec: function(ev){
280      const rc = (
281        'string'===typeof ev.args
282      ) ? {sql: ev.args} : (ev.args || Object.create(null));
283      if(undefined===rc.rowMode){
284        /* Since the default rowMode of 'stmt' is not useful
285           for the Worker interface, we'll default to
286           something else. */
287        rc.rowMode = 'array';
288      }else if('stmt'===rc.rowMode){
289        toss("Invalid rowMode for 'exec': stmt mode",
290             "does not work in the Worker API.");
291      }
292      const db = getMsgDb(ev);
293      if(rc.callback || Array.isArray(rc.resultRows)){
294        // Part of a copy-avoidance optimization for blobs
295        db._blobXfer = wState.xfer;
296      }
297      const callbackMsgType = rc.callback;
298      let rowNumber = 0;
299      if('string' === typeof callbackMsgType){
300        /* Treat this as a worker message type and post each
301           row as a message of that type. */
302        rc.callback =
303          (row)=>wState.post({type: callbackMsgType, rowNumber:++rowNumber, row:row}, wState.xfer);
304      }
305      try {
306        db.exec(rc);
307        if(rc.callback instanceof Function){
308          rc.callback = callbackMsgType;
309          wState.post({type: callbackMsgType, rowNumber: null, row: undefined});
310        }
311      }finally{
312        delete db._blobXfer;
313        if(rc.callback){
314          rc.callback = callbackMsgType;
315        }
316      }
317      return rc;
318    }/*exec()*/,
319    /**
320       Returns a JSON-friendly form of a _subset_ of sqlite3.config,
321       sans any parts which cannot be serialized. Because we cannot,
322       from here, distingush whether or not certain objects can be
323       serialized, this routine selectively copies certain properties
324       rather than trying JSON.stringify() and seeing what happens
325       (the results are horrid if the config object contains an
326       Emscripten module object).
327
328       In addition to the "real" config properties, it sythesizes
329       the following:
330
331       - persistenceEnabled: true if persistent dir support is available,
332       else false.
333    */
334    'config-get': function(){
335      const rc = Object.create(null), src = sqlite3.config;
336      [
337        'persistentDirName', 'bigIntEnabled'
338      ].forEach(function(k){
339        if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k];
340      });
341      rc.persistenceEnabled = !!sqlite3.capi.sqlite3_web_persistent_dir();
342      return rc;
343    },
344    /**
345       TO(RE)DO, once we can abstract away access to the
346       JS environment's virtual filesystem. Currently this
347       always throws.
348
349       Response is (should be) an object:
350
351       {
352         buffer: Uint8Array (db file contents),
353         filename: the current db filename,
354         mimetype: 'application/x-sqlite3'
355       }
356
357       TODO is to determine how/whether this feature can support
358       exports of ":memory:" and "" (temp file) DBs. The latter is
359       ostensibly easy because the file is (potentially) on disk, but
360       the former does not have a structure which maps directly to a
361       db file image. We can VACUUM INTO a :memory:/temp db into a
362       file for that purpose, though.
363    */
364    export: function(ev){
365      toss("export() requires reimplementing for portability reasons.");
366      /**
367         We need to reimplement this to use the Emscripten FS
368         interface. That part used to be in the OO#1 API but that
369         dependency was removed from that level of the API.
370      */
371      /**const db = getMsgDb(ev);
372      const response = {
373        buffer: db.exportBinaryImage(),
374        filename: db.filename,
375        mimetype: 'application/x-sqlite3'
376      };
377      wState.xfer.push(response.buffer.buffer);
378      return response;**/
379    }/*export()*/,
380    toss: function(ev){
381      toss("Testing worker exception");
382    }
383  }/*wMsgHandler*/;
384
385  /**
386     UNDER CONSTRUCTION!
387
388     A subset of the DB API is accessible via Worker messages in the
389     form:
390
391     { type: apiCommand,
392       args: apiArguments,
393       dbId: optional DB ID value (else uses a default db handle),
394       messageId: optional client-specific value
395     }
396
397     As a rule, these commands respond with a postMessage() of their
398     own. The responses always have a `type` property equal to the
399     input message's type and an object-format `result` part. If
400     the inbound object has a `messageId` property, that property is
401     always mirrored in the result object, for use in client-side
402     dispatching of these asynchronous results. For example:
403
404     {
405       type: 'open',
406       messageId: ...copied from inbound message...,
407       dbId: ID of db which was opened,
408       result: {
409         dbId: repeat of ^^^, for API consistency's sake,
410         filename: ...,
411         persistent: false
412       },
413       ...possibly other framework-internal/testing/debugging info...
414     }
415
416     Exceptions thrown during processing result in an `error`-type
417     event with a payload in the form:
418
419     { type: 'error',
420       dbId: DB handle ID,
421       [messageId: if set in the inbound message],
422       result: {
423         operation: "inbound message's 'type' value",
424         message: error string,
425         errorClass: class name of the error type,
426         input: ev.data
427       }
428     }
429
430     The individual APIs are documented in the wMsgHandler object.
431  */
432  self.onmessage = function(ev){
433    ev = ev.data;
434    let result, dbId = ev.dbId, evType = ev.type;
435    const arrivalTime = performance.now();
436    try {
437      if(wMsgHandler.hasOwnProperty(evType) &&
438         wMsgHandler[evType] instanceof Function){
439        result = wMsgHandler[evType](ev);
440      }else{
441        toss("Unknown db worker message type:",ev.type);
442      }
443    }catch(err){
444      evType = 'error';
445      result = {
446        operation: ev.type,
447        message: err.message,
448        errorClass: err.name,
449        input: ev
450      };
451      if(err.stack){
452        result.stack = ('string'===typeof err.stack)
453          ? err.stack.split('\n') : err.stack;
454      }
455      if(0) console.warn("Worker is propagating an exception to main thread.",
456                         "Reporting it _here_ for the stack trace:",err,result);
457    }
458    if(!dbId){
459      dbId = result.dbId/*from 'open' cmd*/
460        || getDefaultDbId();
461    }
462    // Timing info is primarily for use in testing this API. It's not part of
463    // the public API. arrivalTime = when the worker got the message.
464    wState.post({
465      type: evType,
466      dbId: dbId,
467      messageId: ev.messageId,
468      workerReceivedTime: arrivalTime,
469      workerRespondTime: performance.now(),
470      departureTime: ev.departureTime,
471      // TODO: move the timing bits into...
472      //timing:{
473      //  departure: ev.departureTime,
474      //  workerReceived: arrivalTime,
475      //  workerResponse: performance.now();
476      //},
477      result: result
478    }, wState.xfer);
479  };
480  self.postMessage({type:'sqlite3-api',result:'worker1-ready'});
481}.bind({self, sqlite3});
482});
483