1*8c3b7501Sstephan/* 2*8c3b7501Sstephan 2022-05-20 3*8c3b7501Sstephan 4*8c3b7501Sstephan The author disclaims copyright to this source code. In place of a 5*8c3b7501Sstephan legal notice, here is a blessing: 6*8c3b7501Sstephan 7*8c3b7501Sstephan * May you do good and not evil. 8*8c3b7501Sstephan * May you find forgiveness for yourself and forgive others. 9*8c3b7501Sstephan * May you share freely, never taking more than you give. 10*8c3b7501Sstephan 11*8c3b7501Sstephan *********************************************************************** 12*8c3b7501Sstephan 13*8c3b7501Sstephan This is the JS Worker file for the sqlite3 fiddle app. It loads the 14*8c3b7501Sstephan sqlite3 wasm module and offers access to the db via the Worker 15*8c3b7501Sstephan message-passing interface. 16*8c3b7501Sstephan 17*8c3b7501Sstephan Forewarning: this API is still very much Under Construction and 18*8c3b7501Sstephan subject to any number of changes as experience reveals what those 19*8c3b7501Sstephan need to be. 20*8c3b7501Sstephan 21*8c3b7501Sstephan Because we can have only a single message handler, as opposed to an 22*8c3b7501Sstephan arbitrary number of discrete event listeners like with DOM elements, 23*8c3b7501Sstephan we have to define a lower-level message API. Messages abstractly 24*8c3b7501Sstephan look like: 25*8c3b7501Sstephan 26*8c3b7501Sstephan { type: string, data: type-specific value } 27*8c3b7501Sstephan 28*8c3b7501Sstephan Where 'type' is used for dispatching and 'data' is a 29*8c3b7501Sstephan 'type'-dependent value. 30*8c3b7501Sstephan 31*8c3b7501Sstephan The 'type' values expected by each side of the main/worker 32*8c3b7501Sstephan connection vary. The types are described below but subject to 33*8c3b7501Sstephan change at any time as this experiment evolves. 34*8c3b7501Sstephan 35*8c3b7501Sstephan Workers-to-Main types 36*8c3b7501Sstephan 37*8c3b7501Sstephan - stdout, stderr: indicate stdout/stderr output from the wasm 38*8c3b7501Sstephan layer. The data property is the string of the output, noting 39*8c3b7501Sstephan that the emscripten binding emits these one line at a time. Thus, 40*8c3b7501Sstephan if a C-side puts() emits multiple lines in a single call, the JS 41*8c3b7501Sstephan side will see that as multiple calls. Example: 42*8c3b7501Sstephan 43*8c3b7501Sstephan {type:'stdout', data: 'Hi, world.'} 44*8c3b7501Sstephan 45*8c3b7501Sstephan - module: Status text. This is intended to alert the main thread 46*8c3b7501Sstephan about module loading status so that, e.g., the main thread can 47*8c3b7501Sstephan update a progress widget and DTRT when the module is finished 48*8c3b7501Sstephan loading and available for work. Status messages come in the form 49*8c3b7501Sstephan 50*8c3b7501Sstephan {type:'module', data:{ 51*8c3b7501Sstephan type:'status', 52*8c3b7501Sstephan data: {text:string|null, step:1-based-integer} 53*8c3b7501Sstephan } 54*8c3b7501Sstephan 55*8c3b7501Sstephan with an incrementing step value for each subsequent message. When 56*8c3b7501Sstephan the module loading is complete, a message with a text value of 57*8c3b7501Sstephan null is posted. 58*8c3b7501Sstephan 59*8c3b7501Sstephan - working: data='start'|'end'. Indicates that work is about to be 60*8c3b7501Sstephan sent to the module or has just completed. This can be used, e.g., 61*8c3b7501Sstephan to disable UI elements which should not be activated while work 62*8c3b7501Sstephan is pending. Example: 63*8c3b7501Sstephan 64*8c3b7501Sstephan {type:'working', data:'start'} 65*8c3b7501Sstephan 66*8c3b7501Sstephan Main-to-Worker types: 67*8c3b7501Sstephan 68*8c3b7501Sstephan - shellExec: data=text to execute as if it had been entered in the 69*8c3b7501Sstephan sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event 70*8c3b7501Sstephan causes the worker to emit a 'working' event (data='start') before 71*8c3b7501Sstephan it starts and a 'working' event (data='end') when it finished. If 72*8c3b7501Sstephan called while work is currently being executed it emits stderr 73*8c3b7501Sstephan message instead of doing actual work, as the underlying db cannot 74*8c3b7501Sstephan handle concurrent tasks. Example: 75*8c3b7501Sstephan 76*8c3b7501Sstephan {type:'shellExec', data: 'select * from sqlite_master'} 77*8c3b7501Sstephan 78*8c3b7501Sstephan - More TBD as the higher-level db layer develops. 79*8c3b7501Sstephan*/ 80*8c3b7501Sstephan 81*8c3b7501Sstephan/* 82*8c3b7501Sstephan Apparent browser(s) bug: console messages emitted may be duplicated 83*8c3b7501Sstephan in the console, even though they're provably only run once. See: 84*8c3b7501Sstephan 85*8c3b7501Sstephan https://stackoverflow.com/questions/49659464 86*8c3b7501Sstephan 87*8c3b7501Sstephan Noting that it happens in Firefox as well as Chrome. Harmless but 88*8c3b7501Sstephan annoying. 89*8c3b7501Sstephan*/ 90*8c3b7501Sstephan"use strict"; 91*8c3b7501Sstephan(function(){ 92*8c3b7501Sstephan /** 93*8c3b7501Sstephan Posts a message in the form {type,data} unless passed more than 2 94*8c3b7501Sstephan args, in which case it posts {type, data:[arg1...argN]}. 95*8c3b7501Sstephan */ 96*8c3b7501Sstephan const wMsg = function(type,data){ 97*8c3b7501Sstephan postMessage({ 98*8c3b7501Sstephan type, 99*8c3b7501Sstephan data: arguments.length<3 100*8c3b7501Sstephan ? data 101*8c3b7501Sstephan : Array.prototype.slice.call(arguments,1) 102*8c3b7501Sstephan }); 103*8c3b7501Sstephan }; 104*8c3b7501Sstephan 105*8c3b7501Sstephan const stdout = function(){wMsg('stdout', Array.prototype.slice.call(arguments));}; 106*8c3b7501Sstephan const stderr = function(){wMsg('stderr', Array.prototype.slice.call(arguments));}; 107*8c3b7501Sstephan 108*8c3b7501Sstephan self.onerror = function(/*message, source, lineno, colno, error*/) { 109*8c3b7501Sstephan const err = arguments[4]; 110*8c3b7501Sstephan if(err && 'ExitStatus'==err.name){ 111*8c3b7501Sstephan /* This is relevant for the sqlite3 shell binding but not the 112*8c3b7501Sstephan lower-level binding. */ 113*8c3b7501Sstephan fiddleModule.isDead = true; 114*8c3b7501Sstephan stderr("FATAL ERROR:", err.message); 115*8c3b7501Sstephan stderr("Restarting the app requires reloading the page."); 116*8c3b7501Sstephan wMsg('error', err); 117*8c3b7501Sstephan } 118*8c3b7501Sstephan console.error(err); 119*8c3b7501Sstephan fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err); 120*8c3b7501Sstephan }; 121*8c3b7501Sstephan 122*8c3b7501Sstephan const Sqlite3Shell = { 123*8c3b7501Sstephan /** Returns the name of the currently-opened db. */ 124*8c3b7501Sstephan dbFilename: function f(){ 125*8c3b7501Sstephan if(!f._) f._ = fiddleModule.cwrap('fiddle_db_filename', "string", ['string']); 126*8c3b7501Sstephan return f._(); 127*8c3b7501Sstephan }, 128*8c3b7501Sstephan /** 129*8c3b7501Sstephan Runs the given text through the shell as if it had been typed 130*8c3b7501Sstephan in by a user. Fires a working/start event before it starts and 131*8c3b7501Sstephan working/end event when it finishes. 132*8c3b7501Sstephan */ 133*8c3b7501Sstephan exec: function f(sql){ 134*8c3b7501Sstephan if(!f._) f._ = fiddleModule.cwrap('fiddle_exec', null, ['string']); 135*8c3b7501Sstephan if(fiddleModule.isDead){ 136*8c3b7501Sstephan stderr("shell module has exit()ed. Cannot run SQL."); 137*8c3b7501Sstephan return; 138*8c3b7501Sstephan } 139*8c3b7501Sstephan wMsg('working','start'); 140*8c3b7501Sstephan try { 141*8c3b7501Sstephan if(f._running){ 142*8c3b7501Sstephan stderr('Cannot run multiple commands concurrently.'); 143*8c3b7501Sstephan }else{ 144*8c3b7501Sstephan f._running = true; 145*8c3b7501Sstephan f._(sql); 146*8c3b7501Sstephan } 147*8c3b7501Sstephan } finally { 148*8c3b7501Sstephan delete f._running; 149*8c3b7501Sstephan wMsg('working','end'); 150*8c3b7501Sstephan } 151*8c3b7501Sstephan }, 152*8c3b7501Sstephan resetDb: function f(){ 153*8c3b7501Sstephan if(!f._) f._ = fiddleModule.cwrap('fiddle_reset_db', null); 154*8c3b7501Sstephan stdout("Resetting database."); 155*8c3b7501Sstephan f._(); 156*8c3b7501Sstephan stdout("Reset",this.dbFilename()); 157*8c3b7501Sstephan }, 158*8c3b7501Sstephan /* Interrupt can't work: this Worker is tied up working, so won't get the 159*8c3b7501Sstephan interrupt event which would be needed to perform the interrupt. */ 160*8c3b7501Sstephan interrupt: function f(){ 161*8c3b7501Sstephan if(!f._) f._ = fiddleModule.cwrap('fiddle_interrupt', null); 162*8c3b7501Sstephan stdout("Requesting interrupt."); 163*8c3b7501Sstephan f._(); 164*8c3b7501Sstephan } 165*8c3b7501Sstephan }; 166*8c3b7501Sstephan 167*8c3b7501Sstephan self.onmessage = function f(ev){ 168*8c3b7501Sstephan ev = ev.data; 169*8c3b7501Sstephan if(!f.cache){ 170*8c3b7501Sstephan f.cache = { 171*8c3b7501Sstephan prevFilename: null 172*8c3b7501Sstephan }; 173*8c3b7501Sstephan } 174*8c3b7501Sstephan //console.debug("worker: onmessage.data",ev); 175*8c3b7501Sstephan switch(ev.type){ 176*8c3b7501Sstephan case 'shellExec': Sqlite3Shell.exec(ev.data); return; 177*8c3b7501Sstephan case 'db-reset': Sqlite3Shell.resetDb(); return; 178*8c3b7501Sstephan case 'interrupt': Sqlite3Shell.interrupt(); return; 179*8c3b7501Sstephan /** Triggers the export of the current db. Fires an 180*8c3b7501Sstephan event in the form: 181*8c3b7501Sstephan 182*8c3b7501Sstephan {type:'db-export', 183*8c3b7501Sstephan data:{ 184*8c3b7501Sstephan filename: name of db, 185*8c3b7501Sstephan buffer: contents of the db file (Uint8Array), 186*8c3b7501Sstephan error: on error, a message string and no buffer property. 187*8c3b7501Sstephan } 188*8c3b7501Sstephan } 189*8c3b7501Sstephan */ 190*8c3b7501Sstephan case 'db-export': { 191*8c3b7501Sstephan const fn = Sqlite3Shell.dbFilename(); 192*8c3b7501Sstephan stdout("Exporting",fn+"."); 193*8c3b7501Sstephan const fn2 = fn ? fn.split(/[/\\]/).pop() : null; 194*8c3b7501Sstephan try{ 195*8c3b7501Sstephan if(!fn2) throw new Error("DB appears to be closed."); 196*8c3b7501Sstephan wMsg('db-export',{ 197*8c3b7501Sstephan filename: fn2, 198*8c3b7501Sstephan buffer: fiddleModule.FS.readFile(fn, {encoding:"binary"}) 199*8c3b7501Sstephan }); 200*8c3b7501Sstephan }catch(e){ 201*8c3b7501Sstephan /* Post a failure message so that UI elements disabled 202*8c3b7501Sstephan during the export can be re-enabled. */ 203*8c3b7501Sstephan wMsg('db-export',{ 204*8c3b7501Sstephan filename: fn, 205*8c3b7501Sstephan error: e.message 206*8c3b7501Sstephan }); 207*8c3b7501Sstephan } 208*8c3b7501Sstephan return; 209*8c3b7501Sstephan } 210*8c3b7501Sstephan case 'open': { 211*8c3b7501Sstephan /* Expects: { 212*8c3b7501Sstephan buffer: ArrayBuffer | Uint8Array, 213*8c3b7501Sstephan filename: for logging/informational purposes only 214*8c3b7501Sstephan } */ 215*8c3b7501Sstephan const opt = ev.data; 216*8c3b7501Sstephan let buffer = opt.buffer; 217*8c3b7501Sstephan if(buffer instanceof Uint8Array){ 218*8c3b7501Sstephan }else if(buffer instanceof ArrayBuffer){ 219*8c3b7501Sstephan buffer = new Uint8Array(buffer); 220*8c3b7501Sstephan }else{ 221*8c3b7501Sstephan stderr("'open' expects {buffer:Uint8Array} containing an uploaded db."); 222*8c3b7501Sstephan return; 223*8c3b7501Sstephan } 224*8c3b7501Sstephan const fn = ( 225*8c3b7501Sstephan opt.filename 226*8c3b7501Sstephan ? opt.filename.split(/[/\\]/).pop().replace('"','_') 227*8c3b7501Sstephan : ("db-"+((Math.random() * 10000000) | 0)+ 228*8c3b7501Sstephan "-"+((Math.random() * 10000000) | 0)+".sqlite3") 229*8c3b7501Sstephan ); 230*8c3b7501Sstephan /* We cannot delete the existing db file until the new one 231*8c3b7501Sstephan is installed, which means that we risk overflowing our 232*8c3b7501Sstephan quota (if any) by having both the previous and current 233*8c3b7501Sstephan db briefly installed in the virtual filesystem. */ 234*8c3b7501Sstephan fiddleModule.FS.createDataFile("/", fn, buffer, true, true); 235*8c3b7501Sstephan const oldName = Sqlite3Shell.dbFilename(); 236*8c3b7501Sstephan Sqlite3Shell.exec('.open "/'+fn+'"'); 237*8c3b7501Sstephan if(oldName && oldName !== fn){ 238*8c3b7501Sstephan try{fiddleModule.FS.unlink(oldName);} 239*8c3b7501Sstephan catch(e){/*ignored*/} 240*8c3b7501Sstephan } 241*8c3b7501Sstephan stdout("Replaced DB with",fn+"."); 242*8c3b7501Sstephan return; 243*8c3b7501Sstephan } 244*8c3b7501Sstephan }; 245*8c3b7501Sstephan console.warn("Unknown fiddle-worker message type:",ev); 246*8c3b7501Sstephan }; 247*8c3b7501Sstephan 248*8c3b7501Sstephan /** 249*8c3b7501Sstephan emscripten module for use with build mode -sMODULARIZE. 250*8c3b7501Sstephan */ 251*8c3b7501Sstephan const fiddleModule = { 252*8c3b7501Sstephan print: stdout, 253*8c3b7501Sstephan printErr: stderr, 254*8c3b7501Sstephan /** 255*8c3b7501Sstephan Intercepts status updates from the emscripting module init 256*8c3b7501Sstephan and fires worker events with a type of 'status' and a 257*8c3b7501Sstephan payload of: 258*8c3b7501Sstephan 259*8c3b7501Sstephan { 260*8c3b7501Sstephan text: string | null, // null at end of load process 261*8c3b7501Sstephan step: integer // starts at 1, increments 1 per call 262*8c3b7501Sstephan } 263*8c3b7501Sstephan 264*8c3b7501Sstephan We have no way of knowing in advance how many steps will 265*8c3b7501Sstephan be processed/posted, so creating a "percentage done" view is 266*8c3b7501Sstephan not really practical. One can be approximated by giving it a 267*8c3b7501Sstephan current value of message.step and max value of message.step+1, 268*8c3b7501Sstephan though. 269*8c3b7501Sstephan 270*8c3b7501Sstephan When work is finished, a message with a text value of null is 271*8c3b7501Sstephan submitted. 272*8c3b7501Sstephan 273*8c3b7501Sstephan After a message with text==null is posted, the module may later 274*8c3b7501Sstephan post messages about fatal problems, e.g. an exit() being 275*8c3b7501Sstephan triggered, so it is recommended that UI elements for posting 276*8c3b7501Sstephan status messages not be outright removed from the DOM when 277*8c3b7501Sstephan text==null, and that they instead be hidden until/unless 278*8c3b7501Sstephan text!=null. 279*8c3b7501Sstephan */ 280*8c3b7501Sstephan setStatus: function f(text){ 281*8c3b7501Sstephan if(!f.last) f.last = { step: 0, text: '' }; 282*8c3b7501Sstephan else if(text === f.last.text) return; 283*8c3b7501Sstephan f.last.text = text; 284*8c3b7501Sstephan wMsg('module',{ 285*8c3b7501Sstephan type:'status', 286*8c3b7501Sstephan data:{step: ++f.last.step, text: text||null} 287*8c3b7501Sstephan }); 288*8c3b7501Sstephan } 289*8c3b7501Sstephan }; 290*8c3b7501Sstephan 291*8c3b7501Sstephan importScripts('fiddle-module.js'); 292*8c3b7501Sstephan /** 293*8c3b7501Sstephan initFiddleModule() is installed via fiddle-module.js due to 294*8c3b7501Sstephan building with: 295*8c3b7501Sstephan 296*8c3b7501Sstephan emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule 297*8c3b7501Sstephan */ 298*8c3b7501Sstephan initFiddleModule(fiddleModule).then(function(thisModule){ 299*8c3b7501Sstephan wMsg('fiddle-ready'); 300*8c3b7501Sstephan }); 301*8c3b7501Sstephan})(); 302