Skip to content

Instantly share code, notes, and snippets.

@warmwaffles
Last active December 30, 2015 01:59
Show Gist options
  • Save warmwaffles/7759599 to your computer and use it in GitHub Desktop.
Save warmwaffles/7759599 to your computer and use it in GitHub Desktop.
'use strict';
/**
* Block class is used for routing errors to higher level logic.
*/
function Block(errback) {
this._parent = Block.current;
this._errback = errback;
}
Block.current = null;
/**
* Wrap a function such that any exceptions it generates are sent to the error
* callback of the Block that is active at the time of the call to guard().
* If no Block is active, just returns the function.
*
* Example: stream.on('end', Block.guard(function() { ... }));
*/
Block.guard = function (func) {
if (this.current) {
return this.current.guard(func);
}
return func;
};
/**
* Begins a new Block with two callback functions. The first is the main part
* of the block (think 'try body'), the second is the rescue function/error
* callback (think 'catch'). The terminology follows Ruby for no other reason
* than that Block, begin and rescue describe an exception handling paradigm
* and are not reserved words in JavaScript.
*/
Block.begin = function (block, rescue) {
return new Block(rescue).trap(block);
};
/**
* Returns a function(err) that can be invoked at any time to raise an
* exception against the now current block (or the current context if no
* current). Errors are only raised if the err argument is true so this can be
* used in both error callbacks and error events.
*
* Example: request.on('error', Block.errorHandler())
*/
Block.errorHandler = function () {
// Capture the now current Block for later
var current = this.current;
return function (err) {
if (!err) {
return false;
}
if (current) {
return current.raise(err);
}
throw err;
};
};
/**
* Raises an exception on the Block. If the block has an error callback, it is
* given the exception. Otherwise, raise(...) is called on the parent block.
* If there is no parent, the exception is simply raised.
*
* Any nested exceptions from error callbacks will be raised on the block's
* parent.
*/
Block.prototype.raise = function (err) {
if (this._errback) {
try {
this._errback(err);
} catch (nestedE) {
if (this._parent) {
this._parent.raise(nestedE);
} else {
throw nestedE;
}
}
} else {
if (this._parent) {
this._parent.raise(err);
} else {
throw err;
}
}
};
/**
* Executes a callback in the context of this block. Any
* errors will be passed to this Block's raise() method.
* Returns the value of the callback or undefined on error.
*/
Block.prototype.trap = function (callback) {
var origCurrent = Block.current,
ret;
Block.current = this;
try {
ret = callback();
Block.current = origCurrent;
return ret;
} catch (e) {
Block.current = origCurrent;
this.raise(e);
return undefined;
}
};
/**
* Wraps a function and returns a function that routes
* errors to this block. This is similar to trap but
* returns a new function instead of invoking the callback
* immediately.
*/
Block.prototype.guard = function (func) {
if (func.__guarded__) {
return func;
}
var self = this,
wrapped = function () {
var origCurrent = Block.current,
ret;
Block.current = self;
try {
ret = func.apply(this, arguments);
Block.current = origCurrent;
return ret;
} catch (e) {
Block.current = origCurrent;
self.raise(e);
return undefined;
}
};
wrapped.__guarded__ = true;
return wrapped;
};
module.exports = Block;
'use strict';
var PENDING = 1,
RESOLVED = 2,
REJECTED = 3,
slice = Array.prototype.slice;
/**
* Call all the functions in a list.
*
* @private
* @param {function|function[]} list List of callbacks
* @param {Object} context Object for 'this' inside callbacks
* @param {Array} args Arguments array
*/
function call(list, context, args) {
var i, newArg;
if (typeof list === 'function') {
newArg = list.apply(context, args);
if (newArg !== 'undefined') {
args[0] = newArg;
}
} else if (Array.isArray(list)) {
for (i = 0; i < list.length; i += 1) {
call(list[i], context, args);
}
}
}
/**
* Deferred object constructor.
*
* @constructor
*/
function Deferred() {
this.state = PENDING;
this.context = null;
this.args = null;
this.events = {
done: [],
fail: [],
always: []
};
}
Deferred.PENDING = PENDING;
Deferred.RESOLVED = RESOLVED;
Deferred.REJECTED = REJECTED;
/**
* Register a callback that will be fired when the process is resolved.
*
* @param {function|function[]} callback
* @returns {Deferred}
*/
Deferred.prototype.done = function (callback) {
// Fire the event straight away if resolved
if (this.state === RESOLVED) {
call(callback, this.context, this.args);
} else if (this.state === PENDING) {
this.events.done.push(callback);
}
return this;
};
/**
* Register a callback that will be fired when the process fails.
*
* @param {function|function[]|Deferred} callback
* @returns {Deferred}
*/
Deferred.prototype.fail = function (callback) {
// Fire the event straight away if rejected
if (this.state === REJECTED) {
call(callback, this.context, this.args);
} else if (this.state === PENDING) {
this.events.fail.push(callback);
}
return this;
};
/**
* Register a callback that will be fired whatever the result.
*
* @param {function|function[]} callback
* @returns {Deferred}
*/
Deferred.prototype.always = function (callback) {
// Fire the event straight away if not pending
if (this.state !== PENDING) {
call(callback, this.context, this.args);
} else {
this.events.always.push(callback);
}
return this;
};
/**
* Resolve the deferred object and fire all the done and always callbacks, in that order.
* All arguments are passed to the callbacks.
*
* @returns {Deferred}
*/
Deferred.prototype.resolve = function () {
var done, always;
if (this.state === PENDING) {
this.state = RESOLVED;
this.args = arguments;
done = this.events.done;
always = this.events.always;
// Purge all callbacks
this.events = null;
call(done, this.context, this.args);
call(always, this.context, this.args);
}
return this;
};
/**
* Alias for Deferred#resolve, but with the first argument used as the callback context (this).
*
* @param {Object} context Context for the callbacks
* @returns {Deferred}
*/
Deferred.prototype.resolveWith = function (context) {
if (this.state === PENDING) {
this.context = context;
this.resolve.apply(this, slice.call(arguments, 1));
}
return this;
};
/**
* Register a callback that will be fired when the process is rejected. All
* arguments are passed to the callbacks.
*
* @returns {Deferred}
*/
Deferred.prototype.rejectWith = function () {
var fail, always;
if (this.state === PENDING) {
this.state = REJECTED;
this.args = arguments;
fail = this.events.fail;
always = this.events.always;
// Purge all callbacks
this.events = null;
call(fail, this.context, this.args);
call(always, this.context, this.args);
}
return this;
};
/**
* Alias for Deferred#reject, but with the first argument used as the callback context (this).
*
* @param {Object} context Context for the callbacks
* @returns {Deferred}
*/
Deferred.prototype.reject = function (context) {
if (this.state === PENDING) {
this.context = context;
this.reject.apply(this, slice.call(arguments, 1));
}
return this;
};
module.exports = Deferred;
'use strict';
var Block = require('./block');
/**
* A Future class as per the literature on the topic. The two main operations
* are force() and resolve().
*/
function Future(resolution) {
this._resolved = false;
this._resolution = null;
this._pending = null;
if (arguments.length > 0) {
this.resolve(resolution);
}
}
/**
* Cast an arbitrary value to a Future. If already a Future, just return it.
* Otherwise, return a new Future resolved with the value.
*/
Future.cast = function (futureOrLiteral) {
if (futureOrLiteral instanceof Future) {
return futureOrLiteral;
}
return new Future(futureOrLiteral);
};
Future.prototype = {};
/**
* Invokes the callback(result) upon resolution of the future. Return true if
* force executed immediately, false if pended.
*
* If the callback is pended, it is wrapped with Block.guard so that any
* exceptions it throws are routed to the Block in effect at the time of the
* call to force().
*/
Future.prototype.force = function (callback) {
var pended, ret = false;
if (this._resolved) {
if (callback) {
callback(this._resolution);
}
ret = true;
} else if (callback) {
pended = Block.guard(callback);
if (!this._pending) {
this._pending = [pended];
} else {
this._pending.push(pended);
}
}
return ret;
};
/**
* Resolves the future. Any pended force callbacks are executed immediately.
* Any future calls to force() will invoke their callbacks immediately.
*/
Future.prototype.resolve = function (resolution) {
if (this._resolved) {
throw new Error('Logic error. Future resolved multiple times.');
}
this._resolved = true;
this._resolution = resolution;
if (this._pending) {
this._pending.forEach(function (pended) {
pended(resolution);
});
}
};
/**
* Return a new Future whose resolution is dependent on the resolution of this
* future. When this future is resolved, the transformer callback will be
* invoked with its resolution and the callback result will become the
* resolution of the new Future returned by this method.
*/
Future.prototype.chain = function (transformer) {
var chained = new Future();
this.force(function (resolution) {
chained.resolve(transformer(resolution));
});
return chained;
};
/**
* Forward the resolution from this future to another future. This future is
* forced and the resolution is resolved on the passed future.
*/
Future.prototype.forward = function (otherFuture) {
this.force(function (resolution) {
otherFuture.resolve(resolution);
});
};
module.exports = Future;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment