Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f3a763f

Browse files
committedJul 24, 2014
feat($q): add streamlined ES6-style interface for using $q
This potentially helps lead the way towards a more performant fly-weight implementation, as discussed earlier in the year. Using a constructor means we can put things in the prototype chain, and essentially treat $q as a Promise class, and reuse methods as appropriate. Short of that, I feel this style is slightly more convenient and streamlined, compared with the older API. Closes #8311 Closes #6427 (I know it's not really the solution asked for in #6427, sorry!)
1 parent c54228f commit f3a763f

File tree

2 files changed

+676
-6
lines changed

2 files changed

+676
-6
lines changed
 

‎src/ng/q.js

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,46 @@
88
* @description
99
* A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
1010
*
11+
* $q can be used in two fashions --- One, which is more similar to Kris Kowal's Q or jQuery's Deferred
12+
* implementations, the other resembles ES6 promises to some degree.
13+
*
14+
* # $q constructor
15+
*
16+
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
17+
* function as the first argument). This is similar to the native Promise implementation from ES6 Harmony,
18+
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
19+
*
20+
* While the constructor-style use is supported, not all of the supporting methods from Harmony promises are
21+
* available yet.
22+
*
23+
* It can be used like so:
24+
*
25+
* ```js
26+
* return $q(function(resolve, reject) {
27+
* // perform some asynchronous operation, resolve or reject the promise when appropriate.
28+
* setInterval(function() {
29+
* if (pollStatus > 0) {
30+
* resolve(polledValue);
31+
* } else if (pollStatus < 0) {
32+
* reject(polledValue);
33+
* } else {
34+
* pollStatus = pollAgain(function(value) {
35+
* polledValue = value;
36+
* });
37+
* }
38+
* }, 10000);
39+
* }).
40+
* then(function(value) {
41+
* // handle success
42+
* }, function(reason) {
43+
* // handle failure
44+
* });
45+
* ```
46+
*
47+
* Note, progress/notify callbacks are not currently supported via the ES6-style interface.
48+
*
49+
* However, the more traditional CommonJS style usage is still available, and documented below.
50+
*
1151
* [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
1252
* interface for interacting with an object that represents the result of an action that is
1353
* performed asynchronously, and may or may not be finished at any given point in time.
@@ -54,7 +94,6 @@
5494
* For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the
5595
* section on serial or parallel joining of promises.
5696
*
57-
*
5897
* # The Deferred API
5998
*
6099
* A new instance of deferred is constructed by calling `$q.defer()`.
@@ -163,6 +202,12 @@
163202
* expect(resolvedValue).toEqual(123);
164203
* }));
165204
* ```
205+
*
206+
* @param {function(function, function)} resolver Function which is responsible for resolving or
207+
* rejecting the newly created promise. The first parameteter is a function which resolves the
208+
* promise, the second parameter is a function which rejects the promise.
209+
*
210+
* @returns {Promise} The newly created promise.
166211
*/
167212
function $QProvider() {
168213

@@ -519,10 +564,36 @@ function qFactory(nextTick, exceptionHandler) {
519564
return deferred.promise;
520565
}
521566

522-
return {
523-
defer: defer,
524-
reject: reject,
525-
when: when,
526-
all: all
567+
var $Q = function Q(resolver) {
568+
if (!isFunction(resolver)) {
569+
// TODO(@caitp): minErr this
570+
throw new TypeError('Expected resolverFn');
571+
}
572+
573+
if (!(this instanceof Q)) {
574+
// More useful when $Q is the Promise itself.
575+
return new Q(resolver);
576+
}
577+
578+
var deferred = defer();
579+
580+
function resolveFn(value) {
581+
deferred.resolve(value);
582+
}
583+
584+
function rejectFn(reason) {
585+
deferred.reject(reason);
586+
}
587+
588+
resolver(resolveFn, rejectFn);
589+
590+
return deferred.promise;
527591
};
592+
593+
$Q.defer = defer;
594+
$Q.reject = reject;
595+
$Q.when = when;
596+
$Q.all = all;
597+
598+
return $Q;
528599
}

‎test/ng/qSpec.js

Lines changed: 599 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,605 @@ describe('q', function() {
196196
});
197197

198198

199+
describe('$Q', function() {
200+
var resolve, reject, resolve2, reject2;
201+
var createPromise = function() {
202+
return q(function(resolveFn, rejectFn) {
203+
if (resolve === null) {
204+
resolve = resolveFn;
205+
reject = rejectFn;
206+
} else if (resolve2 === null) {
207+
resolve2 = resolveFn;
208+
reject2 = rejectFn;
209+
}
210+
});
211+
};
212+
213+
afterEach(function() {
214+
resolve = reject = resolve2 = reject2 = null;
215+
});
216+
217+
it('should return a Promise', function() {
218+
var promise = q(noop);
219+
expect(typeof promise.then).toBe('function');
220+
expect(typeof promise.catch).toBe('function');
221+
expect(typeof promise.finally).toBe('function');
222+
});
223+
224+
225+
describe('resolve', function() {
226+
it('should fulfill the promise and execute all success callbacks in the registration order',
227+
function() {
228+
var promise = createPromise();
229+
promise.then(success(1), error());
230+
promise.then(success(2), error());
231+
expect(logStr()).toBe('');
232+
233+
resolve('foo');
234+
mockNextTick.flush();
235+
expect(logStr()).toBe('success1(foo)->foo; success2(foo)->foo');
236+
});
237+
238+
239+
it('should do nothing if a promise was previously resolved', function() {
240+
var promise = createPromise();
241+
promise.then(success(), error());
242+
expect(logStr()).toBe('');
243+
244+
resolve('foo');
245+
mockNextTick.flush();
246+
expect(logStr()).toBe('success(foo)->foo');
247+
248+
log = [];
249+
resolve('bar');
250+
reject('baz');
251+
expect(mockNextTick.queue.length).toBe(0);
252+
expect(logStr()).toBe('');
253+
});
254+
255+
256+
it('should do nothing if a promise was previously rejected', function() {
257+
var promise = createPromise();
258+
promise.then(success(), error());
259+
expect(logStr()).toBe('');
260+
261+
reject('foo');
262+
mockNextTick.flush();
263+
expect(logStr()).toBe('error(foo)->reject(foo)');
264+
265+
log = [];
266+
resolve('bar');
267+
reject('baz');
268+
expect(mockNextTick.queue.length).toBe(0);
269+
expect(logStr()).toBe('');
270+
});
271+
272+
273+
it('should allow deferred resolution with a new promise', function() {
274+
var promise = createPromise();
275+
276+
promise.then(success(), error());
277+
278+
resolve(createPromise());
279+
mockNextTick.flush();
280+
expect(logStr()).toBe('');
281+
282+
resolve2('foo');
283+
mockNextTick.flush();
284+
expect(logStr()).toBe('success(foo)->foo');
285+
});
286+
287+
288+
it('should call the callback in the next turn', function() {
289+
var promise = createPromise();
290+
promise.then(success());
291+
expect(logStr()).toBe('');
292+
293+
resolve('foo');
294+
expect(logStr()).toBe('');
295+
296+
mockNextTick.flush();
297+
expect(logStr()).toBe('success(foo)->foo');
298+
});
299+
300+
301+
it('should not break if a callbacks registers another callback', function() {
302+
var promise = createPromise();
303+
promise.then(function() {
304+
log.push('outer');
305+
promise.then(function() {
306+
log.push('inner');
307+
});
308+
});
309+
310+
resolve('foo');
311+
expect(logStr()).toBe('');
312+
313+
mockNextTick.flush();
314+
expect(logStr()).toBe('outer; inner');
315+
});
316+
317+
318+
it('should not break if a callbacks tries to resolve the deferred again', function() {
319+
var promise = createPromise();
320+
promise.then(function(val) {
321+
log.push('then1(' + val + ')->resolve(bar)');
322+
deferred.resolve('bar'); // nop
323+
});
324+
325+
promise.then(success(2));
326+
327+
resolve('foo');
328+
expect(logStr()).toBe('');
329+
330+
mockNextTick.flush();
331+
expect(logStr()).toBe('then1(foo)->resolve(bar); success2(foo)->foo');
332+
});
333+
});
334+
335+
336+
describe('reject', function() {
337+
it('should reject the promise and execute all error callbacks in the registration order',
338+
function() {
339+
var promise = createPromise();
340+
promise.then(success(), error(1));
341+
promise.then(success(), error(2));
342+
expect(logStr()).toBe('');
343+
344+
reject('foo');
345+
mockNextTick.flush();
346+
expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
347+
});
348+
349+
350+
it('should do nothing if a promise was previously resolved', function() {
351+
var promise = createPromise();
352+
promise.then(success(1), error(1));
353+
expect(logStr()).toBe('');
354+
355+
resolve('foo');
356+
mockNextTick.flush();
357+
expect(logStr()).toBe('success1(foo)->foo');
358+
359+
log = [];
360+
reject('bar');
361+
resolve('baz');
362+
expect(mockNextTick.queue.length).toBe(0);
363+
expect(logStr()).toBe('');
364+
365+
promise.then(success(2), error(2));
366+
expect(logStr()).toBe('');
367+
mockNextTick.flush();
368+
expect(logStr()).toBe('success2(foo)->foo');
369+
});
370+
371+
372+
it('should do nothing if a promise was previously rejected', function() {
373+
var promise = createPromise();
374+
promise.then(success(1), error(1));
375+
expect(logStr()).toBe('');
376+
377+
reject('foo');
378+
mockNextTick.flush();
379+
expect(logStr()).toBe('error1(foo)->reject(foo)');
380+
381+
log = [];
382+
reject('bar');
383+
resolve('baz');
384+
expect(mockNextTick.queue.length).toBe(0);
385+
expect(logStr()).toBe('');
386+
387+
promise.then(success(2), error(2));
388+
expect(logStr()).toBe('');
389+
mockNextTick.flush();
390+
expect(logStr()).toBe('error2(foo)->reject(foo)');
391+
});
392+
393+
394+
it('should not defer rejection with a new promise', function() {
395+
var promise = createPromise();
396+
promise.then(success(), error());
397+
398+
reject(createPromise());
399+
mockNextTick.flush();
400+
expect(logStr()).toBe('error({})->reject({})');
401+
});
402+
403+
404+
it('should call the error callback in the next turn', function() {
405+
var promise = createPromise();
406+
promise.then(success(), error());
407+
expect(logStr()).toBe('');
408+
409+
reject('foo');
410+
expect(logStr()).toBe('');
411+
412+
mockNextTick.flush();
413+
expect(logStr()).toBe('error(foo)->reject(foo)');
414+
});
415+
416+
417+
it('should support non-bound execution', function() {
418+
var promise = createPromise();
419+
promise.then(success(), error());
420+
reject('detached');
421+
mockNextTick.flush();
422+
expect(logStr()).toBe('error(detached)->reject(detached)');
423+
});
424+
});
425+
426+
427+
describe('promise', function() {
428+
describe('then', function() {
429+
it('should allow registration of a success callback without an errback or progressback ' +
430+
'and resolve', function() {
431+
var promise = createPromise();
432+
promise.then(success());
433+
resolve('foo');
434+
mockNextTick.flush();
435+
expect(logStr()).toBe('success(foo)->foo');
436+
});
437+
438+
439+
it('should allow registration of a success callback without an errback and reject',
440+
function() {
441+
var promise = createPromise();
442+
promise.then(success());
443+
reject('foo');
444+
mockNextTick.flush();
445+
expect(logStr()).toBe('');
446+
});
447+
448+
449+
it('should allow registration of an errback without a success or progress callback and ' +
450+
' reject', function() {
451+
var promise = createPromise();
452+
promise.then(null, error());
453+
reject('oops!');
454+
mockNextTick.flush();
455+
expect(logStr()).toBe('error(oops!)->reject(oops!)');
456+
});
457+
458+
459+
it('should allow registration of an errback without a success callback and resolve',
460+
function() {
461+
var promise = createPromise();
462+
promise.then(null, error());
463+
resolve('done');
464+
mockNextTick.flush();
465+
expect(logStr()).toBe('');
466+
});
467+
468+
469+
it('should allow registration of an progressback without a success callback and resolve',
470+
function() {
471+
var promise = createPromise();
472+
promise.then(null, null, progress());
473+
resolve('done');
474+
mockNextTick.flush();
475+
expect(logStr()).toBe('');
476+
});
477+
478+
479+
it('should allow registration of an progressback without a error callback and reject',
480+
function() {
481+
var promise = createPromise();
482+
promise.then(null, null, progress());
483+
reject('oops!');
484+
mockNextTick.flush();
485+
expect(logStr()).toBe('');
486+
});
487+
488+
489+
it('should resolve all callbacks with the original value', function() {
490+
var promise = createPromise();
491+
promise.then(success('A', 'aVal'), error(), progress());
492+
promise.then(success('B', 'bErr', true), error(), progress());
493+
promise.then(success('C', q.reject('cReason')), error(), progress());
494+
promise.then(success('D', q.reject('dReason'), true), error(), progress());
495+
promise.then(success('E', 'eVal'), error(), progress());
496+
497+
expect(logStr()).toBe('');
498+
resolve('yup');
499+
mockNextTick.flush();
500+
expect(log).toEqual(['successA(yup)->aVal',
501+
'successB(yup)->throw(bErr)',
502+
'successC(yup)->{}',
503+
'successD(yup)->throw({})',
504+
'successE(yup)->eVal']);
505+
});
506+
507+
508+
it('should reject all callbacks with the original reason', function() {
509+
var promise = createPromise();
510+
promise.then(success(), error('A', 'aVal'), progress());
511+
promise.then(success(), error('B', 'bEr', true), progress());
512+
promise.then(success(), error('C', q.reject('cReason')), progress());
513+
promise.then(success(), error('D', 'dVal'), progress());
514+
515+
expect(logStr()).toBe('');
516+
reject('noo!');
517+
mockNextTick.flush();
518+
expect(logStr()).toBe('errorA(noo!)->aVal; errorB(noo!)->throw(bEr); errorC(noo!)->{}; errorD(noo!)->dVal');
519+
});
520+
521+
522+
it('should propagate resolution and rejection between dependent promises', function() {
523+
var promise = createPromise();
524+
promise.then(success(1, 'x'), error('1')).
525+
then(success(2, 'y', true), error('2')).
526+
then(success(3), error(3, 'z', true)).
527+
then(success(4), error(4, 'done')).
528+
then(success(5), error(5));
529+
530+
expect(logStr()).toBe('');
531+
resolve('sweet!');
532+
mockNextTick.flush();
533+
expect(log).toEqual(['success1(sweet!)->x',
534+
'success2(x)->throw(y)',
535+
'error3(y)->throw(z)',
536+
'error4(z)->done',
537+
'success5(done)->done']);
538+
});
539+
540+
541+
it('should reject a derived promise if an exception is thrown while resolving its parent',
542+
function() {
543+
var promise = createPromise();
544+
promise.then(success(1, 'oops', true), error(1)).
545+
then(success(2), error(2));
546+
resolve('done!');
547+
mockNextTick.flush();
548+
expect(logStr()).toBe('success1(done!)->throw(oops); error2(oops)->reject(oops)');
549+
});
550+
551+
552+
it('should reject a derived promise if an exception is thrown while rejecting its parent',
553+
function() {
554+
var promise = createPromise();
555+
promise.then(null, error(1, 'oops', true)).
556+
then(success(2), error(2));
557+
reject('timeout');
558+
mockNextTick.flush();
559+
expect(logStr()).toBe('error1(timeout)->throw(oops); error2(oops)->reject(oops)');
560+
});
561+
562+
563+
it('should call success callback in the next turn even if promise is already resolved',
564+
function() {
565+
var promise = createPromise();
566+
resolve('done!');
567+
568+
promise.then(success());
569+
expect(logStr()).toBe('');
570+
571+
mockNextTick.flush();
572+
expect(log).toEqual(['success(done!)->done!']);
573+
});
574+
575+
576+
it('should call error callback in the next turn even if promise is already rejected',
577+
function() {
578+
var promise = createPromise();
579+
reject('oops!');
580+
581+
promise.then(null, error());
582+
expect(logStr()).toBe('');
583+
584+
mockNextTick.flush();
585+
expect(log).toEqual(['error(oops!)->reject(oops!)']);
586+
});
587+
588+
it('should forward success resolution when success callbacks are not functions', function() {
589+
var promise = createPromise();
590+
resolve('yay!');
591+
592+
promise.then(1).
593+
then(null).
594+
then({}).
595+
then('gah!').
596+
then([]).
597+
then(success());
598+
599+
expect(logStr()).toBe('');
600+
601+
mockNextTick.flush();
602+
expect(log).toEqual(['success(yay!)->yay!']);
603+
});
604+
605+
it('should forward error resolution when error callbacks are not functions', function() {
606+
var promise = createPromise();
607+
reject('oops!');
608+
609+
promise.then(null, 1).
610+
then(null, null).
611+
then(null, {}).
612+
then(null, 'gah!').
613+
then(null, []).
614+
then(null, error());
615+
616+
expect(logStr()).toBe('');
617+
618+
mockNextTick.flush();
619+
expect(log).toEqual(['error(oops!)->reject(oops!)']);
620+
});
621+
});
622+
623+
624+
describe('finally', function() {
625+
it('should not take an argument',
626+
function() {
627+
var promise = createPromise();
628+
promise['finally'](fin(1));
629+
resolve('foo');
630+
mockNextTick.flush();
631+
expect(logStr()).toBe('finally1()');
632+
});
633+
634+
describe("when the promise is fulfilled", function () {
635+
it('should call the callback',
636+
function() {
637+
var promise = createPromise();
638+
promise.then(success(1))['finally'](fin(1));
639+
resolve('foo');
640+
mockNextTick.flush();
641+
expect(logStr()).toBe('success1(foo)->foo; finally1()');
642+
});
643+
644+
it('should fulfill with the original value',
645+
function() {
646+
var promise = createPromise();
647+
promise['finally'](fin('B', 'b'), error('B')).
648+
then(success('BB', 'bb'), error('BB'));
649+
resolve('RESOLVED_VAL');
650+
mockNextTick.flush();
651+
expect(log).toEqual(['finallyB()->b',
652+
'successBB(RESOLVED_VAL)->bb']);
653+
});
654+
655+
656+
it('should fulfill with the original value (larger test)',
657+
function() {
658+
var promise = createPromise();
659+
promise.then(success('A', 'a'), error('A'));
660+
promise['finally'](fin('B', 'b'), error('B')).
661+
then(success('BB', 'bb'), error('BB'));
662+
promise.then(success('C', 'c'), error('C'))['finally'](fin('CC', 'IGNORED'))
663+
.then(success('CCC', 'cc'), error('CCC'))
664+
.then(success('CCCC', 'ccc'), error('CCCC'));
665+
resolve('RESOLVED_VAL');
666+
mockNextTick.flush();
667+
668+
expect(log).toEqual(['successA(RESOLVED_VAL)->a',
669+
'finallyB()->b',
670+
'successC(RESOLVED_VAL)->c',
671+
'successBB(RESOLVED_VAL)->bb',
672+
'finallyCC()->IGNORED',
673+
'successCCC(c)->cc',
674+
'successCCCC(cc)->ccc']);
675+
});
676+
677+
describe("when the callback returns a promise", function() {
678+
describe("that is fulfilled", function() {
679+
it("should fulfill with the original reason after that promise resolves",
680+
function () {
681+
var promise = createPromise();
682+
var promise2 = createPromise();
683+
resolve2('bar');
684+
685+
promise['finally'](fin(1, promise))
686+
.then(success(2));
687+
688+
resolve('foo');
689+
mockNextTick.flush();
690+
691+
expect(logStr()).toBe('finally1()->{}; success2(foo)->foo');
692+
});
693+
});
694+
695+
describe("that is rejected", function() {
696+
it("should reject with this new rejection reason",
697+
function () {
698+
var promise = createPromise();
699+
var promise2 = createPromise();
700+
reject2('bar');
701+
promise['finally'](fin(1, promise2))
702+
.then(success(2), error(1));
703+
resolve('foo');
704+
mockNextTick.flush();
705+
expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
706+
});
707+
});
708+
709+
});
710+
711+
describe("when the callback throws an exception", function() {
712+
it("should reject with this new exception", function() {
713+
var promise = createPromise();
714+
promise['finally'](fin(1, "exception", true))
715+
.then(success(1), error(2));
716+
resolve('foo');
717+
mockNextTick.flush();
718+
expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
719+
});
720+
});
721+
722+
});
723+
724+
725+
describe("when the promise is rejected", function () {
726+
it("should call the callback", function () {
727+
var promise = createPromise();
728+
promise['finally'](fin(1))
729+
.then(success(2), error(1));
730+
reject('foo');
731+
mockNextTick.flush();
732+
expect(logStr()).toBe('finally1(); error1(foo)->reject(foo)');
733+
});
734+
735+
it('should reject with the original reason', function() {
736+
var promise = createPromise();
737+
promise['finally'](fin(1), "hello")
738+
.then(success(2), error(2));
739+
reject('original');
740+
mockNextTick.flush();
741+
expect(logStr()).toBe('finally1(); error2(original)->reject(original)');
742+
});
743+
744+
describe("when the callback returns a promise", function() {
745+
describe("that is fulfilled", function() {
746+
it("should reject with the original reason after that promise resolves", function () {
747+
var promise = createPromise();
748+
var promise2 = createPromise();
749+
resolve2('bar');
750+
promise['finally'](fin(1, promise2))
751+
.then(success(2), error(2));
752+
reject('original');
753+
mockNextTick.flush();
754+
expect(logStr()).toBe('finally1()->{}; error2(original)->reject(original)');
755+
});
756+
});
757+
758+
describe("that is rejected", function () {
759+
it("should reject with the new reason", function() {
760+
var promise = createPromise();
761+
var promise2 = createPromise();
762+
reject2('bar');
763+
promise['finally'](fin(1, promise2))
764+
.then(success(2), error(1));
765+
resolve('foo');
766+
mockNextTick.flush();
767+
expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
768+
});
769+
});
770+
});
771+
772+
describe("when the callback throws an exception", function() {
773+
it("should reject with this new exception", function() {
774+
var promise = createPromise();
775+
promise['finally'](fin(1, "exception", true))
776+
.then(success(1), error(2));
777+
resolve('foo');
778+
mockNextTick.flush();
779+
expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
780+
});
781+
});
782+
});
783+
});
784+
785+
describe('catch', function() {
786+
it('should be a shorthand for defining promise error handlers', function() {
787+
var promise = createPromise();
788+
promise['catch'](error(1)).then(null, error(2));
789+
reject('foo');
790+
mockNextTick.flush();
791+
expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
792+
});
793+
});
794+
});
795+
});
796+
797+
199798
describe('defer', function() {
200799
it('should create a new deferred', function() {
201800
expect(deferred.promise).toBeDefined();

0 commit comments

Comments
 (0)
This repository has been archived.