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 persistentDirName: path prefix, if any, of persistent storage. 158 An empty string denotes that no persistent storage is available. 159 160 bigIntEnabled: bool. True if BigInt support is enabled. 161 162 persistenceEnabled: true if persistent storage is enabled in the 163 current environment. Only files stored under persistentDirName 164 will persist, however. 165 166 } 167 } 168 ``` 169 170 171 ==================================================================== 172 "open" a database 173 174 Message format: 175 176 ``` 177 { 178 type: "open", 179 messageId: ...as above..., 180 args:{ 181 182 filename [=":memory:" or "" (unspecified)]: the db filename. 183 See the sqlite3.oo1.DB constructor for peculiarities and transformations, 184 185 persistent [=false]: if true and filename is not one of ("", 186 ":memory:"), prepend sqlite3.capi.sqlite3_web_persistent_dir() 187 to the given filename so that it is stored in persistent storage 188 _if_ the environment supports it. If persistent storage is not 189 supported, the filename is used as-is. 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. This determination is 213 independent of the `persistent` input argument. 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: { 229 230 unlink: if truthy, the associated db will be unlinked (removed) 231 from the virtual filesystems. Failure to unlink is silently 232 ignored. 233 234 } 235 } 236 ``` 237 238 If the dbId does not refer to an opened ID, this is a no-op. The 239 inability to close a db (because it's not opened) or delete its 240 file does not trigger an 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 type 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(self.window === self || '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 /** First-opened db is the default for future operations when no 356 dbId is provided by the client. */ 357 defaultDb: undefined, 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.defaultDb) this.defaultDb = db; 368 return db; 369 }, 370 close: function(db,alsoUnlink){ 371 if(db){ 372 delete this.dbs[getDbId(db)]; 373 const filename = db.getFilename(); 374 db.close(); 375 if(db===this.defaultDb) this.defaultDb = undefined; 376 if(alsoUnlink && filename){ 377 /* This isn't necessarily correct: the db might be using a 378 VFS other than the default. How do we best resolve this 379 without having to special-case the kvvfs and opfs 380 VFSes? */ 381 sqlite3.capi.wasm.sqlite3_wasm_vfs_unlink(filename); 382 } 383 } 384 }, 385 /** 386 Posts the given worker message value. If xferList is provided, 387 it must be an array, in which case a copy of it passed as 388 postMessage()'s second argument and xferList.length is set to 389 0. 390 */ 391 post: function(msg,xferList){ 392 if(xferList && xferList.length){ 393 self.postMessage( msg, Array.from(xferList) ); 394 xferList.length = 0; 395 }else{ 396 self.postMessage(msg); 397 } 398 }, 399 /** Map of DB IDs to DBs. */ 400 dbs: Object.create(null), 401 /** Fetch the DB for the given id. Throw if require=true and the 402 id is not valid, else return the db or undefined. */ 403 getDb: function(id,require=true){ 404 return this.dbs[id] 405 || (require ? toss("Unknown (or closed) DB ID:",id) : undefined); 406 } 407 }; 408 409 /** Throws if the given db is falsy or not opened. */ 410 const affirmDbOpen = function(db = wState.defaultDb){ 411 return (db && db.pointer) ? db : toss("DB is not opened."); 412 }; 413 414 /** Extract dbId from the given message payload. */ 415 const getMsgDb = function(msgData,affirmExists=true){ 416 const db = wState.getDb(msgData.dbId,false) || wState.defaultDb; 417 return affirmExists ? affirmDbOpen(db) : db; 418 }; 419 420 const getDefaultDbId = function(){ 421 return wState.defaultDb && getDbId(wState.defaultDb); 422 }; 423 424 /** 425 A level of "organizational abstraction" for the Worker 426 API. Each method in this object must map directly to a Worker 427 message type key. The onmessage() dispatcher attempts to 428 dispatch all inbound messages to a method of this object, 429 passing it the event.data part of the inbound event object. All 430 methods must return a plain Object containing any result 431 state, which the dispatcher may amend. All methods must throw 432 on error. 433 */ 434 const wMsgHandler = { 435 open: function(ev){ 436 const oargs = Object.create(null), args = (ev.args || Object.create(null)); 437 if(args.simulateError){ // undocumented internal testing option 438 toss("Throwing because of simulateError flag."); 439 } 440 const rc = Object.create(null); 441 const pDir = sqlite3.capi.sqlite3_web_persistent_dir(); 442 if(!args.filename || ':memory:'===args.filename){ 443 oargs.filename = args.filename || ''; 444 }else if(pDir){ 445 oargs.filename = pDir + ('/'===args.filename[0] ? args.filename : ('/'+args.filename)); 446 }else{ 447 oargs.filename = args.filename; 448 } 449 const db = wState.open(oargs); 450 rc.filename = db.filename; 451 rc.persistent = !!pDir && db.filename.startsWith(pDir); 452 rc.dbId = getDbId(db); 453 return rc; 454 }, 455 456 close: function(ev){ 457 const db = getMsgDb(ev,false); 458 const response = { 459 filename: db && db.filename 460 }; 461 if(db){ 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 'persistentDirName', 'bigIntEnabled' 525 ].forEach(function(k){ 526 if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k]; 527 }); 528 rc.persistenceEnabled = !!sqlite3.capi.sqlite3_web_persistent_dir(); 529 return rc; 530 }, 531 532 /** 533 TO(RE)DO, once we can abstract away access to the 534 JS environment's virtual filesystem. Currently this 535 always throws. 536 537 Response is (should be) an object: 538 539 { 540 buffer: Uint8Array (db file contents), 541 filename: the current db filename, 542 mimetype: 'application/x-sqlite3' 543 } 544 545 TODO is to determine how/whether this feature can support 546 exports of ":memory:" and "" (temp file) DBs. The latter is 547 ostensibly easy because the file is (potentially) on disk, but 548 the former does not have a structure which maps directly to a 549 db file image. We can VACUUM INTO a :memory:/temp db into a 550 file for that purpose, though. 551 */ 552 export: function(ev){ 553 toss("export() requires reimplementing for portability reasons."); 554 /** 555 We need to reimplement this to use the Emscripten FS 556 interface. That part used to be in the OO#1 API but that 557 dependency was removed from that level of the API. 558 */ 559 /**const db = getMsgDb(ev); 560 const response = { 561 buffer: db.exportBinaryImage(), 562 filename: db.filename, 563 mimetype: 'application/x-sqlite3' 564 }; 565 wState.xfer.push(response.buffer.buffer); 566 return response;**/ 567 }/*export()*/, 568 569 toss: function(ev){ 570 toss("Testing worker exception"); 571 } 572 }/*wMsgHandler*/; 573 574 self.onmessage = function(ev){ 575 ev = ev.data; 576 let result, dbId = ev.dbId, evType = ev.type; 577 const arrivalTime = performance.now(); 578 try { 579 if(wMsgHandler.hasOwnProperty(evType) && 580 wMsgHandler[evType] instanceof Function){ 581 result = wMsgHandler[evType](ev); 582 }else{ 583 toss("Unknown db worker message type:",ev.type); 584 } 585 }catch(err){ 586 evType = 'error'; 587 result = { 588 operation: ev.type, 589 message: err.message, 590 errorClass: err.name, 591 input: ev 592 }; 593 if(err.stack){ 594 result.stack = ('string'===typeof err.stack) 595 ? err.stack.split(/\n\s*/) : err.stack; 596 } 597 if(0) console.warn("Worker is propagating an exception to main thread.", 598 "Reporting it _here_ for the stack trace:",err,result); 599 } 600 if(!dbId){ 601 dbId = result.dbId/*from 'open' cmd*/ 602 || getDefaultDbId(); 603 } 604 // Timing info is primarily for use in testing this API. It's not part of 605 // the public API. arrivalTime = when the worker got the message. 606 wState.post({ 607 type: evType, 608 dbId: dbId, 609 messageId: ev.messageId, 610 workerReceivedTime: arrivalTime, 611 workerRespondTime: performance.now(), 612 departureTime: ev.departureTime, 613 // TODO: move the timing bits into... 614 //timing:{ 615 // departure: ev.departureTime, 616 // workerReceived: arrivalTime, 617 // workerResponse: performance.now(); 618 //}, 619 result: result 620 }, wState.xfer); 621 }; 622 self.postMessage({type:'sqlite3-api',result:'worker1-ready'}); 623}.bind({self, sqlite3}); 624}); 625