diff --git a/Makefile b/Makefile index 340a4fde..dd3099d7 100644 --- a/Makefile +++ b/Makefile @@ -80,13 +80,13 @@ debug: dist/sql-asm-debug.js dist/sql-wasm-debug.js dist/sql-asm-debug.js: $(BITCODE_FILES) $(OUTPUT_WRAPPER_FILES) $(SOURCE_API_FILES) $(EXPORTED_METHODS_JSON_FILES) $(EMCC) $(EMFLAGS) $(EMFLAGS_DEBUG) $(EMFLAGS_ASM) $(BITCODE_FILES) $(EMFLAGS_PRE_JS_FILES) -o $@ mv $@ out/tmp-raw.js - cat src/shell-pre.js out/tmp-raw.js src/shell-post.js > $@ + cat src/shell-pre.js out/tmp-raw.js src/shell-post.js src/worker.js > $@ rm out/tmp-raw.js dist/sql-wasm-debug.js: $(BITCODE_FILES) $(OUTPUT_WRAPPER_FILES) $(SOURCE_API_FILES) $(EXPORTED_METHODS_JSON_FILES) $(EMCC) $(EMFLAGS) $(EMFLAGS_DEBUG) $(EMFLAGS_WASM) $(BITCODE_FILES) $(EMFLAGS_PRE_JS_FILES) -o $@ mv $@ out/tmp-raw.js - cat src/shell-pre.js out/tmp-raw.js src/shell-post.js > $@ + cat src/shell-pre.js out/tmp-raw.js src/shell-post.js src/worker.js > $@ rm out/tmp-raw.js .PHONY: optimized @@ -95,13 +95,13 @@ optimized: dist/sql-asm.js dist/sql-wasm.js dist/sql-asm-memory-growth.js dist/sql-asm.js: $(BITCODE_FILES) $(OUTPUT_WRAPPER_FILES) $(SOURCE_API_FILES) $(EXPORTED_METHODS_JSON_FILES) $(EMCC) $(EMFLAGS) $(EMFLAGS_OPTIMIZED) $(EMFLAGS_ASM) $(BITCODE_FILES) $(EMFLAGS_PRE_JS_FILES) -o $@ mv $@ out/tmp-raw.js - cat src/shell-pre.js out/tmp-raw.js src/shell-post.js > $@ + cat src/shell-pre.js out/tmp-raw.js src/shell-post.js src/worker.js > $@ rm out/tmp-raw.js dist/sql-wasm.js: $(BITCODE_FILES) $(OUTPUT_WRAPPER_FILES) $(SOURCE_API_FILES) $(EXPORTED_METHODS_JSON_FILES) $(EMCC) $(EMFLAGS) $(EMFLAGS_OPTIMIZED) $(EMFLAGS_WASM) $(BITCODE_FILES) $(EMFLAGS_PRE_JS_FILES) -o $@ mv $@ out/tmp-raw.js - cat src/shell-pre.js out/tmp-raw.js src/shell-post.js > $@ + cat src/shell-pre.js out/tmp-raw.js src/shell-post.js src/worker.js > $@ rm out/tmp-raw.js dist/sql-asm-memory-growth.js: $(BITCODE_FILES) $(OUTPUT_WRAPPER_FILES) $(SOURCE_API_FILES) $(EXPORTED_METHODS_JSON_FILES) @@ -110,21 +110,22 @@ dist/sql-asm-memory-growth.js: $(BITCODE_FILES) $(OUTPUT_WRAPPER_FILES) $(SOURCE cat src/shell-pre.js out/tmp-raw.js src/shell-post.js > $@ rm out/tmp-raw.js +# alias dist/worker.sql-xxx.js to dist/sql-xxx.js for legacy-compatibility # Web worker API .PHONY: worker worker: dist/worker.sql-asm.js dist/worker.sql-asm-debug.js dist/worker.sql-wasm.js dist/worker.sql-wasm-debug.js -dist/worker.sql-asm.js: dist/sql-asm.js src/worker.js - cat $^ > $@ +dist/worker.sql-asm.js: dist/sql-asm.js + cp $^ $@ -dist/worker.sql-asm-debug.js: dist/sql-asm-debug.js src/worker.js - cat $^ > $@ +dist/worker.sql-asm-debug.js: dist/sql-asm-debug.js + cp $^ $@ -dist/worker.sql-wasm.js: dist/sql-wasm.js src/worker.js - cat $^ > $@ +dist/worker.sql-wasm.js: dist/sql-wasm.js + cp $^ $@ -dist/worker.sql-wasm-debug.js: dist/sql-wasm-debug.js src/worker.js - cat $^ > $@ +dist/worker.sql-wasm-debug.js: dist/sql-wasm-debug.js + cp $^ $@ # Building it this way gets us a wrapper that _knows_ it's in worker mode, which is nice. # However, since we can't tell emcc that we don't need the wasm generated, and just want the wrapper, we have to pay to have the .wasm generated diff --git a/README.md b/README.md index 773a6a96..11c7db5e 100644 --- a/README.md +++ b/README.md @@ -191,32 +191,32 @@ See : https://github.com/sql-js/sql.js/blob/master/test/test_node_file.js If you don't want to run CPU-intensive SQL queries in your main application thread, you can use the *more limited* WebWorker API. -You will need to download [dist/worker.sql-wasm.js](dist/worker.sql-wasm.js) [dist/worker.sql-wasm.wasm](dist/worker.sql-wasm.wasm). +You will need to download [dist/sql-wasm.js](dist/sql-wasm.js) [dist/sql-wasm.wasm](dist/sql-wasm.wasm). Example: ```html + ``` diff --git a/examples/GUI/gui.js b/examples/GUI/gui.js index df499ab3..af979611 100644 --- a/examples/GUI/gui.js +++ b/examples/GUI/gui.js @@ -1,141 +1,143 @@ -var execBtn = document.getElementById("execute"); -var outputElm = document.getElementById('output'); -var errorElm = document.getElementById('error'); -var commandsElm = document.getElementById('commands'); -var dbFileElm = document.getElementById('dbfile'); -var savedbElm = document.getElementById('savedb'); +(async function () { + "use strict"; + var execBtn = document.getElementById("execute"); + var outputElm = document.getElementById('output'); + var errorElm = document.getElementById('error'); + var commandsElm = document.getElementById('commands'); + var dbFileElm = document.getElementById('dbfile'); + var savedbElm = document.getElementById('savedb'); -// Start the worker in which sql.js will run -var worker = new Worker("../../dist/worker.sql-wasm.js"); -worker.onerror = error; + // Start the worker in which sql.js will run + var SQL = await initSqlJs({ + locateFile: function (file) { + return `../../dist/${file}`; + } + }); + var worker = new SQL.Worker("../../dist/sql-wasm-debug.js"); -// Open a database -worker.postMessage({ action: 'open' }); + // Open a database + worker.postMessage({ action: 'open' }); -// Connect to the HTML element we 'print' to -function print(text) { - outputElm.innerHTML = text.replace(/\n/g, '
'); -} -function error(e) { - console.log(e); - errorElm.style.height = '2em'; - errorElm.textContent = e.message; -} + // Connect to the HTML element we 'print' to + function print(text) { + outputElm.innerHTML = text.replace(/\n/g, '
'); + } + function error(e) { + console.log(e); + errorElm.style.height = '2em'; + errorElm.textContent = e.message; + } -function noerror() { - errorElm.style.height = '0'; -} + function noerror() { + errorElm.style.height = '0'; + } -// Run a command in the database -function execute(commands) { - tic(); - worker.onmessage = function (event) { - var results = event.data.results; - toc("Executing SQL"); - if (!results) { - error({message: event.data.error}); - return; - } + // Run a command in the database + async function execute(commands) { + tic(); + outputElm.textContent = "Fetching results..."; + var data; + try { + data = await worker.postMessage({ action: 'exec', sql: commands }); + } catch (errorCaught) { + error({message: errorCaught}); + return; + } + var results = data.results; + toc("Executing SQL"); + tic(); + outputElm.innerHTML = ""; + for (var i = 0; i < results.length; i++) { + outputElm.appendChild(tableCreate(results[i].columns, results[i].values)); + } + toc("Displaying results"); + } - tic(); - outputElm.innerHTML = ""; - for (var i = 0; i < results.length; i++) { - outputElm.appendChild(tableCreate(results[i].columns, results[i].values)); - } - toc("Displaying results"); - } - worker.postMessage({ action: 'exec', sql: commands }); - outputElm.textContent = "Fetching results..."; -} + // Create an HTML table + var tableCreate = function () { + function valconcat(vals, tagName) { + if (vals.length === 0) return ''; + var open = '<' + tagName + '>', close = ''; + return open + vals.join(close + open) + close; + } + return function (columns, values) { + var tbl = document.createElement('table'); + var html = '' + valconcat(columns, 'th') + ''; + var rows = values.map(function (v) { return valconcat(v, 'td'); }); + html += '' + valconcat(rows, 'tr') + ''; + tbl.innerHTML = html; + return tbl; + } + }(); -// Create an HTML table -var tableCreate = function () { - function valconcat(vals, tagName) { - if (vals.length === 0) return ''; - var open = '<' + tagName + '>', close = ''; - return open + vals.join(close + open) + close; - } - return function (columns, values) { - var tbl = document.createElement('table'); - var html = '' + valconcat(columns, 'th') + ''; - var rows = values.map(function (v) { return valconcat(v, 'td'); }); - html += '' + valconcat(rows, 'tr') + ''; - tbl.innerHTML = html; - return tbl; - } -}(); + // Execute the commands when the button is clicked + function execEditorContents() { + noerror() + execute(editor.getValue() + ';'); + } + execBtn.addEventListener("click", execEditorContents, true); -// Execute the commands when the button is clicked -function execEditorContents() { - noerror() - execute(editor.getValue() + ';'); -} -execBtn.addEventListener("click", execEditorContents, true); + // Performance measurement functions + var tictime; + if (!window.performance || !performance.now) { window.performance = { now: Date.now } } + function tic() { tictime = performance.now() } + function toc(msg) { + var dt = performance.now() - tictime; + console.log((msg || 'toc') + ": " + dt + "ms"); + } -// Performance measurement functions -var tictime; -if (!window.performance || !performance.now) { window.performance = { now: Date.now } } -function tic() { tictime = performance.now() } -function toc(msg) { - var dt = performance.now() - tictime; - console.log((msg || 'toc') + ": " + dt + "ms"); -} + // Add syntax highlihjting to the textarea + var editor = CodeMirror.fromTextArea(commandsElm, { + mode: 'text/x-mysql', + viewportMargin: Infinity, + indentWithTabs: true, + smartIndent: true, + lineNumbers: true, + matchBrackets: true, + autofocus: true, + extraKeys: { + "Ctrl-Enter": execEditorContents, + "Ctrl-S": savedb, + } + }); -// Add syntax highlihjting to the textarea -var editor = CodeMirror.fromTextArea(commandsElm, { - mode: 'text/x-mysql', - viewportMargin: Infinity, - indentWithTabs: true, - smartIndent: true, - lineNumbers: true, - matchBrackets: true, - autofocus: true, - extraKeys: { - "Ctrl-Enter": execEditorContents, - "Ctrl-S": savedb, - } -}); + // Load a db from a file + dbFileElm.onchange = function () { + var f = dbFileElm.files[0]; + var r = new FileReader(); + r.onload = async function () { + tic(); + try { + await worker.postMessage({ action: 'open', buffer: r.result }, [r.result]); + } + catch (exception) { + await worker.postMessage({ action: 'open', buffer: r.result }); + } + toc("Loading database from file"); + // Show the schema of the loaded database + editor.setValue("SELECT `name`, `sql`\n FROM `sqlite_master`\n WHERE type='table';"); + execEditorContents(); + } + r.readAsArrayBuffer(f); + } -// Load a db from a file -dbFileElm.onchange = function () { - var f = dbFileElm.files[0]; - var r = new FileReader(); - r.onload = function () { - worker.onmessage = function () { - toc("Loading database from file"); - // Show the schema of the loaded database - editor.setValue("SELECT `name`, `sql`\n FROM `sqlite_master`\n WHERE type='table';"); - execEditorContents(); - }; - tic(); - try { - worker.postMessage({ action: 'open', buffer: r.result }, [r.result]); - } - catch (exception) { - worker.postMessage({ action: 'open', buffer: r.result }); - } - } - r.readAsArrayBuffer(f); -} - -// Save the db to a file -function savedb() { - worker.onmessage = function (event) { - toc("Exporting the database"); - var arraybuff = event.data.buffer; - var blob = new Blob([arraybuff]); - var a = document.createElement("a"); - document.body.appendChild(a); - a.href = window.URL.createObjectURL(blob); - a.download = "sql.db"; - a.onclick = function () { - setTimeout(function () { - window.URL.revokeObjectURL(a.href); - }, 1500); - }; - a.click(); - }; - tic(); - worker.postMessage({ action: 'export' }); -} -savedbElm.addEventListener("click", savedb, true); + // Save the db to a file + async function savedb() { + tic(); + var data = await worker.postMessage({ action: 'export' }); + toc("Exporting the database"); + var arraybuff = data.buffer; + var blob = new Blob([arraybuff]); + var a = document.createElement("a"); + document.body.appendChild(a); + a.href = window.URL.createObjectURL(blob); + a.download = "sql.db"; + a.onclick = function () { + setTimeout(function () { + window.URL.revokeObjectURL(a.href); + }, 1500); + }; + a.click(); + } + savedbElm.addEventListener("click", savedb, true); +}()); diff --git a/examples/GUI/index.html b/examples/GUI/index.html index ceac5afc..0a17e33c 100644 --- a/examples/GUI/index.html +++ b/examples/GUI/index.html @@ -64,7 +64,8 @@

Online SQL interpreter

Project now maintained by lovasoa + - \ No newline at end of file + diff --git a/src/api.js b/src/api.js index dc07487e..cbec18eb 100644 --- a/src/api.js +++ b/src/api.js @@ -51,6 +51,9 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { "use strict"; // Declare toplevel variables + // variables to keep track of worker-callbacks + var workerCallbackDict = {}; + var workerCallbackId = 0; // register, used for temporary stack values var apiTemp = stackAlloc(4); var cwrap = Module["cwrap"]; @@ -1007,7 +1010,74 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return this; }; + /** @typedef {string} url */ + /** + * @constructs Worker + * @memberof module:SqlJs + * @param {url} web-worker url + * @example + * var worker = new SQL.Worker("/dist/sql-wasm.js"); + */ + function SqlWorker(url) { + this.worker = new Worker(url); + this.worker.onmessage = function onmessage(msg) { + /* + * this function will handle returned from web-worker + */ + var callback = workerCallbackDict[msg.data.id]; + delete workerCallbackDict[msg.data.id]; + if (callback) { + callback(msg.data); + } + }; + } + + /** @typedef {Object} msg */ + /** + * Asynchronously run sql-commands by posting them as web-worker messages + * @param {msg} web-worker message-object + * @example + * var worker = new SQL.Worker("/dist/sql-wasm.js"); + * try { + * var data = await worker.postMessage({ + * action: "exec", + * params: {"@id": 1}, + * sql: "SELECT * FROM myTable WHERE id = @id" + * }); + * console.log(data.results[0]); + * } catch (sqlError) { + * console.error(sqlError); + * } + */ + SqlWorker.prototype["postMessage"] = function postMessage(msg) { + var callback; + var error; + var that; + that = this; + // preserve stack-trace, in case of error + error = new Error(); + return new Promise(function onpromise(resolve, reject) { + callback = function _callback(data) { + // if .error, then prepend it to .stack and reject + if (data.error) { + error.message = data.error; + error.stack = data.error + "\n" + error.stack; + reject(error); + return; + } + resolve(data); + }; + // increment workerCallbackId + // and cycle back to 0 if it overflows past 32-bits + workerCallbackId = (workerCallbackId + 1) | 0; + msg.id = workerCallbackId; + workerCallbackDict[msg.id] = callback; + that.worker.postMessage(msg); + }); + }; // export Database to Module Module.Database = Database; + // export Worker to Module + Module.Worker = SqlWorker; }; diff --git a/src/worker.js b/src/worker.js index c928ab86..8e613d38 100644 --- a/src/worker.js +++ b/src/worker.js @@ -1,91 +1,104 @@ /* global initSqlJs */ /* eslint-env worker */ +/* eslint func-names: ["off"] */ /* eslint no-restricted-globals: ["error"] */ -var db; -function onModuleReady(SQL) { + +// encapsulate web-worker code to run in any env +(function () { "use strict"; - function createDb(data) { - if (db != null) db.close(); - db = new SQL.Database(data); - return db; + // isomorphism - do not run worker.js code if not in web-worker env + if (!( + typeof self === "object" + && typeof importScripts === "function" + && self + && self.importScripts === importScripts + )) { + return; } - var buff; var data; var result; - data = this["data"]; - switch (data && data["action"]) { - case "open": - buff = data["buffer"]; - createDb(buff && new Uint8Array(buff)); - return postMessage({ - id: data["id"], - ready: true - }); - case "exec": - if (db === null) { - createDb(); - } - if (!data["sql"]) { - throw "exec: Missing query string"; - } - return postMessage({ - id: data["id"], - results: db.exec(data["sql"], data["params"]) - }); - case "each": - if (db === null) { - createDb(); - } - var callback = function callback(row) { + // Declare toplevel variables + var db; + var sqlModuleReady; + + function onModuleReady(SQL) { + function createDb(data) { + if (db != null) db.close(); + db = new SQL.Database(data); + return db; + } + + var buff; var data; var result; + data = this["data"]; + switch (data && data["action"]) { + case "open": + buff = data["buffer"]; + createDb(buff && new Uint8Array(buff)); return postMessage({ id: data["id"], - row: row, - finished: false + ready: true }); - }; - var done = function done() { + case "exec": + if (db === null) { + createDb(); + } + if (!data["sql"]) { + throw "exec: Missing query string"; + } return postMessage({ id: data["id"], - finished: true + results: db.exec(data["sql"], data["params"]) }); - }; - return db.each(data["sql"], data["params"], callback, done); - case "export": - buff = db["export"](); - result = { - id: data["id"], - buffer: buff - }; - try { - return postMessage(result, [result]); - } catch (error) { - return postMessage(result); - } - case "close": - return db && db.close(); - default: - throw new Error("Invalid action : " + (data && data["action"])); + case "each": + if (db === null) { + createDb(); + } + var callback = function callback(row) { + return postMessage({ + id: data["id"], + row: row, + finished: false + }); + }; + var done = function done() { + return postMessage({ + id: data["id"], + finished: true + }); + }; + return db.each(data["sql"], data["params"], callback, done); + case "export": + buff = db["export"](); + result = { + id: data["id"], + buffer: buff + }; + try { + return postMessage(result, [result]); + } catch (error) { + return postMessage(result); + } + case "close": + return db && db.close(); + default: + throw new Error("Invalid action : " + (data && data["action"])); + } } -} -function onError(err) { - "use strict"; - - return postMessage({ - id: this["data"]["id"], - error: err["message"] - }); -} + function onError(err) { + return postMessage({ + id: this["data"]["id"], + error: err["message"] + }); + } -if (typeof importScripts === "function") { + // init web-worker onmessage event-handling db = null; - var sqlModuleReady = initSqlJs(); + sqlModuleReady = initSqlJs(); self.onmessage = function onmessage(event) { - "use strict"; - return sqlModuleReady .then(onModuleReady.bind(event)) .catch(onError.bind(event)); }; -} +}()); diff --git a/test/test_worker.js b/test/test_worker.js index c9595054..4aa3fe64 100644 --- a/test/test_worker.js +++ b/test/test_worker.js @@ -7,48 +7,45 @@ var puppeteer = require("puppeteer"); var path = require("path"); var fs = require("fs"); -class Worker { - constructor(handle) { - this.handle = handle; - } - static async fromFile(file) { - const browser = await puppeteer.launch(); - const page = await browser.newPage(); - const source = fs.readFileSync(file, 'utf8'); - const worker = await page.evaluateHandle(x => { - const url = URL.createObjectURL(new Blob([x]), { type: 'application/javascript; charset=utf-8' }); - return new Worker(url); - }, source); - return new Worker(worker); - } - async postMessage(msg) { - return await this.handle.evaluate((worker, msg) => { - return new Promise((accept, reject) => { - setTimeout(reject, 20000, new Error("time out")); - worker.onmessage = evt => accept(evt.data); - worker.onerror = reject; - worker.postMessage(msg); - }) - }, msg); - } -} - exports.test = async function test(SQL, assert) { var target = process.argv[2]; + // init file var file = target ? "sql-" + target : "sql-wasm"; if (file.indexOf('wasm') > -1 || file.indexOf('memory-growth') > -1) { console.error("Skipping worker test for " + file + ". Not implemented yet"); return; }; // If we use puppeteer, we need to pass in this new cwd as the root of the file being loaded: - const filename = "../dist/worker." + file + ".js"; - var worker = await Worker.fromFile(path.join(__dirname, filename)); - var data = await worker.postMessage({ id: 1, action: 'open' }); + file = path.join(__dirname, "../dist/" + file + ".js"); + // test isomorphism - web-worker code can be safely loaded in nodejs + require(file); + // init puppeteer + var browser = await puppeteer.launch({args:["--no-sandbox"]}); + var page = await browser.newPage(); + // inject file as