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

Commit aa2f015

Browse files
committed
feat(shutdown): Add the ability for an app to shutdown
Adds a new `$shutdown` service that can be used to shutdown an app and the `$shutdownProvider` that can be used to register tasks that need to be executed when shutting down an app
1 parent a478f69 commit aa2f015

11 files changed

+355
-135
lines changed

src/.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"angularInit": false,
8787
"bootstrap": false,
8888
"getTestability": false,
89+
"shutdown": false,
8990
"snake_case": false,
9091
"bindJQuery": false,
9192
"assertArg": false,

src/Angular.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -1692,7 +1692,7 @@ function bootstrap(element, modules, config) {
16921692

16931693
modules = modules || [];
16941694
modules.unshift(['$provide', function($provide) {
1695-
$provide.value('$rootElement', element);
1695+
$provide.provider('$rootElement', rootElementProviderFactory(element));
16961696
}]);
16971697

16981698
if (config.debugInfoEnabled) {
@@ -1772,6 +1772,48 @@ function getTestability(rootElement) {
17721772
return injector.get('$$testability');
17731773
}
17741774

1775+
function rootElementProviderFactory(rootElement) {
1776+
return ['$shutdownProvider', function($shutdownProvider) {
1777+
$shutdownProvider.register(function() {
1778+
if (rootElement.dealoc) {
1779+
rootElement.dealoc();
1780+
} else {
1781+
rootElement.find('*').removeData();
1782+
rootElement.removeData();
1783+
}
1784+
});
1785+
this.$get = function() {
1786+
return rootElement;
1787+
};
1788+
}];
1789+
}
1790+
1791+
function $ShutDownProvider() {
1792+
var fns = [];
1793+
this.$get = function() {
1794+
return function() {
1795+
while (fns.length) {
1796+
var fn = fns.shift();
1797+
fn();
1798+
}
1799+
};
1800+
};
1801+
1802+
this.register = function(fn) {
1803+
fns.push(fn);
1804+
};
1805+
}
1806+
1807+
function shutdown(element) {
1808+
var injector;
1809+
1810+
injector = angular.element(element).injector();
1811+
if (!injector) {
1812+
throw ngMinErr('shtdwn', 'Element not part of an app');
1813+
}
1814+
injector.get('$shutdown')();
1815+
}
1816+
17751817
var SNAKE_CASE_REGEXP = /[A-Z]/g;
17761818
function snake_case(name, separator) {
17771819
separator = separator || '_';

src/AngularPublic.js

+4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
$$SanitizeUriProvider,
8787
$SceProvider,
8888
$SceDelegateProvider,
89+
$ShutDownProvider,
8990
$SnifferProvider,
9091
$TemplateCacheProvider,
9192
$TemplateRequestProvider,
@@ -125,6 +126,7 @@ var version = {
125126
function publishExternalAPI(angular) {
126127
extend(angular, {
127128
'bootstrap': bootstrap,
129+
'shutdown': shutdown,
128130
'copy': copy,
129131
'extend': extend,
130132
'merge': merge,
@@ -160,6 +162,8 @@ function publishExternalAPI(angular) {
160162

161163
angularModule('ng', ['ngLocale'], ['$provide',
162164
function ngModule($provide) {
165+
// $shutdown provider needs to be first as other providers might use it.
166+
$provide.provider('$shutdown', $ShutDownProvider);
163167
// $$sanitizeUriProvider needs to be before $compileProvider as it is used by it.
164168
$provide.provider({
165169
$$sanitizeUri: $$SanitizeUriProvider

src/ng/browser.js

+72-19
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ function Browser(window, document, $log, $sniffer) {
2828
history = window.history,
2929
setTimeout = window.setTimeout,
3030
clearTimeout = window.clearTimeout,
31-
pendingDeferIds = {};
31+
setInterval = window.setInterval,
32+
clearInterval = window.clearInterval,
33+
pendingDeferIds = {},
34+
currentIntervalIds = {},
35+
active = true;
3236

3337
self.isMock = false;
3438

@@ -79,6 +83,15 @@ function Browser(window, document, $log, $sniffer) {
7983
}
8084
};
8185

86+
self.shutdown = function() {
87+
active = false;
88+
forEach(currentIntervalIds, function(ignore, intervalId) {
89+
delete currentIntervalIds[intervalId];
90+
clearInterval(+intervalId);
91+
});
92+
jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange);
93+
};
94+
8295
//////////////////////////////////////////////////////////////
8396
// URL API
8497
//////////////////////////////////////////////////////////////
@@ -270,16 +283,6 @@ function Browser(window, document, $log, $sniffer) {
270283
return callback;
271284
};
272285

273-
/**
274-
* @private
275-
* Remove popstate and hashchange handler from window.
276-
*
277-
* NOTE: this api is intended for use only by $rootScope.
278-
*/
279-
self.$$applicationDestroyed = function() {
280-
jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange);
281-
};
282-
283286
/**
284287
* Checks whether the url has changed outside of Angular.
285288
* Needs to be exported to be able to check for changes that have been done in sync,
@@ -321,12 +324,16 @@ function Browser(window, document, $log, $sniffer) {
321324
*/
322325
self.defer = function(fn, delay) {
323326
var timeoutId;
324-
outstandingRequestCount++;
325-
timeoutId = setTimeout(function() {
326-
delete pendingDeferIds[timeoutId];
327-
completeOutstandingRequest(fn);
328-
}, delay || 0);
329-
pendingDeferIds[timeoutId] = true;
327+
if (active) {
328+
outstandingRequestCount++;
329+
timeoutId = setTimeout(function() {
330+
delete pendingDeferIds[timeoutId];
331+
completeOutstandingRequest(fn);
332+
}, delay || 0);
333+
pendingDeferIds[timeoutId] = true;
334+
} else {
335+
timeoutId = 0;
336+
}
330337
return timeoutId;
331338
};
332339

@@ -351,11 +358,57 @@ function Browser(window, document, $log, $sniffer) {
351358
return false;
352359
};
353360

361+
362+
/**
363+
* @name $browser#interval
364+
* @param {function()} fn A function, who's execution should be executed.
365+
* @param {number=} interval in milliseconds on how often to execute the function.
366+
* @returns {*} IntervalId that can be used to cancel the task via `$browser.interval.cancel()`.
367+
*
368+
* @description
369+
* Executes a fn asynchronously via `setInterval(fn, interval)`.
370+
*
371+
*/
372+
self.interval = function(fn, interval) {
373+
var intervalId;
374+
if (active) {
375+
intervalId = setInterval(fn, interval);
376+
currentIntervalIds[intervalId] = true;
377+
} else {
378+
intervalId = 0;
379+
}
380+
return intervalId;
381+
};
382+
383+
384+
/**
385+
* @name $browser#interval.cancel
386+
*
387+
* @description
388+
* Cancels a interval task identified with `intervalId`.
389+
*
390+
* @param {*} intervalId Token returned by the `$browser.interval` function.
391+
* @returns {boolean} Returns `true` if the task was successfully canceled, and
392+
* `false` if the task was already canceled.
393+
*/
394+
self.interval.cancel = function(intervalId) {
395+
if (currentIntervalIds[intervalId]) {
396+
delete currentIntervalIds[intervalId];
397+
clearInterval(intervalId);
398+
return true;
399+
}
400+
return false;
401+
};
354402
}
355403

356-
function $BrowserProvider() {
404+
function $BrowserProvider($shutdownProvider) {
405+
var browser;
406+
407+
$shutdownProvider.register(function() { if (browser) { browser.shutdown(); } });
357408
this.$get = ['$window', '$log', '$sniffer', '$document',
358409
function($window, $log, $sniffer, $document) {
359-
return new Browser($window, $document, $log, $sniffer);
410+
return browser = new Browser($window, $document, $log, $sniffer);
360411
}];
361412
}
413+
414+
$BrowserProvider.$inject = ['$shutdownProvider'];

src/ng/interval.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,14 @@ function $IntervalProvider() {
135135
function interval(fn, delay, count, invokeApply) {
136136
var hasParams = arguments.length > 4,
137137
args = hasParams ? sliceArgs(arguments, 4) : [],
138-
setInterval = $window.setInterval,
139-
clearInterval = $window.clearInterval,
140138
iteration = 0,
141139
skipApply = (isDefined(invokeApply) && !invokeApply),
142140
deferred = (skipApply ? $$q : $q).defer(),
143141
promise = deferred.promise;
144142

145143
count = isDefined(count) ? count : 0;
146144

147-
promise.$$intervalId = setInterval(function tick() {
145+
promise.$$intervalId = $browser.interval(function tick() {
148146
if (skipApply) {
149147
$browser.defer(callback);
150148
} else {
@@ -154,7 +152,7 @@ function $IntervalProvider() {
154152

155153
if (count > 0 && iteration >= count) {
156154
deferred.resolve(iteration);
157-
clearInterval(promise.$$intervalId);
155+
$browser.interval.cancel(promise.$$intervalId);
158156
delete intervals[promise.$$intervalId];
159157
}
160158

@@ -191,7 +189,7 @@ function $IntervalProvider() {
191189
// Interval cancels should not report as unhandled promise.
192190
intervals[promise.$$intervalId].promise.catch(noop);
193191
intervals[promise.$$intervalId].reject('canceled');
194-
$window.clearInterval(promise.$$intervalId);
192+
$browser.interval.cancel(promise.$$intervalId);
195193
delete intervals[promise.$$intervalId];
196194
return true;
197195
}

src/ng/rootScope.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,12 @@
6767
* They also provide event emission/broadcast and subscription facility. See the
6868
* {@link guide/scope developer guide on scopes}.
6969
*/
70-
function $RootScopeProvider() {
70+
function $RootScopeProvider($shutdownProvider) {
7171
var TTL = 10;
7272
var $rootScopeMinErr = minErr('$rootScope');
7373
var lastDirtyWatch = null;
7474
var applyAsyncId = null;
75+
var $rootScope;
7576

7677
this.digestTtl = function(value) {
7778
if (arguments.length) {
@@ -94,8 +95,10 @@ function $RootScopeProvider() {
9495
return ChildScope;
9596
}
9697

97-
this.$get = ['$exceptionHandler', '$parse', '$browser',
98-
function($exceptionHandler, $parse, $browser) {
98+
$shutdownProvider.register(function() { if ($rootScope) { $rootScope.$destroy(); } });
99+
100+
this.$get = ['$exceptionHandler', '$parse', '$browser', '$shutdown',
101+
function($exceptionHandler, $parse, $browser, $shutdown) {
99102

100103
function destroyChildScope($event) {
101104
$event.currentScope.$$destroyed = true;
@@ -907,8 +910,7 @@ function $RootScopeProvider() {
907910
this.$$destroyed = true;
908911

909912
if (this === $rootScope) {
910-
//Remove handlers attached to window when $rootScope is removed
911-
$browser.$$applicationDestroyed();
913+
$shutdown();
912914
}
913915

914916
incrementWatchersCount(this, -this.$$watchersCount);
@@ -1308,7 +1310,7 @@ function $RootScopeProvider() {
13081310
}
13091311
};
13101312

1311-
var $rootScope = new Scope();
1313+
$rootScope = new Scope();
13121314

13131315
//The internal queues. Expose them on the $rootScope for debugging/testing purposes.
13141316
var asyncQueue = $rootScope.$$asyncQueue = [];
@@ -1374,3 +1376,6 @@ function $RootScopeProvider() {
13741376
}
13751377
}];
13761378
}
1379+
1380+
$RootScopeProvider.$inject = ['$shutdownProvider'];
1381+

src/ngMock/angular-mocks.js

+55-6
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ angular.mock = {};
2323
* The api of this service is the same as that of the real {@link ng.$browser $browser}, except
2424
* that there are several helper methods available which can be used in tests.
2525
*/
26-
angular.mock.$BrowserProvider = function() {
26+
angular.mock.$BrowserProvider = function($shutdownProvider) {
27+
var browser;
28+
29+
$shutdownProvider.register(function() { if (browser) { browser.shutdown(); } });
2730
this.$get = function() {
28-
return new angular.mock.$Browser();
31+
return browser = new angular.mock.$Browser();
2932
};
3033
};
3134

35+
angular.mock.$BrowserProvider.$inject = ['$shutdownProvider'];
36+
3237
angular.mock.$Browser = function() {
3338
var self = this;
3439

@@ -135,6 +140,52 @@ angular.mock.$Browser = function() {
135140
self.baseHref = function() {
136141
return this.$$baseHref;
137142
};
143+
144+
self.interval = function(fn, interval) {
145+
self.interval.repeatFns.push({
146+
nextTime:(self.interval.now + interval),
147+
delay: interval,
148+
fn: fn,
149+
id: self.interval.nextRepeatId
150+
});
151+
self.interval.repeatFns.sort(function(a,b) { return a.nextTime - b.nextTime;});
152+
153+
return self.interval.nextRepeatId++;
154+
};
155+
156+
self.interval.cancel = function(id) {
157+
var fnIndex;
158+
159+
angular.forEach(self.interval.repeatFns, function(fn, index) {
160+
if (fn.id === id) fnIndex = index;
161+
});
162+
163+
if (fnIndex !== undefined) {
164+
self.interval.repeatFns.splice(fnIndex, 1);
165+
return true;
166+
}
167+
168+
return false;
169+
};
170+
171+
self.interval.flush = function(millis) {
172+
self.interval.now += millis;
173+
while (self.interval.repeatFns.length &&
174+
self.interval.repeatFns[0].nextTime <= self.interval.now) {
175+
var task = self.interval.repeatFns[0];
176+
task.fn();
177+
task.nextTime += task.delay;
178+
self.interval.repeatFns.sort(function(a,b) { return a.nextTime - b.nextTime;});
179+
}
180+
return millis;
181+
};
182+
183+
self.interval.repeatFns = [];
184+
self.interval.nextRepeatId = 0;
185+
self.interval.now = 0;
186+
187+
self.shutdown = angular.noop;
188+
138189
};
139190
angular.mock.$Browser.prototype = {
140191

@@ -2911,10 +2962,8 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
29112962
}
29122963
angular.element.cleanData(cleanUpNodes);
29132964

2914-
// Ensure `$destroy()` is available, before calling it
2915-
// (a mocked `$rootScope` might not implement it (or not even be an object at all))
2916-
var $rootScope = injector.get('$rootScope');
2917-
if ($rootScope && $rootScope.$destroy) $rootScope.$destroy();
2965+
var $shutdown = injector.get('$shutdown');
2966+
if ($shutdown) $shutdown();
29182967
}
29192968

29202969
// clean up jquery's fragment cache

0 commit comments

Comments
 (0)