Skip to content

Instantly share code, notes, and snippets.

@sompylasar
Last active August 29, 2015 14:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sompylasar/3515c93c1e579c5cc352 to your computer and use it in GitHub Desktop.
Save sompylasar/3515c93c1e579c5cc352 to your computer and use it in GitHub Desktop.
'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
};
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