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} unless passed more than 2 94 args, in which case it posts {type, data:[arg1...argN]}. 95 */ 96 const wMsg = function(type,data){ 97 postMessage({ 98 type, 99 data: arguments.length<3 100 ? data 101 : Array.prototype.slice.call(arguments,1) 102 }); 103 }; 104 105 const stdout = function(){wMsg('stdout', Array.prototype.slice.call(arguments));}; 106 const stderr = function(){wMsg('stderr', Array.prototype.slice.call(arguments));}; 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._ = fiddleModule.cwrap('fiddle_db_filename', "string", ['string']); 126 return f._(); 127 }, 128 /** 129 Runs the given text through the shell as if it had been typed 130 in by a user. Fires a working/start event before it starts and 131 working/end event when it finishes. 132 */ 133 exec: function f(sql){ 134 if(!f._) f._ = fiddleModule.cwrap('fiddle_exec', null, ['string']); 135 if(fiddleModule.isDead){ 136 stderr("shell module has exit()ed. Cannot run SQL."); 137 return; 138 } 139 wMsg('working','start'); 140 try { 141 if(f._running){ 142 stderr('Cannot run multiple commands concurrently.'); 143 }else{ 144 f._running = true; 145 f._(sql); 146 } 147 } finally { 148 delete f._running; 149 wMsg('working','end'); 150 } 151 }, 152 resetDb: function f(){ 153 if(!f._) f._ = fiddleModule.cwrap('fiddle_reset_db', null); 154 stdout("Resetting database."); 155 f._(); 156 stdout("Reset",this.dbFilename()); 157 }, 158 /* Interrupt can't work: this Worker is tied up working, so won't get the 159 interrupt event which would be needed to perform the interrupt. */ 160 interrupt: function f(){ 161 if(!f._) f._ = fiddleModule.cwrap('fiddle_interrupt', null); 162 stdout("Requesting interrupt."); 163 f._(); 164 } 165 }; 166 167 self.onmessage = function f(ev){ 168 ev = ev.data; 169 if(!f.cache){ 170 f.cache = { 171 prevFilename: null 172 }; 173 } 174 //console.debug("worker: onmessage.data",ev); 175 switch(ev.type){ 176 case 'shellExec': Sqlite3Shell.exec(ev.data); return; 177 case 'db-reset': Sqlite3Shell.resetDb(); return; 178 case 'interrupt': Sqlite3Shell.interrupt(); return; 179 /** Triggers the export of the current db. Fires an 180 event in the form: 181 182 {type:'db-export', 183 data:{ 184 filename: name of db, 185 buffer: contents of the db file (Uint8Array), 186 error: on error, a message string and no buffer property. 187 } 188 } 189 */ 190 case 'db-export': { 191 const fn = Sqlite3Shell.dbFilename(); 192 stdout("Exporting",fn+"."); 193 const fn2 = fn ? fn.split(/[/\\]/).pop() : null; 194 try{ 195 if(!fn2) throw new Error("DB appears to be closed."); 196 wMsg('db-export',{ 197 filename: fn2, 198 buffer: fiddleModule.FS.readFile(fn, {encoding:"binary"}) 199 }); 200 }catch(e){ 201 /* Post a failure message so that UI elements disabled 202 during the export can be re-enabled. */ 203 wMsg('db-export',{ 204 filename: fn, 205 error: e.message 206 }); 207 } 208 return; 209 } 210 case 'open': { 211 /* Expects: { 212 buffer: ArrayBuffer | Uint8Array, 213 filename: for logging/informational purposes only 214 } */ 215 const opt = ev.data; 216 let buffer = opt.buffer; 217 if(buffer instanceof Uint8Array){ 218 }else if(buffer instanceof ArrayBuffer){ 219 buffer = new Uint8Array(buffer); 220 }else{ 221 stderr("'open' expects {buffer:Uint8Array} containing an uploaded db."); 222 return; 223 } 224 const fn = ( 225 opt.filename 226 ? opt.filename.split(/[/\\]/).pop().replace('"','_') 227 : ("db-"+((Math.random() * 10000000) | 0)+ 228 "-"+((Math.random() * 10000000) | 0)+".sqlite3") 229 ); 230 /* We cannot delete the existing db file until the new one 231 is installed, which means that we risk overflowing our 232 quota (if any) by having both the previous and current 233 db briefly installed in the virtual filesystem. */ 234 fiddleModule.FS.createDataFile("/", fn, buffer, true, true); 235 const oldName = Sqlite3Shell.dbFilename(); 236 Sqlite3Shell.exec('.open "/'+fn+'"'); 237 if(oldName && oldName !== fn){ 238 try{fiddleModule.fsUnlink(oldName);} 239 catch(e){/*ignored*/} 240 } 241 stdout("Replaced DB with",fn+"."); 242 return; 243 } 244 }; 245 console.warn("Unknown fiddle-worker message type:",ev); 246 }; 247 248 /** 249 emscripten module for use with build mode -sMODULARIZE. 250 */ 251 const fiddleModule = { 252 print: stdout, 253 printErr: stderr, 254 /** 255 Intercepts status updates from the emscripting module init 256 and fires worker events with a type of 'status' and a 257 payload of: 258 259 { 260 text: string | null, // null at end of load process 261 step: integer // starts at 1, increments 1 per call 262 } 263 264 We have no way of knowing in advance how many steps will 265 be processed/posted, so creating a "percentage done" view is 266 not really practical. One can be approximated by giving it a 267 current value of message.step and max value of message.step+1, 268 though. 269 270 When work is finished, a message with a text value of null is 271 submitted. 272 273 After a message with text==null is posted, the module may later 274 post messages about fatal problems, e.g. an exit() being 275 triggered, so it is recommended that UI elements for posting 276 status messages not be outright removed from the DOM when 277 text==null, and that they instead be hidden until/unless 278 text!=null. 279 */ 280 setStatus: function f(text){ 281 if(!f.last) f.last = { step: 0, text: '' }; 282 else if(text === f.last.text) return; 283 f.last.text = text; 284 wMsg('module',{ 285 type:'status', 286 data:{step: ++f.last.step, text: text||null} 287 }); 288 } 289 }; 290 291 importScripts('fiddle-module.js'); 292 /** 293 initFiddleModule() is installed via fiddle-module.js due to 294 building with: 295 296 emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule 297 */ 298 initFiddleModule(fiddleModule).then(function(thisModule){ 299 fiddleModule.fsUnlink = fiddleModule.cwrap('sqlite3_wasm_vfs_unlink','number',['string']); 300 (function initOpfs(){ 301 if(!self.FileSystemHandle || !self.FileSystemDirectoryHandle 302 || !self.FileSystemFileHandle){ 303 stdout("OPFS unavailable. All DB state is transient."); 304 return; 305 } 306 try { 307 if(0===fiddleModule.ccall('sqlite3_wasm_init_opfs', undefined)){ 308 stdout("Initialized OPFS WASMFS backend."); 309 }else{ 310 stderr("Initialization of OPFS WASMFS backend failed."); 311 } 312 }catch(e){ 313 stderr("Apparently missing WASMFS:",e.message); 314 } 315 })(); 316 wMsg('fiddle-ready'); 317 }); 318})(); 319