Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 1339c11

Browse files
lgalfasojeffbcross
authored andcommittedAug 21, 2014
refactor($q): make $q Promises A+ v1.1 compilant
The Promises A+ 1.1 spec introduces new constraints that would cause $q to fail, particularly specs 2.3.1 and 2.3.3. Newly satisfied requirements: * "then" functions that return the same fulfilled/rejected promise will fail with a TypeError * Support for edge cases where "then" is a value other than function Full 1.1 spec: https://github.com/promises-aplus/promises-spec/tree/1.1.0 This commit also modifies the adapter to use "resolve" method instead of "fulfill"
1 parent a603e20 commit 1339c11

File tree

5 files changed

+125
-183
lines changed

5 files changed

+125
-183
lines changed
 

‎lib/promises-aplus/promises-aplus-test-adapter.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33

44
var isFunction = function isFunction(value){return typeof value == 'function';};
55
var isPromiseLike = function isPromiseLike(obj) {return obj && isFunction(obj.then);};
6+
var isObject = function isObject(value){return value != null && typeof value === 'object';};
67

78
var $q = qFactory(process.nextTick, function noopExceptionHandler() {});
89

9-
exports.fulfilled = $q.resolve;
10+
exports.resolved = $q.resolve;
1011
exports.rejected = $q.reject;
11-
exports.pending = function () {
12+
exports.deferred = function () {
1213
var deferred = $q.defer();
1314

1415
return {
1516
promise: deferred.promise,
16-
fulfill: deferred.resolve,
17+
resolve: deferred.resolve,
1718
reject: deferred.reject
1819
};
1920
};

‎npm-shrinkwrap.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"protractor": "1.0.0",
3737
"yaml-js": "~0.0.8",
3838
"rewire": "1.1.3",
39-
"promises-aplus-tests": "~1.3.2",
39+
"promises-aplus-tests": "~2.0.4",
4040
"semver": "~2.1.0",
4141
"lodash": "~2.1.0",
4242
"browserstacktunnel-wrapper": "~1.1.1",

‎src/ng/q.js

+116-167
Original file line numberDiff line numberDiff line change
@@ -229,18 +229,29 @@ function $$QProvider() {
229229
/**
230230
* Constructs a promise manager.
231231
*
232-
* @param {function(Function)} nextTick Function for executing functions in the next turn.
232+
* @param {function(function)} nextTick Function for executing functions in the next turn.
233233
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
234234
* debugging purposes.
235235
* @returns {object} Promise manager.
236236
*/
237237
function qFactory(nextTick, exceptionHandler) {
238+
function callOnce(self, resolveFn, rejectFn) {
239+
var called = false;
240+
function wrap(fn) {
241+
return function(value) {
242+
if (called) return;
243+
called = true;
244+
fn.call(self, value);
245+
};
246+
}
247+
248+
return [wrap(resolveFn), wrap(rejectFn)];
249+
}
238250

239251
/**
240-
* @ngdoc method
241-
* @name $q#defer
242-
* @kind function
243-
*
252+
* @ngdoc
253+
* @name ng.$q#defer
254+
* @methodOf ng.$q
244255
* @description
245256
* Creates a `Deferred` object which represents a task which will finish in the future.
246257
*
@@ -250,58 +261,31 @@ function qFactory(nextTick, exceptionHandler) {
250261
return new Deferred();
251262
};
252263

253-
function Promise () {
254-
this.$$pending = [];
264+
function Promise() {
265+
this.$$state = { status: 0 };
255266
}
256267

257268
Promise.prototype = {
258-
then: function(callback, errback, progressback) {
269+
then: function(onFulfilled, onRejected, progressBack) {
259270
var result = new Deferred();
260271

261-
var wrappedCallback = function(value) {
262-
try {
263-
result.resolve((isFunction(callback) ? callback : defaultCallback)(value));
264-
} catch(e) {
265-
result.reject(e);
266-
exceptionHandler(e);
267-
}
268-
};
269-
270-
var wrappedErrback = function(reason) {
271-
try {
272-
result.resolve((isFunction(errback) ? errback : defaultErrback)(reason));
273-
} catch(e) {
274-
result.reject(e);
275-
exceptionHandler(e);
276-
}
277-
};
278-
279-
var wrappedProgressback = function(progress) {
280-
try {
281-
result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress));
282-
} catch(e) {
283-
exceptionHandler(e);
284-
}
285-
};
286-
287-
if (this.$$pending) {
288-
this.$$pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]);
289-
} else {
290-
this.$$value.then(wrappedCallback, wrappedErrback, wrappedProgressback);
291-
}
272+
this.$$state.pending = this.$$state.pending || [];
273+
this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
274+
if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
292275

293276
return result.promise;
294277
},
295278

296279
"catch": function(callback) {
297280
return this.then(null, callback);
298281
},
299-
"finally": function(callback) {
282+
283+
"finally": function(callback, progressBack) {
300284
return this.then(function(value) {
301285
return handleCallback(value, true, callback);
302286
}, function(error) {
303287
return handleCallback(error, false, callback);
304-
});
288+
}, progressBack);
305289
}
306290
};
307291

@@ -312,7 +296,37 @@ function qFactory(nextTick, exceptionHandler) {
312296
};
313297
}
314298

315-
function Deferred () {
299+
function processQueue(state) {
300+
var fn, promise, pending;
301+
302+
pending = state.pending;
303+
state.processScheduled = false;
304+
state.pending = undefined;
305+
for (var i = 0, ii = pending.length; i < ii; ++i) {
306+
promise = pending[i][0];
307+
fn = pending[i][state.status];
308+
try {
309+
if (isFunction(fn)) {
310+
promise.resolve(fn(state.value));
311+
} else if (state.status === 1) {
312+
promise.resolve(state.value);
313+
} else {
314+
promise.reject(state.value);
315+
}
316+
} catch(e) {
317+
promise.reject(e);
318+
exceptionHandler(e);
319+
}
320+
}
321+
}
322+
323+
function scheduleProcessQueue(state) {
324+
if (state.processScheduled || !state.pending) return;
325+
state.processScheduled = true;
326+
nextTick(function() { processQueue(state); });
327+
}
328+
329+
function Deferred() {
316330
this.promise = new Promise();
317331
//Necessary to support unbound execution :/
318332
this.resolve = simpleBind(this, this.resolve);
@@ -322,61 +336,66 @@ function qFactory(nextTick, exceptionHandler) {
322336

323337
Deferred.prototype = {
324338
resolve: function(val) {
325-
if (this.promise.$$pending) {
326-
var callbacks = this.promise.$$pending;
327-
this.promise.$$pending = undefined;
328-
this.promise.$$value = ref(val);
329-
330-
if (callbacks.length) {
331-
nextTick(simpleBind(this, function() {
332-
var callback;
333-
for (var i = 0, ii = callbacks.length; i < ii; i++) {
334-
callback = callbacks[i];
335-
this.promise.$$value.then(callback[0], callback[1], callback[2]);
336-
}
337-
}));
339+
if (this.promise.$$state.status) return;
340+
if (val === this.promise) throw new TypeError('Cycle detected');
341+
this.$$resolve(val);
342+
},
343+
344+
$$resolve: function(val) {
345+
var then, fns;
346+
347+
fns = callOnce(this, this.$$resolve, this.$$reject);
348+
try {
349+
if ((isObject(val) || isFunction(val))) then = val && val.then;
350+
if (isFunction(then)) {
351+
this.promise.$$state.status = -1;
352+
then.call(val, fns[0], fns[1], this.notify);
353+
} else {
354+
this.promise.$$state.value = val;
355+
this.promise.$$state.status = 1;
356+
scheduleProcessQueue(this.promise.$$state);
338357
}
358+
} catch(e) {
359+
fns[1](e);
360+
exceptionHandler(e);
339361
}
340362
},
363+
341364
reject: function(reason) {
342-
this.resolve(createInternalRejectedPromise(reason));
365+
if (this.promise.$$state.status) return;
366+
this.$$reject(reason);
367+
},
368+
369+
$$reject: function(reason) {
370+
this.promise.$$state.value = reason;
371+
this.promise.$$state.status = 2;
372+
scheduleProcessQueue(this.promise.$$state);
343373
},
374+
344375
notify: function(progress) {
345-
if (this.promise.$$pending) {
346-
var callbacks = this.promise.$$pending;
347-
348-
if (this.promise.$$pending.length) {
349-
nextTick(function() {
350-
var callback;
351-
for (var i = 0, ii = callbacks.length; i < ii; i++) {
352-
callback = callbacks[i];
353-
callback[2](progress);
354-
}
355-
});
356-
}
357-
}
358-
}
359-
};
376+
var callbacks = this.promise.$$state.pending;
360377

361-
var ref = function(value) {
362-
if (isPromiseLike(value)) return value;
363-
return {
364-
then: function(callback) {
365-
var result = new Deferred();
378+
if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) {
366379
nextTick(function() {
367-
result.resolve(callback(value));
380+
var callback, result;
381+
for (var i = 0, ii = callbacks.length; i < ii; i++) {
382+
result = callbacks[i][0];
383+
callback = callbacks[i][3];
384+
try {
385+
result.notify(isFunction(callback) ? callback(progress) : progress);
386+
} catch(e) {
387+
exceptionHandler(e);
388+
}
389+
}
368390
});
369-
return result.promise;
370391
}
371-
};
392+
}
372393
};
373394

374-
375395
/**
376-
* @ngdoc method
377-
* @name $q#reject
378-
* @kind function
379-
*
396+
* @ngdoc
397+
* @name ng.$q#reject
398+
* @methodOf ng.$q
380399
* @description
381400
* Creates a promise that is resolved as rejected with the specified `reason`. This api should be
382401
* used to forward rejection in a chain of promises. If you are dealing with the last promise in
@@ -427,7 +446,7 @@ function qFactory(nextTick, exceptionHandler) {
427446
var handleCallback = function handleCallback(value, isResolved, callback) {
428447
var callbackOutput = null;
429448
try {
430-
callbackOutput = (callback ||defaultCallback)();
449+
if (isFunction(callback)) callbackOutput = callback();
431450
} catch(e) {
432451
return makePromise(e, false);
433452
}
@@ -442,29 +461,10 @@ function qFactory(nextTick, exceptionHandler) {
442461
}
443462
};
444463

445-
var createInternalRejectedPromise = function(reason) {
446-
return {
447-
then: function(callback, errback) {
448-
var result = new Deferred();
449-
nextTick(function() {
450-
try {
451-
result.resolve((isFunction(errback) ? errback : defaultErrback)(reason));
452-
} catch(e) {
453-
result.reject(e);
454-
exceptionHandler(e);
455-
}
456-
});
457-
return result.promise;
458-
}
459-
};
460-
};
461-
462-
463464
/**
464-
* @ngdoc method
465-
* @name $q#when
466-
* @kind function
467-
*
465+
* @ngdoc
466+
* @name ng.$q#when
467+
* @methodOf ng.$q
468468
* @description
469469
* Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise.
470470
* This is useful when you are dealing with an object that might or might not be a promise, or if
@@ -473,70 +473,18 @@ function qFactory(nextTick, exceptionHandler) {
473473
* @param {*} value Value or a promise
474474
* @returns {Promise} Returns a promise of the passed value or promise
475475
*/
476-
var when = function(value, callback, errback, progressback) {
477-
var result = new Deferred(),
478-
done;
479476

480-
var wrappedCallback = function(value) {
481-
try {
482-
return (isFunction(callback) ? callback : defaultCallback)(value);
483-
} catch (e) {
484-
exceptionHandler(e);
485-
return reject(e);
486-
}
487-
};
488477

489-
var wrappedErrback = function(reason) {
490-
try {
491-
return (isFunction(errback) ? errback : defaultErrback)(reason);
492-
} catch (e) {
493-
exceptionHandler(e);
494-
return reject(e);
495-
}
496-
};
497-
498-
var wrappedProgressback = function(progress) {
499-
try {
500-
return (isFunction(progressback) ? progressback : defaultCallback)(progress);
501-
} catch (e) {
502-
exceptionHandler(e);
503-
}
504-
};
505-
506-
nextTick(function() {
507-
ref(value).then(function(value) {
508-
if (done) return;
509-
done = true;
510-
result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback));
511-
}, function(reason) {
512-
if (done) return;
513-
done = true;
514-
result.resolve(wrappedErrback(reason));
515-
}, function(progress) {
516-
if (done) return;
517-
result.notify(wrappedProgressback(progress));
518-
});
519-
});
520-
521-
return result.promise;
478+
var when = function(value, callback, errback, progressBack) {
479+
var result = new Deferred();
480+
result.resolve(value);
481+
return result.promise.then(callback, errback, progressBack);
522482
};
523483

524-
525-
function defaultCallback(value) {
526-
return value;
527-
}
528-
529-
530-
function defaultErrback(reason) {
531-
return reject(reason);
532-
}
533-
534-
535484
/**
536-
* @ngdoc method
537-
* @name $q#all
538-
* @kind function
539-
*
485+
* @ngdoc
486+
* @name ng.$q#all
487+
* @methodOf ng.$q
540488
* @description
541489
* Combines multiple promises into a single promise that is resolved when all of the input
542490
* promises are resolved.
@@ -547,14 +495,15 @@ function qFactory(nextTick, exceptionHandler) {
547495
* If any of the promises is resolved with a rejection, this resulting promise will be rejected
548496
* with the same rejection value.
549497
*/
498+
550499
function all(promises) {
551500
var deferred = new Deferred(),
552501
counter = 0,
553502
results = isArray(promises) ? [] : {};
554503

555504
forEach(promises, function(promise, key) {
556505
counter++;
557-
ref(promise).then(function(value) {
506+
when(promise).then(function(value) {
558507
if (results.hasOwnProperty(key)) return;
559508
results[key] = value;
560509
if (!(--counter)) deferred.resolve(results);

‎test/ng/qSpec.js

+3-11
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ describe('q', function() {
276276
promise.then(success(), error());
277277

278278
resolve(createPromise());
279-
mockNextTick.flush();
280279
expect(logStr()).toBe('');
281280

282281
resolve2('foo');
@@ -668,8 +667,8 @@ describe('q', function() {
668667
expect(log).toEqual(['successA(RESOLVED_VAL)->a',
669668
'finallyB()->b',
670669
'successC(RESOLVED_VAL)->c',
671-
'successBB(RESOLVED_VAL)->bb',
672670
'finallyCC()->IGNORED',
671+
'successBB(RESOLVED_VAL)->bb',
673672
'successCCC(c)->cc',
674673
'successCCCC(cc)->ccc']);
675674
});
@@ -853,7 +852,6 @@ describe('q', function() {
853852
promise.then(success(), error());
854853

855854
deferred.resolve(deferred2.promise);
856-
mockNextTick.flush();
857855
expect(logStr()).toBe('');
858856

859857
deferred2.resolve('foo');
@@ -1406,8 +1404,8 @@ describe('q', function() {
14061404
expect(log).toEqual(['successA(RESOLVED_VAL)->a',
14071405
'finallyB()->b',
14081406
'successC(RESOLVED_VAL)->c',
1409-
'successBB(RESOLVED_VAL)->bb',
14101407
'finallyCC()->IGNORED',
1408+
'successBB(RESOLVED_VAL)->bb',
14111409
'successCCC(c)->cc',
14121410
'successCCCC(cc)->ccc']);
14131411
});
@@ -1603,7 +1601,6 @@ describe('q', function() {
16031601
function() {
16041602
q.when(deferred.promise, success(), error());
16051603
expect(logStr()).toBe('');
1606-
mockNextTick.flush();
16071604
expect(logStr()).toBe('');
16081605
syncResolve(deferred, 'hello');
16091606
expect(logStr()).toBe('success(hello)->hello');
@@ -1614,7 +1611,6 @@ describe('q', function() {
16141611
function() {
16151612
q.when(deferred.promise, success(), error());
16161613
expect(logStr()).toBe('');
1617-
mockNextTick.flush();
16181614
expect(logStr()).toBe('');
16191615
syncReject(deferred, 'nope');
16201616
expect(logStr()).toBe('error(nope)->reject(nope)');
@@ -1626,7 +1622,6 @@ describe('q', function() {
16261622
it('should call the progressback when the value is a promise and gets notified',
16271623
function() {
16281624
q.when(deferred.promise, success(), error(), progress());
1629-
mockNextTick.flush();
16301625
expect(logStr()).toBe('');
16311626
syncNotify(deferred, 'notification');
16321627
expect(logStr()).toBe('progress(notification)->notification');
@@ -1670,7 +1665,6 @@ describe('q', function() {
16701665
it('should not require progressback and propagate notification', function() {
16711666
q.when(deferred.promise).
16721667
then(success(), error(), progress());
1673-
mockNextTick.flush();
16741668
expect(logStr()).toBe('');
16751669
syncNotify(deferred, 'notification');
16761670
expect(logStr()).toBe('progress(notification)->notification');
@@ -1746,7 +1740,6 @@ describe('q', function() {
17461740
};
17471741

17481742
q.when(evilPromise, success(), error());
1749-
mockNextTick.flush();
17501743
expect(logStr()).toBe('');
17511744
evilPromise.success('done');
17521745
mockNextTick.flush(); // TODO(i) wrong queue, evil promise would be resolved outside of the
@@ -1772,9 +1765,9 @@ describe('q', function() {
17721765
};
17731766

17741767
q.when(evilPromise, success(), error());
1775-
mockNextTick.flush();
17761768
expect(logStr()).toBe('');
17771769
evilPromise.error('failed');
1770+
mockNextTick.flush();
17781771
expect(logStr()).toBe('error(failed)->reject(failed)');
17791772

17801773
evilPromise.error('muhaha');
@@ -1794,7 +1787,6 @@ describe('q', function() {
17941787
};
17951788

17961789
q.when(evilPromise, success(), error(), progress());
1797-
mockNextTick.flush();
17981790
expect(logStr()).toBe('');
17991791
evilPromise.progress('notification');
18001792
evilPromise.success('ok');

0 commit comments

Comments
 (0)
This repository has been archived.