Last active
August 29, 2015 14:16
-
-
Save sompylasar/3515c93c1e579c5cc352 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
'use strict'; | |
// The following line enables extended tracking of unhandled promise rejections. | |
// @see https://github.com/cujojs/when/blob/master/docs/api.md#whenmonitorconsole | |
require('when/monitor/console'); | |
var when = require('when'); | |
/** | |
* Converts an object to a thenable that can be resolved to itself. | |
* Adds the `then` function, `promise` and `resolver` properties. | |
* | |
* @param {Object} _this The object to convert (conversion goes in-place). | |
* @return {Object} The converted object (the passed object). | |
*/ | |
function makeThenable(_this) { | |
if (typeof _this !== 'object') { | |
throw new Error('Argument "_this" must be an object.'); | |
} | |
if (when.isPromiseLike(_this)) { | |
throw new Error('Argument "_this" must not be promise-like.'); | |
} | |
// Create an internal deferred object that represents the promised `_this`. | |
// @see https://github.com/cujojs/when/wiki/Deferred | |
var deferred = when.defer(); | |
// The promise API is frozen and does not expose the internal deferred object. | |
// @see https://github.com/cujojs/when/wiki/Deferred#consumers | |
_this.promise = deferred.promise; | |
// Proxy the `then` method to make this object promise-like (thenable). | |
_this.then = function () { | |
return _this.promise.then.apply(_this.promise, arguments); | |
}; | |
// The resolver allows to resolve or reject the promise externally without exposing the internal deferred object. | |
// @see https://github.com/cujojs/when/wiki/Deferred#producers | |
_this.resolver = Object.create(deferred.resolver); | |
// HACK: We must delete the `then` method before resolving to avoid infinite promise pending if resolved with the same object (`_this`). | |
_this.resolver.resolve = function () { | |
_this.then = undefined; | |
return deferred.resolver.resolve.apply(this, arguments); | |
}; | |
Object.defineProperties(_this, { | |
promise: { | |
configurable: false, | |
enumerable: false, | |
writable: false | |
}, | |
then: { | |
configurable: false, | |
enumerable: false, | |
writable: true //< Required for the above HACK with resolve-to-itself. | |
}, | |
resolver: { | |
configurable: false, | |
enumerable: false, | |
writable: false | |
} | |
}); | |
// Return the converted object for convenience. | |
return _this; | |
} | |
/** | |
* Converts a property to a promise that resolves when the property owner resolves. | |
* If the property value has not been modified before the owner resolved, | |
* the property promise resolves to the original property value. | |
* Works best with `makeThenable`. | |
* | |
* @param {Object} _this The owner of the property. | |
* @param {string} property The name of the property to convert. | |
* @param {Thenable} The promise that replaced the original property. | |
*/ | |
function makeThenableProperty(_this, property) { | |
if (typeof _this !== 'object') { | |
throw new Error('Argument "_this" must be an object.'); | |
} | |
if (typeof property !== 'string') { | |
throw new Error('Argument "property" must be a string.'); | |
} | |
// Use the owner promise from `makeThenable`. | |
var ownerPromise = _this.promise; | |
// Remember the default value to resolve to it if the property value wasn't replaced. | |
var defaultValue = _this[property]; | |
// Chain on the owner promise or the owner itself. | |
var propertyPromise = _this[property] = when(ownerPromise || _this).then(function () { | |
if (_this[property] === propertyPromise) { | |
_this[property] = defaultValue; | |
} | |
return _this[property]; | |
}, function (err) { | |
// WARNING: Return the owner's promise to avoid unhandled rejection warnings. | |
// If no owner promise existed, return a new rejected promise. | |
return ownerPromise || when.reject(err); | |
}); | |
// Return the created promise for convenience. | |
return _this[property]; | |
} | |
module.exports = { | |
makeThenable: makeThenable, | |
makeThenableProperty: makeThenableProperty | |
}; |
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
var assert = require('assert'); | |
var when = require('when'); | |
describe('promise-entity', function () { | |
var promiseEntity = require('../lib/promise-entity'); | |
it('should export `makeThenable` function', function () { | |
assert.equal('function', typeof promiseEntity.makeThenable); | |
}); | |
it('should export `makeThenableProperty` function', function () { | |
assert.equal('function', typeof promiseEntity.makeThenableProperty); | |
}); | |
describe('`makeThenable` function', function () { | |
it('should throw on invalid arguments', function () { | |
assert.throws(function () { | |
promiseEntity.makeThenable(); | |
}, 'Error: Argument "_this" must be an object.'); | |
assert.throws(function () { | |
promiseEntity.makeThenable(undefined); | |
}, 'Error: Argument "_this" must be an object.'); | |
assert.throws(function () { | |
promiseEntity.makeThenable("string"); | |
}, 'Error: Argument "_this" must be an object.'); | |
assert.throws(function () { | |
promiseEntity.makeThenable(when.defer().promise); | |
}, 'Error: Argument "_this" must not be promise-like.'); | |
assert.doesNotThrow(function () { | |
promiseEntity.makeThenable({}); | |
}); | |
}); | |
it('should return the passed owner', function () { | |
var owner = {}; | |
var returnedValue = promiseEntity.makeThenable(owner); | |
assert.strictEqual(owner, returnedValue); | |
}); | |
it('should convert an object to a promise-like with a `resolver` and a `promise`', function (done) { | |
var owner = {}; | |
var resolveValue = {}; | |
promiseEntity.makeThenable(owner); | |
assert.equal(true, when.isPromiseLike(owner), 'isPromiseLike'); | |
assert.equal('object', typeof owner.resolver, 'contains `resolver` object'); | |
assert.equal('function', typeof owner.resolver.resolve, '`resolver` has `resolve` function'); | |
assert.equal('function', typeof owner.resolver.reject, '`resolver` has `reject` function'); | |
assert.equal('object', typeof owner.promise, 'contains `promise` object'); | |
assert.equal(true, when.isPromiseLike(owner.promise), '`promise` isPromiseLike'); | |
when(owner).then(function (value) { | |
assert.strictEqual(resolveValue, value); | |
done(); | |
}).done(undefined, done); | |
owner.resolver.resolve(resolveValue); | |
}); | |
it('should allow resolving to itself', function (done) { | |
var owner = {}; | |
promiseEntity.makeThenable(owner); | |
when(owner).then(function (value) { | |
assert.strictEqual(owner, value); | |
done(); | |
}).done(undefined, done); | |
owner.resolver.resolve(owner); | |
}); | |
it('should reject as expected', function (done) { | |
var owner = {}; | |
var rejectValue = new Error('REJECTION'); | |
promiseEntity.makeThenable(owner); | |
when(owner).then(function (value) { | |
done(new Error('Resolved instead of rejected.')); | |
}, function (err) { | |
assert.strictEqual(rejectValue, err); | |
done(); | |
}).done(undefined, done); | |
owner.resolver.reject(rejectValue); | |
}); | |
}); | |
describe('`makeThenableProperty` function', function () { | |
it('should throw on invalid arguments', function () { | |
assert.throws(function () { | |
promiseEntity.makeThenableProperty(); | |
}, 'Error: Argument "_this" must be an object.'); | |
assert.throws(function () { | |
promiseEntity.makeThenableProperty(undefined); | |
}, 'Error: Argument "_this" must be an object.'); | |
assert.throws(function () { | |
promiseEntity.makeThenableProperty("string", "property"); | |
}, 'Error: Argument "_this" must be an object.'); | |
assert.throws(function () { | |
promiseEntity.makeThenableProperty({}); | |
}, 'Error: Argument "property" must be a string.'); | |
assert.throws(function () { | |
promiseEntity.makeThenableProperty({}, 123); | |
}, 'Error: Argument "property" must be a string.'); | |
assert.doesNotThrow(function () { | |
promiseEntity.makeThenableProperty({}, "property"); | |
}); | |
assert.doesNotThrow(function () { | |
promiseEntity.makeThenableProperty(when.defer().promise, "property"); | |
}); | |
}); | |
it('should return the property promise', function () { | |
var owner = { | |
property: "test" | |
}; | |
promiseEntity.makeThenable(owner); | |
var returnedValue = promiseEntity.makeThenableProperty(owner, 'property'); | |
assert.strictEqual(owner.property, returnedValue); | |
}); | |
it('should convert a property to a promise', function (done) { | |
var owner = { | |
property: "test" | |
}; | |
var resolveValue = {}; | |
promiseEntity.makeThenable(owner); | |
promiseEntity.makeThenableProperty(owner, 'property'); | |
assert.equal(true, when.isPromiseLike(owner.property), 'isPromiseLike'); | |
when(owner.property).then(function (value) { | |
assert.strictEqual("test", value); | |
done(); | |
}).done(undefined, done); | |
owner.resolver.resolve(resolveValue); | |
}); | |
it('should reject a property promise to the owner\'s rejection', function (done) { | |
var owner = { | |
property: "test" | |
}; | |
var rejectValue = new Error('REJECTION'); | |
promiseEntity.makeThenable(owner); | |
promiseEntity.makeThenableProperty(owner, 'property'); | |
when(owner.property).then(function (value) { | |
done(new Error('Resolved instead of rejected.')); | |
}, function (err) { | |
assert.strictEqual(rejectValue, err); | |
done(); | |
}).done(undefined, done); | |
owner.resolver.reject(rejectValue); | |
}); | |
it('should convert a property to a promise without `makeThenable`', function (done) { | |
var owner = { | |
property: "test" | |
}; | |
var resolveValue = {}; | |
promiseEntity.makeThenableProperty(owner, 'property'); | |
assert.equal(true, when.isPromiseLike(owner.property), 'isPromiseLike'); | |
when(owner.property).then(function (value) { | |
assert.strictEqual("test", value); | |
done(); | |
}).done(undefined, done); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment