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

Commit 4e9b568

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 4e9b568

11 files changed

+370
-137
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

+76-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,19 @@ 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+
forEach(pendingDeferIds, function(ignore, timeoutId) {
93+
delete pendingDeferIds[timeoutId];
94+
clearTimeout(+timeoutId);
95+
});
96+
jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange);
97+
};
98+
8299
//////////////////////////////////////////////////////////////
83100
// URL API
84101
//////////////////////////////////////////////////////////////
@@ -270,16 +287,6 @@ function Browser(window, document, $log, $sniffer) {
270287
return callback;
271288
};
272289

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-
283290
/**
284291
* Checks whether the url has changed outside of Angular.
285292
* Needs to be exported to be able to check for changes that have been done in sync,
@@ -321,12 +328,16 @@ function Browser(window, document, $log, $sniffer) {
321328
*/
322329
self.defer = function(fn, delay) {
323330
var timeoutId;
324-
outstandingRequestCount++;
325-
timeoutId = setTimeout(function() {
326-
delete pendingDeferIds[timeoutId];
327-
completeOutstandingRequest(fn);
328-
}, delay || 0);
329-
pendingDeferIds[timeoutId] = true;
331+
if (active) {
332+
outstandingRequestCount++;
333+
timeoutId = setTimeout(function() {
334+
delete pendingDeferIds[timeoutId];
335+
completeOutstandingRequest(fn);
336+
}, delay || 0);
337+
pendingDeferIds[timeoutId] = true;
338+
} else {
339+
timeoutId = 0;
340+
}
330341
return timeoutId;
331342
};
332343

@@ -351,11 +362,57 @@ function Browser(window, document, $log, $sniffer) {
351362
return false;
352363
};
353364

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

356-
function $BrowserProvider() {
408+
function $BrowserProvider($shutdownProvider) {
409+
var browser;
410+
411+
$shutdownProvider.register(function() { if (browser) { browser.shutdown(); } });
357412
this.$get = ['$window', '$log', '$sniffer', '$document',
358413
function($window, $log, $sniffer, $document) {
359-
return new Browser($window, $document, $log, $sniffer);
414+
return browser = new Browser($window, $document, $log, $sniffer);
360415
}];
361416
}
417+
418+
$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,13 @@
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+
$RootScopeProvider.$inject = ['$shutdownProvider'];
71+
function $RootScopeProvider($shutdownProvider) {
7172
var TTL = 10;
7273
var $rootScopeMinErr = minErr('$rootScope');
7374
var lastDirtyWatch = null;
7475
var applyAsyncId = null;
76+
var $rootScope;
7577

7678
this.digestTtl = function(value) {
7779
if (arguments.length) {
@@ -94,8 +96,10 @@ function $RootScopeProvider() {
9496
return ChildScope;
9597
}
9698

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

100104
function destroyChildScope($event) {
101105
$event.currentScope.$$destroyed = true;
@@ -907,8 +911,7 @@ function $RootScopeProvider() {
907911
this.$$destroyed = true;
908912

909913
if (this === $rootScope) {
910-
//Remove handlers attached to window when $rootScope is removed
911-
$browser.$$applicationDestroyed();
914+
$shutdown();
912915
}
913916

914917
incrementWatchersCount(this, -this.$$watchersCount);
@@ -1308,7 +1311,7 @@ function $RootScopeProvider() {
13081311
}
13091312
};
13101313

1311-
var $rootScope = new Scope();
1314+
$rootScope = new Scope();
13121315

13131316
//The internal queues. Expose them on the $rootScope for debugging/testing purposes.
13141317
var asyncQueue = $rootScope.$$asyncQueue = [];
@@ -1374,3 +1377,5 @@ function $RootScopeProvider() {
13741377
}
13751378
}];
13761379
}
1380+
1381+

0 commit comments

Comments
 (0)