Skip to content

Instantly share code, notes, and snippets.

@srikumarks
Created March 25, 2012 08:39
Show Gist options
  • Select an option

  • Save srikumarks/2192413 to your computer and use it in GitHub Desktop.

Select an option

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.
// 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;
}
]);
}
@srikumarks
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment