From 343d350af8d556bdc38891831017151c967c2514 Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 14 Feb 2017 14:46:35 -0500 Subject: [PATCH 1/2] Initial async work Operations can now: 1) return their progress directly. 2) throw an error. 3) (ADDED) return a promise: + that resolves to their progress. + that rejects an error message (like throwing but asynchronous). For an example see the new operation "Wait" (Flow Control) Added a flow control operation "Wait", which waits for the number of milliseconds passed in as its argument. It is a fairly useless operation but it does demonstrate how asynchronous operations now work. A recipe like: ``` Fork Wait (1000ms) ``` will only wait for 1000ms (each wait runs at the same time as each other). I have not looked into performance implications yet, also this code is probably more complicated than it needs to be (would love help on this). --- src/css/structure/layout.css | 32 ++++++++ src/html/index.html | 6 ++ src/js/config/Categories.js | 1 + src/js/config/OperationConfig.js | 16 +++- src/js/core/Chef.js | 51 +++++++----- src/js/core/FlowControl.js | 92 ++++++++++++++++----- src/js/core/Recipe.js | 136 +++++++++++++++++++++---------- src/js/views/html/HTMLApp.js | 120 +++++++++++++++++++++------ 8 files changed, 346 insertions(+), 108 deletions(-) diff --git a/src/css/structure/layout.css b/src/css/structure/layout.css index 0880b93e..08121495 100755 --- a/src/css/structure/layout.css +++ b/src/css/structure/layout.css @@ -428,3 +428,35 @@ span.btn img { border-top: none; margin-top: 0; } + +@-moz-keyframes spinner { + from { -moz-transform: rotate(0deg); } + to { -moz-transform: rotate(359deg); } +} +@-webkit-keyframes spinner { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(359deg); } +} +@keyframes spinner { + from {transform:rotate(0deg);} + to {transform:rotate(359deg);} +} + +.loading-icon::before { + content: "\21bb"; +} + +.loading-icon { + -webkit-animation-name: spinner; + -webkit-animation-duration: 1000ms; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + -moz-animation-name: spinner; + -moz-animation-duration: 1000ms; + -moz-animation-iteration-count: infinite; + -moz-animation-timing-function: linear; + -ms-animation-name: spinner; + -ms-animation-duration: 1000ms; + -ms-animation-iteration-count: infinite; + -ms-animation-timing-function: linear; +} diff --git a/src/html/index.html b/src/html/index.html index 3576e8ae..001e9bd9 100755 --- a/src/html/index.html +++ b/src/html/index.html @@ -97,6 +97,9 @@
Input +
@@ -113,6 +116,9 @@
Output +
diff --git a/src/js/config/Categories.js b/src/js/config/Categories.js index aafa702a..b32b8f47 100755 --- a/src/js/config/Categories.js +++ b/src/js/config/Categories.js @@ -285,6 +285,7 @@ var Categories = [ "Jump", "Conditional Jump", "Return", + "Wait", ] }, ]; diff --git a/src/js/config/OperationConfig.js b/src/js/config/OperationConfig.js index 045f2832..77068d41 100755 --- a/src/js/config/OperationConfig.js +++ b/src/js/config/OperationConfig.js @@ -3094,5 +3094,19 @@ var OperationConfig = { outputType: "html", args: [ ] - } + }, + "Wait": { + description: "Waits for a number of milliseconds.", + run: FlowControl.runWait, + inputType: "string", + outputType: "string", + flowControl: true, + args: [ + { + name: "Sleep time in milliseconds", + type: "number", + value: FlowControl.SLEEP_TIME, + } + ] + }, }; diff --git a/src/js/core/Chef.js b/src/js/core/Chef.js index e6bac71d..c10796db 100755 --- a/src/js/core/Chef.js +++ b/src/js/core/Chef.js @@ -34,7 +34,7 @@ Chef.prototype.bake = function(inputText, recipeConfig, options, progress, step) var startTime = new Date().getTime(), recipe = new Recipe(recipeConfig), containsFc = recipe.containsFlowControl(), - error = false; + chef = this; // Reset attemptHighlight flag if (options.hasOwnProperty("attemptHighlight")) { @@ -64,28 +64,41 @@ Chef.prototype.bake = function(inputText, recipeConfig, options, progress, step) // If starting from scratch, load data if (progress === 0) { - this.dish.set(inputText, Dish.STRING); + chef.dish.set(inputText, Dish.STRING); } - try { - progress = recipe.execute(this.dish, progress); - } catch (err) { - // Return the error in the result so that everything else gets correctly updated - // rather than throwing it here and losing state info. - error = err; - progress = err.progress; - } - - return { - result: this.dish.type === Dish.HTML ? - this.dish.get(Dish.HTML) : - this.dish.get(Dish.STRING), - type: Dish.enumLookup(this.dish.type), - progress: progress, + var ret = { options: options, - duration: new Date().getTime() - startTime, - error: error + error: false, }; + + return new Promise(function(resolve) { + recipe.execute(chef.dish, progress) + .then(function(progress) { + ret.result = chef.dish.type === Dish.HTML ? + chef.dish.get(Dish.HTML) : + chef.dish.get(Dish.STRING); + ret.type = Dish.enumLookup(chef.dish.type); + + ret.duration = new Date().getTime() - startTime; + ret.progress = progress; + + resolve(ret); + }) + .catch(function(err) { + ret.result = chef.dish.type === Dish.HTML ? + chef.dish.get(Dish.HTML) : + chef.dish.get(Dish.STRING); + ret.type = Dish.enumLookup(chef.dish.type); + + ret.duration = new Date().getTime() - startTime; + ret.progress = err.progress; + ret.error = err; + + // Resolve not reject: we are packaging the error as a value. + resolve(ret); + }); + }); }; diff --git a/src/js/core/FlowControl.js b/src/js/core/FlowControl.js index 6f56be66..340736f4 100755 --- a/src/js/core/FlowControl.js +++ b/src/js/core/FlowControl.js @@ -59,29 +59,53 @@ var FlowControl = { } } - var recipe = new Recipe(), - output = "", - progress = 0; - + var recipe = new Recipe(); recipe.addOperations(subOpList); - // Run recipe over each tranche - for (i = 0; i < inputs.length; i++) { - var dish = new Dish(inputs[i], inputType); - try { - progress = recipe.execute(dish, 0); - } catch (err) { - if (!ignoreErrors) { - throw err; - } - progress = err.progress + 1; - } - output += dish.get(outputType) + mergeDelim; - } + return new Promise(function(resolve, reject) { + var promises = inputs.map(function(input, i) { + var forkDish = new Dish(input, inputType); - state.dish.set(output, outputType); - state.progress += progress; - return state; + return new Promise(function(resolve, reject) { + recipe.execute(forkDish, 0) + .then(function(progress) { + resolve({ + progress: progress, + dish: forkDish, + }); + }) + .catch(function(err) { + if (ignoreErrors) { + resolve({ + progress: err.progress + 1, + dish: forkDish, + }); + } else { + reject(err); + } + }); + }); + + }); + + Promise.all(promises) + .then(function(values) { + var progress; + + var output = values.map(function(value) { + progress = value.progress; + return value.dish.get(outputType); + }).join(mergeDelim); + + state.progress += progress; + state.dish.set(output, outputType); + + resolve(state); + }) + .catch(function(err) { + reject(err); + }); + }); }, @@ -183,4 +207,32 @@ var FlowControl = { return state; }, + + /** + * @constant + * @default + */ + SLEEP_TIME: 2500, + + + /** + * Wait operation. + * + * @param {Object} state - The current state of the recipe. + * @param {number} state.progress - The current position in the recipe. + * @param {Dish} state.dish - The Dish being operated on. + * @param {Operation[]} state.opList - The list of operations in the recipe. + * @returns {Object} The updated state of the recipe. + */ + runWait: function(state) { + var ings = state.opList[state.progress].getIngValues(), + sleepTime = ings[0]; + + return new Promise(function(resolve, reject) { + setTimeout(function() { + resolve(state); + }, sleepTime); + }); + }, + }; diff --git a/src/js/core/Recipe.js b/src/js/core/Recipe.js index d5f383fc..5a8cadf3 100755 --- a/src/js/core/Recipe.js +++ b/src/js/core/Recipe.js @@ -138,58 +138,112 @@ Recipe.prototype.lastOpIndex = function(startIndex) { * Executes each operation in the recipe over the given Dish. * * @param {Dish} dish - * @param {number} [startFrom=0] - The index of the Operation to start executing from + * @param {number} [currentStep=0] - The index of the Operation to start executing from * @returns {number} - The final progress through the recipe */ -Recipe.prototype.execute = function(dish, startFrom) { - startFrom = startFrom || 0; - var op, input, output, numJumps = 0; +Recipe.prototype.execute = function(dish, currentStep, state) { + var recipe = this; - for (var i = startFrom; i < this.opList.length; i++) { - op = this.opList[i]; - if (op.isDisabled()) { - continue; - } - if (op.isBreakpoint()) { - return i; + var formatErrMsg = function(err, step, op) { + var e = typeof err == "string" ? { message: err } : err; + + e.progress = step; + if (e.fileName) { + e.displayStr = op.name + " - " + e.name + " in " + + e.fileName + " on line " + e.lineNumber + + ".

Message: " + (e.displayStr || e.message); + } else { + e.displayStr = op.name + " - " + (e.displayStr || e.message); } - try { + return e; + }; + + // Operations can be asynchronous so we have to return a Promise to a + // future value. + return new Promise(function(resolve, reject) { + // Helper function to clean up recursing to the next recipe step. + // It is a closure to avoid having to pass in resolve and reject. + var execRecipe = function(recipe, dish, step, state) { + return recipe.execute(dish, step, state) + .then(function(progress) { + resolve(progress); + }) + .catch(function(err) { + // Pass back the error to the previous caller. + // We don't want to handle the error here as the current + // operation did not cause the error, and so it should + // not appear in the error message. + reject(err); + }); + }; + + currentStep = currentStep || 0; + + if (currentStep === recipe.opList.length) { + resolve(currentStep); + return; + } + + var op = recipe.opList[currentStep], input = dish.get(op.inputType); + if (op.isDisabled()) { + // Skip to next operation + var nextStep = currentStep + 1; + execRecipe(recipe, dish, nextStep, state); + } else if (op.isBreakpoint()) { + // We are at a breakpoint, we shouldn't recurse to the next op. + resolve(currentStep); + } else { + var operationResult; + + // We must try/catch here because op.run can either return + // A) a value + // B) a promise + // Promise.resolve -> .catch will handle errors from promises + // try/catch will handle errors from values + try { + if (op.isFlowControl()) { + state = { + progress: currentStep, + dish: dish, + opList: recipe.opList, + numJumps: (state && state.numJumps) || 0, + }; + operationResult = op.run(state); + } else { + operationResult = op.run(input, op.getIngValues()); + } + } catch (err) { + reject(formatErrMsg(err, currentStep, op)); + return; + } + if (op.isFlowControl()) { - // Package up the current state - var state = { - "progress" : i, - "dish" : dish, - "opList" : this.opList, - "numJumps" : numJumps - }; - - state = op.run(state); - i = state.progress; - numJumps = state.numJumps; + Promise.resolve(operationResult) + .then(function(state) { + return recipe.execute(state.dish, state.progress + 1); + }) + .then(function(progress) { + resolve(progress); + }) + .catch(function(err) { + reject(formatErrMsg(err, currentStep, op)); + }); } else { - output = op.run(input, op.getIngValues()); - dish.set(output, op.outputType); + Promise.resolve(operationResult) + .then(function(output) { + dish.set(output, op.outputType); + var nextStep = currentStep + 1; + execRecipe(recipe, dish, nextStep, state); + }) + .catch(function(err) { + reject(formatErrMsg(err, currentStep, op)); + }); } - } catch (err) { - var e = typeof err == "string" ? { message: err } : err; - - e.progress = i; - if (e.fileName) { - e.displayStr = op.name + " - " + e.name + " in " + - e.fileName + " on line " + e.lineNumber + - ".

Message: " + (e.displayStr || e.message); - } else { - e.displayStr = op.name + " - " + (e.displayStr || e.message); - } - - throw e; } - } - - return this.opList.length; + }); }; diff --git a/src/js/views/html/HTMLApp.js b/src/js/views/html/HTMLApp.js index 4e7aa78d..604fe449 100755 --- a/src/js/views/html/HTMLApp.js +++ b/src/js/views/html/HTMLApp.js @@ -28,6 +28,9 @@ var HTMLApp = function(categories, operations, defaultFavourites, defaultOptions this.progress = 0; this.ingId = 0; + this.baking = false; + this.rebake = false; + window.chef = this.chef; }; @@ -61,6 +64,36 @@ HTMLApp.prototype.handleError = function(err) { }; +/** + * Updates the UI to show if baking is in process or not. + * + * @param {bakingStatus} + */ +HTMLApp.prototype.setBakingStatus = function(bakingStatus) { + var inputLoadingIcon = document.querySelector("#input .title .loading-icon"); + var outputLoadingIcon = document.querySelector("#output .title .loading-icon"); + + var inputElement = document.querySelector("#input-text"); + var outputElement = document.querySelector("#output-text"); + + if (bakingStatus) { + inputLoadingIcon.style.display = "inline-block"; + outputLoadingIcon.style.display = "inline-block"; + inputElement.classList.add("disabled"); + outputElement.classList.add("disabled"); + inputElement.disabled = true; + outputElement.disabled = true; + } else { + inputLoadingIcon.style.display = "none"; + outputLoadingIcon.style.display = "none"; + inputElement.classList.remove("disabled"); + outputElement.classList.remove("disabled"); + inputElement.disabled = false; + outputElement.disabled = false; + } +}; + + /** * Calls the Chef to bake the current input using the current recipe. * @@ -68,37 +101,70 @@ HTMLApp.prototype.handleError = function(err) { * whole recipe. */ HTMLApp.prototype.bake = function(step) { - var response; + var app = this; + + if (app.baking) { + if (!app.rebake) { + // We do not want to keep autobaking + // Say that we will rebake and then try again later + app.rebake = true; + setTimeout(function() { + app.bake(step); + }, 500); + } + + return; + } + + app.rebake = false; + app.baking = true; + app.setBakingStatus(true); try { - response = this.chef.bake( - this.getInput(), // The user's input - this.getRecipeConfig(), // The configuration of the recipe - this.options, // Options set by the user - this.progress, // The current position in the recipe + app.chef.bake( + app.getInput(), // The user's input + app.getRecipeConfig(), // The configuration of the recipe + app.options, // Options set by the user + app.progress, // The current position in the recipe step // Whether or not to take one step or execute the whole recipe - ); + ) + .then(function(response) { + app.baking = false; + app.setBakingStatus(false); + + if (!response) { + return; + } + if (response.error) { + app.handleError(response.error); + } + + app.options = response.options; + + if (response.type === "html") { + app.dishStr = Utils.stripHtmlTags(response.result, true); + } else { + app.dishStr = response.result; + } + + app.progress = response.progress; + app.manager.recipe.updateBreakpointIndicator(response.progress); + app.manager.output.set(response.result, response.type, response.duration); + + // If baking took too long, disable auto-bake + if (response.duration > app.options.autoBakeThreshold && app.autoBake_) { + app.manager.controls.setAutoBake(false); + app.alert("Baking took longer than " + app.options.autoBakeThreshold + + "ms, Auto Bake has been disabled.", "warning", 5000); + } + }) + .catch(function(err) { + console.error("Chef's promise was rejected, should never occur"); + }); } catch (err) { - this.handleError(err); - } - - if (!response) return; - - if (response.error) { - this.handleError(response.error); - } - - this.options = response.options; - this.dishStr = response.type === "html" ? Utils.stripHtmlTags(response.result, true) : response.result; - this.progress = response.progress; - this.manager.recipe.updateBreakpointIndicator(response.progress); - this.manager.output.set(response.result, response.type, response.duration); - - // If baking took too long, disable auto-bake - if (response.duration > this.options.autoBakeThreshold && this.autoBake_) { - this.manager.controls.setAutoBake(false); - this.alert("Baking took longer than " + this.options.autoBakeThreshold + - "ms, Auto Bake has been disabled.", "warning", 5000); + app.baking = false; + app.setBakingStatus(false); + app.handleError(err); } }; From f268f11d7294dbca8006d41bcd0fcc50a89f02d7 Mon Sep 17 00:00:00 2001 From: toby Date: Tue, 14 Feb 2017 14:55:27 -0500 Subject: [PATCH 2/2] Update stats --- src/static/stats.txt | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/static/stats.txt b/src/static/stats.txt index c069dd07..9384f4bc 100644 --- a/src/static/stats.txt +++ b/src/static/stats.txt @@ -1,21 +1,21 @@ -211 source files -115651 lines -4.3M size +213 source files +116058 lines + size -142 JavaScript source files -106461 lines -3.8M size +143 JavaScript source files +106830 lines +4.9M size 83 third party JavaScript source files 86258 lines -3.0M size +3.7M size -59 first party JavaScript source files -20203 lines -752K size +60 first party JavaScript source files +20572 lines +1.3M size 3.5M uncompressed JavaScript size -1.9M compressed JavaScript size + compressed JavaScript size 15 categories -172 operations +175 operations