diff --git a/docs/content/error/$interval/badprom.ngdoc b/docs/content/error/$interval/badprom.ngdoc new file mode 100644 index 000000000000..2c9f8c5371a9 --- /dev/null +++ b/docs/content/error/$interval/badprom.ngdoc @@ -0,0 +1,25 @@ +@ngdoc error +@name $interval:badprom +@fullName Non-$interval promise +@description + +This error occurs when calling {@link ng.$interval#cancel $interval.cancel()} with a promise that +was not generated by the {@link ng.$interval $interval} service. This can, for example, happen when +calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new +promise, and pass that new promise to {@link ng.$interval#cancel $interval.cancel()}. + +Example of incorrect usage that leads to this error: + +```js +var promise = $interval(doSomething, 1000, 5).then(doSomethingElse); +$interval.cancel(promise); +``` + +To fix the example above, keep a reference to the promise returned by +{@link ng.$interval $interval()} and pass that to {@link ng.$interval#cancel $interval.cancel()}: + +```js +var promise = $interval(doSomething, 1000, 5); +var newPromise = promise.then(doSomethingElse); +$interval.cancel(promise); +``` diff --git a/docs/content/error/$timeout/badprom.ngdoc b/docs/content/error/$timeout/badprom.ngdoc new file mode 100644 index 000000000000..c1b0d025ad8f --- /dev/null +++ b/docs/content/error/$timeout/badprom.ngdoc @@ -0,0 +1,25 @@ +@ngdoc error +@name $timeout:badprom +@fullName Non-$timeout promise +@description + +This error occurs when calling {@link ng.$timeout#cancel $timeout.cancel()} with a promise that +was not generated by the {@link ng.$timeout $timeout} service. This can, for example, happen when +calling {@link ng.$q#the-promise-api then()/catch()} on the returned promise, which creates a new +promise, and pass that new promise to {@link ng.$timeout#cancel $timeout.cancel()}. + +Example of incorrect usage that leads to this error: + +```js +var promise = $timeout(doSomething, 1000).then(doSomethingElse); +$timeout.cancel(promise); +``` + +To fix the example above, keep a reference to the promise returned by +{@link ng.$timeout $timeout()} and pass that to {@link ng.$timeout#cancel $timeout.cancel()}: + +```js +var promise = $timeout(doSomething, 1000); +var newPromise = promise.then(doSomethingElse); +$timeout.cancel(promise); +``` diff --git a/src/ng/interval.js b/src/ng/interval.js index e5f2538f8b7b..750a6ba3df1c 100644 --- a/src/ng/interval.js +++ b/src/ng/interval.js @@ -1,5 +1,7 @@ 'use strict'; +var $intervalMinErr = minErr('$interval'); + /** @this */ function $IntervalProvider() { this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser', @@ -7,132 +9,132 @@ function $IntervalProvider() { var intervals = {}; - /** - * @ngdoc service - * @name $interval - * - * @description - * AngularJS's wrapper for `window.setInterval`. The `fn` function is executed every `delay` - * milliseconds. - * - * The return value of registering an interval function is a promise. This promise will be - * notified upon each tick of the interval, and will be resolved after `count` iterations, or - * run indefinitely if `count` is not defined. The value of the notification will be the - * number of iterations that have run. - * To cancel an interval, call `$interval.cancel(promise)`. - * - * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to - * move forward by `millis` milliseconds and trigger any functions scheduled to run in that - * time. - * - *
- * **Note**: Intervals created by this service must be explicitly destroyed when you are finished - * with them. In particular they are not automatically destroyed when a controller's scope or a - * directive's element are destroyed. - * You should take this into consideration and make sure to always cancel the interval at the - * appropriate moment. See the example below for more details on how and when to do this. - *
- * - * @param {function()} fn A function that should be called repeatedly. If no additional arguments - * are passed (see below), the function is called with the current iteration count. - * @param {number} delay Number of milliseconds between each function call. - * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat - * indefinitely. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete. - * - * @example - * - * - * - * - *
- *
- *
- * Current time is: - *
- * Blood 1 : {{blood_1}} - * Blood 2 : {{blood_2}} - * - * - * - *
- *
- * - *
- *
- */ + /** + * @ngdoc service + * @name $interval + * + * @description + * AngularJS's wrapper for `window.setInterval`. The `fn` function is executed every `delay` + * milliseconds. + * + * The return value of registering an interval function is a promise. This promise will be + * notified upon each tick of the interval, and will be resolved after `count` iterations, or + * run indefinitely if `count` is not defined. The value of the notification will be the + * number of iterations that have run. + * To cancel an interval, call `$interval.cancel(promise)`. + * + * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + *
+ * **Note**: Intervals created by this service must be explicitly destroyed when you are finished + * with them. In particular they are not automatically destroyed when a controller's scope or a + * directive's element are destroyed. + * You should take this into consideration and make sure to always cancel the interval at the + * appropriate moment. See the example below for more details on how and when to do this. + *
+ * + * @param {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete. + * + * @example + * + * + * + * + *
+ *
+ *
+ * Current time is: + *
+ * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
+ *
+ * + *
+ *
+ */ function interval(fn, delay, count, invokeApply) { var hasParams = arguments.length > 4, args = hasParams ? sliceArgs(arguments, 4) : [], @@ -177,26 +179,36 @@ function $IntervalProvider() { } - /** - * @ngdoc method - * @name $interval#cancel - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {Promise=} promise returned by the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully canceled. - */ + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {Promise=} promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ interval.cancel = function(promise) { - if (promise && promise.$$intervalId in intervals) { - // Interval cancels should not report as unhandled promise. - markQExceptionHandled(intervals[promise.$$intervalId].promise); - intervals[promise.$$intervalId].reject('canceled'); - $window.clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - return true; + if (!promise) return false; + + if (!promise.hasOwnProperty('$$intervalId')) { + throw $intervalMinErr('badprom', + '`$interval.cancel()` called with a promise that was not generated by `$interval()`.'); } - return false; + + if (!intervals.hasOwnProperty(promise.$$intervalId)) return false; + + var id = promise.$$intervalId; + var deferred = intervals[id]; + + // Interval cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + $window.clearInterval(id); + delete intervals[id]; + + return true; }; return interval; diff --git a/src/ng/timeout.js b/src/ng/timeout.js index 5c22c8b9ba55..1e4eaad3349f 100644 --- a/src/ng/timeout.js +++ b/src/ng/timeout.js @@ -1,5 +1,7 @@ 'use strict'; +var $timeoutMinErr = minErr('$timeout'); + /** @this */ function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', @@ -8,35 +10,35 @@ function $TimeoutProvider() { var deferreds = {}; - /** - * @ngdoc service - * @name $timeout - * - * @description - * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch - * block and delegates any exceptions to - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * The return value of calling `$timeout` is a promise, which will be resolved when - * the delay has passed and the timeout function, if provided, is executed. - * - * To cancel a timeout request, call `$timeout.cancel(promise)`. - * - * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to - * synchronously flush the queue of deferred functions. - * - * If you only want a promise that will be resolved after some specified delay - * then you can call `$timeout` without the `fn` function. - * - * @param {function()=} fn A function, whose execution should be delayed. - * @param {number=} [delay=0] Delay in milliseconds. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise - * will be resolved with the return value of the `fn` function. - * - */ + /** + * @ngdoc service + * @name $timeout + * + * @description + * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch + * block and delegates any exceptions to + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * The return value of calling `$timeout` is a promise, which will be resolved when + * the delay has passed and the timeout function, if provided, is executed. + * + * To cancel a timeout request, call `$timeout.cancel(promise)`. + * + * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to + * synchronously flush the queue of deferred functions. + * + * If you only want a promise that will be resolved after some specified delay + * then you can call `$timeout` without the `fn` function. + * + * @param {function()=} fn A function, whose execution should be delayed. + * @param {number=} [delay=0] Delay in milliseconds. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise + * will be resolved with the return value of the `fn` function. + * + */ function timeout(fn, delay, invokeApply) { if (!isFunction(fn)) { invokeApply = delay; @@ -70,27 +72,37 @@ function $TimeoutProvider() { } - /** - * @ngdoc method - * @name $timeout#cancel - * - * @description - * Cancels a task associated with the `promise`. As a result of this, the promise will be - * resolved with a rejection. - * - * @param {Promise=} promise Promise returned by the `$timeout` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ + /** + * @ngdoc method + * @name $timeout#cancel + * + * @description + * Cancels a task associated with the `promise`. As a result of this, the promise will be + * resolved with a rejection. + * + * @param {Promise=} promise Promise returned by the `$timeout` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ timeout.cancel = function(promise) { - if (promise && promise.$$timeoutId in deferreds) { - // Timeout cancels should not report an unhandled promise. - markQExceptionHandled(deferreds[promise.$$timeoutId].promise); - deferreds[promise.$$timeoutId].reject('canceled'); - delete deferreds[promise.$$timeoutId]; - return $browser.defer.cancel(promise.$$timeoutId); + if (!promise) return false; + + if (!promise.hasOwnProperty('$$timeoutId')) { + throw $timeoutMinErr('badprom', + '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.'); } - return false; + + if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false; + + var id = promise.$$timeoutId; + var deferred = deferreds[id]; + + // Timeout cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + delete deferreds[id]; + + return $browser.defer.cancel(id); }; return timeout; diff --git a/test/ng/intervalSpec.js b/test/ng/intervalSpec.js index 47281429e0b2..3b23250d1f98 100644 --- a/test/ng/intervalSpec.js +++ b/test/ng/intervalSpec.js @@ -335,12 +335,17 @@ describe('$interval', function() { })); - it('should not throw a runtime exception when given an undefined promise', - inject(function($interval) { + it('should not throw an error when given an undefined promise', inject(function($interval) { expect($interval.cancel()).toBe(false); })); + it('should throw an error when given a non-$interval promise', inject(function($interval) { + var promise = $interval(noop).then(noop); + expect(function() { $interval.cancel(promise); }).toThrowMinErr('$interval', 'badprom'); + })); + + it('should not trigger digest when cancelled', inject(function($interval, $rootScope, $browser) { var watchSpy = jasmine.createSpy('watchSpy'); $rootScope.$watch(watchSpy); diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js index 648c39663c0d..bfd5d53285e7 100644 --- a/test/ng/timeoutSpec.js +++ b/test/ng/timeoutSpec.js @@ -280,11 +280,17 @@ describe('$timeout', function() { })); - it('should not throw a runtime exception when given an undefined promise', inject(function($timeout) { + it('should not throw an error when given an undefined promise', inject(function($timeout) { expect($timeout.cancel()).toBe(false); })); + it('should throw an error when given a non-$timeout promise', inject(function($timeout) { + var promise = $timeout(noop).then(noop); + expect(function() { $timeout.cancel(promise); }).toThrowMinErr('$timeout', 'badprom'); + })); + + it('should forget references to relevant deferred', inject(function($timeout, $browser) { // $browser.defer.cancel is only called on cancel if the deferred object is still referenced var cancelSpy = spyOn($browser.defer, 'cancel').and.callThrough();