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',data:'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     - Revisit how virtual files are managed. We currently delete DBs
65     from the virtual filesystem when we close them, for the sake of
66     saving memory (the VFS lives in RAM). Supporting multiple DBs may
67     require that we give up that habit. Similarly, fully supporting
68     ATTACH, where a user can upload multiple DBs and ATTACH them,
69     also requires the that we manage the VFS entries better.
70     Related: we most definitely do not want to delete persistent DBs
71     (e.g. stored on OPFS) when they're closed.
72  */
73  const toss = (...args)=>{throw new Error(args.join(' '))};
74  if('function' !== typeof importScripts){
75    toss("Cannot initalize the sqlite3 worker API in the main thread.");
76  }
77  const self = this.self;
78  const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object.");
79  const SQLite3 = sqlite3.oo1 || toss("Missing this.sqlite3.oo1 OO API.");
80  const DB = SQLite3.DB;
81
82  /**
83     Returns the app-wide unique ID for the given db, creating one if
84     needed.
85  */
86  const getDbId = function(db){
87    let id = wState.idMap.get(db);
88    if(id) return id;
89    id = 'db#'+(++wState.idSeq)+'@'+db.pointer;
90    /** ^^^ can't simply use db.pointer b/c closing/opening may re-use
91        the same address, which could map pending messages to a wrong
92        instance. */
93    wState.idMap.set(db, id);
94    return id;
95  };
96
97  /**
98     Helper for managing Worker-level state.
99  */
100  const wState = {
101    defaultDb: undefined,
102    idSeq: 0,
103    idMap: new WeakMap,
104    open: function(arg){
105      // TODO: if arg is a filename, look for a db in this.dbs with the
106      // same filename and close/reopen it (or just pass it back as is?).
107      if(!arg && this.defaultDb) return this.defaultDb;
108      //???if(this.defaultDb) this.defaultDb.close();
109      const db = (Array.isArray(arg) ? new DB(...arg) : new DB(arg));
110      this.dbs[getDbId(db)] = db;
111      if(!this.defaultDb) this.defaultDb = db;
112      return db;
113    },
114    close: function(db,alsoUnlink){
115      if(db){
116        delete this.dbs[getDbId(db)];
117        const filename = db.fileName();
118        db.close();
119        if(db===this.defaultDb) this.defaultDb = undefined;
120        if(alsoUnlink && filename){
121          sqlite3.capi.sqlite3_wasm_vfs_unlink(filename);
122        }
123      }
124    },
125    post: function(type,data,xferList){
126      if(xferList){
127        self.postMessage( {type, data}, xferList );
128        xferList.length = 0;
129      }else{
130        self.postMessage({type, data});
131      }
132    },
133    /** Map of DB IDs to DBs. */
134    dbs: Object.create(null),
135    getDb: function(id,require=true){
136      return this.dbs[id]
137        || (require ? toss("Unknown (or closed) DB ID:",id) : undefined);
138    }
139  };
140
141  /** Throws if the given db is falsy or not opened. */
142  const affirmDbOpen = function(db = wState.defaultDb){
143    return (db && db.pointer) ? db : toss("DB is not opened.");
144  };
145
146  /** Extract dbId from the given message payload. */
147  const getMsgDb = function(msgData,affirmExists=true){
148    const db = wState.getDb(msgData.dbId,false) || wState.defaultDb;
149    return affirmExists ? affirmDbOpen(db) : db;
150  };
151
152  const getDefaultDbId = function(){
153    return wState.defaultDb && getDbId(wState.defaultDb);
154  };
155
156  /**
157     A level of "organizational abstraction" for the Worker
158     API. Each method in this object must map directly to a Worker
159     message type key. The onmessage() dispatcher attempts to
160     dispatch all inbound messages to a method of this object,
161     passing it the event.data part of the inbound event object. All
162     methods must return a plain Object containing any response
163     state, which the dispatcher may amend. All methods must throw
164     on error.
165  */
166  const wMsgHandler = {
167    xfer: [/*Temp holder for "transferable" postMessage() state.*/],
168    /**
169       Proxy for the DB constructor. Expects to be passed a single
170       object or a falsy value to use defaults. The object may
171       have a filename property to name the db file (see the DB
172       constructor for peculiarities and transformations) and/or a
173       buffer property (a Uint8Array holding a complete database
174       file's contents). The response is an object:
175
176       {
177         filename: db filename (possibly differing from the input),
178
179         dbId: an opaque ID value which must be passed to other calls
180               in this API to tell them which db to use. If it is not
181               provided to future calls, they will default to
182               operating on the first-opened db.
183
184          messageId: if the client-sent message included this field,
185              it is mirrored in the response.
186       }
187    */
188    open: function(ev){
189      const args = [], data = (ev.data || {});
190      if(data.simulateError){ // undocumented internal testing option
191        toss("Throwing because of simulateError flag.");
192      }
193      if(data.filename) args.push(data.filename);
194      const db = wState.open(args);
195      return {
196        filename: db.filename,
197        dbId: getDbId(db)
198      };
199    },
200    /**
201       Proxy for DB.close(). If ev.data may either be a boolean or
202       an object with an `unlink` property. If that value is
203       truthy then the db file (if the db is currently open) will
204       be unlinked from the virtual filesystem, else it will be
205       kept intact. The response object is:
206
207       {
208         filename: db filename _if_ the db is opened when this
209                   is called, else the undefined value,
210         unlink: boolean. If true, unlink() (delete) the db file
211                 after closing int. Any error while deleting it is
212                 ignored.
213       }
214
215       It does not error if the given db is already closed or no db is
216       provided. It is simply does nothing useful in that case.
217    */
218    close: function(ev){
219      const db = getMsgDb(ev,false);
220      const response = {
221        filename: db && db.filename,
222        dbId: db ? getDbId(db) : undefined
223      };
224      if(db){
225        wState.close(db, !!((ev.data && 'object'===typeof ev.data)
226                            ? ev.data.unlink : false));
227      }
228      return response;
229    },
230    /**
231       Proxy for DB.exec() which expects a single argument of type
232       string (SQL to execute) or an options object in the form
233       expected by exec(). The notable differences from exec()
234       include:
235
236       - The default value for options.rowMode is 'array' because
237       the normal default cannot cross the window/Worker boundary.
238
239       - A function-type options.callback property cannot cross
240       the window/Worker boundary, so is not useful here. If
241       options.callback is a string then it is assumed to be a
242       message type key, in which case a callback function will be
243       applied which posts each row result via:
244
245       postMessage({type: thatKeyType, data: theRow})
246
247       And, at the end of the result set (whether or not any
248       result rows were produced), it will post an identical
249       message with data:null to alert the caller than the result
250       set is completed.
251
252       The callback proxy must not recurse into this interface, or
253       results are undefined. (It hypothetically cannot recurse
254       because an exec() call will be tying up the Worker thread,
255       causing any recursion attempt to wait until the first
256       exec() is completed.)
257
258       The response is the input options object (or a synthesized
259       one if passed only a string), noting that
260       options.resultRows and options.columnNames may be populated
261       by the call to exec().
262
263       This opens/creates the Worker's db if needed.
264    */
265    exec: function(ev){
266      const opt = (
267        'string'===typeof ev.data
268      ) ? {sql: ev.data} : (ev.data || Object.create(null));
269      if(undefined===opt.rowMode){
270        /* Since the default rowMode of 'stmt' is not useful
271           for the Worker interface, we'll default to
272           something else. */
273        opt.rowMode = 'array';
274      }else if('stmt'===opt.rowMode){
275        toss("Invalid rowMode for exec(): stmt mode",
276             "does not work in the Worker API.");
277      }
278      const db = getMsgDb(ev);
279      if(opt.callback || Array.isArray(opt.resultRows)){
280        // Part of a copy-avoidance optimization for blobs
281        db._blobXfer = this.xfer;
282      }
283      const callbackMsgType = opt.callback;
284      if('string' === typeof callbackMsgType){
285        /* Treat this as a worker message type and post each
286           row as a message of that type. */
287        const that = this;
288        opt.callback =
289          (row)=>wState.post(callbackMsgType,row,this.xfer);
290      }
291      try {
292        db.exec(opt);
293        if(opt.callback instanceof Function){
294          opt.callback = callbackMsgType;
295          wState.post(callbackMsgType, null);
296        }
297      }/*catch(e){
298         console.warn("Worker is propagating:",e);throw e;
299         }*/finally{
300           delete db._blobXfer;
301           if(opt.callback){
302             opt.callback = callbackMsgType;
303           }
304         }
305      return opt;
306    }/*exec()*/,
307    /**
308       TO(re)DO, once we can abstract away access to the
309       JS environment's virtual filesystem. Currently this
310       always throws.
311
312       Response is (should be) an object:
313
314       {
315         buffer: Uint8Array (db file contents),
316         filename: the current db filename,
317         mimetype: 'application/x-sqlite3'
318       }
319
320       TODO is to determine how/whether this feature can support
321       exports of ":memory:" and "" (temp file) DBs. The latter is
322       ostensibly easy because the file is (potentially) on disk, but
323       the former does not have a structure which maps directly to a
324       db file image. We can VACUUM INTO a :memory:/temp db into a
325       file for that purpose, though.
326    */
327    export: function(ev){
328      toss("export() requires reimplementing for portability reasons.");
329      /**
330         We need to reimplement this to use the Emscripten FS
331         interface. That part used to be in the OO#1 API but that
332         dependency was removed from that level of the API.
333      */
334      /**const db = getMsgDb(ev);
335      const response = {
336        buffer: db.exportBinaryImage(),
337        filename: db.filename,
338        mimetype: 'application/x-sqlite3'
339      };
340      this.xfer.push(response.buffer.buffer);
341      return response;**/
342    }/*export()*/,
343    toss: function(ev){
344      toss("Testing worker exception");
345    }
346  }/*wMsgHandler*/;
347
348  /**
349     UNDER CONSTRUCTION!
350
351     A subset of the DB API is accessible via Worker messages in the
352     form:
353
354     { type: apiCommand,
355       dbId: optional DB ID value (else uses a default db handle)
356       data: apiArguments,
357       messageId: optional client-specific value
358     }
359
360     As a rule, these commands respond with a postMessage() of their
361     own in the same form, but will, if needed, transform the `data`
362     member to an object and may add state to it. The responses
363     always have an object-format `data` part. If the inbound `data`
364     is an object which has a `messageId` property, that property is
365     always mirrored in the result object, for use in client-side
366     dispatching of these asynchronous results. Exceptions thrown
367     during processing result in an `error`-type event with a
368     payload in the form:
369
370     {
371       message: error string,
372       errorClass: class name of the error type,
373       dbId: DB handle ID,
374       input: ev.data,
375       [messageId: if set in the inbound message]
376     }
377
378     The individual APIs are documented in the wMsgHandler object.
379  */
380  self.onmessage = function(ev){
381    ev = ev.data;
382    let response, dbId = ev.dbId, evType = ev.type;
383    const arrivalTime = performance.now();
384    try {
385      if(wMsgHandler.hasOwnProperty(evType) &&
386         wMsgHandler[evType] instanceof Function){
387        response = wMsgHandler[evType](ev);
388      }else{
389        toss("Unknown db worker message type:",ev.type);
390      }
391    }catch(err){
392      evType = 'error';
393      response = {
394        message: err.message,
395        errorClass: err.name,
396        input: ev
397      };
398      if(err.stack){
399        response.stack = ('string'===typeof err.stack)
400          ? err.stack.split('\n') : err.stack;
401      }
402      if(0) console.warn("Worker is propagating an exception to main thread.",
403                         "Reporting it _here_ for the stack trace:",err,response);
404    }
405    if(!response.messageId && ev.data
406       && 'object'===typeof ev.data && ev.data.messageId){
407      response.messageId = ev.data.messageId;
408    }
409    if(!dbId){
410      dbId = response.dbId/*from 'open' cmd*/
411        || getDefaultDbId();
412    }
413    if(!response.dbId) response.dbId = dbId;
414    // Timing info is primarily for use in testing this API. It's not part of
415    // the public API. arrivalTime = when the worker got the message.
416    response.workerReceivedTime = arrivalTime;
417    response.workerRespondTime = performance.now();
418    response.departureTime = ev.departureTime;
419    wState.post(evType, response, wMsgHandler.xfer);
420  };
421  setTimeout(()=>self.postMessage({type:'sqlite3-api',data:'worker1-ready'}), 0);
422}.bind({self, sqlite3});
423});
424
425