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