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