18c3b7501Sstephan/* 28c3b7501Sstephan 2022-05-20 38c3b7501Sstephan 48c3b7501Sstephan The author disclaims copyright to this source code. In place of a 58c3b7501Sstephan legal notice, here is a blessing: 68c3b7501Sstephan 78c3b7501Sstephan * May you do good and not evil. 88c3b7501Sstephan * May you find forgiveness for yourself and forgive others. 98c3b7501Sstephan * May you share freely, never taking more than you give. 108c3b7501Sstephan 118c3b7501Sstephan *********************************************************************** 128c3b7501Sstephan 138c3b7501Sstephan This is the JS Worker file for the sqlite3 fiddle app. It loads the 148c3b7501Sstephan sqlite3 wasm module and offers access to the db via the Worker 158c3b7501Sstephan message-passing interface. 168c3b7501Sstephan 178c3b7501Sstephan Forewarning: this API is still very much Under Construction and 188c3b7501Sstephan subject to any number of changes as experience reveals what those 198c3b7501Sstephan need to be. 208c3b7501Sstephan 218c3b7501Sstephan Because we can have only a single message handler, as opposed to an 228c3b7501Sstephan arbitrary number of discrete event listeners like with DOM elements, 238c3b7501Sstephan we have to define a lower-level message API. Messages abstractly 248c3b7501Sstephan look like: 258c3b7501Sstephan 268c3b7501Sstephan { type: string, data: type-specific value } 278c3b7501Sstephan 288c3b7501Sstephan Where 'type' is used for dispatching and 'data' is a 298c3b7501Sstephan 'type'-dependent value. 308c3b7501Sstephan 318c3b7501Sstephan The 'type' values expected by each side of the main/worker 328c3b7501Sstephan connection vary. The types are described below but subject to 338c3b7501Sstephan change at any time as this experiment evolves. 348c3b7501Sstephan 358c3b7501Sstephan Workers-to-Main types 368c3b7501Sstephan 378c3b7501Sstephan - stdout, stderr: indicate stdout/stderr output from the wasm 388c3b7501Sstephan layer. The data property is the string of the output, noting 398c3b7501Sstephan that the emscripten binding emits these one line at a time. Thus, 408c3b7501Sstephan if a C-side puts() emits multiple lines in a single call, the JS 418c3b7501Sstephan side will see that as multiple calls. Example: 428c3b7501Sstephan 438c3b7501Sstephan {type:'stdout', data: 'Hi, world.'} 448c3b7501Sstephan 458c3b7501Sstephan - module: Status text. This is intended to alert the main thread 468c3b7501Sstephan about module loading status so that, e.g., the main thread can 478c3b7501Sstephan update a progress widget and DTRT when the module is finished 488c3b7501Sstephan loading and available for work. Status messages come in the form 498c3b7501Sstephan 508c3b7501Sstephan {type:'module', data:{ 518c3b7501Sstephan type:'status', 528c3b7501Sstephan data: {text:string|null, step:1-based-integer} 538c3b7501Sstephan } 548c3b7501Sstephan 558c3b7501Sstephan with an incrementing step value for each subsequent message. When 568c3b7501Sstephan the module loading is complete, a message with a text value of 578c3b7501Sstephan null is posted. 588c3b7501Sstephan 598c3b7501Sstephan - working: data='start'|'end'. Indicates that work is about to be 608c3b7501Sstephan sent to the module or has just completed. This can be used, e.g., 618c3b7501Sstephan to disable UI elements which should not be activated while work 628c3b7501Sstephan is pending. Example: 638c3b7501Sstephan 648c3b7501Sstephan {type:'working', data:'start'} 658c3b7501Sstephan 668c3b7501Sstephan Main-to-Worker types: 678c3b7501Sstephan 688c3b7501Sstephan - shellExec: data=text to execute as if it had been entered in the 698c3b7501Sstephan sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event 708c3b7501Sstephan causes the worker to emit a 'working' event (data='start') before 718c3b7501Sstephan it starts and a 'working' event (data='end') when it finished. If 728c3b7501Sstephan called while work is currently being executed it emits stderr 738c3b7501Sstephan message instead of doing actual work, as the underlying db cannot 748c3b7501Sstephan handle concurrent tasks. Example: 758c3b7501Sstephan 768c3b7501Sstephan {type:'shellExec', data: 'select * from sqlite_master'} 778c3b7501Sstephan 788c3b7501Sstephan - More TBD as the higher-level db layer develops. 798c3b7501Sstephan*/ 808c3b7501Sstephan 818c3b7501Sstephan/* 828c3b7501Sstephan Apparent browser(s) bug: console messages emitted may be duplicated 838c3b7501Sstephan in the console, even though they're provably only run once. See: 848c3b7501Sstephan 858c3b7501Sstephan https://stackoverflow.com/questions/49659464 868c3b7501Sstephan 878c3b7501Sstephan Noting that it happens in Firefox as well as Chrome. Harmless but 888c3b7501Sstephan annoying. 898c3b7501Sstephan*/ 908c3b7501Sstephan"use strict"; 918c3b7501Sstephan(function(){ 928c3b7501Sstephan /** 938c3b7501Sstephan Posts a message in the form {type,data} unless passed more than 2 948c3b7501Sstephan args, in which case it posts {type, data:[arg1...argN]}. 958c3b7501Sstephan */ 968c3b7501Sstephan const wMsg = function(type,data){ 978c3b7501Sstephan postMessage({ 988c3b7501Sstephan type, 998c3b7501Sstephan data: arguments.length<3 1008c3b7501Sstephan ? data 1018c3b7501Sstephan : Array.prototype.slice.call(arguments,1) 1028c3b7501Sstephan }); 1038c3b7501Sstephan }; 1048c3b7501Sstephan 1056110a5d0Sstephan const stdout = (...args)=>wMsg('stdout', args); 1066110a5d0Sstephan const stderr = (...args)=>wMsg('stderr', args); 1078c3b7501Sstephan 10860d9aa7cSstephan const fixmeOPFS = "(FIXME: won't work with vanilla OPFS.)"; 10960d9aa7cSstephan 1108c3b7501Sstephan self.onerror = function(/*message, source, lineno, colno, error*/) { 1118c3b7501Sstephan const err = arguments[4]; 1128c3b7501Sstephan if(err && 'ExitStatus'==err.name){ 1138c3b7501Sstephan /* This is relevant for the sqlite3 shell binding but not the 1148c3b7501Sstephan lower-level binding. */ 1158c3b7501Sstephan fiddleModule.isDead = true; 1168c3b7501Sstephan stderr("FATAL ERROR:", err.message); 1178c3b7501Sstephan stderr("Restarting the app requires reloading the page."); 1188c3b7501Sstephan wMsg('error', err); 1198c3b7501Sstephan } 1208c3b7501Sstephan console.error(err); 1218c3b7501Sstephan fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err); 1228c3b7501Sstephan }; 1238c3b7501Sstephan 1248c3b7501Sstephan const Sqlite3Shell = { 1258c3b7501Sstephan /** Returns the name of the currently-opened db. */ 1268c3b7501Sstephan dbFilename: function f(){ 1278c3b7501Sstephan if(!f._) f._ = fiddleModule.cwrap('fiddle_db_filename', "string", ['string']); 1288c3b7501Sstephan return f._(); 1298c3b7501Sstephan }, 13060d9aa7cSstephan runMain: function f(){ 131*ef11fb91Sstephan if(f.argv) return 0===f.argv.rc; 13260d9aa7cSstephan const dbName = "/fiddle.sqlite3"; 13360d9aa7cSstephan f.argv = [ 13460d9aa7cSstephan 'sqlite3-fiddle.wasm', 13560d9aa7cSstephan '-bail', '-safe', 13660d9aa7cSstephan dbName 13760d9aa7cSstephan /* Reminder: because of how we run fiddle, we have to ensure 13860d9aa7cSstephan that any argv strings passed to its main() are valid until 13960d9aa7cSstephan the wasm environment shuts down. */ 14060d9aa7cSstephan ]; 141*ef11fb91Sstephan const S = fiddleModule.sqlite3; 142*ef11fb91Sstephan /* We need to call sqlite3_shutdown() in order to avoid numerous 143*ef11fb91Sstephan legitimate warnings from the shell about it being initialized 144*ef11fb91Sstephan after sqlite3_initialize() has been called. This mean , 145*ef11fb91Sstephan however, that any initialization done by the JS code may need 146*ef11fb91Sstephan to be re-done (e.g. re-registration of dynamically-loaded 147*ef11fb91Sstephan VFSes). */ 148*ef11fb91Sstephan S.capi.sqlite3_shutdown(); 149*ef11fb91Sstephan f.argv.pArgv = S.capi.wasm.allocMainArgv(f.argv); 150*ef11fb91Sstephan f.argv.rc = S.capi.wasm.exports.fiddle_main( 15160d9aa7cSstephan f.argv.length, f.argv.pArgv 15260d9aa7cSstephan ); 15360d9aa7cSstephan if(f.argv.rc){ 15460d9aa7cSstephan stderr("Fatal error initializing sqlite3 shell."); 15560d9aa7cSstephan fiddleModule.isDead = true; 15660d9aa7cSstephan return false; 15760d9aa7cSstephan } 158*ef11fb91Sstephan stdout("SQLite version", S.capi.sqlite3_libversion(), 159*ef11fb91Sstephan S.capi.sqlite3_sourceid().substr(0,19)); 160*ef11fb91Sstephan stdout('Welcome to the "fiddle" shell.'); 161*ef11fb91Sstephan if(S.opfs){ 162*ef11fb91Sstephan stdout("\nOPFS is available. To open a persistent db, use:\n\n", 163*ef11fb91Sstephan " .open file:name?vfs=opfs\n\nbut note that some", 164*ef11fb91Sstephan "features (e.g. export) do not yet work with OPFS."); 165*ef11fb91Sstephan S.opfs.reregisterVfs(); 166*ef11fb91Sstephan } 167*ef11fb91Sstephan stdout('\nEnter ".help" for usage hints.'); 168*ef11fb91Sstephan this.exec([ // initialization commands... 169*ef11fb91Sstephan '.nullvalue NULL', 170*ef11fb91Sstephan '.headers on' 171*ef11fb91Sstephan ].join('\n')); 17260d9aa7cSstephan return true; 17360d9aa7cSstephan }, 1748c3b7501Sstephan /** 1758c3b7501Sstephan Runs the given text through the shell as if it had been typed 1768c3b7501Sstephan in by a user. Fires a working/start event before it starts and 1778c3b7501Sstephan working/end event when it finishes. 1788c3b7501Sstephan */ 1798c3b7501Sstephan exec: function f(sql){ 18060d9aa7cSstephan if(!f._){ 18160d9aa7cSstephan if(!this.runMain()) return; 18260d9aa7cSstephan f._ = fiddleModule.cwrap('fiddle_exec', null, ['string']); 18360d9aa7cSstephan } 1848c3b7501Sstephan if(fiddleModule.isDead){ 1858c3b7501Sstephan stderr("shell module has exit()ed. Cannot run SQL."); 1868c3b7501Sstephan return; 1878c3b7501Sstephan } 1888c3b7501Sstephan wMsg('working','start'); 1898c3b7501Sstephan try { 1908c3b7501Sstephan if(f._running){ 1918c3b7501Sstephan stderr('Cannot run multiple commands concurrently.'); 19260d9aa7cSstephan }else if(sql){ 1938c3b7501Sstephan f._running = true; 1948c3b7501Sstephan f._(sql); 1958c3b7501Sstephan } 1968c3b7501Sstephan }finally{ 1978c3b7501Sstephan delete f._running; 1988c3b7501Sstephan wMsg('working','end'); 1998c3b7501Sstephan } 2008c3b7501Sstephan }, 2018c3b7501Sstephan resetDb: function f(){ 2028c3b7501Sstephan if(!f._) f._ = fiddleModule.cwrap('fiddle_reset_db', null); 20360d9aa7cSstephan stdout("Resetting database.",fixmeOPFS); 2048c3b7501Sstephan f._(); 2058c3b7501Sstephan stdout("Reset",this.dbFilename()); 2068c3b7501Sstephan }, 2078c3b7501Sstephan /* Interrupt can't work: this Worker is tied up working, so won't get the 2088c3b7501Sstephan interrupt event which would be needed to perform the interrupt. */ 2098c3b7501Sstephan interrupt: function f(){ 2108c3b7501Sstephan if(!f._) f._ = fiddleModule.cwrap('fiddle_interrupt', null); 2118c3b7501Sstephan stdout("Requesting interrupt."); 2128c3b7501Sstephan f._(); 2138c3b7501Sstephan } 2148c3b7501Sstephan }; 2158c3b7501Sstephan 2168c3b7501Sstephan self.onmessage = function f(ev){ 2178c3b7501Sstephan ev = ev.data; 2188c3b7501Sstephan if(!f.cache){ 2198c3b7501Sstephan f.cache = { 2208c3b7501Sstephan prevFilename: null 2218c3b7501Sstephan }; 2228c3b7501Sstephan } 2238c3b7501Sstephan //console.debug("worker: onmessage.data",ev); 2248c3b7501Sstephan switch(ev.type){ 2258c3b7501Sstephan case 'shellExec': Sqlite3Shell.exec(ev.data); return; 2268c3b7501Sstephan case 'db-reset': Sqlite3Shell.resetDb(); return; 2278c3b7501Sstephan case 'interrupt': Sqlite3Shell.interrupt(); return; 2288c3b7501Sstephan /** Triggers the export of the current db. Fires an 2298c3b7501Sstephan event in the form: 2308c3b7501Sstephan 2318c3b7501Sstephan {type:'db-export', 2328c3b7501Sstephan data:{ 2338c3b7501Sstephan filename: name of db, 2348c3b7501Sstephan buffer: contents of the db file (Uint8Array), 2358c3b7501Sstephan error: on error, a message string and no buffer property. 2368c3b7501Sstephan } 2378c3b7501Sstephan } 2388c3b7501Sstephan */ 2398c3b7501Sstephan case 'db-export': { 2408c3b7501Sstephan const fn = Sqlite3Shell.dbFilename(); 24160d9aa7cSstephan stdout("Exporting",fn+".",fixmeOPFS); 2428c3b7501Sstephan const fn2 = fn ? fn.split(/[/\\]/).pop() : null; 2438c3b7501Sstephan try{ 2448c3b7501Sstephan if(!fn2) throw new Error("DB appears to be closed."); 2458c3b7501Sstephan wMsg('db-export',{ 2468c3b7501Sstephan filename: fn2, 2478c3b7501Sstephan buffer: fiddleModule.FS.readFile(fn, {encoding:"binary"}) 2488c3b7501Sstephan }); 2498c3b7501Sstephan }catch(e){ 2508c3b7501Sstephan /* Post a failure message so that UI elements disabled 2518c3b7501Sstephan during the export can be re-enabled. */ 2528c3b7501Sstephan wMsg('db-export',{ 2538c3b7501Sstephan filename: fn, 2548c3b7501Sstephan error: e.message 2558c3b7501Sstephan }); 2568c3b7501Sstephan } 2578c3b7501Sstephan return; 2588c3b7501Sstephan } 2598c3b7501Sstephan case 'open': { 2608c3b7501Sstephan /* Expects: { 2618c3b7501Sstephan buffer: ArrayBuffer | Uint8Array, 2628c3b7501Sstephan filename: for logging/informational purposes only 2638c3b7501Sstephan } */ 2648c3b7501Sstephan const opt = ev.data; 2658c3b7501Sstephan let buffer = opt.buffer; 266*ef11fb91Sstephan stderr('open():',fixmeOPFS); 2678c3b7501Sstephan if(buffer instanceof Uint8Array){ 2688c3b7501Sstephan }else if(buffer instanceof ArrayBuffer){ 2698c3b7501Sstephan buffer = new Uint8Array(buffer); 2708c3b7501Sstephan }else{ 2718c3b7501Sstephan stderr("'open' expects {buffer:Uint8Array} containing an uploaded db."); 2728c3b7501Sstephan return; 2738c3b7501Sstephan } 2748c3b7501Sstephan const fn = ( 2758c3b7501Sstephan opt.filename 2768c3b7501Sstephan ? opt.filename.split(/[/\\]/).pop().replace('"','_') 2778c3b7501Sstephan : ("db-"+((Math.random() * 10000000) | 0)+ 2788c3b7501Sstephan "-"+((Math.random() * 10000000) | 0)+".sqlite3") 2798c3b7501Sstephan ); 2808c3b7501Sstephan /* We cannot delete the existing db file until the new one 2818c3b7501Sstephan is installed, which means that we risk overflowing our 2828c3b7501Sstephan quota (if any) by having both the previous and current 2838c3b7501Sstephan db briefly installed in the virtual filesystem. */ 2848c3b7501Sstephan fiddleModule.FS.createDataFile("/", fn, buffer, true, true); 2858c3b7501Sstephan const oldName = Sqlite3Shell.dbFilename(); 2868c3b7501Sstephan Sqlite3Shell.exec('.open "/'+fn+'"'); 2878c3b7501Sstephan if(oldName && oldName !== fn){ 288a579f440Sstephan try{fiddleModule.fsUnlink(oldName);} 2898c3b7501Sstephan catch(e){/*ignored*/} 2908c3b7501Sstephan } 2918c3b7501Sstephan stdout("Replaced DB with",fn+"."); 2928c3b7501Sstephan return; 2938c3b7501Sstephan } 2948c3b7501Sstephan }; 2958c3b7501Sstephan console.warn("Unknown fiddle-worker message type:",ev); 2968c3b7501Sstephan }; 2978c3b7501Sstephan 2988c3b7501Sstephan /** 2998c3b7501Sstephan emscripten module for use with build mode -sMODULARIZE. 3008c3b7501Sstephan */ 3018c3b7501Sstephan const fiddleModule = { 3028c3b7501Sstephan print: stdout, 3038c3b7501Sstephan printErr: stderr, 3048c3b7501Sstephan /** 3058c3b7501Sstephan Intercepts status updates from the emscripting module init 3068c3b7501Sstephan and fires worker events with a type of 'status' and a 3078c3b7501Sstephan payload of: 3088c3b7501Sstephan 3098c3b7501Sstephan { 3108c3b7501Sstephan text: string | null, // null at end of load process 3118c3b7501Sstephan step: integer // starts at 1, increments 1 per call 3128c3b7501Sstephan } 3138c3b7501Sstephan 3148c3b7501Sstephan We have no way of knowing in advance how many steps will 3158c3b7501Sstephan be processed/posted, so creating a "percentage done" view is 3168c3b7501Sstephan not really practical. One can be approximated by giving it a 3178c3b7501Sstephan current value of message.step and max value of message.step+1, 3188c3b7501Sstephan though. 3198c3b7501Sstephan 3208c3b7501Sstephan When work is finished, a message with a text value of null is 3218c3b7501Sstephan submitted. 3228c3b7501Sstephan 3238c3b7501Sstephan After a message with text==null is posted, the module may later 3248c3b7501Sstephan post messages about fatal problems, e.g. an exit() being 3258c3b7501Sstephan triggered, so it is recommended that UI elements for posting 3268c3b7501Sstephan status messages not be outright removed from the DOM when 3278c3b7501Sstephan text==null, and that they instead be hidden until/unless 3288c3b7501Sstephan text!=null. 3298c3b7501Sstephan */ 3308c3b7501Sstephan setStatus: function f(text){ 3318c3b7501Sstephan if(!f.last) f.last = { step: 0, text: '' }; 3328c3b7501Sstephan else if(text === f.last.text) return; 3338c3b7501Sstephan f.last.text = text; 3348c3b7501Sstephan wMsg('module',{ 3358c3b7501Sstephan type:'status', 3368c3b7501Sstephan data:{step: ++f.last.step, text: text||null} 3378c3b7501Sstephan }); 3388c3b7501Sstephan } 3398c3b7501Sstephan }; 3408c3b7501Sstephan 3416110a5d0Sstephan importScripts('fiddle-module.js'+self.location.search); 3428c3b7501Sstephan /** 3438c3b7501Sstephan initFiddleModule() is installed via fiddle-module.js due to 3448c3b7501Sstephan building with: 3458c3b7501Sstephan 3468c3b7501Sstephan emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule 3478c3b7501Sstephan */ 3488c3b7501Sstephan initFiddleModule(fiddleModule).then(function(thisModule){ 3496110a5d0Sstephan const S = thisModule.sqlite3; 3506110a5d0Sstephan const atEnd = ()=>{ 351*ef11fb91Sstephan thisModule.fsUnlink = (fn)=>{ 35260d9aa7cSstephan stderr("unlink:",fixmeOPFS); 353*ef11fb91Sstephan return S.capi.wasm.sqlite3_wasm_vfs_unlink(fn); 35460d9aa7cSstephan }; 3558c3b7501Sstephan wMsg('fiddle-ready'); 3566110a5d0Sstephan }; 357*ef11fb91Sstephan if(S.installOpfsVfs) S.installOpfsVfs().finally(atEnd); 358*ef11fb91Sstephan else atEnd(); 35960d9aa7cSstephan })/*then()*/; 3608c3b7501Sstephan})(); 361