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