Created
March 25, 2012 08:39
-
-
Save srikumarks/2192413 to your computer and use it in GitHub Desktop.
A customizable way to code sequences of potentially asynchronous actions in Javascript - either for GUI purposes or in node.js.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // There is a recent Hacker News thread discussing Python | |
| // creator Guido van Rossum's objections to a callback | |
| // based API, based on its poor ability to work with | |
| // exceptions. (http://news.ycombinator.com/item?id=3750817) | |
| // | |
| // Since Node.js and much of web stuff deals with callbacks | |
| // ... a lot, I thought it might be possible to address that concern | |
| // using a simple, flexible sequencing function. Here is my take | |
| // on it - | |
| // | |
| // run(M, args, actions): | |
| // | |
| // "actions" is an array of functions of the form - | |
| // function (M, ...) { } | |
| // that take in as arguments the results provided by | |
| // the previous actions. The first argument named "M" | |
| // is reserved for the purpose of coordinating the sequence. | |
| // | |
| // "args" is an array of arguments to pass to first action | |
| // in the sequence of actions. | |
| // | |
| // "M" is an object to customize the action chaining mechanism. | |
| // At the very least, it is expected to provide the following | |
| // two methods - | |
| // M.cont(arg1, arg2, ...): | |
| // Action functions call this to continue on with the following | |
| // action, passing the given arguments to it. Typically, once | |
| // their task is finished and they have a result, they would do | |
| // this - | |
| // return M.cont(result); | |
| // somewhere in their bodies. The run() function automatically | |
| // provides a M.cont implementation. | |
| // | |
| // M.fail(e): | |
| // If an action fails with an error e, the action function should | |
| // call this M.fail(e) instead of M.cont(..). | |
| // | |
| function run(M, args, actions) { | |
| if (actions.length === 0) { | |
| return M; | |
| } | |
| var first = actions[0]; | |
| actions = actions.slice(1); | |
| try { | |
| M.cont = function () { | |
| return run(M, [].slice.call(arguments, 0), actions); | |
| }; | |
| first.apply(null, [M].concat(args)); | |
| return M; | |
| } catch (e) { | |
| return M.fail(e); | |
| } | |
| } | |
| // SeqM is the simplest sequencing of actions possible. | |
| // This just invokes the actions in the sequence, | |
| // passing the results of the previous actions | |
| // onwards to the following ones. Upon failure, | |
| // an error is printed to console.error and the chain | |
| // of actions is stopped. Does not support error recovery. | |
| function SeqM() { | |
| var M = this; | |
| M.cont = function () { | |
| return M; | |
| }; | |
| M.fail = function (e) { | |
| console.error("Failed with " + e); | |
| return M; | |
| }; | |
| } | |
| // ExM is a more sophisticated sequencer which provides | |
| // for keeping some undo actions to do in case something | |
| // failed after the point where an action function does | |
| // not have control. An action function installs such | |
| // an undo function like this - | |
| // | |
| // M.UndoAction(function (M, e) { | |
| // ... | |
| // }); | |
| // which is likely to be followed by a M.cont(...). | |
| // Note that the undo action sequence can itself consist | |
| // of asynchronous actions. | |
| // | |
| // ExM.Try(actions, handler) produces an action function | |
| // for use within an outer action sequence, that traps errors | |
| // occurring within the nested action sequence and provides some | |
| // basic facility to recover from these errors (apart from | |
| // the undo actions) through a handler(M, e) function. | |
| // The handler function may choose to recover by calling | |
| // M.cont(..) in which case the actions following the | |
| // "Try" sequence will play using the arguments provided | |
| // in the M.cont(...) call within the handler. | |
| // ... Or the handler may abort the outer sequence by | |
| // calling M.fail(e). ExM.Try is, hopefully, useful to implement | |
| // commit-or-rollback semantics. | |
| // | |
| function ExM() { | |
| var M = this; | |
| var undoActions = []; | |
| M.cont = function () { | |
| return M; | |
| }; | |
| M.fail = function (e) { | |
| // We have to do the undo actions in the reverse order | |
| // in which they were installed. | |
| var seq = undoActions.reverse(); | |
| undoActions = []; | |
| // Do the undo actions within a new sequence context (i.e. M). | |
| run(new ExM, [e], seq); | |
| return M; | |
| } | |
| M.UndoAction = function (action) { | |
| undoActions.push(action); | |
| }; | |
| } | |
| ExM.Try = function (actions, handler) { | |
| return function (M) { | |
| var args = [].slice.call(arguments, 1); | |
| var nestedM = new ExM(); | |
| // Reuse the Undo mechanism to run the handler, | |
| // but run the handler on the outer M. | |
| nestedM.UndoAction(function (nestedM, e) { | |
| return handler(M, e); | |
| }); | |
| // Append an action that will continue with | |
| // whatever we're supposed to do once the Try | |
| // sequence completes successfully. | |
| return run(nestedM, args, actions.concat([function () { | |
| return M.cont.apply(M, [].slice.call(arguments, 1)); | |
| }])); | |
| }; | |
| }; | |
| // A sequence of actions in which one of them in the middle | |
| // may randomly fail. | |
| function simpleTest(arg) { | |
| run(new ExM, [], [ | |
| function (M) { | |
| console.log("started with " + arg); | |
| M.UndoAction(function (M, e) { | |
| console.error("Undo at " + arg); | |
| return e; | |
| }); | |
| return M.cont(arg + 1); | |
| }, | |
| function (M, arg1) { | |
| console.log("Next step " + arg1); | |
| // Pretend we're undoing something. | |
| M.UndoAction(function (M, e) { | |
| console.error("Undo stage 2 at " + arg1); | |
| // Follow up with the remaining undo actions. | |
| return M.cont(e); | |
| }); | |
| // Fail half the time just for fun. | |
| if (Math.random() > 0.5) { | |
| return M.fail("bad dice"); | |
| } else { | |
| return M.cont(arg1, arg1 + 1); | |
| } | |
| }, | |
| function (M, arg1, arg2) { | |
| console.log("Two args " + arg1 + ", " + arg2); | |
| return M; | |
| } | |
| ]); | |
| } | |
| // Similar to simpleTest(), but the middle action that may | |
| // randomly fail has a recovery action installed that permits | |
| // it to continue after taking an alternative path. | |
| function exTest(arg) { | |
| run(new ExM, [], [ | |
| function (M) { | |
| console.log("started with " + arg); | |
| M.UndoAction(function (M, e) { | |
| console.error("Undo at " + arg); | |
| return e; | |
| }); | |
| return M.cont(arg + 1); | |
| }, | |
| ExM.Try([ | |
| function (M, arg1) { | |
| console.log("Next step " + arg1); | |
| // Pretend we're undoing something. | |
| M.UndoAction(function (M, e) { | |
| console.error("Undo stage 2 at " + arg1); | |
| // Follow up with the remaining undo actions. | |
| return M.cont(e); | |
| }); | |
| // Fail half the time just for fun. | |
| if (Math.random() > 0.5) { | |
| return M.fail("bad dice"); | |
| } else { | |
| return M.cont(arg1, arg1 + 1); | |
| } | |
| }], | |
| function (M, e) { | |
| console.log("\tRecovered from " + e + " !"); | |
| return M.cont(100, 200); | |
| }), | |
| function (M, arg1, arg2) { | |
| console.log("Two args " + arg1 + ", " + arg2); | |
| return M; | |
| } | |
| ]); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This has evolved into https://github.com/srikumarks/IO.js