butler.js | |
---|---|
A simple asynchronous chaining butler | var util = require("util"),
events = require("events"); |
The Butler! Initializes some stuff like action list and storage object. It also defines himself as not working. | function Butler() {
events.EventEmitter.call(this);
this.actions = [];
this.storage = {};
this._working = false;
this._last_wait = 0;
this._scope = null;
}
util.inherits(Butler, events.EventEmitter); |
Before adding a method from an object you must call
| Butler.prototype.scope = function (scope) {
this._scope = scope || null;
return this;
}; |
Add an asynchronous call to the Butler. The first parameter is a call reference and the other parameters are possible arguments (if needed). A callback will be appended to the arguments so your asynchronous call must have a last parameter which is the return callback that must have an error argument as first parameter or don't have any arguments at all. | Butler.prototype.add = function () {
var params = Array.prototype.slice.apply(arguments);
if (!params.length || typeof params[0] != "function") {
throw new Error({ code: 1, message: "Function not specified" });
}
this.actions.push({
"call" : params[0],
"scope": this._scope,
"args" : params.slice(1)
});
return this;
}; |
After an asynchronous call, save return param (on index | Butler.prototype.store = function (key, paramIndex) {
this.actions.push({
"store": key,
"index": paramIndex
});
return this;
}; |
Add a waiting callback. This immediately start running the previously added callbacks one by one. After all run successfull, this callback is executed with 3 parameters: an error object (if some callback in the middle fails), the return parameters from the final callback and a butler object with 2 methods:
| Butler.prototype.wait = function (cb) {
this.actions.push({
"wait": cb
});
this.resume();
return this;
}; |
Instructs the butler that the previous calls are to be runned in parallel, without arguments passing from one to another. The callback is executed at the end of all the asynchronous calls with the result of all the callbacks. | Butler.prototype.parallel = function (cb) {
this.actions.push({
"parallel": cb
});
this.resume();
return this;
}; |
Returns | Butler.prototype.busy = function (cb) {
return this._working;
};
Butler.prototype.resume = function (params) {
if (this._working || !this.actions.length) {
return;
}
this._working = true;
var parallelResume = -1;
for (var i = 0; i < this.actions.length; i++) {
if (this.actions[i].hasOwnProperty("call")) continue;
if (this.actions[i].hasOwnProperty("wait")) break;
if (this.actions[i].hasOwnProperty("store")) break;
if (this.actions[i].hasOwnProperty("parallel")) {
parallelResume = i;
break;
}
}
if (parallelResume == 0) {
/* ??? */
return console.log("parallel resume without anything to run");
} else if (parallelResume > 0) {
var actions = this.actions.splice(0, parallelResume + 1),
parallelCb = actions.pop(),
missingActions = actions.length,
errors = [],
returns = [];
this._last_wait = 0;
for (var i = 0; i < actions.length; i++) {
returns[i] = [];
}
for (var i = 0; i < actions.length; i++) {
var callName = findCallName(actions[i].call);
actions[i].args.push(
(function (butler, n, callbackName) {
return function () {
missingActions--;
var params = Array.prototype.slice.apply(arguments);
if (params.length == 0) {
returns[n].push(null);
} else {
/* 1st argument must always be an error object */
if (params[0]) {
params[0].callbackIndex = n + 1;
errors.push(params[0]);
}
returns[n] = params.slice(1) || null;
}
butler.emit("call.end",
callbackName,
params ? params[0] : null,
params ? params.slice(1) : []);
if (missingActions == 0) {
var ret, ctx = butler._getButlerCallbackObject();
butler._working = false;
ctx.errors = errors;
ctx.returns = returns;
ret = parallelCb.parallel.apply(ctx);
if (ret !== false) {
butler.resume(typeof ret == "object" ? ret
: butler._next_params);
}
}
};
})(this, i, callName)
);
this.emit("call.start", callName, actions[i].args.slice(0, -1));
actions[i].call.apply(actions[i].scope, actions[i].args);
}
return;
}
var action = this.actions.splice(0, 1).pop();
params = params || this._next_params;
delete this._next_params;
if (action.hasOwnProperty("store")) {
var index = Math.max(action.index || 0, 0);
this.storage[action.store] = params && params.length > index ? params[index]
: null;
this._next_params = params;
this._working = false;
return this.resume();
}
if (action.hasOwnProperty("call")) {
if (params && params.length) {
for (var i = params.length - 1; i >= 0; i--) {
action.args.unshift(params[i]);
}
}
var callName = findCallName(action.call);
action.args.push(
(function (butler) {
return function () {
var return_params = Array.prototype.slice.apply(arguments);
butler._working = false;
butler.emit("call.end",
callName,
return_params ? return_params[0] : null,
return_params ? return_params.slice(1) : []);
butler._handleReturn(return_params);
};
})(this)
);
this._last_wait++;
this.emit("call.start", callName, action.args.slice(0, -1));
action.call.apply(action.scope, action.args);
return;
}
if (action.hasOwnProperty("wait")) {
var ctx = this._getButlerCallbackObject();
this._next_params = params || [];
this._last_wait = 0;
this._working = false;
var ret = action.wait.apply(ctx, [ null, this._next_params ]);
if (ret !== false) {
this.resume(typeof ret == "object" ? ret : this._next_params);
}
return;
}
};
Butler.prototype._handleReturn = function (params) {
if (!params.length) {
return this.resume();
}
if (params[0]) {
/* error */
for (var i = 0; i < this.actions.length; i++) {
if (this.actions[i].hasOwnProperty("wait")) {
var action = this.actions.splice(0, i + 1).pop(),
ctx = this._getButlerCallbackObject();
params[0].callbackIndex = this._last_wait;
this._last_wait = 0;
this._working = false;
var ret = action.wait.apply(ctx, [ params[0] ]);
if (ret !== false) {
this.resume(typeof ret == "object" ? ret : null);
}
return;
}
}
return console.log("error without a wait() call to handle!",
params[0], this._last_wait);
}
this.resume(params.slice(1));
};
Butler.prototype._getButlerCallbackObject = function () {
return (function (butler) {
return {
"params": {
"length": function () {
return (butler._next_params || []).length;
},
"set": function () {
butler._next_params = Array.prototype.slice.apply(arguments);
return this;
},
"get": function (i) {
if (typeof i == "number") {
if (butler._next_params.length) {
if (i >= 0 && i < butler._next_params.length) {
return butler._next_params[i];
}
}
return null;
}
return butler._next_params || [];
},
"first": function () {
return this.get(0);
},
"last": function () {
return this.get(butler._next_params.length - 1);
},
"clear": function () {
butler._next_params = [];
return this;
},
"append": function () {
var args = Array.prototype.slice.apply(arguments);
butler._next_params = (butler._next_params || []).concat(args);
return this;
},
"prepend": function () {
var args = Array.prototype.slice.apply(arguments);
butler._next_params = args.concat(butler._next_params || []);
return this;
}
},
"storage": butler.storage,
"resume": function () {
butler.resume();
}
};
})(this);
};
exports.Butler = Butler;
/*
Utilities
*/
function findCallName(cb) {
var m = cb.toString().match(/^function\s+([^\s\(]+)/i);
if (m !== null) return m[1];
return 'anonymous'; // is it?
}
|