pendings
Version:
Better control of promises
326 lines (286 loc) • 8.31 kB
JavaScript
/**
* Controls single pending promise.
*/
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var TimeoutError = require('./timeout-error');
var _require = require('./utils'),
mergeOptions = _require.mergeOptions;
var AUTO_RESET = {
NEVER: 'never',
FULFILLED: 'fulfilled',
RESOLVED: 'resolved',
REJECTED: 'rejected'
};
var DEFAULT_OPTIONS = {
timeout: 0,
autoReset: AUTO_RESET.NEVER
};
var Pending = function () {
/**
* Creates instance of single pending promise. It holds `resolve / reject` callbacks for future fulfillment.
* @param {Object} [options]
* @param {Number} [options.timeout=0]
* @param {String} [options.autoReset='never'] automatically reset pending to initial state.
* Possible values are: `never`, `fufilled`, `resolved`, `rejected`.
*/
function Pending(options) {
_classCallCheck(this, Pending);
this._options = mergeOptions(DEFAULT_OPTIONS, options);
this._resolve = null;
this._reject = null;
// it seems that isPending can be calculated as `Boolean(this._promise) && !this.isFulfilled`,
// but it is not true: if call(fn) throws error in the same tick, `this._promise` is yet undefined.
this._isPending = false;
this._isResolved = false;
this._isRejected = false;
this._value = undefined;
this._promise = null;
this._timer = null;
this._onFulfilled = function () {};
}
/**
* Returns promise itself.
*
* @returns {Promise}
*/
_createClass(Pending, [{
key: 'call',
/**
* For the first time this method calls `fn` and returns new promise. Also holds `resolve` / `reject` callbacks
* to allow fulfill promise via `pending.resolve()` and `pending.reject()`. All subsequent calls of `.call(fn)`
* will return the same promise, which can be still pending or already fulfilled.
* To reset this behavior use `.reset()`. If `options.timeout` is specified, the promise will be automatically
* rejected after `timeout` milliseconds with `TimeoutError`.
*
* @param {Function} fn
* @param {Object} [options]
* @param {Number} [options.timeout=0] timeout after which promise will be automatically rejected
* @returns {Promise}
*/
value: function call(fn, options) {
if (this._isPending || this.isFulfilled) {
return this._promise;
} else {
this.reset();
this._callOptions = mergeOptions(this._options, options);
this._initPromise(fn);
this._initTimer();
return this._promise;
}
}
/**
* Resolves pending promise with specified `value`.
*
* @param {*} [value]
*/
}, {
key: 'resolve',
value: function resolve(value) {
if (this._isPending) {
this._preFulfill();
this._isResolved = true;
this._value = value;
this._resolve(value);
this._postFulfill();
}
}
/**
* Rejects pending promise with specified `value`.
*
* @param {*} [value]
*/
}, {
key: 'reject',
value: function reject(value) {
if (this._isPending) {
this._preFulfill();
this._isRejected = true;
this._value = value;
this._reject(value);
this._postFulfill();
}
}
/**
* Helper method: rejects if `rejectValue` is truthy, otherwise resolves with `resolveValue`.
*
* @param {*} [resolveValue]
* @param {*} [rejectValue]
*/
}, {
key: 'fulfill',
value: function fulfill(resolveValue, rejectValue) {
if (rejectValue) {
this.reject(rejectValue);
} else {
this.resolve(resolveValue);
}
}
/**
* Resets to initial state.
*
* @param {Error} [error] custom rejection error if promise is in pending state.
*/
}, {
key: 'reset',
value: function reset(error) {
if (this._isPending) {
error = error || new Error('Promise rejected by reset');
this.reject(error);
}
this._promise = null;
this._isPending = false;
this._isResolved = false;
this._isRejected = false;
this._value = undefined;
this._clearTimer();
}
}, {
key: '_initPromise',
value: function _initPromise(fn) {
var _this = this;
this._promise = new Promise(function (resolve, reject) {
_this._isPending = true;
_this._resolve = resolve;
_this._reject = reject;
if (typeof fn === 'function') {
_this._callFn(fn);
}
});
}
}, {
key: '_initTimer',
value: function _initTimer() {
var _this2 = this;
var timeout = this._callOptions.timeout;
if (timeout) {
this._timer = setTimeout(function () {
return _this2.reject(new TimeoutError(timeout));
}, timeout);
}
}
}, {
key: '_clearTimer',
value: function _clearTimer() {
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
}
}, {
key: '_callFn',
value: function _callFn(fn) {
try {
var res = fn();
this._attachToFnPromise(res);
} catch (e) {
this.reject(e);
}
}
}, {
key: '_attachToFnPromise',
value: function _attachToFnPromise(p) {
var _this3 = this;
if (p && typeof p.then === 'function') {
p.then(function (value) {
return _this3.resolve(value);
}, function (e) {
return _this3.reject(e);
});
}
}
}, {
key: '_preFulfill',
value: function _preFulfill() {
this._isPending = false;
this._clearTimer();
}
}, {
key: '_postFulfill',
value: function _postFulfill() {
this._onFulfilled(this);
this._applyAutoReset();
}
}, {
key: '_applyAutoReset',
value: function _applyAutoReset() {
var autoReset = this._options.autoReset;
var needReset = [autoReset === AUTO_RESET.FULFILLED, this._isResolved && autoReset === AUTO_RESET.RESOLVED, this._isRejected && autoReset === AUTO_RESET.REJECTED].some(Boolean);
if (needReset) {
this.reset();
}
}
}, {
key: 'promise',
get: function get() {
return this._promise;
}
/**
* Returns value with that promise was fulfilled (resolved or rejected).
*
* @returns {*}
*/
}, {
key: 'value',
get: function get() {
return this._value;
}
/**
* Returns true if promise resolved.
*
* @returns {Boolean}
*/
}, {
key: 'isResolved',
get: function get() {
return this._isResolved;
}
/**
* Returns true if promise rejected.
*
* @returns {Boolean}
*/
}, {
key: 'isRejected',
get: function get() {
return this._isRejected;
}
/**
* Returns true if promise fulfilled (resolved or rejected).
*
* @returns {Boolean}
*/
}, {
key: 'isFulfilled',
get: function get() {
return this._isResolved || this._isRejected;
}
/**
* Returns true if promise is pending.
*
* @returns {Boolean}
*/
}, {
key: 'isPending',
get: function get() {
return this._isPending;
}
/**
* Callback called when promise is fulfilled (resolved or rejected).
*
* @param {Function} fn
*/
}, {
key: 'onFulfilled',
set: function set(fn) {
if (typeof fn === 'function') {
this._onFulfilled = fn;
} else {
throw new Error('onFulfilled should be a function.');
}
}
}]);
return Pending;
}();
module.exports = Pending;