This repository has been archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27.5k
/
q.js
693 lines (642 loc) · 23 KB
/
q.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
'use strict';
/**
* @ngdoc service
* @name $q
* @requires $rootScope
*
* @description
* A service that helps you run functions asynchronously, and use their return values (or exceptions)
* when they are done processing.
*
* This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred
* objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
* implementations, and the other which resembles ES6 (ES2015) promises to some degree.
*
* ## $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
* function as the first argument. This is similar to the native Promise implementation from ES6,
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
*
* While the constructor-style use is supported, not all of the supporting methods from ES6 promises are
* available yet.
*
* It can be used like so:
*
* ```js
* // for the purpose of this example let's assume that variables `$q` and `okToGreet`
* // are available in the current lexical scope (they could have been injected or passed in).
*
* function asyncGreet(name) {
* // perform some asynchronous operation, resolve or reject the promise when appropriate.
* return $q(function(resolve, reject) {
* setTimeout(function() {
* if (okToGreet(name)) {
* resolve('Hello, ' + name + '!');
* } else {
* reject('Greeting ' + name + ' is not allowed.');
* }
* }, 1000);
* });
* }
*
* var promise = asyncGreet('Robin Hood');
* promise.then(function(greeting) {
* alert('Success: ' + greeting);
* }, function(reason) {
* alert('Failed: ' + reason);
* });
* ```
*
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
*
* Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise.
*
* However, the more traditional CommonJS-style usage is still available, and documented below.
*
* [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
* interface for interacting with an object that represents the result of an action that is
* performed asynchronously, and may or may not be finished at any given point in time.
*
* From the perspective of dealing with error handling, deferred and promise APIs are to
* asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming.
*
* ```js
* // for the purpose of this example let's assume that variables `$q` and `okToGreet`
* // are available in the current lexical scope (they could have been injected or passed in).
*
* function asyncGreet(name) {
* var deferred = $q.defer();
*
* setTimeout(function() {
* deferred.notify('About to greet ' + name + '.');
*
* if (okToGreet(name)) {
* deferred.resolve('Hello, ' + name + '!');
* } else {
* deferred.reject('Greeting ' + name + ' is not allowed.');
* }
* }, 1000);
*
* return deferred.promise;
* }
*
* var promise = asyncGreet('Robin Hood');
* promise.then(function(greeting) {
* alert('Success: ' + greeting);
* }, function(reason) {
* alert('Failed: ' + reason);
* }, function(update) {
* alert('Got notification: ' + update);
* });
* ```
*
* At first it might not be obvious why this extra complexity is worth the trouble. The payoff
* comes in the way of guarantees that promise and deferred APIs make, see
* https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md.
*
* Additionally the promise api allows for composition that is very hard to do with the
* traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach.
* For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the
* section on serial or parallel joining of promises.
*
* ## The Deferred API
*
* A new instance of deferred is constructed by calling `$q.defer()`.
*
* The purpose of the deferred object is to expose the associated Promise instance as well as APIs
* that can be used for signaling the successful or unsuccessful completion, as well as the status
* of the task.
*
* **Methods**
*
* - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection
* constructed via `$q.reject`, the promise will be rejected instead.
* - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to
* resolving it with a rejection constructed via `$q.reject`.
* - `notify(value)` - provides updates on the status of the promise's execution. This may be called
* multiple times before the promise is either resolved or rejected.
*
* **Properties**
*
* - promise – `{Promise}` – promise object associated with this deferred.
*
*
* ## The Promise API
*
* A new promise instance is created when a deferred instance is created and can be retrieved by
* calling `deferred.promise`.
*
* The purpose of the promise object is to allow for interested parties to get access to the result
* of the deferred task when it completes.
*
* **Methods**
*
* - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or
* will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously
* as soon as the result is available. The callbacks are called with a single argument: the result
* or rejection reason. Additionally, the notify callback may be called zero or more times to
* provide a progress indication, before the promise is resolved or rejected.
*
* This method *returns a new promise* which is resolved or rejected via the return value of the
* `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved
* with the value which is resolved in that promise using
* [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)).
* It also notifies via the return value of the `notifyCallback` method. The promise cannot be
* resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback
* arguments are optional.
*
* - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
*
* - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise,
* but to do so without modifying the final value. This is useful to release resources or do some
* clean-up that needs to be done whether the promise was rejected or resolved. See the [full
* specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
* more information.
*
* ## Chaining promises
*
* Because calling the `then` method of a promise returns a new derived promise, it is easily
* possible to create a chain of promises:
*
* ```js
* promiseB = promiseA.then(function(result) {
* return result + 1;
* });
*
* // promiseB will be resolved immediately after promiseA is resolved and its value
* // will be the result of promiseA incremented by 1
* ```
*
* It is possible to create chains of any length and since a promise can be resolved with another
* promise (which will defer its resolution further), it is possible to pause/defer resolution of
* the promises at any point in the chain. This makes it possible to implement powerful APIs like
* $http's response interceptors.
*
*
* ## Differences between Kris Kowal's Q and $q
*
* There are two main differences:
*
* - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation
* mechanism in AngularJS, which means faster propagation of resolution or rejection into your
* models and avoiding unnecessary browser repaints, which would result in flickering UI.
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
* ## Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
* var deferred = $q.defer();
* var promise = deferred.promise;
* var resolvedValue;
*
* promise.then(function(value) { resolvedValue = value; });
* expect(resolvedValue).toBeUndefined();
*
* // Simulate resolving of promise
* deferred.resolve(123);
* // Note that the 'then' function does not get called synchronously.
* // This is because we want the promise API to always be async, whether or not
* // it got called synchronously or asynchronously.
* expect(resolvedValue).toBeUndefined();
*
* // Propagate promise resolution to 'then' functions using $apply().
* $rootScope.$apply();
* expect(resolvedValue).toEqual(123);
* }));
* ```
*
* @param {function(function, function)} resolver Function which is responsible for resolving or
* rejecting the newly created promise. The first parameter is a function which resolves the
* promise, the second parameter is a function which rejects the promise.
*
* @returns {Promise} The newly created promise.
*/
/**
* @ngdoc provider
* @name $qProvider
* @this
*
* @description
*/
function $QProvider() {
var errorOnUnhandledRejections = true;
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
return qFactory(function(callback) {
$rootScope.$evalAsync(callback);
}, $exceptionHandler, errorOnUnhandledRejections);
}];
/**
* @ngdoc method
* @name $qProvider#errorOnUnhandledRejections
* @kind function
*
* @description
* Retrieves or overrides whether to generate an error when a rejected promise is not handled.
* This feature is enabled by default.
*
* @param {boolean=} value Whether to generate an error when a rejected promise is not handled.
* @returns {boolean|ng.$qProvider} Current value when called without a new value or self for
* chaining otherwise.
*/
this.errorOnUnhandledRejections = function(value) {
if (isDefined(value)) {
errorOnUnhandledRejections = value;
return this;
} else {
return errorOnUnhandledRejections;
}
};
}
/** @this */
function $$QProvider() {
var errorOnUnhandledRejections = true;
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
return qFactory(function(callback) {
$browser.defer(callback);
}, $exceptionHandler, errorOnUnhandledRejections);
}];
this.errorOnUnhandledRejections = function(value) {
if (isDefined(value)) {
errorOnUnhandledRejections = value;
return this;
} else {
return errorOnUnhandledRejections;
}
};
}
/**
* Constructs a promise manager.
*
* @param {function(function)} nextTick Function for executing functions in the next turn.
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
* debugging purposes.
* @param {boolean=} errorOnUnhandledRejections Whether an error should be generated on unhandled
* promises rejections.
* @returns {object} Promise manager.
*/
function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
var $qMinErr = minErr('$q', TypeError);
var queueSize = 0;
var checkQueue = [];
/**
* @ngdoc method
* @name ng.$q#defer
* @kind function
*
* @description
* Creates a `Deferred` object which represents a task which will finish in the future.
*
* @returns {Deferred} Returns a new instance of deferred.
*/
function defer() {
return new Deferred();
}
function Deferred() {
var promise = this.promise = new Promise();
//Non prototype methods necessary to support unbound execution :/
this.resolve = function(val) { resolvePromise(promise, val); };
this.reject = function(reason) { rejectPromise(promise, reason); };
this.notify = function(progress) { notifyPromise(promise, progress); };
}
function Promise() {
this.$$state = { status: 0 };
}
extend(Promise.prototype, {
then: function(onFulfilled, onRejected, progressBack) {
if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) {
return this;
}
var result = new Promise();
this.$$state.pending = this.$$state.pending || [];
this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
return result;
},
'catch': function(callback) {
return this.then(null, callback);
},
'finally': function(callback, progressBack) {
return this.then(function(value) {
return handleCallback(value, resolve, callback);
}, function(error) {
return handleCallback(error, reject, callback);
}, progressBack);
}
});
function processQueue(state) {
var fn, promise, pending;
pending = state.pending;
state.processScheduled = false;
state.pending = undefined;
try {
for (var i = 0, ii = pending.length; i < ii; ++i) {
markQStateExceptionHandled(state);
promise = pending[i][0];
fn = pending[i][state.status];
try {
if (isFunction(fn)) {
resolvePromise(promise, fn(state.value));
} else if (state.status === 1) {
resolvePromise(promise, state.value);
} else {
rejectPromise(promise, state.value);
}
} catch (e) {
rejectPromise(promise, e);
// This error is explicitly marked for being passed to the $exceptionHandler
if (e && e.$$passToExceptionHandler === true) {
exceptionHandler(e);
}
}
}
} finally {
--queueSize;
if (errorOnUnhandledRejections && queueSize === 0) {
nextTick(processChecks);
}
}
}
function processChecks() {
// eslint-disable-next-line no-unmodified-loop-condition
while (!queueSize && checkQueue.length) {
var toCheck = checkQueue.shift();
if (!isStateExceptionHandled(toCheck)) {
markQStateExceptionHandled(toCheck);
var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value);
if (isError(toCheck.value)) {
exceptionHandler(toCheck.value, errorMessage);
} else {
exceptionHandler(errorMessage);
}
}
}
}
function scheduleProcessQueue(state) {
if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) {
if (queueSize === 0 && checkQueue.length === 0) {
nextTick(processChecks);
}
checkQueue.push(state);
}
if (state.processScheduled || !state.pending) return;
state.processScheduled = true;
++queueSize;
nextTick(function() { processQueue(state); });
}
function resolvePromise(promise, val) {
if (promise.$$state.status) return;
if (val === promise) {
$$reject(promise, $qMinErr(
'qcycle',
'Expected promise to be resolved with value other than itself \'{0}\'',
val));
} else {
$$resolve(promise, val);
}
}
function $$resolve(promise, val) {
var then;
var done = false;
try {
if (isObject(val) || isFunction(val)) then = val.then;
if (isFunction(then)) {
promise.$$state.status = -1;
then.call(val, doResolve, doReject, doNotify);
} else {
promise.$$state.value = val;
promise.$$state.status = 1;
scheduleProcessQueue(promise.$$state);
}
} catch (e) {
doReject(e);
}
function doResolve(val) {
if (done) return;
done = true;
$$resolve(promise, val);
}
function doReject(val) {
if (done) return;
done = true;
$$reject(promise, val);
}
function doNotify(progress) {
notifyPromise(promise, progress);
}
}
function rejectPromise(promise, reason) {
if (promise.$$state.status) return;
$$reject(promise, reason);
}
function $$reject(promise, reason) {
promise.$$state.value = reason;
promise.$$state.status = 2;
scheduleProcessQueue(promise.$$state);
}
function notifyPromise(promise, progress) {
var callbacks = promise.$$state.pending;
if ((promise.$$state.status <= 0) && callbacks && callbacks.length) {
nextTick(function() {
var callback, result;
for (var i = 0, ii = callbacks.length; i < ii; i++) {
result = callbacks[i][0];
callback = callbacks[i][3];
try {
notifyPromise(result, isFunction(callback) ? callback(progress) : progress);
} catch (e) {
exceptionHandler(e);
}
}
});
}
}
/**
* @ngdoc method
* @name $q#reject
* @kind function
*
* @description
* Creates a promise that is resolved as rejected with the specified `reason`. This api should be
* used to forward rejection in a chain of promises. If you are dealing with the last promise in
* a promise chain, you don't need to worry about it.
*
* When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of
* `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via
* a promise error callback and you want to forward the error to the promise derived from the
* current promise, you have to "rethrow" the error by returning a rejection constructed via
* `reject`.
*
* ```js
* promiseB = promiseA.then(function(result) {
* // success: do something and resolve promiseB
* // with the old or a new result
* return result;
* }, function(reason) {
* // error: handle the error if possible and
* // resolve promiseB with newPromiseOrValue,
* // otherwise forward the rejection to promiseB
* if (canHandle(reason)) {
* // handle the error and recover
* return newPromiseOrValue;
* }
* return $q.reject(reason);
* });
* ```
*
* @param {*} reason Constant, message, exception or an object representing the rejection reason.
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
*/
function reject(reason) {
var result = new Promise();
rejectPromise(result, reason);
return result;
}
function handleCallback(value, resolver, callback) {
var callbackOutput = null;
try {
if (isFunction(callback)) callbackOutput = callback();
} catch (e) {
return reject(e);
}
if (isPromiseLike(callbackOutput)) {
return callbackOutput.then(function() {
return resolver(value);
}, reject);
} else {
return resolver(value);
}
}
/**
* @ngdoc method
* @name $q#when
* @kind function
*
* @description
* Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise.
* This is useful when you are dealing with an object that might or might not be a promise, or if
* the promise comes from a source that can't be trusted.
*
* @param {*} value Value or a promise
* @param {Function=} successCallback
* @param {Function=} errorCallback
* @param {Function=} progressCallback
* @returns {Promise} Returns a promise of the passed value or promise
*/
function when(value, callback, errback, progressBack) {
var result = new Promise();
resolvePromise(result, value);
return result.then(callback, errback, progressBack);
}
/**
* @ngdoc method
* @name $q#resolve
* @kind function
*
* @description
* Alias of {@link ng.$q#when when} to maintain naming consistency with ES6.
*
* @param {*} value Value or a promise
* @param {Function=} successCallback
* @param {Function=} errorCallback
* @param {Function=} progressCallback
* @returns {Promise} Returns a promise of the passed value or promise
*/
var resolve = when;
/**
* @ngdoc method
* @name $q#all
* @kind function
*
* @description
* Combines multiple promises into a single promise that is resolved when all of the input
* promises are resolved.
*
* @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises.
* @returns {Promise} Returns a single promise that will be resolved with an array/hash of values,
* each value corresponding to the promise at the same index/key in the `promises` array/hash.
* If any of the promises is resolved with a rejection, this resulting promise will be rejected
* with the same rejection value.
*/
function all(promises) {
var result = new Promise(),
counter = 0,
results = isArray(promises) ? [] : {};
forEach(promises, function(promise, key) {
counter++;
when(promise).then(function(value) {
results[key] = value;
if (!(--counter)) resolvePromise(result, results);
}, function(reason) {
rejectPromise(result, reason);
});
});
if (counter === 0) {
resolvePromise(result, results);
}
return result;
}
/**
* @ngdoc method
* @name $q#race
* @kind function
*
* @description
* Returns a promise that resolves or rejects as soon as one of those promises
* resolves or rejects, with the value or reason from that promise.
*
* @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises.
* @returns {Promise} a promise that resolves or rejects as soon as one of the `promises`
* resolves or rejects, with the value or reason from that promise.
*/
function race(promises) {
var deferred = defer();
forEach(promises, function(promise) {
when(promise).then(deferred.resolve, deferred.reject);
});
return deferred.promise;
}
function $Q(resolver) {
if (!isFunction(resolver)) {
throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver);
}
var promise = new Promise();
function resolveFn(value) {
resolvePromise(promise, value);
}
function rejectFn(reason) {
rejectPromise(promise, reason);
}
resolver(resolveFn, rejectFn);
return promise;
}
// Let's make the instanceof operator work for promises, so that
// `new $q(fn) instanceof $q` would evaluate to true.
$Q.prototype = Promise.prototype;
$Q.defer = defer;
$Q.reject = reject;
$Q.when = when;
$Q.resolve = resolve;
$Q.all = all;
$Q.race = race;
return $Q;
}
function isStateExceptionHandled(state) {
return !!state.pur;
}
function markQStateExceptionHandled(state) {
state.pur = true;
}
function markQExceptionHandled(q) {
// Built-in `$q` promises will always have a `$$state` property. This check is to allow
// overwriting `$q` with a different promise library (e.g. Bluebird + angular-bluebird-promises).
// (Currently, this is the only method that might be called with a promise, even if it is not
// created by the built-in `$q`.)
if (q.$$state) {
markQStateExceptionHandled(q.$$state);
}
}