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,
253                    rowNumber: 1-based-#,
254                    row: theRow,
255                    columnNames: anArray
256                    })
257
258       And, at the end of the result set (whether or not any result
259       rows were produced), it will post an identical message with
260       (row=undefined, rowNumber=null) to alert the caller than the
261       result set is completed. Note that a row value of `null` is
262       a legal row result for certain `rowMode` values.
263
264       (Design note: we don't use (row=undefined, rowNumber=undefined)
265       to indicate end-of-results because fetching those would be
266       indistinguishable from fetching from an empty object unless the
267       client used hasOwnProperty() (or similar) to distinguish
268       "missing property" from "property with the undefined value".
269       Similarly, `null` is a legal value for `row` in some case ,
270       whereas the db layer won't emit a result value of `undefined`.)
271
272       The callback proxy must not recurse into this interface, or
273       results are undefined. (It hypothetically cannot recurse
274       because an exec() call will be tying up the Worker thread,
275       causing any recursion attempt to wait until the first
276       exec() is completed.)
277
278       The response is the input options object (or a synthesized
279       one if passed only a string), noting that
280       options.resultRows and options.columnNames may be populated
281       by the call to db.exec().
282    */
283    exec: function(ev){
284      const rc = (
285        'string'===typeof ev.args
286      ) ? {sql: ev.args} : (ev.args || Object.create(null));
287      if('stmt'===rc.rowMode){
288        toss("Invalid rowMode for 'exec': stmt mode",
289             "does not work in the Worker API.");
290      }
291      const db = getMsgDb(ev);
292      if(rc.callback || Array.isArray(rc.resultRows)){
293        // Part of a copy-avoidance optimization for blobs
294        db._blobXfer = wState.xfer;
295      }
296      const theCallback = rc.callback;
297      let rowNumber = 0;
298      const hadColNames = !!rc.columnNames;
299      if('string' === typeof theCallback){
300        if(!hadColNames) rc.columnNames = [];
301        /* Treat this as a worker message type and post each
302           row as a message of that type. */
303        rc.callback = function(row,stmt){
304          wState.post({
305            type: theCallback,
306            columnNames: rc.columnNames,
307            rowNumber: ++rowNumber,
308            row: row
309          }, wState.xfer);
310        }
311      }
312      try {
313        db.exec(rc);
314        if(rc.callback instanceof Function){
315          rc.callback = theCallback;
316          /* Post a sentinel message to tell the client that the end
317             of the result set has been reached (possibly with zero
318             rows). */
319          wState.post({
320            type: theCallback,
321            columnNames: rc.columnNames,
322            rowNumber: null /*null to distinguish from "property not set"*/,
323            row: undefined /*undefined because null is a legal row value
324                             for some rowType values, but undefined is not*/
325          });
326        }
327      }finally{
328        delete db._blobXfer;
329        if(rc.callback) rc.callback = theCallback;
330      }
331      return rc;
332    }/*exec()*/,
333    /**
334       Returns a JSON-friendly form of a _subset_ of sqlite3.config,
335       sans any parts which cannot be serialized. Because we cannot,
336       from here, distingush whether or not certain objects can be
337       serialized, this routine selectively copies certain properties
338       rather than trying JSON.stringify() and seeing what happens
339       (the results are horrid if the config object contains an
340       Emscripten module object).
341
342       In addition to the "real" config properties, it sythesizes
343       the following:
344
345       - persistenceEnabled: true if persistent dir support is available,
346       else false.
347    */
348    'config-get': function(){
349      const rc = Object.create(null), src = sqlite3.config;
350      [
351        'persistentDirName', 'bigIntEnabled'
352      ].forEach(function(k){
353        if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k];
354      });
355      rc.persistenceEnabled = !!sqlite3.capi.sqlite3_web_persistent_dir();
356      return rc;
357    },
358    /**
359       TO(RE)DO, once we can abstract away access to the
360       JS environment's virtual filesystem. Currently this
361       always throws.
362
363       Response is (should be) an object:
364
365       {
366         buffer: Uint8Array (db file contents),
367         filename: the current db filename,
368         mimetype: 'application/x-sqlite3'
369       }
370
371       TODO is to determine how/whether this feature can support
372       exports of ":memory:" and "" (temp file) DBs. The latter is
373       ostensibly easy because the file is (potentially) on disk, but
374       the former does not have a structure which maps directly to a
375       db file image. We can VACUUM INTO a :memory:/temp db into a
376       file for that purpose, though.
377    */
378    export: function(ev){
379      toss("export() requires reimplementing for portability reasons.");
380      /**
381         We need to reimplement this to use the Emscripten FS
382         interface. That part used to be in the OO#1 API but that
383         dependency was removed from that level of the API.
384      */
385      /**const db = getMsgDb(ev);
386      const response = {
387        buffer: db.exportBinaryImage(),
388        filename: db.filename,
389        mimetype: 'application/x-sqlite3'
390      };
391      wState.xfer.push(response.buffer.buffer);
392      return response;**/
393    }/*export()*/,
394    toss: function(ev){
395      toss("Testing worker exception");
396    }
397  }/*wMsgHandler*/;
398
399  /**
400     UNDER CONSTRUCTION!
401
402     A subset of the DB API is accessible via Worker messages in the
403     form:
404
405     { type: apiCommand,
406       args: apiArguments,
407       dbId: optional DB ID value (else uses a default db handle),
408       messageId: optional client-specific value
409     }
410
411     As a rule, these commands respond with a postMessage() of their
412     own. The responses always have a `type` property equal to the
413     input message's type and an object-format `result` part. If
414     the inbound object has a `messageId` property, that property is
415     always mirrored in the result object, for use in client-side
416     dispatching of these asynchronous results. For example:
417
418     {
419       type: 'open',
420       messageId: ...copied from inbound message...,
421       dbId: ID of db which was opened,
422       result: {
423         dbId: repeat of ^^^, for API consistency's sake,
424         filename: ...,
425         persistent: false
426       },
427       ...possibly other framework-internal/testing/debugging info...
428     }
429
430     Exceptions thrown during processing result in an `error`-type
431     event with a payload in the form:
432
433     { type: 'error',
434       dbId: DB handle ID,
435       [messageId: if set in the inbound message],
436       result: {
437         operation: "inbound message's 'type' value",
438         message: error string,
439         errorClass: class name of the error type,
440         input: ev.data
441       }
442     }
443
444     The individual APIs are documented in the wMsgHandler object.
445  */
446  self.onmessage = function(ev){
447    ev = ev.data;
448    let result, dbId = ev.dbId, evType = ev.type;
449    const arrivalTime = performance.now();
450    try {
451      if(wMsgHandler.hasOwnProperty(evType) &&
452         wMsgHandler[evType] instanceof Function){
453        result = wMsgHandler[evType](ev);
454      }else{
455        toss("Unknown db worker message type:",ev.type);
456      }
457    }catch(err){
458      evType = 'error';
459      result = {
460        operation: ev.type,
461        message: err.message,
462        errorClass: err.name,
463        input: ev
464      };
465      if(err.stack){
466        result.stack = ('string'===typeof err.stack)
467          ? err.stack.split('\n') : err.stack;
468      }
469      if(0) console.warn("Worker is propagating an exception to main thread.",
470                         "Reporting it _here_ for the stack trace:",err,result);
471    }
472    if(!dbId){
473      dbId = result.dbId/*from 'open' cmd*/
474        || getDefaultDbId();
475    }
476    // Timing info is primarily for use in testing this API. It's not part of
477    // the public API. arrivalTime = when the worker got the message.
478    wState.post({
479      type: evType,
480      dbId: dbId,
481      messageId: ev.messageId,
482      workerReceivedTime: arrivalTime,
483      workerRespondTime: performance.now(),
484      departureTime: ev.departureTime,
485      // TODO: move the timing bits into...
486      //timing:{
487      //  departure: ev.departureTime,
488      //  workerReceived: arrivalTime,
489      //  workerResponse: performance.now();
490      //},
491      result: result
492    }, wState.xfer);
493  };
494  self.postMessage({type:'sqlite3-api',result:'worker1-ready'});
495}.bind({self, sqlite3});
496});
497