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(arg){
96      // TODO? if arg is a filename, look for a db in this.dbs with the
97      // same filename and close/reopen it (or just pass it back as is?).
98      if(!arg && this.defaultDb) return this.defaultDb;
99      const db = (Array.isArray(arg) ? new DB(...arg) : new DB(arg));
100      this.dbs[getDbId(db)] = db;
101      if(!this.defaultDb) this.defaultDb = db;
102      return db;
103    },
104    close: function(db,alsoUnlink){
105      if(db){
106        delete this.dbs[getDbId(db)];
107        const filename = db.fileName();
108        db.close();
109        if(db===this.defaultDb) this.defaultDb = undefined;
110        if(alsoUnlink && filename){
111          sqlite3.capi.sqlite3_wasm_vfs_unlink(filename);
112        }
113      }
114    },
115    post: function(msg,xferList){
116      if(xferList){
117        self.postMessage( msg, 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 response
153     state, which the dispatcher may amend. All methods must throw
154     on error.
155  */
156  const wMsgHandler = {
157    xfer: [/*Temp holder for "transferable" postMessage() state.*/],
158    /**
159       Proxy for the DB constructor. Expects to be passed a single
160       object or a falsy value to use defaults. The object may have a
161       filename property to name the db file (see the DB constructor
162       for peculiarities and transformations). The response is an
163       object:
164
165       {
166         filename: db filename (possibly differing from the input),
167
168         dbId: an opaque ID value which must be passed in the message
169               envelope to other calls in this API to tell them which
170               db to use. If it is not provided to future calls, they
171               will default to operating on the first-opened db.
172       }
173    */
174    open: function(ev){
175      const oargs = [], args = (ev.args || {});
176      if(args.simulateError){ // undocumented internal testing option
177        toss("Throwing because of simulateError flag.");
178      }
179      if(args.filename) oargs.push(args.filename);
180      const db = wState.open(oargs);
181      return {
182        filename: db.filename,
183        dbId: getDbId(db)
184      };
185    },
186    /**
187       Proxy for DB.close(). ev.args may either be a boolean or an
188       object with an `unlink` property. If that value is truthy then
189       the db file (if the db is currently open) will be unlinked from
190       the virtual filesystem, else it will be kept intact. The
191       result object is:
192
193       {
194         filename: db filename _if_ the db is opened when this
195                   is called, else the undefined value
196       }
197
198       It does not error if the given db is already closed or no db is
199       provided. It is simply does nothing useful in that case.
200    */
201    close: function(ev){
202      const db = getMsgDb(ev,false);
203      const response = {
204        filename: db && db.filename,
205        dbId: db ? getDbId(db) : undefined
206      };
207      if(db){
208        wState.close(db, ((ev.args && 'object'===typeof ev.args)
209                          ? !!ev.args.unlink : false));
210      }
211      return response;
212    },
213    /**
214       Proxy for DB.exec() which expects a single argument of type
215       string (SQL to execute) or an options object in the form
216       expected by exec(). The notable differences from exec()
217       include:
218
219       - The default value for options.rowMode is 'array' because
220       the normal default cannot cross the window/Worker boundary.
221
222       - A function-type options.callback property cannot cross
223       the window/Worker boundary, so is not useful here. If
224       options.callback is a string then it is assumed to be a
225       message type key, in which case a callback function will be
226       applied which posts each row result via:
227
228       postMessage({type: thatKeyType, row: theRow})
229
230       And, at the end of the result set (whether or not any
231       result rows were produced), it will post an identical
232       message with row:null to alert the caller than the result
233       set is completed.
234
235       The callback proxy must not recurse into this interface, or
236       results are undefined. (It hypothetically cannot recurse
237       because an exec() call will be tying up the Worker thread,
238       causing any recursion attempt to wait until the first
239       exec() is completed.)
240
241       The response is the input options object (or a synthesized
242       one if passed only a string), noting that
243       options.resultRows and options.columnNames may be populated
244       by the call to exec().
245
246       This opens/creates the Worker's db if needed.
247    */
248    exec: function(ev){
249      const opt = (
250        'string'===typeof ev.args
251      ) ? {sql: ev.args} : (ev.args || Object.create(null));
252      if(undefined===opt.rowMode){
253        /* Since the default rowMode of 'stmt' is not useful
254           for the Worker interface, we'll default to
255           something else. */
256        opt.rowMode = 'array';
257      }else if('stmt'===opt.rowMode){
258        toss("Invalid rowMode for exec(): stmt mode",
259             "does not work in the Worker API.");
260      }
261      const db = getMsgDb(ev);
262      if(opt.callback || Array.isArray(opt.resultRows)){
263        // Part of a copy-avoidance optimization for blobs
264        db._blobXfer = this.xfer;
265      }
266      const callbackMsgType = opt.callback;
267      if('string' === typeof callbackMsgType){
268        /* Treat this as a worker message type and post each
269           row as a message of that type. */
270        const that = this;
271        opt.callback =
272          (row)=>wState.post({type: callbackMsgType, row:row}, this.xfer);
273      }
274      try {
275        db.exec(opt);
276        if(opt.callback instanceof Function){
277          opt.callback = callbackMsgType;
278          wState.post({type: callbackMsgType, row: null});
279        }
280      }/*catch(e){
281         console.warn("Worker is propagating:",e);throw e;
282         }*/finally{
283           delete db._blobXfer;
284           if(opt.callback){
285             opt.callback = callbackMsgType;
286           }
287         }
288      return opt;
289    }/*exec()*/,
290    /**
291       TO(RE)DO, once we can abstract away access to the
292       JS environment's virtual filesystem. Currently this
293       always throws.
294
295       Response is (should be) an object:
296
297       {
298         buffer: Uint8Array (db file contents),
299         filename: the current db filename,
300         mimetype: 'application/x-sqlite3'
301       }
302
303       TODO is to determine how/whether this feature can support
304       exports of ":memory:" and "" (temp file) DBs. The latter is
305       ostensibly easy because the file is (potentially) on disk, but
306       the former does not have a structure which maps directly to a
307       db file image. We can VACUUM INTO a :memory:/temp db into a
308       file for that purpose, though.
309    */
310    export: function(ev){
311      toss("export() requires reimplementing for portability reasons.");
312      /**
313         We need to reimplement this to use the Emscripten FS
314         interface. That part used to be in the OO#1 API but that
315         dependency was removed from that level of the API.
316      */
317      /**const db = getMsgDb(ev);
318      const response = {
319        buffer: db.exportBinaryImage(),
320        filename: db.filename,
321        mimetype: 'application/x-sqlite3'
322      };
323      this.xfer.push(response.buffer.buffer);
324      return response;**/
325    }/*export()*/,
326    toss: function(ev){
327      toss("Testing worker exception");
328    }
329  }/*wMsgHandler*/;
330
331  /**
332     UNDER CONSTRUCTION!
333
334     A subset of the DB API is accessible via Worker messages in the
335     form:
336
337     { type: apiCommand,
338       dbId: optional DB ID value (else uses a default db handle),
339       args: apiArguments,
340       messageId: optional client-specific value
341     }
342
343     As a rule, these commands respond with a postMessage() of their
344     own in the form:
345
346     TODO: refactoring is underway.
347
348     The responses always have an object-format `result` part. If the
349     inbound object has a `messageId` property, that property is
350     always mirrored in the result object, for use in client-side
351     dispatching of these asynchronous results. Exceptions thrown
352     during processing result in an `error`-type event with a payload
353     in the form:
354
355     { type: 'error',
356       dbId: DB handle ID,
357       [messageId: if set in the inbound message],
358       result: {
359         message: error string,
360         errorClass: class name of the error type,
361         input: ev.data
362       }
363     }
364
365     The individual APIs are documented in the wMsgHandler object.
366  */
367  self.onmessage = function(ev){
368    ev = ev.data;
369    let result, dbId = ev.dbId, evType = ev.type;
370    const arrivalTime = performance.now();
371    try {
372      if(wMsgHandler.hasOwnProperty(evType) &&
373         wMsgHandler[evType] instanceof Function){
374        result = wMsgHandler[evType](ev);
375      }else{
376        toss("Unknown db worker message type:",ev.type);
377      }
378    }catch(err){
379      evType = 'error';
380      result = {
381        message: err.message,
382        errorClass: err.name,
383        input: ev
384      };
385      if(err.stack){
386        result.stack = ('string'===typeof err.stack)
387          ? err.stack.split('\n') : err.stack;
388      }
389      if(0) console.warn("Worker is propagating an exception to main thread.",
390                         "Reporting it _here_ for the stack trace:",err,result);
391    }
392    if(!dbId){
393      dbId = result.dbId/*from 'open' cmd*/
394        || getDefaultDbId();
395    }
396    // Timing info is primarily for use in testing this API. It's not part of
397    // the public API. arrivalTime = when the worker got the message.
398    wState.post({
399      type: evType,
400      dbId: dbId,
401      messageId: ev.messageId,
402      workerReceivedTime: arrivalTime,
403      workerRespondTime: performance.now(),
404      departureTime: ev.departureTime,
405      result: result
406    }, wMsgHandler.xfer);
407  };
408  setTimeout(()=>self.postMessage({type:'sqlite3-api',result:'worker1-ready'}), 0);
409}.bind({self, sqlite3});
410});
411
412