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