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(arg){ 96 // TODO? if arg is a filename, look for a db in this.dbs with the 97 // same filename and close/reopen it (or just pass it back as is?). 98 if(!arg && this.defaultDb) return this.defaultDb; 99 const db = (Array.isArray(arg) ? new DB(...arg) : new DB(arg)); 100 this.dbs[getDbId(db)] = db; 101 if(!this.defaultDb) this.defaultDb = db; 102 return db; 103 }, 104 close: function(db,alsoUnlink){ 105 if(db){ 106 delete this.dbs[getDbId(db)]; 107 const filename = db.fileName(); 108 db.close(); 109 if(db===this.defaultDb) this.defaultDb = undefined; 110 if(alsoUnlink && filename){ 111 sqlite3.capi.sqlite3_wasm_vfs_unlink(filename); 112 } 113 } 114 }, 115 post: function(msg,xferList){ 116 if(xferList){ 117 self.postMessage( msg, 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 response 153 state, which the dispatcher may amend. All methods must throw 154 on error. 155 */ 156 const wMsgHandler = { 157 xfer: [/*Temp holder for "transferable" postMessage() state.*/], 158 /** 159 Proxy for the DB constructor. Expects to be passed a single 160 object or a falsy value to use defaults. The object may have a 161 filename property to name the db file (see the DB constructor 162 for peculiarities and transformations). The response is an 163 object: 164 165 { 166 filename: db filename (possibly differing from the input), 167 168 dbId: an opaque ID value which must be passed in the message 169 envelope to other calls in this API to tell them which 170 db to use. If it is not provided to future calls, they 171 will default to operating on the first-opened db. 172 } 173 */ 174 open: function(ev){ 175 const oargs = [], args = (ev.args || {}); 176 if(args.simulateError){ // undocumented internal testing option 177 toss("Throwing because of simulateError flag."); 178 } 179 if(args.filename) oargs.push(args.filename); 180 const db = wState.open(oargs); 181 return { 182 filename: db.filename, 183 dbId: getDbId(db) 184 }; 185 }, 186 /** 187 Proxy for DB.close(). ev.args may either be a boolean or an 188 object with an `unlink` property. If that value is truthy then 189 the db file (if the db is currently open) will be unlinked from 190 the virtual filesystem, else it will be kept intact. The 191 result object is: 192 193 { 194 filename: db filename _if_ the db is opened when this 195 is called, else the undefined value 196 } 197 198 It does not error if the given db is already closed or no db is 199 provided. It is simply does nothing useful in that case. 200 */ 201 close: function(ev){ 202 const db = getMsgDb(ev,false); 203 const response = { 204 filename: db && db.filename, 205 dbId: db ? getDbId(db) : undefined 206 }; 207 if(db){ 208 wState.close(db, ((ev.args && 'object'===typeof ev.args) 209 ? !!ev.args.unlink : false)); 210 } 211 return response; 212 }, 213 /** 214 Proxy for DB.exec() which expects a single argument of type 215 string (SQL to execute) or an options object in the form 216 expected by exec(). The notable differences from exec() 217 include: 218 219 - The default value for options.rowMode is 'array' because 220 the normal default cannot cross the window/Worker boundary. 221 222 - A function-type options.callback property cannot cross 223 the window/Worker boundary, so is not useful here. If 224 options.callback is a string then it is assumed to be a 225 message type key, in which case a callback function will be 226 applied which posts each row result via: 227 228 postMessage({type: thatKeyType, row: theRow}) 229 230 And, at the end of the result set (whether or not any 231 result rows were produced), it will post an identical 232 message with row:null to alert the caller than the result 233 set is completed. 234 235 The callback proxy must not recurse into this interface, or 236 results are undefined. (It hypothetically cannot recurse 237 because an exec() call will be tying up the Worker thread, 238 causing any recursion attempt to wait until the first 239 exec() is completed.) 240 241 The response is the input options object (or a synthesized 242 one if passed only a string), noting that 243 options.resultRows and options.columnNames may be populated 244 by the call to exec(). 245 246 This opens/creates the Worker's db if needed. 247 */ 248 exec: function(ev){ 249 const opt = ( 250 'string'===typeof ev.args 251 ) ? {sql: ev.args} : (ev.args || Object.create(null)); 252 if(undefined===opt.rowMode){ 253 /* Since the default rowMode of 'stmt' is not useful 254 for the Worker interface, we'll default to 255 something else. */ 256 opt.rowMode = 'array'; 257 }else if('stmt'===opt.rowMode){ 258 toss("Invalid rowMode for exec(): stmt mode", 259 "does not work in the Worker API."); 260 } 261 const db = getMsgDb(ev); 262 if(opt.callback || Array.isArray(opt.resultRows)){ 263 // Part of a copy-avoidance optimization for blobs 264 db._blobXfer = this.xfer; 265 } 266 const callbackMsgType = opt.callback; 267 if('string' === typeof callbackMsgType){ 268 /* Treat this as a worker message type and post each 269 row as a message of that type. */ 270 const that = this; 271 opt.callback = 272 (row)=>wState.post({type: callbackMsgType, row:row}, this.xfer); 273 } 274 try { 275 db.exec(opt); 276 if(opt.callback instanceof Function){ 277 opt.callback = callbackMsgType; 278 wState.post({type: callbackMsgType, row: null}); 279 } 280 }/*catch(e){ 281 console.warn("Worker is propagating:",e);throw e; 282 }*/finally{ 283 delete db._blobXfer; 284 if(opt.callback){ 285 opt.callback = callbackMsgType; 286 } 287 } 288 return opt; 289 }/*exec()*/, 290 /** 291 TO(RE)DO, once we can abstract away access to the 292 JS environment's virtual filesystem. Currently this 293 always throws. 294 295 Response is (should be) an object: 296 297 { 298 buffer: Uint8Array (db file contents), 299 filename: the current db filename, 300 mimetype: 'application/x-sqlite3' 301 } 302 303 TODO is to determine how/whether this feature can support 304 exports of ":memory:" and "" (temp file) DBs. The latter is 305 ostensibly easy because the file is (potentially) on disk, but 306 the former does not have a structure which maps directly to a 307 db file image. We can VACUUM INTO a :memory:/temp db into a 308 file for that purpose, though. 309 */ 310 export: function(ev){ 311 toss("export() requires reimplementing for portability reasons."); 312 /** 313 We need to reimplement this to use the Emscripten FS 314 interface. That part used to be in the OO#1 API but that 315 dependency was removed from that level of the API. 316 */ 317 /**const db = getMsgDb(ev); 318 const response = { 319 buffer: db.exportBinaryImage(), 320 filename: db.filename, 321 mimetype: 'application/x-sqlite3' 322 }; 323 this.xfer.push(response.buffer.buffer); 324 return response;**/ 325 }/*export()*/, 326 toss: function(ev){ 327 toss("Testing worker exception"); 328 } 329 }/*wMsgHandler*/; 330 331 /** 332 UNDER CONSTRUCTION! 333 334 A subset of the DB API is accessible via Worker messages in the 335 form: 336 337 { type: apiCommand, 338 dbId: optional DB ID value (else uses a default db handle), 339 args: apiArguments, 340 messageId: optional client-specific value 341 } 342 343 As a rule, these commands respond with a postMessage() of their 344 own in the form: 345 346 TODO: refactoring is underway. 347 348 The responses always have an object-format `result` part. If the 349 inbound object has a `messageId` property, that property is 350 always mirrored in the result object, for use in client-side 351 dispatching of these asynchronous results. Exceptions thrown 352 during processing result in an `error`-type event with a payload 353 in the form: 354 355 { type: 'error', 356 dbId: DB handle ID, 357 [messageId: if set in the inbound message], 358 result: { 359 message: error string, 360 errorClass: class name of the error type, 361 input: ev.data 362 } 363 } 364 365 The individual APIs are documented in the wMsgHandler object. 366 */ 367 self.onmessage = function(ev){ 368 ev = ev.data; 369 let result, dbId = ev.dbId, evType = ev.type; 370 const arrivalTime = performance.now(); 371 try { 372 if(wMsgHandler.hasOwnProperty(evType) && 373 wMsgHandler[evType] instanceof Function){ 374 result = wMsgHandler[evType](ev); 375 }else{ 376 toss("Unknown db worker message type:",ev.type); 377 } 378 }catch(err){ 379 evType = 'error'; 380 result = { 381 message: err.message, 382 errorClass: err.name, 383 input: ev 384 }; 385 if(err.stack){ 386 result.stack = ('string'===typeof err.stack) 387 ? err.stack.split('\n') : err.stack; 388 } 389 if(0) console.warn("Worker is propagating an exception to main thread.", 390 "Reporting it _here_ for the stack trace:",err,result); 391 } 392 if(!dbId){ 393 dbId = result.dbId/*from 'open' cmd*/ 394 || getDefaultDbId(); 395 } 396 // Timing info is primarily for use in testing this API. It's not part of 397 // the public API. arrivalTime = when the worker got the message. 398 wState.post({ 399 type: evType, 400 dbId: dbId, 401 messageId: ev.messageId, 402 workerReceivedTime: arrivalTime, 403 workerRespondTime: performance.now(), 404 departureTime: ev.departureTime, 405 result: result 406 }, wMsgHandler.xfer); 407 }; 408 setTimeout(()=>self.postMessage({type:'sqlite3-api',result:'worker1-ready'}), 0); 409}.bind({self, sqlite3}); 410}); 411 412