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', 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 vfses: result of sqlite3.capi.sqlite3_web_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 contaning 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: none 229 } 230 ``` 231 232 If the dbId does not refer to an opened ID, this is a no-op. The 233 inability to close a db (because it's not opened) or delete its 234 file does not trigger an error. 235 236 Response: 237 238 ``` 239 { 240 type: 'close', 241 messageId: ...as above..., 242 result: { 243 244 filename: filename of closed db, or undefined if no db was closed 245 246 } 247 } 248 ``` 249 250 ==================================================================== 251 "exec" SQL 252 253 All SQL execution is processed through the exec operation. It offers 254 most of the features of the oo1.DB.exec() method, with a few limitations 255 imposed by the state having to cross thread boundaries. 256 257 Message format: 258 259 ``` 260 { 261 type: "exec", 262 messageId: ...as above... 263 dbId: ...as above... 264 args: string (SQL) or {... see below ...} 265 } 266 ``` 267 268 Response: 269 270 ``` 271 { 272 type: 'exec', 273 messageId: ...as above..., 274 dbId: ...as above... 275 result: { 276 input arguments, possibly modified. See below. 277 } 278 } 279 ``` 280 281 The arguments are in the same form accepted by oo1.DB.exec(), with 282 the exceptions noted below. 283 284 A function-type args.callback property cannot cross 285 the window/Worker boundary, so is not useful here. If 286 args.callback is a string then it is assumed to be a 287 message type key, in which case a callback function will be 288 applied which posts each row result via: 289 290 postMessage({type: thatKeyType, 291 rowNumber: 1-based-#, 292 row: theRow, 293 columnNames: anArray 294 }) 295 296 And, at the end of the result set (whether or not any result rows 297 were produced), it will post an identical message with 298 (row=undefined, rowNumber=null) to alert the caller than the result 299 set is completed. Note that a row value of `null` is a legal row 300 result for certain arg.rowMode values. 301 302 (Design note: we don't use (row=undefined, rowNumber=undefined) to 303 indicate end-of-results because fetching those would be 304 indistinguishable from fetching from an empty object unless the 305 client used hasOwnProperty() (or similar) to distinguish "missing 306 property" from "property with the undefined value". Similarly, 307 `null` is a legal value for `row` in some case , whereas the db 308 layer won't emit a result value of `undefined`.) 309 310 The callback proxy must not recurse into this interface. An exec() 311 call will tie up the Worker thread, causing any recursion attempt 312 to wait until the first exec() is completed. 313 314 The response is the input options object (or a synthesized one if 315 passed only a string), noting that options.resultRows and 316 options.columnNames may be populated by the call to db.exec(). 317 318*/ 319self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 320sqlite3.initWorker1API = function(){ 321 'use strict'; 322 const toss = (...args)=>{throw new Error(args.join(' '))}; 323 if('function' !== typeof importScripts){ 324 toss("initWorker1API() must be run from a Worker thread."); 325 } 326 const self = this.self; 327 const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object."); 328 const DB = sqlite3.oo1.DB; 329 330 /** 331 Returns the app-wide unique ID for the given db, creating one if 332 needed. 333 */ 334 const getDbId = function(db){ 335 let id = wState.idMap.get(db); 336 if(id) return id; 337 id = 'db#'+(++wState.idSeq)+'@'+db.pointer; 338 /** ^^^ can't simply use db.pointer b/c closing/opening may re-use 339 the same address, which could map pending messages to a wrong 340 instance. */ 341 wState.idMap.set(db, id); 342 return id; 343 }; 344 345 /** 346 Internal helper for managing Worker-level state. 347 */ 348 const wState = { 349 /** First-opened db is the default for future operations when no 350 dbId is provided by the client. */ 351 defaultDb: undefined, 352 /** Sequence number of dbId generation. */ 353 idSeq: 0, 354 /** Map of DB instances to dbId. */ 355 idMap: new WeakMap, 356 /** Temp holder for "transferable" postMessage() state. */ 357 xfer: [], 358 open: function(opt){ 359 const db = new DB(opt.filename); 360 this.dbs[getDbId(db)] = db; 361 if(!this.defaultDb) this.defaultDb = db; 362 return db; 363 }, 364 close: function(db,alsoUnlink){ 365 if(db){ 366 delete this.dbs[getDbId(db)]; 367 const filename = db.getFilename(); 368 db.close(); 369 if(db===this.defaultDb) this.defaultDb = undefined; 370 if(alsoUnlink && filename){ 371 /* This isn't necessarily correct: the db might be using a 372 VFS other than the default. How do we best resolve this 373 without having to special-case the kvvfs and opfs 374 VFSes? */ 375 sqlite3.capi.wasm.sqlite3_wasm_vfs_unlink(filename); 376 } 377 } 378 }, 379 /** 380 Posts the given worker message value. If xferList is provided, 381 it must be an array, in which case a copy of it passed as 382 postMessage()'s second argument and xferList.length is set to 383 0. 384 */ 385 post: function(msg,xferList){ 386 if(xferList && xferList.length){ 387 self.postMessage( msg, Array.from(xferList) ); 388 xferList.length = 0; 389 }else{ 390 self.postMessage(msg); 391 } 392 }, 393 /** Map of DB IDs to DBs. */ 394 dbs: Object.create(null), 395 /** Fetch the DB for the given id. Throw if require=true and the 396 id is not valid, else return the db or undefined. */ 397 getDb: function(id,require=true){ 398 return this.dbs[id] 399 || (require ? toss("Unknown (or closed) DB ID:",id) : undefined); 400 } 401 }; 402 403 /** Throws if the given db is falsy or not opened. */ 404 const affirmDbOpen = function(db = wState.defaultDb){ 405 return (db && db.pointer) ? db : toss("DB is not opened."); 406 }; 407 408 /** Extract dbId from the given message payload. */ 409 const getMsgDb = function(msgData,affirmExists=true){ 410 const db = wState.getDb(msgData.dbId,false) || wState.defaultDb; 411 return affirmExists ? affirmDbOpen(db) : db; 412 }; 413 414 const getDefaultDbId = function(){ 415 return wState.defaultDb && getDbId(wState.defaultDb); 416 }; 417 418 /** 419 A level of "organizational abstraction" for the Worker 420 API. Each method in this object must map directly to a Worker 421 message type key. The onmessage() dispatcher attempts to 422 dispatch all inbound messages to a method of this object, 423 passing it the event.data part of the inbound event object. All 424 methods must return a plain Object containing any result 425 state, which the dispatcher may amend. All methods must throw 426 on error. 427 */ 428 const wMsgHandler = { 429 open: function(ev){ 430 const oargs = Object.create(null), args = (ev.args || Object.create(null)); 431 if(args.simulateError){ // undocumented internal testing option 432 toss("Throwing because of simulateError flag."); 433 } 434 const rc = Object.create(null); 435 const pDir = sqlite3.capi.sqlite3_wasmfs_opfs_dir(); 436 if(!args.filename || ':memory:'===args.filename){ 437 oargs.filename = args.filename || ''; 438 }else if(pDir){ 439 oargs.filename = pDir + ('/'===args.filename[0] ? args.filename : ('/'+args.filename)); 440 }else{ 441 oargs.filename = args.filename; 442 } 443 const db = wState.open(oargs); 444 rc.filename = db.filename; 445 rc.persistent = (!!pDir && db.filename.startsWith(pDir)) 446 || sqlite3.capi.sqlite3_web_db_uses_vfs(db.pointer, "opfs"); 447 rc.dbId = getDbId(db); 448 return rc; 449 }, 450 451 close: function(ev){ 452 const db = getMsgDb(ev,false); 453 const response = { 454 filename: db && db.filename 455 }; 456 if(db){ 457 // Keep the "unlink" flag undocumented until we figure out how 458 // to apply it consistently, independent of the db storage. 459 wState.close(db, ((ev.args && 'object'===typeof ev.args) 460 ? !!ev.args.unlink : false)); 461 } 462 return response; 463 }, 464 465 exec: function(ev){ 466 const rc = ( 467 'string'===typeof ev.args 468 ) ? {sql: ev.args} : (ev.args || Object.create(null)); 469 if('stmt'===rc.rowMode){ 470 toss("Invalid rowMode for 'exec': stmt mode", 471 "does not work in the Worker API."); 472 }else if(!rc.sql){ 473 toss("'exec' requires input SQL."); 474 } 475 const db = getMsgDb(ev); 476 if(rc.callback || Array.isArray(rc.resultRows)){ 477 // Part of a copy-avoidance optimization for blobs 478 db._blobXfer = wState.xfer; 479 } 480 const theCallback = rc.callback; 481 let rowNumber = 0; 482 const hadColNames = !!rc.columnNames; 483 if('string' === typeof theCallback){ 484 if(!hadColNames) rc.columnNames = []; 485 /* Treat this as a worker message type and post each 486 row as a message of that type. */ 487 rc.callback = function(row,stmt){ 488 wState.post({ 489 type: theCallback, 490 columnNames: rc.columnNames, 491 rowNumber: ++rowNumber, 492 row: row 493 }, wState.xfer); 494 } 495 } 496 try { 497 db.exec(rc); 498 if(rc.callback instanceof Function){ 499 rc.callback = theCallback; 500 /* Post a sentinel message to tell the client that the end 501 of the result set has been reached (possibly with zero 502 rows). */ 503 wState.post({ 504 type: theCallback, 505 columnNames: rc.columnNames, 506 rowNumber: null /*null to distinguish from "property not set"*/, 507 row: undefined /*undefined because null is a legal row value 508 for some rowType values, but undefined is not*/ 509 }); 510 } 511 }finally{ 512 delete db._blobXfer; 513 if(rc.callback) rc.callback = theCallback; 514 } 515 return rc; 516 }/*exec()*/, 517 518 'config-get': function(){ 519 const rc = Object.create(null), src = sqlite3.config; 520 [ 521 'wasmfsOpfsDir', 'bigIntEnabled' 522 ].forEach(function(k){ 523 if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k]; 524 }); 525 rc.wasmfsOpfsEnabled = !!sqlite3.capi.sqlite3_wasmfs_opfs_dir(); 526 rc.version = sqlite3.version; 527 rc.vfses = sqlite3.capi.sqlite3_web_vfs_list(); 528 return rc; 529 }, 530 531 /** 532 TO(RE)DO, once we can abstract away access to the 533 JS environment's virtual filesystem. Currently this 534 always throws. 535 536 Response is (should be) an object: 537 538 { 539 buffer: Uint8Array (db file contents), 540 filename: the current db filename, 541 mimetype: 'application/x-sqlite3' 542 } 543 544 2022-09-30: we have shell.c:fiddle_export_db() which works fine 545 for disk-based databases (even if it's a virtual disk like an 546 Emscripten VFS). sqlite3_serialize() can return this for 547 :memory: and temp databases. 548 */ 549 export: function(ev){ 550 toss("export() requires reimplementing for portability reasons."); 551 /** 552 We need to reimplement this to use the Emscripten FS 553 interface. That part used to be in the OO#1 API but that 554 dependency was removed from that level of the API. 555 */ 556 /**const db = getMsgDb(ev); 557 const response = { 558 buffer: db.exportBinaryImage(), 559 filename: db.filename, 560 mimetype: 'application/x-sqlite3' 561 }; 562 wState.xfer.push(response.buffer.buffer); 563 return response;**/ 564 }/*export()*/, 565 566 toss: function(ev){ 567 toss("Testing worker exception"); 568 } 569 }/*wMsgHandler*/; 570 571 self.onmessage = function(ev){ 572 ev = ev.data; 573 let result, dbId = ev.dbId, evType = ev.type; 574 const arrivalTime = performance.now(); 575 try { 576 if(wMsgHandler.hasOwnProperty(evType) && 577 wMsgHandler[evType] instanceof Function){ 578 result = wMsgHandler[evType](ev); 579 }else{ 580 toss("Unknown db worker message type:",ev.type); 581 } 582 }catch(err){ 583 evType = 'error'; 584 result = { 585 operation: ev.type, 586 message: err.message, 587 errorClass: err.name, 588 input: ev 589 }; 590 if(err.stack){ 591 result.stack = ('string'===typeof err.stack) 592 ? err.stack.split(/\n\s*/) : err.stack; 593 } 594 if(0) console.warn("Worker is propagating an exception to main thread.", 595 "Reporting it _here_ for the stack trace:",err,result); 596 } 597 if(!dbId){ 598 dbId = result.dbId/*from 'open' cmd*/ 599 || getDefaultDbId(); 600 } 601 // Timing info is primarily for use in testing this API. It's not part of 602 // the public API. arrivalTime = when the worker got the message. 603 wState.post({ 604 type: evType, 605 dbId: dbId, 606 messageId: ev.messageId, 607 workerReceivedTime: arrivalTime, 608 workerRespondTime: performance.now(), 609 departureTime: ev.departureTime, 610 // TODO: move the timing bits into... 611 //timing:{ 612 // departure: ev.departureTime, 613 // workerReceived: arrivalTime, 614 // workerResponse: performance.now(); 615 //}, 616 result: result 617 }, wState.xfer); 618 }; 619 self.postMessage({type:'sqlite3-api',result:'worker1-ready'}); 620}.bind({self, sqlite3}); 621}); 622