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 /** 427 A level of "organizational abstraction" for the Worker1 428 API. Each method in this object must map directly to a Worker1 429 message type key. The onmessage() dispatcher attempts to 430 dispatch all inbound messages to a method of this object, 431 passing it the event.data part of the inbound event object. All 432 methods must return a plain Object containing any result 433 state, which the dispatcher may amend. All methods must throw 434 on error. 435 */ 436 const wMsgHandler = { 437 open: function(ev){ 438 const oargs = Object.create(null), args = (ev.args || Object.create(null)); 439 if(args.simulateError){ // undocumented internal testing option 440 toss("Throwing because of simulateError flag."); 441 } 442 const rc = Object.create(null); 443 const pDir = sqlite3.capi.sqlite3_wasmfs_opfs_dir(); 444 if(!args.filename || ':memory:'===args.filename){ 445 oargs.filename = args.filename || ''; 446 }else{ 447 oargs.filename = args.filename; 448 oargs.vfs = args.vfs; 449 } 450 const db = wState.open(oargs); 451 rc.filename = db.filename; 452 rc.persistent = (!!pDir && db.filename.startsWith(pDir+'/')) 453 || !!sqlite3.capi.sqlite3_js_db_uses_vfs(db.pointer, "opfs"); 454 rc.dbId = getDbId(db); 455 rc.vfs = db.dbVfsName(); 456 return rc; 457 }, 458 459 close: function(ev){ 460 const db = getMsgDb(ev,false); 461 const response = { 462 filename: db && db.filename 463 }; 464 if(db){ 465 // Keep the "unlink" flag undocumented until we figure out how 466 // to apply it consistently, independent of the db storage. 467 wState.close(db, ((ev.args && 'object'===typeof ev.args) 468 ? !!ev.args.unlink : false)); 469 } 470 return response; 471 }, 472 473 exec: function(ev){ 474 const rc = ( 475 'string'===typeof ev.args 476 ) ? {sql: ev.args} : (ev.args || Object.create(null)); 477 if('stmt'===rc.rowMode){ 478 toss("Invalid rowMode for 'exec': stmt mode", 479 "does not work in the Worker API."); 480 }else if(!rc.sql){ 481 toss("'exec' requires input SQL."); 482 } 483 const db = getMsgDb(ev); 484 if(rc.callback || Array.isArray(rc.resultRows)){ 485 // Part of a copy-avoidance optimization for blobs 486 db._blobXfer = wState.xfer; 487 } 488 const theCallback = rc.callback; 489 let rowNumber = 0; 490 const hadColNames = !!rc.columnNames; 491 if('string' === typeof theCallback){ 492 if(!hadColNames) rc.columnNames = []; 493 /* Treat this as a worker message type and post each 494 row as a message of that type. */ 495 rc.callback = function(row,stmt){ 496 wState.post({ 497 type: theCallback, 498 columnNames: rc.columnNames, 499 rowNumber: ++rowNumber, 500 row: row 501 }, wState.xfer); 502 } 503 } 504 try { 505 db.exec(rc); 506 if(rc.callback instanceof Function){ 507 rc.callback = theCallback; 508 /* Post a sentinel message to tell the client that the end 509 of the result set has been reached (possibly with zero 510 rows). */ 511 wState.post({ 512 type: theCallback, 513 columnNames: rc.columnNames, 514 rowNumber: null /*null to distinguish from "property not set"*/, 515 row: undefined /*undefined because null is a legal row value 516 for some rowType values, but undefined is not*/ 517 }); 518 } 519 }finally{ 520 delete db._blobXfer; 521 if(rc.callback) rc.callback = theCallback; 522 } 523 return rc; 524 }/*exec()*/, 525 526 'config-get': function(){ 527 const rc = Object.create(null), src = sqlite3.config; 528 [ 529 'wasmfsOpfsDir', 'bigIntEnabled' 530 ].forEach(function(k){ 531 if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k]; 532 }); 533 rc.wasmfsOpfsEnabled = !!sqlite3.capi.sqlite3_wasmfs_opfs_dir(); 534 rc.version = sqlite3.version; 535 rc.vfsList = sqlite3.capi.sqlite3_js_vfs_list(); 536 rc.opfsEnabled = !!sqlite3.opfs; 537 return rc; 538 }, 539 540 /** 541 Exports the database to a byte array, as per 542 sqlite3_serialize(). Response is an object: 543 544 { 545 bytearray: Uint8Array (db file contents), 546 filename: the current db filename, 547 mimetype: 'application/x-sqlite3' 548 } 549 */ 550 export: function(ev){ 551 const db = getMsgDb(ev); 552 const response = { 553 bytearray: sqlite3.capi.sqlite3_js_db_export(db.pointer), 554 filename: db.filename, 555 mimetype: 'application/x-sqlite3' 556 }; 557 wState.xfer.push(response.bytearray.buffer); 558 return response; 559 }/*export()*/, 560 561 toss: function(ev){ 562 toss("Testing worker exception"); 563 }, 564 565 'opfs-tree': async function(ev){ 566 if(!sqlite3.opfs) toss("OPFS support is unavailable."); 567 const response = await sqlite3.opfs.treeList(); 568 return response; 569 } 570 }/*wMsgHandler*/; 571 572 self.onmessage = async function(ev){ 573 ev = ev.data; 574 let result, dbId = ev.dbId, evType = ev.type; 575 const arrivalTime = performance.now(); 576 try { 577 if(wMsgHandler.hasOwnProperty(evType) && 578 wMsgHandler[evType] instanceof Function){ 579 result = await wMsgHandler[evType](ev); 580 }else{ 581 toss("Unknown db worker message type:",ev.type); 582 } 583 }catch(err){ 584 evType = 'error'; 585 result = { 586 operation: ev.type, 587 message: err.message, 588 errorClass: err.name, 589 input: ev 590 }; 591 if(err.stack){ 592 result.stack = ('string'===typeof err.stack) 593 ? err.stack.split(/\n\s*/) : err.stack; 594 } 595 if(0) console.warn("Worker is propagating an exception to main thread.", 596 "Reporting it _here_ for the stack trace:",err,result); 597 } 598 if(!dbId){ 599 dbId = result.dbId/*from 'open' cmd*/ 600 || getDefaultDbId(); 601 } 602 // Timing info is primarily for use in testing this API. It's not part of 603 // the public API. arrivalTime = when the worker got the message. 604 wState.post({ 605 type: evType, 606 dbId: dbId, 607 messageId: ev.messageId, 608 workerReceivedTime: arrivalTime, 609 workerRespondTime: performance.now(), 610 departureTime: ev.departureTime, 611 // TODO: move the timing bits into... 612 //timing:{ 613 // departure: ev.departureTime, 614 // workerReceived: arrivalTime, 615 // workerResponse: performance.now(); 616 //}, 617 result: result 618 }, wState.xfer); 619 }; 620 self.postMessage({type:'sqlite3-api',result:'worker1-ready'}); 621}.bind({self, sqlite3}); 622}); 623