Skip to content

Instantly share code, notes, and snippets.

@bleroy
Created December 8, 2014 01:40
Show Gist options
  • Save bleroy/768ce4147e3a9a23aaed to your computer and use it in GitHub Desktop.
Save bleroy/768ce4147e3a9a23aaed to your computer and use it in GitHub Desktop.
// Flasync Fluent Asynchronous API Helper (c) 2014 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';
/**
* @description
* This mix-in adds utility methods and infrastructure to an object
* to help build a fluent and asynchronous API.
* @param {object} thing The object to flasync.
*/
function flasync(thing) {
thing._todo = [];
thing._isAsync = false;
/**
* @description
* Adds an asynchronous task to be executed.
* @param {Function} callback the task to add.
* It must take a "done" function parameter and call it when it's done.
* @returns {Object} The fluent object.
*/
thing.then = function then(callback) {
thing._todo.push(callback);
// Ask the first task to execute if it doesn't already exist.
if (!thing._isAsync) {
thing._nextTask();
}
return thing;
};
/**
* @description
* Triggers the next task to execute.
*/
thing._nextTask = function nextTask() {
if (thing._todo.length === 0) {
thing._isAsync = false;
return;
}
var nextTask = thing._todo.shift();
try {
thing._isAsync = true;
nextTask(function (err) {
thing._nextTask(err);
});
}
catch(err) {
// If there's an error, we call onError and stop the chain.
thing._todo = [];
if (thing._onError) {thing._onError(err);}
else {throw err;}
}
};
/**
* @description
* Surround non-asynchronous method declarations with a call to this method,
* in order to instrument them to behave asynchronously as necessary.
* @param {Function} method The method to async-ify.
* @returns {Function} The async-ified method, that can be called exactly
* like the normal method, but will act nicely with the rest of the
* asynchronous API.
*/
thing.asyncify = function asyncify(method) {
return function asyncified() {
if (!thing._isAsync) {
// If not async yet, just do it.
return method.apply(thing, arguments);
}
// Otherwise, bind it and enqueue it
var args = Array.prototype.slice.call(arguments);
args.unshift(thing);
var bound = Function.prototype.bind.apply(method, args);
thing.then(function(done) {
bound.apply(thing);
done();
});
return thing;
};
};
/**
* @description
* Surround asynchronous method declarations with a call to this method,
* in order to instrument them to behave well with the rest of the API.
* @param {Function} method The asynchronous method to instrument.
* It must take a 'done' callback parameter as its last parameter.
* The user will never pass that parameter, instead it will be generated.
* @returns {Function} The public method that users of the API will call.
*/
thing.async = function async(method) {
return function asyncMethod() {
var args = Array.prototype.slice.call(arguments);
args.unshift(thing);
var bound = Function.prototype.bind.apply(method, args);
return thing.then(bound);
};
};
/**
* @description
* Sets-up an error handler for API calls.
* @param {Function} errorHandler The function that will handle errors.
* The function must take the error object as its parameter.
*/
thing.onError = function(errorHandler) {
thing._onError = errorHandler;
return thing;
};
return thing;
}
module.exports = flasync;
// DecentCMS (c) 2014 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';
var expect = require('chai').expect;
var flasync = require('../lib/flasync');
describe('Flasync Fluent Async API helper', function() {
// Build an API using the library
var Api = function Api(output) {
var self = this;
this.output = output || [];
flasync(this);
// It has one synchronous method
this.writeSync = this.asyncify(function(text) {
this.output.push(text);
return this;
});
// And one asynchronous method
this.write = this.async(function(text, next) {
process.nextTick(function () {
self.output.push(text);
next();
});
return this;
});
};
it('behaves as a synchronous API as long as only synchronous methods are called', function() {
var api = new Api();
api
.writeSync('foo')
.writeSync('bar')
.writeSync('baz');
expect(api.output).to.deep.equal(['foo', 'bar', 'baz']);
});
it('preserves call order even for synchronous methods called after an asynchronous one', function(done) {
var api = new Api();
api
.writeSync('foo')
.write('bar')
.writeSync('baz')
.then(function() {
expect(api.output).to.deep.equal(['foo', 'bar', 'baz']);
done();
});
});
it('executes asynchronous continuations in order', function(done) {
var api = new Api();
api
.write('foo')
.then(function(next) {
api.output.push('bar');
next();
})
.then(function(next) {
api.output.push('baz');
next();
})
.then(function() {
expect(api.output).to.deep.equal(['foo', 'bar', 'baz']);
done();
});
});
it('can nest async calls', function(done) {
var api = new Api();
api
.write('foo')
.then(function(next) {
api
.writeSync('bar')
.write('baz')
.then(function() {
expect(api.output).to.deep.equal(['foo', 'bar', 'baz']);
done();
});
next();
});
});
it('can handle exceptions and suspend execution', function(done) {
var api = new Api();
var hit = false;
api
.onError(function(err) {
expect(hit).to.be.false;
done();
})
.then(function(next) {
throw new Error('oops');
})
.then(function(next) {
hit = true;
});
});
it('lets exceptions through if no error handler is defined', function() {
var api = new Api();
expect(function() {
api
.then(function(next) {
throw new Error('oops');
})
})
.to.throw('oops');
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment