Snapshot of upstream SQLite 3.46.1
[sqlcipher.git] / ext / wasm / api / sqlite3-api-worker1.js
blob2e597613e186669d964b040f138aedc45c28437c
1 //#ifnot omit-oo1
2 /**
3 2022-07-22
5 The author disclaims copyright to this source code. In place of a
6 legal notice, here is a blessing:
8 * May you do good and not evil.
9 * May you find forgiveness for yourself and forgive others.
10 * May you share freely, never taking more than you give.
12 ***********************************************************************
14 This file implements the initializer for SQLite's "Worker API #1", a
15 very basic DB access API intended to be scripted from a main window
16 thread via Worker-style messages. Because of limitations in that
17 type of communication, this API is minimalistic and only capable of
18 serving relatively basic DB requests (e.g. it cannot process nested
19 query loops concurrently).
21 This file requires that the core C-style sqlite3 API and OO API #1
22 have been loaded.
25 /**
26 sqlite3.initWorker1API() implements a Worker-based wrapper around
27 SQLite3 OO API #1, colloquially known as "Worker API #1".
29 In order to permit this API to be loaded in worker threads without
30 automatically registering onmessage handlers, initializing the
31 worker API requires calling initWorker1API(). If this function is
32 called from a non-worker thread then it throws an exception. It
33 must only be called once per Worker.
35 When initialized, it installs message listeners to receive Worker
36 messages and then it posts a message in the form:
38 ```
39 {type:'sqlite3-api', result:'worker1-ready'}
40 ```
42 to let the client know that it has been initialized. Clients may
43 optionally depend on this function not returning until
44 initialization is complete, as the initialization is synchronous.
45 In some contexts, however, listening for the above message is
46 a better fit.
48 Note that the worker-based interface can be slightly quirky because
49 of its async nature. In particular, any number of messages may be posted
50 to the worker before it starts handling any of them. If, e.g., an
51 "open" operation fails, any subsequent messages will fail. The
52 Promise-based wrapper for this API (`sqlite3-worker1-promiser.js`)
53 is more comfortable to use in that regard.
55 The documentation for the input and output worker messages for
56 this API follows...
58 ====================================================================
59 Common message format...
61 Each message posted to the worker has an operation-independent
62 envelope and operation-dependent arguments:
64 ```
66 type: string, // one of: 'open', 'close', 'exec', 'export', 'config-get'
68 messageId: OPTIONAL arbitrary value. The worker will copy it as-is
69 into response messages to assist in client-side dispatching.
71 dbId: a db identifier string (returned by 'open') which tells the
72 operation which database instance to work on. If not provided, the
73 first-opened db is used. This is an "opaque" value, with no
74 inherently useful syntax or information. Its value is subject to
75 change with any given build of this API and cannot be used as a
76 basis for anything useful beyond its one intended purpose.
78 args: ...operation-dependent arguments...
80 // the framework may add other properties for testing or debugging
81 // purposes.
84 ```
86 Response messages, posted back to the main thread, look like:
88 ```
90 type: string. Same as above except for error responses, which have the type
91 'error',
93 messageId: same value, if any, provided by the inbound message
95 dbId: the id of the db which was operated on, if any, as returned
96 by the corresponding 'open' operation.
98 result: ...operation-dependent result...
103 ====================================================================
104 Error responses
106 Errors are reported messages in an operation-independent format:
110 type: "error",
112 messageId: ...as above...,
114 dbId: ...as above...
116 result: {
118 operation: type of the triggering operation: 'open', 'close', ...
120 message: ...error message text...
122 errorClass: string. The ErrorClass.name property from the thrown exception.
124 input: the message object which triggered the error.
126 stack: _if available_, a stack trace array.
134 ====================================================================
135 "config-get"
137 This operation fetches the serializable parts of the sqlite3 API
138 configuration.
140 Message format:
144 type: "config-get",
145 messageId: ...as above...,
146 args: currently ignored and may be elided.
150 Response:
154 type: "config-get",
155 messageId: ...as above...,
156 result: {
158 version: sqlite3.version object
160 bigIntEnabled: bool. True if BigInt support is enabled.
162 vfsList: result of sqlite3.capi.sqlite3_js_vfs_list()
168 ====================================================================
169 "open" a database
171 Message format:
175 type: "open",
176 messageId: ...as above...,
177 args:{
179 filename [=":memory:" or "" (unspecified)]: the db filename.
180 See the sqlite3.oo1.DB constructor for peculiarities and
181 transformations,
183 vfs: sqlite3_vfs name. Ignored if filename is ":memory:" or "".
184 This may change how the given filename is resolved.
189 Response:
193 type: "open",
194 messageId: ...as above...,
195 result: {
196 filename: db filename, possibly differing from the input.
198 dbId: an opaque ID value which must be passed in the message
199 envelope to other calls in this API to tell them which db to
200 use. If it is not provided to future calls, they will default to
201 operating on the least-recently-opened db. This property is, for
202 API consistency's sake, also part of the containing message
203 envelope. Only the `open` operation includes it in the `result`
204 property.
206 persistent: true if the given filename resides in the
207 known-persistent storage, else false.
209 vfs: name of the VFS the "main" db is using.
214 ====================================================================
215 "close" a database
217 Message format:
221 type: "close",
222 messageId: ...as above...
223 dbId: ...as above...
224 args: OPTIONAL {unlink: boolean}
228 If the `dbId` does not refer to an opened ID, this is a no-op. If
229 the `args` object contains a truthy `unlink` value then the database
230 will be unlinked (deleted) after closing it. The inability to close a
231 db (because it's not opened) or delete its file does not trigger an
232 error.
234 Response:
238 type: "close",
239 messageId: ...as above...,
240 result: {
242 filename: filename of closed db, or undefined if no db was closed
248 ====================================================================
249 "exec" SQL
251 All SQL execution is processed through the exec operation. It offers
252 most of the features of the oo1.DB.exec() method, with a few limitations
253 imposed by the state having to cross thread boundaries.
255 Message format:
259 type: "exec",
260 messageId: ...as above...
261 dbId: ...as above...
262 args: string (SQL) or {... see below ...}
266 Response:
270 type: "exec",
271 messageId: ...as above...,
272 dbId: ...as above...
273 result: {
274 input arguments, possibly modified. See below.
279 The arguments are in the same form accepted by oo1.DB.exec(), with
280 the exceptions noted below.
282 If the `countChanges` arguments property (added in version 3.43) is
283 truthy then the `result` property contained by the returned object
284 will have a `changeCount` property which holds the number of changes
285 made by the provided SQL. Because the SQL may contain an arbitrary
286 number of statements, the `changeCount` is calculated by calling
287 `sqlite3_total_changes()` before and after the SQL is evaluated. If
288 the value of `countChanges` is 64 then the `changeCount` property
289 will be returned as a 64-bit integer in the form of a BigInt (noting
290 that that will trigger an exception if used in a BigInt-incapable
291 build). In the latter case, the number of changes is calculated by
292 calling `sqlite3_total_changes64()` before and after the SQL is
293 evaluated.
295 A function-type args.callback property cannot cross
296 the window/Worker boundary, so is not useful here. If
297 args.callback is a string then it is assumed to be a
298 message type key, in which case a callback function will be
299 applied which posts each row result via:
301 postMessage({type: thatKeyType,
302 rowNumber: 1-based-#,
303 row: theRow,
304 columnNames: anArray
307 And, at the end of the result set (whether or not any result rows
308 were produced), it will post an identical message with
309 (row=undefined, rowNumber=null) to alert the caller than the result
310 set is completed. Note that a row value of `null` is a legal row
311 result for certain arg.rowMode values.
313 (Design note: we don't use (row=undefined, rowNumber=undefined) to
314 indicate end-of-results because fetching those would be
315 indistinguishable from fetching from an empty object unless the
316 client used hasOwnProperty() (or similar) to distinguish "missing
317 property" from "property with the undefined value". Similarly,
318 `null` is a legal value for `row` in some case , whereas the db
319 layer won't emit a result value of `undefined`.)
321 The callback proxy must not recurse into this interface. An exec()
322 call will tie up the Worker thread, causing any recursion attempt
323 to wait until the first exec() is completed.
325 The response is the input options object (or a synthesized one if
326 passed only a string), noting that options.resultRows and
327 options.columnNames may be populated by the call to db.exec().
330 ====================================================================
331 "export" the current db
333 To export the underlying database as a byte array...
335 Message format:
339 type: "export",
340 messageId: ...as above...,
341 dbId: ...as above...
345 Response:
349 type: "export",
350 messageId: ...as above...,
351 dbId: ...as above...
352 result: {
353 byteArray: Uint8Array (as per sqlite3_js_db_export()),
354 filename: the db filename,
355 mimetype: "application/x-sqlite3"
361 globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
362 const util = sqlite3.util;
363 sqlite3.initWorker1API = function(){
364 'use strict';
365 const toss = (...args)=>{throw new Error(args.join(' '))};
366 if(!(globalThis.WorkerGlobalScope instanceof Function)){
367 toss("initWorker1API() must be run from a Worker thread.");
369 const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object.");
370 const DB = sqlite3.oo1.DB;
373 Returns the app-wide unique ID for the given db, creating one if
374 needed.
376 const getDbId = function(db){
377 let id = wState.idMap.get(db);
378 if(id) return id;
379 id = 'db#'+(++wState.idSeq)+'@'+db.pointer;
380 /** ^^^ can't simply use db.pointer b/c closing/opening may re-use
381 the same address, which could map pending messages to a wrong
382 instance. */
383 wState.idMap.set(db, id);
384 return id;
388 Internal helper for managing Worker-level state.
390 const wState = {
392 Each opened DB is added to this.dbList, and the first entry in
393 that list is the default db. As each db is closed, its entry is
394 removed from the list.
396 dbList: [],
397 /** Sequence number of dbId generation. */
398 idSeq: 0,
399 /** Map of DB instances to dbId. */
400 idMap: new WeakMap,
401 /** Temp holder for "transferable" postMessage() state. */
402 xfer: [],
403 open: function(opt){
404 const db = new DB(opt);
405 this.dbs[getDbId(db)] = db;
406 if(this.dbList.indexOf(db)<0) this.dbList.push(db);
407 return db;
409 close: function(db,alsoUnlink){
410 if(db){
411 delete this.dbs[getDbId(db)];
412 const filename = db.filename;
413 const pVfs = util.sqlite3__wasm_db_vfs(db.pointer, 0);
414 db.close();
415 const ddNdx = this.dbList.indexOf(db);
416 if(ddNdx>=0) this.dbList.splice(ddNdx, 1);
417 if(alsoUnlink && filename && pVfs){
418 util.sqlite3__wasm_vfs_unlink(pVfs, filename);
423 Posts the given worker message value. If xferList is provided,
424 it must be an array, in which case a copy of it passed as
425 postMessage()'s second argument and xferList.length is set to
428 post: function(msg,xferList){
429 if(xferList && xferList.length){
430 globalThis.postMessage( msg, Array.from(xferList) );
431 xferList.length = 0;
432 }else{
433 globalThis.postMessage(msg);
436 /** Map of DB IDs to DBs. */
437 dbs: Object.create(null),
438 /** Fetch the DB for the given id. Throw if require=true and the
439 id is not valid, else return the db or undefined. */
440 getDb: function(id,require=true){
441 return this.dbs[id]
442 || (require ? toss("Unknown (or closed) DB ID:",id) : undefined);
446 /** Throws if the given db is falsy or not opened, else returns its
447 argument. */
448 const affirmDbOpen = function(db = wState.dbList[0]){
449 return (db && db.pointer) ? db : toss("DB is not opened.");
452 /** Extract dbId from the given message payload. */
453 const getMsgDb = function(msgData,affirmExists=true){
454 const db = wState.getDb(msgData.dbId,false) || wState.dbList[0];
455 return affirmExists ? affirmDbOpen(db) : db;
458 const getDefaultDbId = function(){
459 return wState.dbList[0] && getDbId(wState.dbList[0]);
462 const isSpecialDbFilename = (n)=>{
463 return ""===n || ':'===n[0];
467 A level of "organizational abstraction" for the Worker1
468 API. Each method in this object must map directly to a Worker1
469 message type key. The onmessage() dispatcher attempts to
470 dispatch all inbound messages to a method of this object,
471 passing it the event.data part of the inbound event object. All
472 methods must return a plain Object containing any result
473 state, which the dispatcher may amend. All methods must throw
474 on error.
476 const wMsgHandler = {
477 open: function(ev){
478 const oargs = Object.create(null), args = (ev.args || Object.create(null));
479 if(args.simulateError){ // undocumented internal testing option
480 toss("Throwing because of simulateError flag.");
482 const rc = Object.create(null);
483 oargs.vfs = args.vfs;
484 oargs.filename = args.filename || "";
485 const db = wState.open(oargs);
486 rc.filename = db.filename;
487 rc.persistent = !!sqlite3.capi.sqlite3_js_db_uses_vfs(db.pointer, "opfs");
488 rc.dbId = getDbId(db);
489 rc.vfs = db.dbVfsName();
490 return rc;
493 close: function(ev){
494 const db = getMsgDb(ev,false);
495 const response = {
496 filename: db && db.filename
498 if(db){
499 const doUnlink = ((ev.args && 'object'===typeof ev.args)
500 ? !!ev.args.unlink : false);
501 wState.close(db, doUnlink);
503 return response;
506 exec: function(ev){
507 const rc = (
508 'string'===typeof ev.args
509 ) ? {sql: ev.args} : (ev.args || Object.create(null));
510 if('stmt'===rc.rowMode){
511 toss("Invalid rowMode for 'exec': stmt mode",
512 "does not work in the Worker API.");
513 }else if(!rc.sql){
514 toss("'exec' requires input SQL.");
516 const db = getMsgDb(ev);
517 if(rc.callback || Array.isArray(rc.resultRows)){
518 // Part of a copy-avoidance optimization for blobs
519 db._blobXfer = wState.xfer;
521 const theCallback = rc.callback;
522 let rowNumber = 0;
523 const hadColNames = !!rc.columnNames;
524 if('string' === typeof theCallback){
525 if(!hadColNames) rc.columnNames = [];
526 /* Treat this as a worker message type and post each
527 row as a message of that type. */
528 rc.callback = function(row,stmt){
529 wState.post({
530 type: theCallback,
531 columnNames: rc.columnNames,
532 rowNumber: ++rowNumber,
533 row: row
534 }, wState.xfer);
537 try {
538 const changeCount = !!rc.countChanges
539 ? db.changes(true,(64===rc.countChanges))
540 : undefined;
541 db.exec(rc);
542 if(undefined !== changeCount){
543 rc.changeCount = db.changes(true,64===rc.countChanges) - changeCount;
545 if(rc.callback instanceof Function){
546 rc.callback = theCallback;
547 /* Post a sentinel message to tell the client that the end
548 of the result set has been reached (possibly with zero
549 rows). */
550 wState.post({
551 type: theCallback,
552 columnNames: rc.columnNames,
553 rowNumber: null /*null to distinguish from "property not set"*/,
554 row: undefined /*undefined because null is a legal row value
555 for some rowType values, but undefined is not*/
558 }finally{
559 delete db._blobXfer;
560 if(rc.callback) rc.callback = theCallback;
562 return rc;
563 }/*exec()*/,
565 'config-get': function(){
566 const rc = Object.create(null), src = sqlite3.config;
568 'bigIntEnabled'
569 ].forEach(function(k){
570 if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k];
572 rc.version = sqlite3.version;
573 rc.vfsList = sqlite3.capi.sqlite3_js_vfs_list();
574 rc.opfsEnabled = !!sqlite3.opfs;
575 return rc;
579 Exports the database to a byte array, as per
580 sqlite3_serialize(). Response is an object:
583 byteArray: Uint8Array (db file contents),
584 filename: the current db filename,
585 mimetype: 'application/x-sqlite3'
588 export: function(ev){
589 const db = getMsgDb(ev);
590 const response = {
591 byteArray: sqlite3.capi.sqlite3_js_db_export(db.pointer),
592 filename: db.filename,
593 mimetype: 'application/x-sqlite3'
595 wState.xfer.push(response.byteArray.buffer);
596 return response;
597 }/*export()*/,
599 toss: function(ev){
600 toss("Testing worker exception");
603 'opfs-tree': async function(ev){
604 if(!sqlite3.opfs) toss("OPFS support is unavailable.");
605 const response = await sqlite3.opfs.treeList();
606 return response;
608 }/*wMsgHandler*/;
610 globalThis.onmessage = async function(ev){
611 ev = ev.data;
612 let result, dbId = ev.dbId, evType = ev.type;
613 const arrivalTime = performance.now();
614 try {
615 if(wMsgHandler.hasOwnProperty(evType) &&
616 wMsgHandler[evType] instanceof Function){
617 result = await wMsgHandler[evType](ev);
618 }else{
619 toss("Unknown db worker message type:",ev.type);
621 }catch(err){
622 evType = 'error';
623 result = {
624 operation: ev.type,
625 message: err.message,
626 errorClass: err.name,
627 input: ev
629 if(err.stack){
630 result.stack = ('string'===typeof err.stack)
631 ? err.stack.split(/\n\s*/) : err.stack;
633 if(0) sqlite3.config.warn("Worker is propagating an exception to main thread.",
634 "Reporting it _here_ for the stack trace:",err,result);
636 if(!dbId){
637 dbId = result.dbId/*from 'open' cmd*/
638 || getDefaultDbId();
640 // Timing info is primarily for use in testing this API. It's not part of
641 // the public API. arrivalTime = when the worker got the message.
642 wState.post({
643 type: evType,
644 dbId: dbId,
645 messageId: ev.messageId,
646 workerReceivedTime: arrivalTime,
647 workerRespondTime: performance.now(),
648 departureTime: ev.departureTime,
649 // TODO: move the timing bits into...
650 //timing:{
651 // departure: ev.departureTime,
652 // workerReceived: arrivalTime,
653 // workerResponse: performance.now();
654 //},
655 result: result
656 }, wState.xfer);
658 globalThis.postMessage({type:'sqlite3-api',result:'worker1-ready'});
659 }.bind({sqlite3});
661 //#else
662 /* Built with the omit-oo1 flag. */
663 //#endif ifnot omit-oo1