4 The author disclaims copyright to this source code. In place of a
5 legal notice, here is a blessing:
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.
11 ***********************************************************************
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.
17 Forewarning: this API is still very much Under Construction and
18 subject to any number of changes as experience reveals what those
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
26 { type: string, data: type-specific value }
28 Where 'type' is used for dispatching and 'data' is a
29 'type'-dependent value.
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.
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:
43 {type:'stdout', data: 'Hi, world.'}
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
50 {type:'module', data:{
52 data: {text:string|null, step:1-based-integer}
55 with an incrementing step value for each subsequent message. When
56 the module loading is complete, a message with a text value of
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
64 {type:'working', data:'start'}
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:
76 {type:'shellExec', data: 'select * from sqlite_master'}
78 - More TBD as the higher-level db layer develops.
82 Apparent browser(s) bug: console messages emitted may be duplicated
83 in the console, even though they're provably only run once. See:
85 https://stackoverflow.com/questions/49659464
87 Noting that it happens in Firefox as well as Chrome. Harmless but
93 Posts a message in the form {type,data}. If passed more than 2
94 args, the 3rd must be an array of "transferable" values to pass
95 as the 2nd argument to postMessage(). */
97 (type
,data
,transferables
)=>{
98 postMessage({type
, data
}, transferables
|| []);
100 const stdout
= (...args
)=>wMsg('stdout', args
);
101 const stderr
= (...args
)=>wMsg('stderr', args
);
102 const toss
= (...args
)=>{
103 throw new Error(args
.join(' '));
105 const fixmeOPFS
= "(FIXME: won't work with OPFS-over-sqlite3_vfs.)";
106 let sqlite3
/* gets assigned when the wasm module is loaded */;
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.");
119 fiddleModule
.setStatus('Exception thrown, see JavaScript console: '+err
);
122 const Sqlite3Shell
= {
123 /** Returns the name of the currently-opened db. */
124 dbFilename
: function f(){
125 if(!f
._
) f
._
= sqlite3
.wasm
.xWrap('fiddle_db_filename', "string", ['string']);
128 dbHandle
: function f(){
129 if(!f
._
) f
._
= sqlite3
.wasm
.xWrap("fiddle_db_handle", "sqlite3*");
132 dbIsOpfs
: function f(){
133 return sqlite3
.opfs
&& sqlite3
.capi
.sqlite3_js_db_uses_vfs(
134 this.dbHandle(), "opfs"
137 runMain
: function f(){
138 if(f
.argv
) return 0===f
.argv
.rc
;
139 const dbName
= "/fiddle.sqlite3";
141 'sqlite3-fiddle.wasm',
144 /* Reminder: because of how we run fiddle, we have to ensure
145 that any argv strings passed to its main() are valid until
146 the wasm environment shuts down. */
148 const capi
= sqlite3
.capi
, wasm
= sqlite3
.wasm
;
149 /* We need to call sqlite3_shutdown() in order to avoid numerous
150 legitimate warnings from the shell about it being initialized
151 after sqlite3_initialize() has been called. This means,
152 however, that any initialization done by the JS code may need
153 to be re-done (e.g. re-registration of dynamically-loaded
154 VFSes). We need a more generic approach to running such
156 capi
.sqlite3_shutdown();
157 f
.argv
.pArgv
= wasm
.allocMainArgv(f
.argv
);
158 f
.argv
.rc
= wasm
.exports
.fiddle_main(
159 f
.argv
.length
, f
.argv
.pArgv
162 stderr("Fatal error initializing sqlite3 shell.");
163 fiddleModule
.isDead
= true;
166 stdout("SQLite version", capi
.sqlite3_libversion(),
167 capi
.sqlite3_sourceid().substr(0,19));
168 stdout('Welcome to the "fiddle" shell.');
169 if(capi
.sqlite3_vfs_find("opfs")){
170 stdout("\nOPFS is available. To open a persistent db, use:\n\n",
171 " .open file:name?vfs=opfs\n\nbut note that some",
172 "features (e.g. upload) do not yet work with OPFS.");
174 stdout('\nEnter ".help" for usage hints.');
175 this.exec([ // initialization commands...
182 Runs the given text through the shell as if it had been typed
183 in by a user. Fires a working/start event before it starts and
184 working/end event when it finishes.
186 exec
: function f(sql
){
188 if(!this.runMain()) return;
189 f
._
= sqlite3
.wasm
.xWrap('fiddle_exec', null, ['string']);
191 if(fiddleModule
.isDead
){
192 stderr("shell module has exit()ed. Cannot run SQL.");
195 wMsg('working','start');
198 stderr('Cannot run multiple commands concurrently.');
200 if(Array
.isArray(sql
)) sql
= sql
.join('');
206 wMsg('working','end');
209 resetDb
: function f(){
210 if(!f
._
) f
._
= sqlite3
.wasm
.xWrap('fiddle_reset_db', null);
211 stdout("Resetting database.");
213 stdout("Reset",this.dbFilename());
215 /* Interrupt can't work: this Worker is tied up working, so won't get the
216 interrupt event which would be needed to perform the interrupt. */
217 interrupt
: function f(){
218 if(!f
._
) f
._
= sqlite3
.wasm
.xWrap('fiddle_interrupt', null);
219 stdout("Requesting interrupt.");
224 self
.onmessage
= function f(ev
){
231 //console.debug("worker: onmessage.data",ev);
233 case 'shellExec': Sqlite3Shell
.exec(ev
.data
); return;
234 case 'db-reset': Sqlite3Shell
.resetDb(); return;
235 case 'interrupt': Sqlite3Shell
.interrupt(); return;
236 /** Triggers the export of the current db. Fires an
241 filename: name of db,
242 buffer: contents of the db file (Uint8Array),
243 error: on error, a message string and no buffer property.
248 const fn
= Sqlite3Shell
.dbFilename();
249 stdout("Exporting",fn
+".");
250 const fn2
= fn
? fn
.split(/[/\\]/).pop() : null;
252 if(!fn2
) toss("DB appears to be closed.");
253 const buffer
= sqlite3
.capi
.sqlite3_js_db_export(
254 Sqlite3Shell
.dbHandle()
256 wMsg('db-export',{filename
: fn2
, buffer
: buffer
.buffer
}, [buffer
.buffer
]);
258 console
.error("Export failed:",e
);
259 /* Post a failure message so that UI elements disabled
260 during the export can be re-enabled. */
270 buffer: ArrayBuffer | Uint8Array,
271 filename: the filename for the db. Any dir part is
276 let buffer
= opt
.buffer
;
277 stderr('open():',fixmeOPFS
);
278 if(buffer
instanceof ArrayBuffer
){
279 buffer
= new Uint8Array(buffer
);
280 }else if(!(buffer
instanceof Uint8Array
)){
281 stderr("'open' expects {buffer:Uint8Array} containing an uploaded db.");
286 ? opt
.filename
.split(/[/\\]/).pop().replace('"','_')
287 : ("db-"+((Math
.random() * 10000000) | 0)+
288 "-"+((Math
.random() * 10000000) | 0)+".sqlite3")
291 /* We cannot delete the existing db file until the new one
292 is installed, which means that we risk overflowing our
293 quota (if any) by having both the previous and current
294 db briefly installed in the virtual filesystem. */
295 const fnAbs
= '/'+fn
;
296 const oldName
= Sqlite3Shell
.dbFilename();
297 if(oldName
&& oldName
===fnAbs
){
298 /* We cannot create the replacement file while the current file
299 is opened, nor does the shell have a .close command, so we
300 must temporarily switch to another db... */
301 Sqlite3Shell
.exec('.open :memory:');
302 fiddleModule
.FS
.unlink(fnAbs
);
304 fiddleModule
.FS
.createDataFile("/", fn
, buffer
, true, true);
305 Sqlite3Shell
.exec('.open "'+fnAbs
+'"');
306 if(oldName
&& oldName
!==fnAbs
){
307 try{fiddleModule
.fsUnlink(oldName
)}
308 catch(e
){/*ignored*/}
310 stdout("Replaced DB with",fn
+".");
312 stderr("Error installing db",fn
+":",e
.message
);
317 console
.warn("Unknown fiddle-worker message type:",ev
);
321 emscripten module for use with build mode -sMODULARIZE.
323 const fiddleModule
= {
327 Intercepts status updates from the emscripting module init
328 and fires worker events with a type of 'status' and a
332 text: string | null, // null at end of load process
333 step: integer // starts at 1, increments 1 per call
336 We have no way of knowing in advance how many steps will
337 be processed/posted, so creating a "percentage done" view is
338 not really practical. One can be approximated by giving it a
339 current value of message.step and max value of message.step+1,
342 When work is finished, a message with a text value of null is
345 After a message with text==null is posted, the module may later
346 post messages about fatal problems, e.g. an exit() being
347 triggered, so it is recommended that UI elements for posting
348 status messages not be outright removed from the DOM when
349 text==null, and that they instead be hidden until/unless
352 setStatus
: function f(text
){
353 if(!f
.last
) f
.last
= { step
: 0, text
: '' };
354 else if(text
=== f
.last
.text
) return;
358 data
:{step
: ++f
.last
.step
, text
: text
||null}
363 importScripts('fiddle-module.js'+self
.location
.search
);
365 initFiddleModule() is installed via fiddle-module.js due to
368 emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule
370 sqlite3InitModule(fiddleModule
).then((_sqlite3
)=>{
372 console
.warn("Installing sqlite3 module globally (in Worker)",
373 "for use in the dev console.", sqlite3
);
374 globalThis
.sqlite3
= sqlite3
;
375 const dbVfs
= sqlite3
.wasm
.xWrap('fiddle_db_vfs', "*", ['string']);
376 fiddleModule
.fsUnlink
= (fn
)=>fiddleModule
.FS
.unlink(fn
);
377 wMsg('fiddle-ready');
379 console
.error("Fiddle worker init failed:",e
);