From 07021b8dd579a2818e8be9f2df224b8ddadc2433 Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 7 May 2019 09:26:55 +0100 Subject: [PATCH] Add new worker for zipping outputs. Use bakeId to track which outputs are stale. --- src/core/ChefWorker.js | 7 +- src/web/OutputWaiter.mjs | 132 ++++++++++++++++++++++- src/web/WorkerWaiter.mjs | 16 +-- src/web/ZipWorker.mjs | 69 ++++++++++++ src/web/stylesheets/layout/_controls.css | 12 +++ 5 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 src/web/ZipWorker.mjs diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index 995503c8..de6e7eac 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -93,7 +93,6 @@ self.addEventListener("message", function(e) { async function bake(data) { // Ensure the relevant modules are loaded self.loadRequiredModules(data.recipeConfig); - try { self.inputNum = parseInt(data.inputNum, 10); const response = await self.chef.bake( @@ -119,7 +118,8 @@ async function bake(data) { action: "bakeComplete", data: Object.assign(response, { id: data.id, - inputNum: data.inputNum + inputNum: data.inputNum, + bakeId: data.bakeId }) }); } @@ -128,7 +128,8 @@ async function bake(data) { action: "bakeComplete", data: Object.assign(response, { id: data.id, - inputNum: data.inputNum + inputNum: data.inputNum, + bakeId: data.bakeId }) }, [response.result]); } diff --git a/src/web/OutputWaiter.mjs b/src/web/OutputWaiter.mjs index 9fdf9b41..2c151c53 100755 --- a/src/web/OutputWaiter.mjs +++ b/src/web/OutputWaiter.mjs @@ -8,6 +8,7 @@ import Utils from "../core/Utils"; import FileSaver from "file-saver"; import zip from "zlibjs/bin/zip.min"; +import ZipWorker from "worker-loader?inline&fallback=false!./ZipWorker"; const Zlib = zip.Zlib; @@ -29,6 +30,8 @@ class OutputWaiter { this.outputs = {}; this.activeTab = -1; + this.zipWorker = null; + this.maxTabs = 4; // Calculate this } @@ -85,7 +88,8 @@ class OutputWaiter { inputNum: inputNum, statusMessage: `Input ${inputNum} has not been baked yet.`, error: null, - status: "inactive" + status: "inactive", + bakeId: -1 }; this.outputs[inputNum] = newOutput; @@ -163,6 +167,17 @@ class OutputWaiter { this.set(inputNum); } + /** + * Updates the stored bake ID for the output in the ouptut array + * + * @param {number} bakeId + * @param {number} inputNum + */ + updateOutputBakeId(bakeId, inputNum) { + if (this.getOutput(inputNum) === -1) return; + this.outputs[inputNum].bakeId = bakeId; + } + /** * Removes an output from the output array. * @@ -202,11 +217,13 @@ class OutputWaiter { const outputFile = document.getElementById("output-file"); const outputHighlighter = document.getElementById("output-highlighter"); const inputHighlighter = document.getElementById("input-highlighter"); - // If inactive, show blank // If pending or baking, show loader and status message // If error, style the tab and handle the error // If done, display the output if it's the active tab - if (output.status === "inactive" || output.status === "stale") { + // If inactive, show the last bake value (or blank) + if (output.status === "inactive" || + output.status === "stale" || + (output.status === "baked" && output.bakeId < this.manager.worker.bakeId)) { this.manager.controls.showStaleIndicator(); } else { this.manager.controls.hideStaleIndicator(); @@ -421,10 +438,111 @@ class OutputWaiter { this.downloadAllFiles(); } + + /** + * Spawns a new ZipWorker and sends it the outputs so that they can + * be zipped for download + */ + downloadAllFiles() { + + const inputNums = Object.keys(this.outputs); + for (let i = 0; i < inputNums.length; i++) { + const iNum = inputNums[i]; + if (this.outputs[iNum].status !== "baked" || + this.outputs[iNum].bakeId !== this.manager.worker.bakeId) { + if (window.confirm("Not all outputs have been baked yet. Continue downloading outputs?")) { + break; + } else { + return; + } + } + } + + const fileName = window.prompt("Please enter a filename: ", "download.zip"); + + if (fileName === null || fileName === "") { + // Don't zip the files if there isn't a filename + this.app.alert("No filename was specified.", 3000); + return; + } + + let fileExt = window.prompt("Please enter a file extension for the files: ", ".txt"); + + if (fileExt === null) { + // Use .dat as the default file extension + fileExt = ".dat"; + } + + if (this.zipWorker !== null) { + this.terminateZipWorker(); + } + + const downloadButton = document.getElementById("save-all-to-file"); + + downloadButton.disabled = true; + downloadButton.classList.add("spin"); + $("[data-toggle='tooltip']").tooltip("hide"); + + downloadButton.firstElementChild.innerHTML = "autorenew"; + + log.debug("Creating ZipWorker"); + this.zipWorker = new ZipWorker(); + this.zipWorker.postMessage({ + outputs: this.outputs, + filename: fileName, + fileExtension: fileExt + }); + this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this)); + + } + + /** + * Terminate the ZipWorker + */ + terminateZipWorker() { + if (this.zipWorker === null) return; // Already terminated + + log.debug("Terminating ZipWorker."); + + this.zipWorker.terminate(); + this.zipWorker = null; + } + + + /** + * Handle messages sent back by the ZipWorker + */ + handleZipWorkerMessage(e) { + const r = e.data; + if (!r.hasOwnProperty("zippedFile")) { + log.error("No zipped file was sent in the message."); + this.terminateZipWorker(); + return; + } + if (!r.hasOwnProperty("filename")) { + log.error("No filename was sent in the message."); + this.terminateZipWorker(); + return; + } + + const file = new File([r.zippedFile], r.filename); + FileSaver.saveAs(file, r.filename, false); + + this.terminateZipWorker(); + + const downloadButton = document.getElementById("save-all-to-file"); + + downloadButton.disabled = false; + downloadButton.classList.remove("spin"); + + downloadButton.firstElementChild.innerHTML = "archive"; + } + + /** * Handler for download all files events. */ - async downloadAllFiles() { + async downloadAllFilesOld() { const fileName = window.prompt("Please enter a filename: ", "download.zip"); const fileExt = window.prompt("Please enter a file extension for the files: ", ".txt"); const zip = new Zlib.Zip(); @@ -931,7 +1049,11 @@ class OutputWaiter { * Copies the output to the clipboard */ copyClick() { - const output = this.getActive(); + let output = this.getActive(); + + if (typeof output !== "string") { + output = Utils.arrayBufferToStr(output); + } // Create invisible textarea to populate with the raw dish string (not the printable version that // contains dots instead of the actual bytes) diff --git a/src/web/WorkerWaiter.mjs b/src/web/WorkerWaiter.mjs index fb8939fd..7e9eec28 100644 --- a/src/web/WorkerWaiter.mjs +++ b/src/web/WorkerWaiter.mjs @@ -26,6 +26,7 @@ class WorkerWaiter { this.maxWorkers = navigator.hardwareConcurrency || 4; this.inputs = []; this.totalOutputs = 0; + this.bakeId = 0; } /** @@ -141,7 +142,7 @@ class WorkerWaiter { switch (r.action) { case "bakeComplete": log.debug(`Bake ${inputNum} complete.`); - this.updateOutput(r.data, r.data.inputNum); + this.updateOutput(r.data, r.data.inputNum, r.data.bakeId); this.workerFinished(currentWorker); break; case "bakeError": @@ -187,11 +188,12 @@ class WorkerWaiter { * * @param {Object} data * @param {number} inputNum + * @param {number} bakeId */ - updateOutput(data, inputNum) { + updateOutput(data, inputNum, bakeId) { + this.manager.output.updateOutputBakeId(bakeId, inputNum); this.manager.output.updateOutputValue(data, inputNum); this.manager.output.updateOutputStatus("baked", inputNum); - // this.manager.recipe.updateBreakpointIndicator(this.app.progress); } @@ -337,7 +339,8 @@ class WorkerWaiter { options: this.options, progress: this.progress, step: this.step, - inputNum: nextInput.inputNum + inputNum: nextInput.inputNum, + bakeId: this.bakeId } }); } else { @@ -349,7 +352,8 @@ class WorkerWaiter { options: this.options, progress: this.progress, step: this.step, - inputNum: nextInput.inputNum + inputNum: nextInput.inputNum, + bakeId: this.bakeId } }, [nextInput.input]); } @@ -366,7 +370,7 @@ class WorkerWaiter { bake(recipeConfig, options, progress, step) { this.setBakingStatus(true); this.bakeStartTime = new Date().getTime(); - + this.bakeId++; this.recipeConfig = recipeConfig; this.options = options; this.progress = progress; diff --git a/src/web/ZipWorker.mjs b/src/web/ZipWorker.mjs new file mode 100644 index 00000000..1c44d183 --- /dev/null +++ b/src/web/ZipWorker.mjs @@ -0,0 +1,69 @@ +/** + * Web Worker to handle zipping the outputs for download. + * + * @author j433866 [j433866@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import zip from "zlibjs/bin/zip.min"; +import Utils from "../core/Utils"; + +const Zlib = zip.Zlib; + +/** + * Respond to message from parent thread. + */ +self.addEventListener("message", function(e) { + const r = e.data; + if (!r.hasOwnProperty("outputs")) { + log.error("No files were passed to the ZipWorker."); + return; + } + if (!r.hasOwnProperty("filename")) { + log.error("No filename was passed to the ZipWorker"); + return; + } + if (!r.hasOwnProperty("fileExtension")) { + log.error("No file extension was passed to the ZipWorker"); + return; + } + + self.zipFiles(r.outputs, r.filename, r.fileExtension); +}); + +self.setOption = function(...args) {}; + +/** + * Compress the files into a zip file and send the zip back + * to the OutputWaiter. + * + * @param {object} outputs + * @param {string} filename + * @param {string} fileExtension + */ +self.zipFiles = function(outputs, filename, fileExtension) { + const zip = new Zlib.Zip(); + const inputNums = Object.keys(outputs); + + for (let i = 0; i < inputNums.length; i++) { + const iNum = inputNums[i]; + const name = Utils.strToByteArray(iNum + fileExtension); + + let output; + if (outputs[iNum].data === null) { + output = new Uint8Array(0); + } else if (typeof outputs[iNum].data.result === "string") { + output = new Uint8Array(Utils.strToArrayBuffer(outputs[iNum].data.result)); + } else { + output = new Uint8Array(outputs[iNum].data.result); + } + zip.addFile(output, {filename: name}); + } + + const zippedFile = zip.compress(); + self.postMessage({ + zippedFile: zippedFile.buffer, + filename: filename + }, [zippedFile.buffer]); +}; diff --git a/src/web/stylesheets/layout/_controls.css b/src/web/stylesheets/layout/_controls.css index c8b8b3d4..ae66be91 100755 --- a/src/web/stylesheets/layout/_controls.css +++ b/src/web/stylesheets/layout/_controls.css @@ -49,3 +49,15 @@ #controls .btn { border-radius: 30px; } + +.spin { + animation-name: spin; + animation-duration: 3s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +@keyframes spin { + 0% {transform: rotate(0deg);} + 100% {transform: rotate(360deg);} +}