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
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:
39 {type:'sqlite3-api', result:'worker1-ready'}
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
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
58 ====================================================================
59 Common message format...
61 Each message posted to the worker has an operation-independent
62 envelope and operation-dependent arguments:
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
86 Response messages, posted back to the main thread, look like:
90 type: string. Same as above except for error responses, which have the type
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 ====================================================================
106 Errors are reported messages in an operation-independent format:
112 messageId: ...as above...,
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 ====================================================================
137 This operation fetches the serializable parts of the sqlite3 API
145 messageId: ...as above...,
146 args: currently ignored and may be elided.
155 messageId: ...as above...,
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 ====================================================================
176 messageId: ...as above...,
179 filename [=":memory:" or "" (unspecified)]: the db filename.
180 See the sqlite3.oo1.DB constructor for peculiarities and
183 vfs: sqlite3_vfs name. Ignored if filename is ":memory:" or "".
184 This may change how the given filename is resolved.
194 messageId: ...as above...,
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`
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 ====================================================================
222 messageId: ...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
239 messageId: ...as above...,
242 filename: filename of closed db, or undefined if no db was closed
248 ====================================================================
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.
260 messageId: ...as above...
262 args: string (SQL) or {... see below ...}
271 messageId: ...as above...,
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
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-#,
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...
340 messageId: ...as above...,
350 messageId: ...as above...,
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(){
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
376 const getDbId = function(db
){
377 let id
= wState
.idMap
.get(db
);
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
383 wState
.idMap
.set(db
, id
);
388 Internal helper for managing Worker-level state.
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.
397 /** Sequence number of dbId generation. */
399 /** Map of DB instances to dbId. */
401 /** Temp holder for "transferable" postMessage() state. */
404 const db
= new DB(opt
);
405 this.dbs
[getDbId(db
)] = db
;
406 if(this.dbList
.indexOf(db
)<0) this.dbList
.push(db
);
409 close: function(db
,alsoUnlink
){
411 delete this.dbs
[getDbId(db
)];
412 const filename
= db
.filename
;
413 const pVfs
= util
.sqlite3__wasm_db_vfs(db
.pointer
, 0);
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
) );
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){
442 || (require
? toss("Unknown (or closed) DB ID:",id
) : undefined);
446 /** Throws if the given db is falsy or not opened, else returns its
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
476 const wMsgHandler
= {
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();
494 const db
= getMsgDb(ev
,false);
496 filename
: db
&& db
.filename
499 const doUnlink
= ((ev
.args
&& 'object'===typeof ev
.args
)
500 ? !!ev
.args
.unlink
: false);
501 wState
.close(db
, doUnlink
);
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.");
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
;
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
){
531 columnNames
: rc
.columnNames
,
532 rowNumber
: ++rowNumber
,
538 const changeCount
= !!rc
.countChanges
539 ? db
.changes(true,(64===rc
.countChanges
))
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
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*/
560 if(rc
.callback
) rc
.callback
= theCallback
;
565 'config-get': function(){
566 const rc
= Object
.create(null), src
= sqlite3
.config
;
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
;
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
);
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
);
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();
610 globalThis
.onmessage
= async
function(ev
){
612 let result
, dbId
= ev
.dbId
, evType
= ev
.type
;
613 const arrivalTime
= performance
.now();
615 if(wMsgHandler
.hasOwnProperty(evType
) &&
616 wMsgHandler
[evType
] instanceof Function
){
617 result
= await wMsgHandler
[evType
](ev
);
619 toss("Unknown db worker message type:",ev
.type
);
625 message
: err
.message
,
626 errorClass
: err
.name
,
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
);
637 dbId
= result
.dbId
/*from 'open' cmd*/
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.
645 messageId
: ev
.messageId
,
646 workerReceivedTime
: arrivalTime
,
647 workerRespondTime
: performance
.now(),
648 departureTime
: ev
.departureTime
,
649 // TODO: move the timing bits into...
651 // departure: ev.departureTime,
652 // workerReceived: arrivalTime,
653 // workerResponse: performance.now();
658 globalThis
.postMessage({type
:'sqlite3-api',result
:'worker1-ready'});
662 /* Built with the omit-oo1 flag. */
663 //#endif ifnot omit-oo1