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