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 This function implements a Worker-based wrapper around SQLite3 OO 26 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 31 is called from a non-worker thread then it throws an exception. 32 33 When initialized, it installs message listeners to receive Worker 34 messages and then it posts a message in the form: 35 36 ``` 37 {type:'sqlite3-api',result:'worker1-ready'} 38 ``` 39 40 to let the client know that it has been initialized. Clients may 41 optionally depend on this function not returning until 42 initialization is complete, as the initialization is synchronous. 43 In some contexts, however, listening for the above message is 44 a better fit. 45 46 Note that the worker-based interface can be slightly quirky because 47 of its async nature. In particular, any number of messages may be posted 48 to the worker before it starts handling any of them. If, e.g., an 49 "open" operation fails, any subsequent messages will fail. The 50 Promise-based wrapper for this API (`sqlite3-worker1-promiser.js`) 51 is more comfortable to use in that regard. 52 53 54 TODO: hoist the message API docs from deep in this code to here. 55 56*/ 57self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 58sqlite3.initWorker1API = function(){ 59 'use strict'; 60 const toss = (...args)=>{throw new Error(args.join(' '))}; 61 if('function' !== typeof importScripts){ 62 toss("Cannot initalize the sqlite3 worker API in the main thread."); 63 } 64 const self = this.self; 65 const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object."); 66 const SQLite3 = sqlite3.oo1 || toss("Missing this.sqlite3.oo1 OO API."); 67 const DB = SQLite3.DB; 68 69 /** 70 Returns the app-wide unique ID for the given db, creating one if 71 needed. 72 */ 73 const getDbId = function(db){ 74 let id = wState.idMap.get(db); 75 if(id) return id; 76 id = 'db#'+(++wState.idSeq)+'@'+db.pointer; 77 /** ^^^ can't simply use db.pointer b/c closing/opening may re-use 78 the same address, which could map pending messages to a wrong 79 instance. */ 80 wState.idMap.set(db, id); 81 return id; 82 }; 83 84 /** 85 Internal helper for managing Worker-level state. 86 */ 87 const wState = { 88 defaultDb: undefined, 89 idSeq: 0, 90 idMap: new WeakMap, 91 xfer: [/*Temp holder for "transferable" postMessage() state.*/], 92 open: function(opt){ 93 const db = new DB(opt.filename); 94 this.dbs[getDbId(db)] = db; 95 if(!this.defaultDb) this.defaultDb = db; 96 return db; 97 }, 98 close: function(db,alsoUnlink){ 99 if(db){ 100 delete this.dbs[getDbId(db)]; 101 const filename = db.fileName(); 102 db.close(); 103 if(db===this.defaultDb) this.defaultDb = undefined; 104 if(alsoUnlink && filename){ 105 sqlite3.capi.sqlite3_wasm_vfs_unlink(filename); 106 } 107 } 108 }, 109 /** 110 Posts the given worker message value. If xferList is provided, 111 it must be an array, in which case a copy of it passed as 112 postMessage()'s second argument and xferList.length is set to 113 0. 114 */ 115 post: function(msg,xferList){ 116 if(xferList && xferList.length){ 117 self.postMessage( msg, Array.from(xferList) ); 118 xferList.length = 0; 119 }else{ 120 self.postMessage(msg); 121 } 122 }, 123 /** Map of DB IDs to DBs. */ 124 dbs: Object.create(null), 125 getDb: function(id,require=true){ 126 return this.dbs[id] 127 || (require ? toss("Unknown (or closed) DB ID:",id) : undefined); 128 } 129 }; 130 131 /** Throws if the given db is falsy or not opened. */ 132 const affirmDbOpen = function(db = wState.defaultDb){ 133 return (db && db.pointer) ? db : toss("DB is not opened."); 134 }; 135 136 /** Extract dbId from the given message payload. */ 137 const getMsgDb = function(msgData,affirmExists=true){ 138 const db = wState.getDb(msgData.dbId,false) || wState.defaultDb; 139 return affirmExists ? affirmDbOpen(db) : db; 140 }; 141 142 const getDefaultDbId = function(){ 143 return wState.defaultDb && getDbId(wState.defaultDb); 144 }; 145 146 /** 147 A level of "organizational abstraction" for the Worker 148 API. Each method in this object must map directly to a Worker 149 message type key. The onmessage() dispatcher attempts to 150 dispatch all inbound messages to a method of this object, 151 passing it the event.data part of the inbound event object. All 152 methods must return a plain Object containing any result 153 state, which the dispatcher may amend. All methods must throw 154 on error. 155 */ 156 const wMsgHandler = { 157 /** 158 Proxy for the DB constructor. Expects to be passed a single 159 object or a falsy value to use defaults: 160 161 { 162 filename [=":memory:" or "" (unspecified)]: the db filename. 163 See the sqlite3.oo1.DB constructor for peculiarities and transformations, 164 165 persistent [=false]: if true and filename is not one of ("", 166 ":memory:"), prepend 167 sqlite3.capi.sqlite3_web_persistent_dir() to the given 168 filename so that it is stored in persistent storage _if_ the 169 environment supports it. If persistent storage is not 170 supported, the filename is used as-is. 171 } 172 173 The response object looks like: 174 175 { 176 filename: db filename, possibly differing from the input. 177 178 dbId: an opaque ID value which must be passed in the message 179 envelope to other calls in this API to tell them which db to 180 use. If it is not provided to future calls, they will default 181 to operating on the first-opened db. 182 183 persistent: true if the given filename resides in the 184 known-persistent storage, else false. This determination is 185 independent of the `persistent` input argument. 186 } 187 */ 188 open: function(ev){ 189 const oargs = Object.create(null), args = (ev.args || Object.create(null)); 190 if(args.simulateError){ // undocumented internal testing option 191 toss("Throwing because of simulateError flag."); 192 } 193 const rc = Object.create(null); 194 const pDir = sqlite3.capi.sqlite3_web_persistent_dir(); 195 if(!args.filename || ':memory:'===args.filename){ 196 oargs.filename = args.filename || ''; 197 }else if(pDir){ 198 oargs.filename = pDir + ('/'===args.filename[0] ? args.filename : ('/'+args.filename)); 199 }else{ 200 oargs.filename = args.filename; 201 } 202 const db = wState.open(oargs); 203 rc.filename = db.filename; 204 rc.persistent = !!pDir && db.filename.startsWith(pDir); 205 rc.dbId = getDbId(db); 206 return rc; 207 }, 208 /** 209 Proxy for DB.close(). ev.args may be elided or an object with 210 an `unlink` property. If that value is truthy then the db file 211 (if the db is currently open) will be unlinked from the virtual 212 filesystem, else it will be kept intact, noting that unlink 213 failure is ignored. The result object is: 214 215 { 216 filename: db filename _if_ the db is opened when this 217 is called, else the undefined value 218 219 dbId: the ID of the closed b, or undefined if none is closed 220 } 221 222 It does not error if the given db is already closed or no db is 223 provided. It is simply does nothing useful in that case. 224 */ 225 close: function(ev){ 226 const db = getMsgDb(ev,false); 227 const response = { 228 filename: db && db.filename, 229 dbId: db && getDbId(db) 230 }; 231 if(db){ 232 wState.close(db, ((ev.args && 'object'===typeof ev.args) 233 ? !!ev.args.unlink : false)); 234 } 235 return response; 236 }, 237 /** 238 Proxy for oo1.DB.exec() which expects a single argument of type 239 string (SQL to execute) or an options object in the form 240 expected by exec(). The notable differences from exec() 241 include: 242 243 - The default value for options.rowMode is 'array' because 244 the normal default cannot cross the window/Worker boundary. 245 246 - A function-type options.callback property cannot cross 247 the window/Worker boundary, so is not useful here. If 248 options.callback is a string then it is assumed to be a 249 message type key, in which case a callback function will be 250 applied which posts each row result via: 251 252 postMessage({type: thatKeyType, 253 rowNumber: 1-based-#, 254 row: theRow, 255 columnNames: anArray 256 }) 257 258 And, at the end of the result set (whether or not any result 259 rows were produced), it will post an identical message with 260 (row=undefined, rowNumber=null) to alert the caller than the 261 result set is completed. Note that a row value of `null` is 262 a legal row result for certain `rowMode` values. 263 264 (Design note: we don't use (row=undefined, rowNumber=undefined) 265 to indicate end-of-results because fetching those would be 266 indistinguishable from fetching from an empty object unless the 267 client used hasOwnProperty() (or similar) to distinguish 268 "missing property" from "property with the undefined value". 269 Similarly, `null` is a legal value for `row` in some case , 270 whereas the db layer won't emit a result value of `undefined`.) 271 272 The callback proxy must not recurse into this interface, or 273 results are undefined. (It hypothetically cannot recurse 274 because an exec() call will be tying up the Worker thread, 275 causing any recursion attempt to wait until the first 276 exec() is completed.) 277 278 The response is the input options object (or a synthesized 279 one if passed only a string), noting that 280 options.resultRows and options.columnNames may be populated 281 by the call to db.exec(). 282 */ 283 exec: function(ev){ 284 const rc = ( 285 'string'===typeof ev.args 286 ) ? {sql: ev.args} : (ev.args || Object.create(null)); 287 if('stmt'===rc.rowMode){ 288 toss("Invalid rowMode for 'exec': stmt mode", 289 "does not work in the Worker API."); 290 } 291 const db = getMsgDb(ev); 292 if(rc.callback || Array.isArray(rc.resultRows)){ 293 // Part of a copy-avoidance optimization for blobs 294 db._blobXfer = wState.xfer; 295 } 296 const theCallback = rc.callback; 297 let rowNumber = 0; 298 const hadColNames = !!rc.columnNames; 299 if('string' === typeof theCallback){ 300 if(!hadColNames) rc.columnNames = []; 301 /* Treat this as a worker message type and post each 302 row as a message of that type. */ 303 rc.callback = function(row,stmt){ 304 wState.post({ 305 type: theCallback, 306 columnNames: rc.columnNames, 307 rowNumber: ++rowNumber, 308 row: row 309 }, wState.xfer); 310 } 311 } 312 try { 313 db.exec(rc); 314 if(rc.callback instanceof Function){ 315 rc.callback = theCallback; 316 /* Post a sentinel message to tell the client that the end 317 of the result set has been reached (possibly with zero 318 rows). */ 319 wState.post({ 320 type: theCallback, 321 columnNames: rc.columnNames, 322 rowNumber: null /*null to distinguish from "property not set"*/, 323 row: undefined /*undefined because null is a legal row value 324 for some rowType values, but undefined is not*/ 325 }); 326 } 327 }finally{ 328 delete db._blobXfer; 329 if(rc.callback) rc.callback = theCallback; 330 } 331 return rc; 332 }/*exec()*/, 333 /** 334 Returns a JSON-friendly form of a _subset_ of sqlite3.config, 335 sans any parts which cannot be serialized. Because we cannot, 336 from here, distingush whether or not certain objects can be 337 serialized, this routine selectively copies certain properties 338 rather than trying JSON.stringify() and seeing what happens 339 (the results are horrid if the config object contains an 340 Emscripten module object). 341 342 In addition to the "real" config properties, it sythesizes 343 the following: 344 345 - persistenceEnabled: true if persistent dir support is available, 346 else false. 347 */ 348 'config-get': function(){ 349 const rc = Object.create(null), src = sqlite3.config; 350 [ 351 'persistentDirName', 'bigIntEnabled' 352 ].forEach(function(k){ 353 if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k]; 354 }); 355 rc.persistenceEnabled = !!sqlite3.capi.sqlite3_web_persistent_dir(); 356 return rc; 357 }, 358 /** 359 TO(RE)DO, once we can abstract away access to the 360 JS environment's virtual filesystem. Currently this 361 always throws. 362 363 Response is (should be) an object: 364 365 { 366 buffer: Uint8Array (db file contents), 367 filename: the current db filename, 368 mimetype: 'application/x-sqlite3' 369 } 370 371 TODO is to determine how/whether this feature can support 372 exports of ":memory:" and "" (temp file) DBs. The latter is 373 ostensibly easy because the file is (potentially) on disk, but 374 the former does not have a structure which maps directly to a 375 db file image. We can VACUUM INTO a :memory:/temp db into a 376 file for that purpose, though. 377 */ 378 export: function(ev){ 379 toss("export() requires reimplementing for portability reasons."); 380 /** 381 We need to reimplement this to use the Emscripten FS 382 interface. That part used to be in the OO#1 API but that 383 dependency was removed from that level of the API. 384 */ 385 /**const db = getMsgDb(ev); 386 const response = { 387 buffer: db.exportBinaryImage(), 388 filename: db.filename, 389 mimetype: 'application/x-sqlite3' 390 }; 391 wState.xfer.push(response.buffer.buffer); 392 return response;**/ 393 }/*export()*/, 394 toss: function(ev){ 395 toss("Testing worker exception"); 396 } 397 }/*wMsgHandler*/; 398 399 /** 400 UNDER CONSTRUCTION! 401 402 A subset of the DB API is accessible via Worker messages in the 403 form: 404 405 { type: apiCommand, 406 args: apiArguments, 407 dbId: optional DB ID value (else uses a default db handle), 408 messageId: optional client-specific value 409 } 410 411 As a rule, these commands respond with a postMessage() of their 412 own. The responses always have a `type` property equal to the 413 input message's type and an object-format `result` part. If 414 the inbound object has a `messageId` property, that property is 415 always mirrored in the result object, for use in client-side 416 dispatching of these asynchronous results. For example: 417 418 { 419 type: 'open', 420 messageId: ...copied from inbound message..., 421 dbId: ID of db which was opened, 422 result: { 423 dbId: repeat of ^^^, for API consistency's sake, 424 filename: ..., 425 persistent: false 426 }, 427 ...possibly other framework-internal/testing/debugging info... 428 } 429 430 Exceptions thrown during processing result in an `error`-type 431 event with a payload in the form: 432 433 { type: 'error', 434 dbId: DB handle ID, 435 [messageId: if set in the inbound message], 436 result: { 437 operation: "inbound message's 'type' value", 438 message: error string, 439 errorClass: class name of the error type, 440 input: ev.data 441 } 442 } 443 444 The individual APIs are documented in the wMsgHandler object. 445 */ 446 self.onmessage = function(ev){ 447 ev = ev.data; 448 let result, dbId = ev.dbId, evType = ev.type; 449 const arrivalTime = performance.now(); 450 try { 451 if(wMsgHandler.hasOwnProperty(evType) && 452 wMsgHandler[evType] instanceof Function){ 453 result = wMsgHandler[evType](ev); 454 }else{ 455 toss("Unknown db worker message type:",ev.type); 456 } 457 }catch(err){ 458 evType = 'error'; 459 result = { 460 operation: ev.type, 461 message: err.message, 462 errorClass: err.name, 463 input: ev 464 }; 465 if(err.stack){ 466 result.stack = ('string'===typeof err.stack) 467 ? err.stack.split('\n') : err.stack; 468 } 469 if(0) console.warn("Worker is propagating an exception to main thread.", 470 "Reporting it _here_ for the stack trace:",err,result); 471 } 472 if(!dbId){ 473 dbId = result.dbId/*from 'open' cmd*/ 474 || getDefaultDbId(); 475 } 476 // Timing info is primarily for use in testing this API. It's not part of 477 // the public API. arrivalTime = when the worker got the message. 478 wState.post({ 479 type: evType, 480 dbId: dbId, 481 messageId: ev.messageId, 482 workerReceivedTime: arrivalTime, 483 workerRespondTime: performance.now(), 484 departureTime: ev.departureTime, 485 // TODO: move the timing bits into... 486 //timing:{ 487 // departure: ev.departureTime, 488 // workerReceived: arrivalTime, 489 // workerResponse: performance.now(); 490 //}, 491 result: result 492 }, wState.xfer); 493 }; 494 self.postMessage({type:'sqlite3-api',result:'worker1-ready'}); 495}.bind({self, sqlite3}); 496}); 497