/**
Represents an asynchronous operation. Provides a
standard API for subscribing to the moment that the operation completes either
successfully (`fulfill()`) or unsuccessfully (`reject()`).
@class Promise.Resolver
@constructor
@param {Promise} promise The promise instance this resolver will be handling
**/
function Resolver(promise) {
/**
List of success callbacks
@property _callbacks
@type Array
@private
**/
this._callbacks = [];
/**
List of failure callbacks
@property _errbacks
@type Array
@private
**/
this._errbacks = [];
/**
The promise for this Resolver.
@property promise
@type Promise
**/
this.promise = promise;
/**
The status of the operation. This property may take only one of the following
values: 'pending', 'fulfilled' or 'rejected'.
@property _status
@type String
@default 'pending'
@private
**/
this._status = 'pending';
}
Y.mix(Resolver.prototype, {
/**
Resolves the promise, signaling successful completion of the
represented operation. All "onFulfilled" subscriptions are executed and passed
the value provided to this method. After calling `fulfill()`, `reject()` and
`notify()` are disabled.
@method fulfill
@param {Any} value Value to pass along to the "onFulfilled" subscribers
**/
fulfill: function (value) {
if (this._status === 'pending') {
this._result = value;
}
if (this._status !== 'rejected') {
this._notify(this._callbacks, this._result);
// Reset the callback list so that future calls to fulfill()
// won't call the same callbacks again. Promises keep a list
// of callbacks, they're not the same as events. In practice,
// calls to fulfill() after the first one should not be made by
// the user but by then()
this._callbacks = [];
// Once a promise gets fulfilled it can't be rejected, so
// there is no point in keeping the list. Remove it to help
// garbage collection
this._errbacks = null;
this._status = 'fulfilled';
}
},
/**
Resolves the promise, signaling *un*successful completion of the
represented operation. All "onRejected" subscriptions are executed with
the value provided to this method. After calling `reject()`, `resolve()`
and `notify()` are disabled.
@method reject
@param {Any} value Value to pass along to the "reject" subscribers
**/
reject: function (reason) {
if (this._status === 'pending') {
this._result = reason;
}
if (this._status !== 'fulfilled') {
this._notify(this._errbacks, this._result);
// See fulfill()
this._callbacks = null;
this._errbacks = [];
this._status = 'rejected';
}
},
/**
Schedule execution of a callback to either or both of "resolve" and
"reject" resolutions for the Resolver. The callbacks
are wrapped in a new Resolver and that Resolver's corresponding promise
is returned. This allows operation chaining ala
`functionA().then(functionB).then(functionC)` where `functionA` returns
a promise, and `functionB` and `functionC` _may_ return promises.
@method then
@param {Function} [callback] function to execute if the Resolver
resolves successfully
@param {Function} [errback] function to execute if the Resolver
resolves unsuccessfully
@return {Promise} The promise of a new Resolver wrapping the resolution
of either "resolve" or "reject" callback
**/
then: function (callback, errback) {
// When the current promise is fulfilled or rejected, either the
// callback or errback will be executed via the function pushed onto
// this._callbacks or this._errbacks. However, to allow then()
// chaining, the execution of either function needs to be represented
// by a Resolver (the same Resolver can represent both flow paths), and
// its promise returned.
var promise = this.promise,
thenFulfill, thenReject,
// using promise constructor allows for customized promises to be
// returned instead of plain ones
then = new promise.constructor(function (fulfill, reject) {
thenFulfill = fulfill;
thenReject = reject;
}),
callbackList = this._callbacks || [],
errbackList = this._errbacks || [];
// Because the callback and errback are represented by a Resolver, it
// must be fulfilled or rejected to propagate through the then() chain.
// The same logic applies to resolve() and reject() for fulfillment.
callbackList.push(typeof callback === 'function' ?
this._wrap(thenFulfill, thenReject, callback) : thenFulfill);
errbackList.push(typeof errback === 'function' ?
this._wrap(thenFulfill, thenReject, errback) : thenReject);
// If a promise is already fulfilled or rejected, notify the newly added
// callbacks by calling fulfill() or reject()
if (this._status === 'fulfilled') {
this.fulfill(this._result);
} else if (this._status === 'rejected') {
this.reject(this._result);
}
return then;
},
/**
Wraps the callback in Y.soon to guarantee its asynchronous execution. It
also catches exceptions to turn them into rejections and links promises
returned from the `then` callback.
@method _wrap
@param {Function} thenFulfill Fulfillment function of the resolver that
handles this promise
@param {Function} thenReject Rejection function of the resolver that
handles this promise
@param {Function} fn Callback to wrap
@return {Function}
@private
**/
_wrap: function (thenFulfill, thenReject, fn) {
// callbacks and errbacks only get one argument
return function (valueOrReason) {
var result;
// Promises model exception handling through callbacks
// making both synchronous and asynchronous errors behave
// the same way
try {
// Use the argument coming in to the callback/errback from the
// resolution of the parent promise.
// The function must be called as a normal function, with no
// special value for |this|, as per Promises A+
result = fn(valueOrReason);
} catch (e) {
// calling return only to stop here
return thenReject(e);
}
if (Promise.isPromise(result)) {
// Returning a promise from a callback makes the current
// promise sync up with the returned promise
result.then(thenFulfill, thenReject);
} else {
// Non-promise return values always trigger resolve()
// because callback is affirmative, and errback is
// recovery. To continue on the rejection path, errbacks
// must return rejected promises or throw.
thenFulfill(result);
}
};
},
/**
Returns the current status of the Resolver as a string "pending",
"fulfilled", or "rejected".
@method getStatus
@return {String}
**/
getStatus: function () {
return this._status;
},
/**
Executes an array of callbacks from a specified context, passing a set of
arguments.
@method _notify
@param {Function[]} subs The array of subscriber callbacks
@param {Any} result Value to pass the callbacks
@protected
**/
_notify: function (subs, result) {
// Since callback lists are reset synchronously, the subs list never
// changes after _notify() receives it. Avoid calling Y.soon() for
// an empty list
if (subs.length) {
// Calling all callbacks after Y.soon to guarantee
// asynchronicity. Because setTimeout can cause unnecessary
// delays that *can* become noticeable in some situations
// (especially in Node.js)
Y.soon(function () {
var i, len;
for (i = 0, len = subs.length; i < len; ++i) {
subs[i](result);
}
});
}
}
}, true);
Y.Promise.Resolver = Resolver;