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 sqlite3.initWorker1API() implements a Worker-based wrapper around 26 SQLite3 OO 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 is 31 called from a non-worker thread then it throws an exception. It 32 must only be called once per Worker. 33 34 When initialized, it installs message listeners to receive Worker 35 messages and then it posts a message in the form: 36 37 ``` 38 {type:'sqlite3-api', result:'worker1-ready'} 39 ``` 40 41 to let the client know that it has been initialized. Clients may 42 optionally depend on this function not returning until 43 initialization is complete, as the initialization is synchronous. 44 In some contexts, however, listening for the above message is 45 a better fit. 46 47 Note that the worker-based interface can be slightly quirky because 48 of its async nature. In particular, any number of messages may be posted 49 to the worker before it starts handling any of them. If, e.g., an 50 "open" operation fails, any subsequent messages will fail. The 51 Promise-based wrapper for this API (`sqlite3-worker1-promiser.js`) 52 is more comfortable to use in that regard. 53 54 The documentation for the input and output worker messages for 55 this API follows... 56 57 ==================================================================== 58 Common message format... 59 60 Each message posted to the worker has an operation-independent 61 envelope and operation-dependent arguments: 62 63 ``` 64 { 65 type: string, // one of: 'open', 'close', 'exec', 'config-get' 66 67 messageId: OPTIONAL arbitrary value. The worker will copy it as-is 68 into response messages to assist in client-side dispatching. 69 70 dbId: a db identifier string (returned by 'open') which tells the 71 operation which database instance to work on. If not provided, the 72 first-opened db is used. This is an "opaque" value, with no 73 inherently useful syntax or information. Its value is subject to 74 change with any given build of this API and cannot be used as a 75 basis for anything useful beyond its one intended purpose. 76 77 args: ...operation-dependent arguments... 78 79 // the framework may add other properties for testing or debugging 80 // purposes. 81 82 } 83 ``` 84 85 Response messages, posted back to the main thread, look like: 86 87 ``` 88 { 89 type: string. Same as above except for error responses, which have the type 90 'error', 91 92 messageId: same value, if any, provided by the inbound message 93 94 dbId: the id of the db which was operated on, if any, as returned 95 by the corresponding 'open' operation. 96 97 result: ...operation-dependent result... 98 99 } 100 ``` 101 102 ==================================================================== 103 Error responses 104 105 Errors are reported messages in an operation-independent format: 106 107 ``` 108 { 109 type: "error", 110 111 messageId: ...as above..., 112 113 dbId: ...as above... 114 115 result: { 116 117 operation: type of the triggering operation: 'open', 'close', ... 118 119 message: ...error message text... 120 121 errorClass: string. The ErrorClass.name property from the thrown exception. 122 123 input: the message object which triggered the error. 124 125 stack: _if available_, a stack trace array. 126 127 } 128 129 } 130 ``` 131 132 133 ==================================================================== 134 "config-get" 135 136 This operation fetches the serializable parts of the sqlite3 API 137 configuration. 138 139 Message format: 140 141 ``` 142 { 143 type: "config-get", 144 messageId: ...as above..., 145 args: currently ignored and may be elided. 146 } 147 ``` 148 149 Response: 150 151 ``` 152 { 153 type: "config-get", 154 messageId: ...as above..., 155 result: { 156 157 version: sqlite3.version object 158 159 bigIntEnabled: bool. True if BigInt support is enabled. 160 161 wasmfsOpfsDir: path prefix, if any, _intended_ for use with 162 WASMFS OPFS persistent storage. 163 164 wasmfsOpfsEnabled: true if persistent storage is enabled in the 165 current environment. Only files stored under wasmfsOpfsDir 166 will persist using that mechanism, however. It is legal to use 167 the non-WASMFS OPFS VFS to open a database via a URI-style 168 db filename. 169 170 vfsList: result of sqlite3.capi.sqlite3_js_vfs_list() 171 } 172 } 173 ``` 174 175 176 ==================================================================== 177 "open" a database 178 179 Message format: 180 181 ``` 182 { 183 type: "open", 184 messageId: ...as above..., 185 args:{ 186 187 filename [=":memory:" or "" (unspecified)]: the db filename. 188 See the sqlite3.oo1.DB constructor for peculiarities and 189 transformations, 190 191 } 192 } 193 ``` 194 195 Response: 196 197 ``` 198 { 199 type: "open", 200 messageId: ...as above..., 201 result: { 202 filename: db filename, possibly differing from the input. 203 204 dbId: an opaque ID value which must be passed in the message 205 envelope to other calls in this API to tell them which db to 206 use. If it is not provided to future calls, they will default to 207 operating on the first-opened db. This property is, for API 208 consistency's sake, also part of the containing message envelope. 209 Only the `open` operation includes it in the `result` property. 210 211 persistent: true if the given filename resides in the 212 known-persistent storage, else false. 213 214 } 215 } 216 ``` 217 218 ==================================================================== 219 "close" a database 220 221 Message format: 222 223 ``` 224 { 225 type: "close", 226 messageId: ...as above... 227 dbId: ...as above... 228 args: OPTIONAL {unlink: boolean} 229 } 230 ``` 231 232 If the `dbId` does not refer to an opened ID, this is a no-op. If 233 the `args` object contains a truthy `unlink` value then the database 234 will be unlinked (deleted) after closing it. The inability to close a 235 db (because it's not opened) or delete its file does not trigger an 236 error. 237 238 Response: 239 240 ``` 241 { 242 type: "close", 243 messageId: ...as above..., 244 result: { 245 246 filename: filename of closed db, or undefined if no db was closed 247 248 } 249 } 250 ``` 251 252 ==================================================================== 253 "exec" SQL 254 255 All SQL execution is processed through the exec operation. It offers 256 most of the features of the oo1.DB.exec() method, with a few limitations 257 imposed by the state having to cross thread boundaries. 258 259 Message format: 260 261 ``` 262 { 263 type: "exec", 264 messageId: ...as above... 265 dbId: ...as above... 266 args: string (SQL) or {... see below ...} 267 } 268 ``` 269 270 Response: 271 272 ``` 273 { 274 type: "exec", 275 messageId: ...as above..., 276 dbId: ...as above... 277 result: { 278 input arguments, possibly modified. See below. 279 } 280 } 281 ``` 282 283 The arguments are in the same form accepted by oo1.DB.exec(), with 284 the exceptions noted below. 285 286 A function-type args.callback property cannot cross 287 the window/Worker boundary, so is not useful here. If 288 args.callback is a string then it is assumed to be a 289 message type key, in which case a callback function will be 290 applied which posts each row result via: 291 292 postMessage({type: thatKeyType, 293 rowNumber: 1-based-#, 294 row: theRow, 295 columnNames: anArray 296 }) 297 298 And, at the end of the result set (whether or not any result rows 299 were produced), it will post an identical message with 300 (row=undefined, rowNumber=null) to alert the caller than the result 301 set is completed. Note that a row value of `null` is a legal row 302 result for certain arg.rowMode values. 303 304 (Design note: we don't use (row=undefined, rowNumber=undefined) to 305 indicate end-of-results because fetching those would be 306 indistinguishable from fetching from an empty object unless the 307 client used hasOwnProperty() (or similar) to distinguish "missing 308 property" from "property with the undefined value". Similarly, 309 `null` is a legal value for `row` in some case , whereas the db 310 layer won't emit a result value of `undefined`.) 311 312 The callback proxy must not recurse into this interface. An exec() 313 call will tie up the Worker thread, causing any recursion attempt 314 to wait until the first exec() is completed. 315 316 The response is the input options object (or a synthesized one if 317 passed only a string), noting that options.resultRows and 318 options.columnNames may be populated by the call to db.exec(). 319 320*/ 321self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 322sqlite3.initWorker1API = function(){ 323 'use strict'; 324 const toss = (...args)=>{throw new Error(args.join(' '))}; 325 if('function' !== typeof importScripts){ 326 toss("initWorker1API() must be run from a Worker thread."); 327 } 328 const self = this.self; 329 const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object."); 330 const DB = sqlite3.oo1.DB; 331 332 /** 333 Returns the app-wide unique ID for the given db, creating one if 334 needed. 335 */ 336 const getDbId = function(db){ 337 let id = wState.idMap.get(db); 338 if(id) return id; 339 id = 'db#'+(++wState.idSeq)+'@'+db.pointer; 340 /** ^^^ can't simply use db.pointer b/c closing/opening may re-use 341 the same address, which could map pending messages to a wrong 342 instance. */ 343 wState.idMap.set(db, id); 344 return id; 345 }; 346 347 /** 348 Internal helper for managing Worker-level state. 349 */ 350 const wState = { 351 /** First-opened db is the default for future operations when no 352 dbId is provided by the client. */ 353 defaultDb: undefined, 354 /** Sequence number of dbId generation. */ 355 idSeq: 0, 356 /** Map of DB instances to dbId. */ 357 idMap: new WeakMap, 358 /** Temp holder for "transferable" postMessage() state. */ 359 xfer: [], 360 open: function(opt){ 361 const db = new DB(opt.filename); 362 this.dbs[getDbId(db)] = db; 363 if(!this.defaultDb) this.defaultDb = db; 364 return db; 365 }, 366 close: function(db,alsoUnlink){ 367 if(db){ 368 delete this.dbs[getDbId(db)]; 369 const filename = db.getFilename(); 370 const pVfs = sqlite3.capi.wasm.sqlite3_wasm_db_vfs(db.pointer, 0); 371 db.close(); 372 if(db===this.defaultDb) this.defaultDb = undefined; 373 if(alsoUnlink && filename && pVfs){ 374 sqlite3.capi.wasm.sqlite3_wasm_vfs_unlink(pVfs, filename); 375 } 376 } 377 }, 378 /** 379 Posts the given worker message value. If xferList is provided, 380 it must be an array, in which case a copy of it passed as 381 postMessage()'s second argument and xferList.length is set to 382 0. 383 */ 384 post: function(msg,xferList){ 385 if(xferList && xferList.length){ 386 self.postMessage( msg, Array.from(xferList) ); 387 xferList.length = 0; 388 }else{ 389 self.postMessage(msg); 390 } 391 }, 392 /** Map of DB IDs to DBs. */ 393 dbs: Object.create(null), 394 /** Fetch the DB for the given id. Throw if require=true and the 395 id is not valid, else return the db or undefined. */ 396 getDb: function(id,require=true){ 397 return this.dbs[id] 398 || (require ? toss("Unknown (or closed) DB ID:",id) : undefined); 399 } 400 }; 401 402 /** Throws if the given db is falsy or not opened. */ 403 const affirmDbOpen = function(db = wState.defaultDb){ 404 return (db && db.pointer) ? db : toss("DB is not opened."); 405 }; 406 407 /** Extract dbId from the given message payload. */ 408 const getMsgDb = function(msgData,affirmExists=true){ 409 const db = wState.getDb(msgData.dbId,false) || wState.defaultDb; 410 return affirmExists ? affirmDbOpen(db) : db; 411 }; 412 413 const getDefaultDbId = function(){ 414 return wState.defaultDb && getDbId(wState.defaultDb); 415 }; 416 417 /** 418 A level of "organizational abstraction" for the Worker 419 API. Each method in this object must map directly to a Worker 420 message type key. The onmessage() dispatcher attempts to 421 dispatch all inbound messages to a method of this object, 422 passing it the event.data part of the inbound event object. All 423 methods must return a plain Object containing any result 424 state, which the dispatcher may amend. All methods must throw 425 on error. 426 */ 427 const wMsgHandler = { 428 open: function(ev){ 429 const oargs = Object.create(null), args = (ev.args || Object.create(null)); 430 if(args.simulateError){ // undocumented internal testing option 431 toss("Throwing because of simulateError flag."); 432 } 433 const rc = Object.create(null); 434 const pDir = sqlite3.capi.sqlite3_wasmfs_opfs_dir(); 435 if(!args.filename || ':memory:'===args.filename){ 436 oargs.filename = args.filename || ''; 437 }else{ 438 oargs.filename = args.filename; 439 } 440 const db = wState.open(oargs); 441 rc.filename = db.filename; 442 rc.persistent = (!!pDir && db.filename.startsWith(pDir+'/')) 443 || sqlite3.capi.sqlite3_js_db_uses_vfs(db.pointer, "opfs"); 444 rc.dbId = getDbId(db); 445 return rc; 446 }, 447 448 close: function(ev){ 449 const db = getMsgDb(ev,false); 450 const response = { 451 filename: db && db.filename 452 }; 453 if(db){ 454 // Keep the "unlink" flag undocumented until we figure out how 455 // to apply it consistently, independent of the db storage. 456 wState.close(db, ((ev.args && 'object'===typeof ev.args) 457 ? !!ev.args.unlink : false)); 458 } 459 return response; 460 }, 461 462 exec: function(ev){ 463 const rc = ( 464 'string'===typeof ev.args 465 ) ? {sql: ev.args} : (ev.args || Object.create(null)); 466 if('stmt'===rc.rowMode){ 467 toss("Invalid rowMode for 'exec': stmt mode", 468 "does not work in the Worker API."); 469 }else if(!rc.sql){ 470 toss("'exec' requires input SQL."); 471 } 472 const db = getMsgDb(ev); 473 if(rc.callback || Array.isArray(rc.resultRows)){ 474 // Part of a copy-avoidance optimization for blobs 475 db._blobXfer = wState.xfer; 476 } 477 const theCallback = rc.callback; 478 let rowNumber = 0; 479 const hadColNames = !!rc.columnNames; 480 if('string' === typeof theCallback){ 481 if(!hadColNames) rc.columnNames = []; 482 /* Treat this as a worker message type and post each 483 row as a message of that type. */ 484 rc.callback = function(row,stmt){ 485 wState.post({ 486 type: theCallback, 487 columnNames: rc.columnNames, 488 rowNumber: ++rowNumber, 489 row: row 490 }, wState.xfer); 491 } 492 } 493 try { 494 db.exec(rc); 495 if(rc.callback instanceof Function){ 496 rc.callback = theCallback; 497 /* Post a sentinel message to tell the client that the end 498 of the result set has been reached (possibly with zero 499 rows). */ 500 wState.post({ 501 type: theCallback, 502 columnNames: rc.columnNames, 503 rowNumber: null /*null to distinguish from "property not set"*/, 504 row: undefined /*undefined because null is a legal row value 505 for some rowType values, but undefined is not*/ 506 }); 507 } 508 }finally{ 509 delete db._blobXfer; 510 if(rc.callback) rc.callback = theCallback; 511 } 512 return rc; 513 }/*exec()*/, 514 515 'config-get': function(){ 516 const rc = Object.create(null), src = sqlite3.config; 517 [ 518 'wasmfsOpfsDir', 'bigIntEnabled' 519 ].forEach(function(k){ 520 if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k]; 521 }); 522 rc.wasmfsOpfsEnabled = !!sqlite3.capi.sqlite3_wasmfs_opfs_dir(); 523 rc.version = sqlite3.version; 524 rc.vfsList = sqlite3.capi.sqlite3_js_vfs_list(); 525 return rc; 526 }, 527 528 /** 529 TO(RE)DO, once we can abstract away access to the 530 JS environment's virtual filesystem. Currently this 531 always throws. 532 533 Response is (should be) an object: 534 535 { 536 buffer: Uint8Array (db file contents), 537 filename: the current db filename, 538 mimetype: 'application/x-sqlite3' 539 } 540 541 2022-09-30: we have shell.c:fiddle_export_db() which works fine 542 for disk-based databases (even if it's a virtual disk like an 543 Emscripten VFS). sqlite3_serialize() can return this for 544 :memory: and temp databases. 545 */ 546 export: function(ev){ 547 toss("export() requires reimplementing for portability reasons."); 548 /** 549 We need to reimplement this to use the Emscripten FS 550 interface. That part used to be in the OO#1 API but that 551 dependency was removed from that level of the API. 552 */ 553 /**const db = getMsgDb(ev); 554 const response = { 555 buffer: db.exportBinaryImage(), 556 filename: db.filename, 557 mimetype: 'application/x-sqlite3' 558 }; 559 wState.xfer.push(response.buffer.buffer); 560 return response;**/ 561 }/*export()*/, 562 563 toss: function(ev){ 564 toss("Testing worker exception"); 565 } 566 }/*wMsgHandler*/; 567 568 self.onmessage = function(ev){ 569 ev = ev.data; 570 let result, dbId = ev.dbId, evType = ev.type; 571 const arrivalTime = performance.now(); 572 try { 573 if(wMsgHandler.hasOwnProperty(evType) && 574 wMsgHandler[evType] instanceof Function){ 575 result = wMsgHandler[evType](ev); 576 }else{ 577 toss("Unknown db worker message type:",ev.type); 578 } 579 }catch(err){ 580 evType = 'error'; 581 result = { 582 operation: ev.type, 583 message: err.message, 584 errorClass: err.name, 585 input: ev 586 }; 587 if(err.stack){ 588 result.stack = ('string'===typeof err.stack) 589 ? err.stack.split(/\n\s*/) : err.stack; 590 } 591 if(0) console.warn("Worker is propagating an exception to main thread.", 592 "Reporting it _here_ for the stack trace:",err,result); 593 } 594 if(!dbId){ 595 dbId = result.dbId/*from 'open' cmd*/ 596 || getDefaultDbId(); 597 } 598 // Timing info is primarily for use in testing this API. It's not part of 599 // the public API. arrivalTime = when the worker got the message. 600 wState.post({ 601 type: evType, 602 dbId: dbId, 603 messageId: ev.messageId, 604 workerReceivedTime: arrivalTime, 605 workerRespondTime: performance.now(), 606 departureTime: ev.departureTime, 607 // TODO: move the timing bits into... 608 //timing:{ 609 // departure: ev.departureTime, 610 // workerReceived: arrivalTime, 611 // workerResponse: performance.now(); 612 //}, 613 result: result 614 }, wState.xfer); 615 }; 616 self.postMessage({type:'sqlite3-api',result:'worker1-ready'}); 617}.bind({self, sqlite3}); 618}); 619