Created
December 8, 2014 01:40
-
-
Save bleroy/768ce4147e3a9a23aaed to your computer and use it in GitHub Desktop.
This file contains 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
// 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; |
This file contains 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
// 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