1/* 2 2022-05-20 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 is the JS Worker file for the sqlite3 fiddle app. It loads the 14 sqlite3 wasm module and offers access to the db via the Worker 15 message-passing interface. 16 17 Forewarning: this API is still very much Under Construction and 18 subject to any number of changes as experience reveals what those 19 need to be. 20 21 Because we can have only a single message handler, as opposed to an 22 arbitrary number of discrete event listeners like with DOM elements, 23 we have to define a lower-level message API. Messages abstractly 24 look like: 25 26 { type: string, data: type-specific value } 27 28 Where 'type' is used for dispatching and 'data' is a 29 'type'-dependent value. 30 31 The 'type' values expected by each side of the main/worker 32 connection vary. The types are described below but subject to 33 change at any time as this experiment evolves. 34 35 Workers-to-Main types 36 37 - stdout, stderr: indicate stdout/stderr output from the wasm 38 layer. The data property is the string of the output, noting 39 that the emscripten binding emits these one line at a time. Thus, 40 if a C-side puts() emits multiple lines in a single call, the JS 41 side will see that as multiple calls. Example: 42 43 {type:'stdout', data: 'Hi, world.'} 44 45 - module: Status text. This is intended to alert the main thread 46 about module loading status so that, e.g., the main thread can 47 update a progress widget and DTRT when the module is finished 48 loading and available for work. Status messages come in the form 49 50 {type:'module', data:{ 51 type:'status', 52 data: {text:string|null, step:1-based-integer} 53 } 54 55 with an incrementing step value for each subsequent message. When 56 the module loading is complete, a message with a text value of 57 null is posted. 58 59 - working: data='start'|'end'. Indicates that work is about to be 60 sent to the module or has just completed. This can be used, e.g., 61 to disable UI elements which should not be activated while work 62 is pending. Example: 63 64 {type:'working', data:'start'} 65 66 Main-to-Worker types: 67 68 - shellExec: data=text to execute as if it had been entered in the 69 sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event 70 causes the worker to emit a 'working' event (data='start') before 71 it starts and a 'working' event (data='end') when it finished. If 72 called while work is currently being executed it emits stderr 73 message instead of doing actual work, as the underlying db cannot 74 handle concurrent tasks. Example: 75 76 {type:'shellExec', data: 'select * from sqlite_master'} 77 78 - More TBD as the higher-level db layer develops. 79*/ 80 81/* 82 Apparent browser(s) bug: console messages emitted may be duplicated 83 in the console, even though they're provably only run once. See: 84 85 https://stackoverflow.com/questions/49659464 86 87 Noting that it happens in Firefox as well as Chrome. Harmless but 88 annoying. 89*/ 90"use strict"; 91(function(){ 92 /** 93 Posts a message in the form {type,data}. If passed more than 2 94 args, the 3rd must be an array of "transferable" values to pass 95 as the 2nd argument to postMessage(). */ 96 const wMsg = 97 (type,data,transferables)=>{ 98 postMessage({type, data}, transferables || []); 99 }; 100 const stdout = (...args)=>wMsg('stdout', args); 101 const stderr = (...args)=>wMsg('stderr', args); 102 const toss = (...args)=>{ 103 throw new Error(args.join(' ')); 104 }; 105 const fixmeOPFS = "(FIXME: won't work with vanilla OPFS.)"; 106 let sqlite3 /* gets assigned when the wasm module is loaded */; 107 108 self.onerror = function(/*message, source, lineno, colno, error*/) { 109 const err = arguments[4]; 110 if(err && 'ExitStatus'==err.name){ 111 /* This is relevant for the sqlite3 shell binding but not the 112 lower-level binding. */ 113 fiddleModule.isDead = true; 114 stderr("FATAL ERROR:", err.message); 115 stderr("Restarting the app requires reloading the page."); 116 wMsg('error', err); 117 } 118 console.error(err); 119 fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err); 120 }; 121 122 const Sqlite3Shell = { 123 /** Returns the name of the currently-opened db. */ 124 dbFilename: function f(){ 125 if(!f._) f._ = sqlite3.capi.wasm.xWrap('fiddle_db_filename', "string", ['string']); 126 return f._(0); 127 }, 128 dbHandle: function f(){ 129 if(!f._) f._ = sqlite3.capi.wasm.xWrap("fiddle_db_handle", "sqlite3*"); 130 return f._(); 131 }, 132 dbIsOpfs: function f(){ 133 return sqlite3.opfs && sqlite3.capi.sqlite3_web_db_uses_vfs( 134 this.dbHandle(), "opfs" 135 ); 136 }, 137 runMain: function f(){ 138 if(f.argv) return 0===f.argv.rc; 139 const dbName = "/fiddle.sqlite3"; 140 f.argv = [ 141 'sqlite3-fiddle.wasm', 142 '-bail', '-safe', 143 dbName 144 /* Reminder: because of how we run fiddle, we have to ensure 145 that any argv strings passed to its main() are valid until 146 the wasm environment shuts down. */ 147 ]; 148 const capi = sqlite3.capi; 149 /* We need to call sqlite3_shutdown() in order to avoid numerous 150 legitimate warnings from the shell about it being initialized 151 after sqlite3_initialize() has been called. This means, 152 however, that any initialization done by the JS code may need 153 to be re-done (e.g. re-registration of dynamically-loaded 154 VFSes). We need a more generic approach to running such 155 init-level code. */ 156 capi.sqlite3_shutdown(); 157 f.argv.pArgv = capi.wasm.allocMainArgv(f.argv); 158 f.argv.rc = capi.wasm.exports.fiddle_main( 159 f.argv.length, f.argv.pArgv 160 ); 161 if(f.argv.rc){ 162 stderr("Fatal error initializing sqlite3 shell."); 163 fiddleModule.isDead = true; 164 return false; 165 } 166 stdout("SQLite version", capi.sqlite3_libversion(), 167 capi.sqlite3_sourceid().substr(0,19)); 168 stdout('Welcome to the "fiddle" shell.'); 169 if(S.opfs){ 170 stdout("\nOPFS is available. To open a persistent db, use:\n\n", 171 " .open file:name?vfs=opfs\n\nbut note that some", 172 "features (e.g. upload) do not yet work with OPFS."); 173 S.opfs.registerVfs(); 174 } 175 stdout('\nEnter ".help" for usage hints.'); 176 this.exec([ // initialization commands... 177 '.nullvalue NULL', 178 '.headers on' 179 ].join('\n')); 180 return true; 181 }, 182 /** 183 Runs the given text through the shell as if it had been typed 184 in by a user. Fires a working/start event before it starts and 185 working/end event when it finishes. 186 */ 187 exec: function f(sql){ 188 if(!f._){ 189 if(!this.runMain()) return; 190 f._ = sqlite3.capi.wasm.xWrap('fiddle_exec', null, ['string']); 191 } 192 if(fiddleModule.isDead){ 193 stderr("shell module has exit()ed. Cannot run SQL."); 194 return; 195 } 196 wMsg('working','start'); 197 try { 198 if(f._running){ 199 stderr('Cannot run multiple commands concurrently.'); 200 }else if(sql){ 201 if(Array.isArray(sql)) sql = sql.join(''); 202 f._running = true; 203 f._(sql); 204 } 205 }finally{ 206 delete f._running; 207 wMsg('working','end'); 208 } 209 }, 210 resetDb: function f(){ 211 if(Sqlite3Shell.dbIsOpfs()){ 212 /* The problem is that fiddle_reset_db() uses the POSIX APIs 213 for file removal, which cannot see OPFS-hosted files. */ 214 stderr("TODO: cannot currently reset an OPFS-hosted db."); 215 return; 216 } 217 if(!f._) f._ = sqlite3.capi.wasm.xWrap('fiddle_reset_db', null); 218 stdout("Resetting database.",fixmeOPFS); 219 f._(); 220 stdout("Reset",this.dbFilename()); 221 }, 222 /* Interrupt can't work: this Worker is tied up working, so won't get the 223 interrupt event which would be needed to perform the interrupt. */ 224 interrupt: function f(){ 225 if(!f._) f._ = sqlite3.capi.wasm.xWrap('fiddle_interrupt', null); 226 stdout("Requesting interrupt."); 227 f._(); 228 } 229 }; 230 231 self.onmessage = function f(ev){ 232 ev = ev.data; 233 if(!f.cache){ 234 f.cache = { 235 prevFilename: null 236 }; 237 } 238 //console.debug("worker: onmessage.data",ev); 239 switch(ev.type){ 240 case 'shellExec': Sqlite3Shell.exec(ev.data); return; 241 case 'db-reset': Sqlite3Shell.resetDb(); return; 242 case 'interrupt': Sqlite3Shell.interrupt(); return; 243 /** Triggers the export of the current db. Fires an 244 event in the form: 245 246 {type:'db-export', 247 data:{ 248 filename: name of db, 249 buffer: contents of the db file (Uint8Array), 250 error: on error, a message string and no buffer property. 251 } 252 } 253 */ 254 case 'db-export': { 255 const fn = Sqlite3Shell.dbFilename(); 256 stdout("Exporting",fn+"."); 257 const fn2 = fn ? fn.split(/[/\\]/).pop() : null; 258 try{ 259 if(!fn2) toss("DB appears to be closed."); 260 const buffer = sqlite3.capi.sqlite3_web_db_export( 261 Sqlite3Shell.dbHandle() 262 ); 263 wMsg('db-export',{filename: fn2, buffer: buffer.buffer}, [buffer.buffer]); 264 }catch(e){ 265 console.error("Export failed:",e); 266 /* Post a failure message so that UI elements disabled 267 during the export can be re-enabled. */ 268 wMsg('db-export',{ 269 filename: fn, 270 error: e.message 271 }); 272 } 273 return; 274 } 275 case 'open': { 276 /* Expects: { 277 buffer: ArrayBuffer | Uint8Array, 278 filename: the filename for the db. Any dir part is 279 stripped. 280 } 281 */ 282 const opt = ev.data; 283 let buffer = opt.buffer; 284 stderr('open():',fixmeOPFS); 285 if(buffer instanceof ArrayBuffer){ 286 buffer = new Uint8Array(buffer); 287 }else if(!(buffer instanceof Uint8Array)){ 288 stderr("'open' expects {buffer:Uint8Array} containing an uploaded db."); 289 return; 290 } 291 const fn = ( 292 opt.filename 293 ? opt.filename.split(/[/\\]/).pop().replace('"','_') 294 : ("db-"+((Math.random() * 10000000) | 0)+ 295 "-"+((Math.random() * 10000000) | 0)+".sqlite3") 296 ); 297 try { 298 /* We cannot delete the existing db file until the new one 299 is installed, which means that we risk overflowing our 300 quota (if any) by having both the previous and current 301 db briefly installed in the virtual filesystem. */ 302 const fnAbs = '/'+fn; 303 const oldName = Sqlite3Shell.dbFilename(); 304 if(oldName && oldName===fnAbs){ 305 /* We cannot create the replacement file while the current file 306 is opened, nor does the shell have a .close command, so we 307 must temporarily switch to another db... */ 308 Sqlite3Shell.exec('.open :memory:'); 309 fiddleModule.FS.unlink(fnAbs); 310 } 311 fiddleModule.FS.createDataFile("/", fn, buffer, true, true); 312 Sqlite3Shell.exec('.open "'+fnAbs+'"'); 313 if(oldName && oldName!==fnAbs){ 314 try{fiddleModule.fsUnlink(oldName)} 315 catch(e){/*ignored*/} 316 } 317 stdout("Replaced DB with",fn+"."); 318 }catch(e){ 319 stderr("Error installing db",fn+":",e.message); 320 } 321 return; 322 } 323 }; 324 console.warn("Unknown fiddle-worker message type:",ev); 325 }; 326 327 /** 328 emscripten module for use with build mode -sMODULARIZE. 329 */ 330 const fiddleModule = { 331 print: stdout, 332 printErr: stderr, 333 /** 334 Intercepts status updates from the emscripting module init 335 and fires worker events with a type of 'status' and a 336 payload of: 337 338 { 339 text: string | null, // null at end of load process 340 step: integer // starts at 1, increments 1 per call 341 } 342 343 We have no way of knowing in advance how many steps will 344 be processed/posted, so creating a "percentage done" view is 345 not really practical. One can be approximated by giving it a 346 current value of message.step and max value of message.step+1, 347 though. 348 349 When work is finished, a message with a text value of null is 350 submitted. 351 352 After a message with text==null is posted, the module may later 353 post messages about fatal problems, e.g. an exit() being 354 triggered, so it is recommended that UI elements for posting 355 status messages not be outright removed from the DOM when 356 text==null, and that they instead be hidden until/unless 357 text!=null. 358 */ 359 setStatus: function f(text){ 360 if(!f.last) f.last = { step: 0, text: '' }; 361 else if(text === f.last.text) return; 362 f.last.text = text; 363 wMsg('module',{ 364 type:'status', 365 data:{step: ++f.last.step, text: text||null} 366 }); 367 } 368 }; 369 370 importScripts('fiddle-module.js'+self.location.search); 371 /** 372 initFiddleModule() is installed via fiddle-module.js due to 373 building with: 374 375 emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule 376 */ 377 sqlite3InitModule(fiddleModule).then((_sqlite3)=>{ 378 sqlite3 = _sqlite3; 379 fiddleModule.fsUnlink = (fn)=>{ 380 stderr("unlink:",fixmeOPFS); 381 return sqlite3.capi.wasm.sqlite3_wasm_vfs_unlink(fn); 382 }; 383 wMsg('fiddle-ready'); 384 })/*then()*/; 385})(); 386