/** * Web Worker to handle communications between the front-end and the core. * * @author n1474335 [n1474335@gmail.com] * @copyright Crown Copyright 2017 * @license Apache-2.0 */ import Chef from "./Chef.mjs"; import OperationConfig from "./config/OperationConfig.json" assert {type: "json"}; import OpModules from "./config/modules/OpModules.mjs"; // Add ">" to the start of all log messages in the Chef Worker import loglevelMessagePrefix from "loglevel-message-prefix"; loglevelMessagePrefix(log, { prefixes: [], staticPrefixes: [">"], prefixFormat: "%p" }); // Set up Chef instance self.chef = new Chef(); self.OpModules = OpModules; self.OperationConfig = OperationConfig; self.inputNum = -1; // Tell the app that the worker has loaded and is ready to operate self.postMessage({ action: "workerLoaded", data: {} }); /** * Respond to message from parent thread. * * inputNum is optional and only used for baking multiple inputs. * Defaults to -1 when one isn't sent with the bake message. * * Messages should have the following format: * { * action: "bake" | "silentBake", * data: { * input: {string}, * recipeConfig: {[Object]}, * options: {Object}, * progress: {number}, * step: {boolean}, * [inputNum=-1]: {number} * } * } */ self.addEventListener("message", function(e) { // Handle message const r = e.data; log.debug("ChefWorker receiving command '" + r.action + "'"); switch (r.action) { case "bake": bake(r.data); break; case "silentBake": silentBake(r.data); break; case "getDishAs": getDishAs(r.data); break; case "getDishTitle": getDishTitle(r.data); break; case "docURL": // Used to set the URL of the current document so that scripts can be // imported into an inline worker. self.docURL = r.data; break; case "highlight": calculateHighlights( r.data.recipeConfig, r.data.direction, r.data.pos ); break; case "setLogLevel": log.setLevel(r.data, false); break; default: break; } }); /** * Baking handler * * @param {Object} data */ async function bake(data) { // Ensure the relevant modules are loaded self.loadRequiredModules(data.recipeConfig); try { self.inputNum = (data.inputNum !== undefined) ? data.inputNum : -1; const response = await self.chef.bake( data.input, // The user's input data.recipeConfig, // The configuration of the recipe data.options // Options set by the user ); const transferable = (data.input instanceof ArrayBuffer) ? [data.input] : undefined; self.postMessage({ action: "bakeComplete", data: Object.assign(response, { id: data.id, inputNum: data.inputNum, bakeId: data.bakeId }) }, transferable); } catch (err) { self.postMessage({ action: "bakeError", data: { error: err.message || err, id: data.id, inputNum: data.inputNum } }); } self.inputNum = -1; } /** * Silent baking handler */ function silentBake(data) { const duration = self.chef.silentBake(data.recipeConfig); self.postMessage({ action: "silentBakeComplete", data: duration }); } /** * Translates the dish to a given type. */ async function getDishAs(data) { const value = await self.chef.getDishAs(data.dish, data.type); const transferable = (data.type === "ArrayBuffer") ? [value] : undefined; self.postMessage({ action: "dishReturned", data: { value: value, id: data.id } }, transferable); } /** * Gets the dish title * * @param {object} data * @param {Dish} data.dish * @param {number} data.maxLength * @param {number} data.id */ async function getDishTitle(data) { const title = await self.chef.getDishTitle(data.dish, data.maxLength); self.postMessage({ action: "dishReturned", data: { value: title, id: data.id } }); } /** * Calculates highlight offsets if possible. * * @param {Object[]} recipeConfig * @param {string} direction * @param {Object} pos - The position object for the highlight. * @param {number} pos.start - The start offset. * @param {number} pos.end - The end offset. */ async function calculateHighlights(recipeConfig, direction, pos) { pos = await self.chef.calculateHighlights(recipeConfig, direction, pos); self.postMessage({ action: "highlightsCalculated", data: pos }); } /** * Checks that all required modules are loaded and loads them if not. * * @param {Object} recipeConfig */ self.loadRequiredModules = function(recipeConfig) { recipeConfig.forEach(op => { const module = self.OperationConfig[op.op].module; if (!(module in OpModules)) { log.info(`Loading ${module} module`); self.sendStatusMessage(`Loading ${module} module`); self.importScripts(`${self.docURL}/modules/${module}.js`); // lgtm [js/client-side-unvalidated-url-redirection] self.sendStatusMessage(""); } }); }; /** * Send status update to the app. * * @param {string} msg */ self.sendStatusMessage = function(msg) { self.postMessage({ action: "statusMessage", data: { message: msg, inputNum: self.inputNum } }); }; /** * Send progress update to the app. * * @param {number} progress * @param {number} total */ self.sendProgressMessage = function(progress, total) { self.postMessage({ action: "progressMessage", data: { progress: progress, total: total, inputNum: self.inputNum } }); }; /** * Send an option value update to the app. * * @param {string} option * @param {*} value */ self.setOption = function(option, value) { self.postMessage({ action: "optionUpdate", data: { option: option, value: value } }); }; /** * Send register values back to the app. * * @param {number} opIndex * @param {number} numPrevRegisters * @param {string[]} registers */ self.setRegisters = function(opIndex, numPrevRegisters, registers) { self.postMessage({ action: "setRegisters", data: { opIndex: opIndex, numPrevRegisters: numPrevRegisters, registers: registers } }); };