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*/ 46self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 47sqlite3.initWorker1API = function(){ 48 'use strict'; 49 /** 50 UNDER CONSTRUCTION 51 52 We need an API which can proxy the DB API via a Worker message 53 interface. The primary quirky factor in such an API is that we 54 cannot pass callback functions between the window thread and a 55 worker thread, so we have to receive all db results via 56 asynchronous message-passing. That requires an asychronous API 57 with a distinctly different shape than OO API #1. 58 59 TODOs include, but are not necessarily limited to: 60 61 - Support for handling multiple DBs via this interface is under 62 development. 63 */ 64 const toss = (...args)=>{throw new Error(args.join(' '))}; 65 if('function' !== typeof importScripts){ 66 toss("Cannot initalize the sqlite3 worker API in the main thread."); 67 } 68 const self = this.self; 69 const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object."); 70 const SQLite3 = sqlite3.oo1 || toss("Missing this.sqlite3.oo1 OO API."); 71 const DB = SQLite3.DB; 72 73 /** 74 Returns the app-wide unique ID for the given db, creating one if 75 needed. 76 */ 77 const getDbId = function(db){ 78 let id = wState.idMap.get(db); 79 if(id) return id; 80 id = 'db#'+(++wState.idSeq)+'@'+db.pointer; 81 /** ^^^ can't simply use db.pointer b/c closing/opening may re-use 82 the same address, which could map pending messages to a wrong 83 instance. */ 84 wState.idMap.set(db, id); 85 return id; 86 }; 87 88 /** 89 Helper for managing Worker-level state. 90 */ 91 const wState = { 92 defaultDb: undefined, 93 idSeq: 0, 94 idMap: new WeakMap, 95 open: function(opt){ 96 const db = new DB(opt.filename); 97 this.dbs[getDbId(db)] = db; 98 if(!this.defaultDb) this.defaultDb = db; 99 return db; 100 }, 101 close: function(db,alsoUnlink){ 102 if(db){ 103 delete this.dbs[getDbId(db)]; 104 const filename = db.fileName(); 105 db.close(); 106 if(db===this.defaultDb) this.defaultDb = undefined; 107 if(alsoUnlink && filename){ 108 sqlite3.capi.sqlite3_wasm_vfs_unlink(filename); 109 } 110 } 111 }, 112 post: function(msg,xferList){ 113 if(xferList){ 114 self.postMessage( msg, xferList ); 115 xferList.length = 0; 116 }else{ 117 self.postMessage(msg); 118 } 119 }, 120 /** Map of DB IDs to DBs. */ 121 dbs: Object.create(null), 122 getDb: function(id,require=true){ 123 return this.dbs[id] 124 || (require ? toss("Unknown (or closed) DB ID:",id) : undefined); 125 } 126 }; 127 128 /** Throws if the given db is falsy or not opened. */ 129 const affirmDbOpen = function(db = wState.defaultDb){ 130 return (db && db.pointer) ? db : toss("DB is not opened."); 131 }; 132 133 /** Extract dbId from the given message payload. */ 134 const getMsgDb = function(msgData,affirmExists=true){ 135 const db = wState.getDb(msgData.dbId,false) || wState.defaultDb; 136 return affirmExists ? affirmDbOpen(db) : db; 137 }; 138 139 const getDefaultDbId = function(){ 140 return wState.defaultDb && getDbId(wState.defaultDb); 141 }; 142 143 /** 144 A level of "organizational abstraction" for the Worker 145 API. Each method in this object must map directly to a Worker 146 message type key. The onmessage() dispatcher attempts to 147 dispatch all inbound messages to a method of this object, 148 passing it the event.data part of the inbound event object. All 149 methods must return a plain Object containing any response 150 state, which the dispatcher may amend. All methods must throw 151 on error. 152 */ 153 const wMsgHandler = { 154 xfer: [/*Temp holder for "transferable" postMessage() state.*/], 155 /** 156 Proxy for the DB constructor. Expects to be passed a single 157 object or a falsy value to use defaults. The object may have a 158 filename property to name the db file (see the DB constructor 159 for peculiarities and transformations). The response is an 160 object: 161 162 { 163 filename: db filename (possibly differing from the input), 164 165 dbId: an opaque ID value which must be passed in the message 166 envelope to other calls in this API to tell them which 167 db to use. If it is not provided to future calls, they 168 will default to operating on the first-opened db. 169 170 persistent: prepend sqlite3.capi.sqlite3_web_persistent_dir() 171 to the given filename so that it is stored 172 in persistent storage _if_ the environment supports it. 173 If persistent storage is not supported, the filename 174 is used as-is. 175 } 176 */ 177 open: function(ev){ 178 const oargs = Object.create(null), args = (ev.args || Object.create(null)); 179 if(args.simulateError){ // undocumented internal testing option 180 toss("Throwing because of simulateError flag."); 181 } 182 if(args.persistent && args.filename){ 183 oargs.filename = sqlite3.capi.sqlite3_web_persistent_dir() + args.filename; 184 }else if('' === args.filename){ 185 oargs.filename = args.filename; 186 }else{ 187 oargs.filename = args.filename || ':memory:'; 188 } 189 const db = wState.open(oargs); 190 return { 191 filename: db.filename, 192 dbId: getDbId(db) 193 }; 194 }, 195 /** 196 Proxy for DB.close(). ev.args may be elided or an object with 197 an `unlink` property. If that value is truthy then the db file 198 (if the db is currently open) will be unlinked from the virtual 199 filesystem, else it will be kept intact. The result object is: 200 201 { 202 filename: db filename _if_ the db is opened when this 203 is called, else the undefined value 204 dbId: the ID of the closed b, or undefined if none is closed 205 } 206 207 It does not error if the given db is already closed or no db is 208 provided. It is simply does nothing useful in that case. 209 */ 210 close: function(ev){ 211 const db = getMsgDb(ev,false); 212 const response = { 213 filename: db && db.filename, 214 dbId: db ? getDbId(db) : undefined 215 }; 216 if(db){ 217 wState.close(db, ((ev.args && 'object'===typeof ev.args) 218 ? !!ev.args.unlink : false)); 219 } 220 return response; 221 }, 222 /** 223 Proxy for DB.exec() which expects a single argument of type 224 string (SQL to execute) or an options object in the form 225 expected by exec(). The notable differences from exec() 226 include: 227 228 - The default value for options.rowMode is 'array' because 229 the normal default cannot cross the window/Worker boundary. 230 231 - A function-type options.callback property cannot cross 232 the window/Worker boundary, so is not useful here. If 233 options.callback is a string then it is assumed to be a 234 message type key, in which case a callback function will be 235 applied which posts each row result via: 236 237 postMessage({type: thatKeyType, row: theRow}) 238 239 And, at the end of the result set (whether or not any 240 result rows were produced), it will post an identical 241 message with row:null to alert the caller than the result 242 set is completed. 243 244 The callback proxy must not recurse into this interface, or 245 results are undefined. (It hypothetically cannot recurse 246 because an exec() call will be tying up the Worker thread, 247 causing any recursion attempt to wait until the first 248 exec() is completed.) 249 250 The response is the input options object (or a synthesized 251 one if passed only a string), noting that 252 options.resultRows and options.columnNames may be populated 253 by the call to exec(). 254 255 This opens/creates the Worker's db if needed. 256 */ 257 exec: function(ev){ 258 const opt = ( 259 'string'===typeof ev.args 260 ) ? {sql: ev.args} : (ev.args || Object.create(null)); 261 if(undefined===opt.rowMode){ 262 /* Since the default rowMode of 'stmt' is not useful 263 for the Worker interface, we'll default to 264 something else. */ 265 opt.rowMode = 'array'; 266 }else if('stmt'===opt.rowMode){ 267 toss("Invalid rowMode for exec(): stmt mode", 268 "does not work in the Worker API."); 269 } 270 const db = getMsgDb(ev); 271 if(opt.callback || Array.isArray(opt.resultRows)){ 272 // Part of a copy-avoidance optimization for blobs 273 db._blobXfer = this.xfer; 274 } 275 const callbackMsgType = opt.callback; 276 if('string' === typeof callbackMsgType){ 277 /* Treat this as a worker message type and post each 278 row as a message of that type. */ 279 const that = this; 280 opt.callback = 281 (row)=>wState.post({type: callbackMsgType, row:row}, this.xfer); 282 } 283 try { 284 db.exec(opt); 285 if(opt.callback instanceof Function){ 286 opt.callback = callbackMsgType; 287 wState.post({type: callbackMsgType, row: null}); 288 } 289 }/*catch(e){ 290 console.warn("Worker is propagating:",e);throw e; 291 }*/finally{ 292 delete db._blobXfer; 293 if(opt.callback){ 294 opt.callback = callbackMsgType; 295 } 296 } 297 return opt; 298 }/*exec()*/, 299 /** 300 TO(RE)DO, once we can abstract away access to the 301 JS environment's virtual filesystem. Currently this 302 always throws. 303 304 Response is (should be) an object: 305 306 { 307 buffer: Uint8Array (db file contents), 308 filename: the current db filename, 309 mimetype: 'application/x-sqlite3' 310 } 311 312 TODO is to determine how/whether this feature can support 313 exports of ":memory:" and "" (temp file) DBs. The latter is 314 ostensibly easy because the file is (potentially) on disk, but 315 the former does not have a structure which maps directly to a 316 db file image. We can VACUUM INTO a :memory:/temp db into a 317 file for that purpose, though. 318 */ 319 export: function(ev){ 320 toss("export() requires reimplementing for portability reasons."); 321 /** 322 We need to reimplement this to use the Emscripten FS 323 interface. That part used to be in the OO#1 API but that 324 dependency was removed from that level of the API. 325 */ 326 /**const db = getMsgDb(ev); 327 const response = { 328 buffer: db.exportBinaryImage(), 329 filename: db.filename, 330 mimetype: 'application/x-sqlite3' 331 }; 332 this.xfer.push(response.buffer.buffer); 333 return response;**/ 334 }/*export()*/, 335 toss: function(ev){ 336 toss("Testing worker exception"); 337 } 338 }/*wMsgHandler*/; 339 340 /** 341 UNDER CONSTRUCTION! 342 343 A subset of the DB API is accessible via Worker messages in the 344 form: 345 346 { type: apiCommand, 347 dbId: optional DB ID value (else uses a default db handle), 348 args: apiArguments, 349 messageId: optional client-specific value 350 } 351 352 As a rule, these commands respond with a postMessage() of their 353 own in the form: 354 355 TODO: refactoring is underway. 356 357 The responses always have an object-format `result` part. If the 358 inbound object has a `messageId` property, that property is 359 always mirrored in the result object, for use in client-side 360 dispatching of these asynchronous results. Exceptions thrown 361 during processing result in an `error`-type event with a payload 362 in the form: 363 364 { type: 'error', 365 dbId: DB handle ID, 366 [messageId: if set in the inbound message], 367 result: { 368 operation: "inbound message's 'type' value", 369 message: error string, 370 errorClass: class name of the error type, 371 input: ev.data 372 } 373 } 374 375 The individual APIs are documented in the wMsgHandler object. 376 */ 377 self.onmessage = function(ev){ 378 ev = ev.data; 379 let result, dbId = ev.dbId, evType = ev.type; 380 const arrivalTime = performance.now(); 381 try { 382 if(wMsgHandler.hasOwnProperty(evType) && 383 wMsgHandler[evType] instanceof Function){ 384 result = wMsgHandler[evType](ev); 385 }else{ 386 toss("Unknown db worker message type:",ev.type); 387 } 388 }catch(err){ 389 evType = 'error'; 390 result = { 391 operation: ev.type, 392 message: err.message, 393 errorClass: err.name, 394 input: ev 395 }; 396 if(err.stack){ 397 result.stack = ('string'===typeof err.stack) 398 ? err.stack.split('\n') : err.stack; 399 } 400 if(0) console.warn("Worker is propagating an exception to main thread.", 401 "Reporting it _here_ for the stack trace:",err,result); 402 } 403 if(!dbId){ 404 dbId = result.dbId/*from 'open' cmd*/ 405 || getDefaultDbId(); 406 } 407 // Timing info is primarily for use in testing this API. It's not part of 408 // the public API. arrivalTime = when the worker got the message. 409 wState.post({ 410 type: evType, 411 dbId: dbId, 412 messageId: ev.messageId, 413 workerReceivedTime: arrivalTime, 414 workerRespondTime: performance.now(), 415 departureTime: ev.departureTime, 416 result: result 417 }, wMsgHandler.xfer); 418 }; 419 self.postMessage({type:'sqlite3-api',result:'worker1-ready'}); 420}.bind({self, sqlite3}); 421}); 422 423