Skip to content

Commit

Permalink
Merge branch 'feature/completable-promise' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
etki committed Aug 14, 2017
2 parents 3294d42 + f3fedaa commit 8148613
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 1 deletion.
121 changes: 121 additions & 0 deletions lib/concurrent/Future.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @callback Future~resolver
*
* @param {Function} fulfill
* @param {Function} reject
*/

/**
* This is a very standard promise interface implementation, but with ability
* to be externally completed or cancelled by `#resolve()` and `#reject()`
* methods.
*
* It is named after Java's CompletableFuture.
*
* @class
* @template T
*
* @param {Future~resolver} [resolver] Standard promise resolver
*/
function Future (resolver) {
var self = this
var fulfiller
var rejector
var container = new Promise(function (resolve, reject) {
if (resolver) {
resolver(resolve, reject)
}
fulfiller = resolve
rejector = reject
})

/**
* Rejects current instance with provided value
*
* @param {*} [value]
* @returns {Future.<T>} Current instance
*/
this.reject = function (value) {
rejector(value)
return self
}

/**
* Resolves (fulfills) current instance with provided value
*
* @param {*} [value]
* @returns {Future.<T>} Current instance
*/
this.resolve = function (value) {
fulfiller(value)
return self
}

this.fulfill = this.resolve

/**
* Standard then-implementation
*
* @param {Function} [fulfillmentHandler]
* @param {Function} [rejectionHandler]
* @returns {Future.<T>}
*/
this.then = function (fulfillmentHandler, rejectionHandler) {
return Future.wrap(container.then(fulfillmentHandler, rejectionHandler))
}
}

/**
* Returns promise that awaits all passed promises.
*
* @param {Array.<Promise.<*>|Thenable<*>>} promises
* @returns {Future.<*>}
*/
Future.all = function (promises) {
return Future.wrap(Promise.all(promises))
}

/**
* Returns result of first resolved promise, be it fulfillment or rejection
*
* @param {Array.<Promise.<*>|Thenable<*>>} promises
* @returns {Future.<*>}
*/
Future.race = function (promises) {
return Future.wrap(Promise.race(promises))
}

/**
* Returns resolved promise.
*
* @param {*} value
* @returns {Future.<*>}
*/
Future.resolve = function (value) {
return new Future().resolve(value)
}

/**
* Returns rejected promise.
*
* @param {*} value
* @returns {Future.<*>}
*/
Future.reject = function (value) {
return new Future().reject(value)
}

/**
* Wraps given promise in a Future, giving user code an option
* to reject/resolve it.
*
* @param {Promise.<*>|Thenable.<*>} promise
* @returns {Future.<*>}
*/
Future.wrap = function (promise) {
return new Future(promise.then.bind(promise))
}

module.exports = {
Future: Future
}
3 changes: 3 additions & 0 deletions lib/concurrent/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
Future: require('./Future').Future
}
3 changes: 2 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var exports = {
Http: require('./http'),
Logger: require('./logger')
Logger: require('./logger'),
Concurrent: require('./concurrent')
}
/** @deprecated */
exports.http = exports.Http
Expand Down
214 changes: 214 additions & 0 deletions test/suites/integration/concurrent/Future.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/* eslint-env mocha */
/* eslint-disable no-unused-expressions */

var Future = require('../../../../lib').Concurrent.Future
var Sinon = require('sinon')
var Chai = require('chai')
var expect = Chai.expect

Chai.use(require('chai-as-promised'))

var branchStopper = function () {
throw new Error('Unexpected branch execution')
}

describe('Integration', function () {
describe('/concurrent', function () {
describe('/Future.js', function () {
describe('Future', function () {
describe('< new', function () {
it('does not require arguments', function () {
expect(new Future()).to.be.instanceOf(Future)
})

it('accepts resolver function', function () {
var value = 12
var resolver = function (resolve) {
resolve(value)
}
return expect(new Future(resolver)).to.eventually.equal(value)
})
})

describe('#resolve', function () {
it('resolves externally', function () {
var future = new Future()
var value = 12
return expect(future.resolve(value)).to.eventually.equal(value)
})

it('doesn\'t resolve twice', function () {
var future = new Future()
var value = 12
future.resolve(value)
future.resolve([value])
return expect(future).to.eventually.equal(value)
})
})

describe('#reject', function () {
it('rejects externally', function () {
var future = new Future()
var error = new Error()
future.reject(error)
return expect(future).to.eventually.be.rejectedWith(error)
})

it('doesn\'t reject twice', function () {
var future = new Future()
var error = new Error()
future.reject(error)
future.reject(new Error())
return expect(future).to.eventually.be.rejectedWith(error)
})
})

describe('#then', function () {
it('composes new Future', function () {
var future = new Future()
var nextFuture = future.then()
expect(nextFuture).to.be.instanceOf(Future)
expect(nextFuture).not.to.equal(future)
})

it('invokes resolution handler when fulfilled', function () {
var future = new Future()
var handler = Sinon.stub()
var nextFuture = future.then(handler)
var value = {value: 12}
future.resolve(value)
return nextFuture.then(function () {
expect(handler.calledOnce).to.be.true
expect(handler.getCall(0).args[0]).to.equal(value)
})
})

it('invokes resolution handler even if set after fulfillment', function () {
var future = new Future()
var value = {value: 12}
future.resolve(value)
var handler = Sinon.stub()
var nextFuture = future.then(handler)
return nextFuture.then(function () {
expect(handler.calledOnce).to.be.true
expect(handler.getCall(0).args[0]).to.equal(value)
})
})

it('invokes rejection handler when rejected', function () {
var future = new Future()
var handler = Sinon.stub()
var nextFuture = future.then(null, handler)
var value = {value: 12}
future.reject(value)
return nextFuture.then(function () {
expect(handler.calledOnce).to.be.true
expect(handler.getCall(0).args[0]).to.equal(value)
})
})

it('invokes rejection handler even if set after rejection', function () {
var future = new Future()
var value = {value: 12}
future.reject(value)
var handler = Sinon.stub()
var nextFuture = future.then(null, handler)
return nextFuture.then(function () {
expect(handler.calledOnce).to.be.true
expect(handler.getCall(0).args[0]).to.equal(value)
})
})
})

describe('.resolve', function () {
it('returns resolved promise', function () {
var value = {x: 12}
return expect(Future.resolve(value)).to.eventually.equal(value)
})
})

describe('.reject', function () {
it('returns rejected promise', function () {
var value = {x: 12}
var future = Future.reject(value)
return expect(future).to.eventually.be.rejectedWith(value)
})
})

describe('.wrap', function () {
it('wraps Promise with a Future', function () {
var promise = new Promise(function () {})
return expect(Future.wrap(promise)).to.be.instanceOf(Future)
})

it('catches resolved Promise value', function () {
var value = {x: 12}
var promise = new Promise(function (resolve) {
resolve(value)
})
var future = Future.wrap(promise)
promise
.then(function () {
future.resolve([value])
})
return expect(future).to.eventually.equal(value)
})

it('catches rejected Promise value', function () {
var value = {x: 12}
var promise = new Promise(function (resolve, reject) {
reject(value)
})
var future = Future.wrap(promise)
promise
.then(branchStopper, function () {
future.reject([value])
})
return expect(future).to.eventually.be.rejectedWith(value)
})
})

describe('.all', function () {
it('returns resolved promise if nothing is passed', function () {
return expect(Future.all([]))
})

it('awaits all promises and returns their results as array', function () {
var promises = [new Future(), new Future(), new Future()]
var target = Future.all(promises)
var expectation = []
for (var i = 0; i < promises.length; i++) {
promises[i].resolve(i)
expectation.push(i)
}
return expect(target).to.eventually.deep.eq(expectation)
})
})

describe('.race', function () {
it('doesn\'t resolve if nothing passed', function () {
var value = {x: 12}
var result = new Future()
Future.race([]).then(function () {
result.reject()
})
setTimeout(function () {
result.resolve(value)
}, 1)
return expect(result).to.eventually.equal(value)
})

it('resolves with first value of the first resolved promise', function () {
var value = {x: 12}
var first = new Future()
var second = new Future()
var result = Future.race([first, second])
first.resolve(value)
second.reject()
return expect(result).to.eventually.equal(value)
})
})
})
})
})
})

0 comments on commit 8148613

Please sign in to comment.