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