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

feat(shutdown): Add the ability for an app to shutdown #8005

Closed
wants to merge 1 commit into from

Conversation

lgalfaso
Copy link
Contributor

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.

Added tasks for $rootElement, $rootScope and $browser to be able to shutdown an app

Refactor $interval so it delegates to $browser just like $timeout does

@mary-poppins
Copy link

Thanks for the PR! Please check the items below to help us merge this faster. See the contributing docs for more information.

  • Uses the issue template (#8005)

If you need to make changes to your pull request, you can update the commit with git commit --amend.
Then, update the pull request with git push -f.

Thanks again for your help!

@lgalfaso lgalfaso added cla: yes and removed cla: no labels Jun 27, 2014
expect(element.text()).toBe('Bootstrap me!');
});

it('should throw if trying to bootstrap an element that is not part of an app', function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be "to shutdown"

@Narretz Narretz added this to the Backlog milestone Jul 16, 2014
function rootElementProviderFactory(rootElement) {
return ['$shutdownProvider', function($shutdownProvider) {
$shutdownProvider.register(function() {
if (rootElement.dealoc) rootElement.dealoc();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this jQuery specific?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rootElement.dealoc() is the jqLite specific way to recursively remove the data from an element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This api was removed from master recently in 9c5b407.

so the truthy branch should be removed. removeData() will do the job.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no way to say "recursively remove all the data" (well, not without manually traversing the tree). In jQuery this is possible doing rootElement.find('*').removeData(), but not with jqLite.
Will write a manual traversing and repost the PR.

@JohannUlbrich
Copy link

The possibility to shutdown an Angular app is still a big issue for me. Are there any plans to include this in Angular 1.4?

@gkalpak
Copy link
Member

gkalpak commented Jun 1, 2016

@lgalfaso, what is the status of this ?

@gkalpak
Copy link
Member

gkalpak commented Jun 1, 2016

Fixes #7578

@lgalfaso
Copy link
Contributor Author

lgalfaso commented Jun 1, 2016

This is quite old but the approach still valid. Can work on getting this up
to date so it can land of the need is still needed

On Wednesday, June 1, 2016, Georgios Kalpakas [email protected]
wrote:

@lgalfaso https://github.com/lgalfaso, what is the status of this ?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#8005 (comment),
or mute the thread
https://github.com/notifications/unsubscribe/AAX44oIWPieqwtvK7iwNFOZtdg_Q2treks5qHYBagaJpZM4CIKVw
.

@lgalfaso lgalfaso force-pushed the shutdown branch 2 times, most recently from ae83ce2 to f77e965 Compare June 3, 2016 18:20
@lgalfaso
Copy link
Contributor Author

lgalfaso commented Jun 3, 2016

Updated the PR. @gkalpak do you want to take a look at it?

if (rootElement.dealoc) {
rootElement.dealoc();
} else {
rootElement.find('*').removeData();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a common case for sure, but this would also "clean" inside [ng-non-bindable] element (where theoretically could leave other angular apps).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there is an outer and an inner app, and the outer app is shutdown (remember that this will trigger a $destroy event on $rootScope), I would expect that it is reasonable for the inner app to also get destroyed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it shouldn't. These are independent apps, so destroying the outer, shouldn't destroy the inner. To be clear, this is what I am talking about:

<body app1>
  Hello from {{ 'app 1' }}!
  <div ng-non-bindable>
    <div app2>
      Hello from {{ 'app 2' }}!
    </div>
  </div>
</body>
angular.module('app1', []);
angular.module('app2', []);

angular.bootstrap(document.querySelector('[app1]'), ['app1']);
angular.bootstrap(document.querySelector('[app2]'), ['app2']);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is currently not possible, as in cases like this we do not keep track of what elements belong to app1 and what elements belong to app2

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to ignore anything under ng-non-bindable (because we haven't touched it anyway).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like this (which is not super efficient, but shouldn't really matter, when shutting down):

var unvisitedNodes = [$rootElement[0]];
var foundNodes = [$rootElement[0]];
var node;

while ((node = unvisitedNodes.pop())) {
  var newNodes = Array.prototype.filter.call(node.children, function (child) {
    return directiveNormalize(nodeName_(child)) !== 'ngNonBindable';
  });
  Array.prototype.push.apply(unvisitedNodes, newNodes);
  Array.prototype.push.apply(foundNodes, newNodes);
}

jqLite.cleanData(foundNodes);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gkalpak if we want to support nested ng-apps in ng-non-bindable containers perhaps we should add at least basic tests verifying that it works?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps. But basic tests wouldn't help much (I mean even if basic tests worked, some less basic code could still be broken). The question is whether we want to break this usecase or not (I still wouldn't).

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
@lgalfaso
Copy link
Contributor Author

@gkalpak PTAL

@@ -907,8 +911,7 @@ function $RootScopeProvider() {
this.$$destroyed = true;

if (this === $rootScope) {
//Remove handlers attached to window when $rootScope is removed
$browser.$$applicationDestroyed();
$shutdown();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we need this, now that $shutdown is a public service.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gkalpak What do you mean?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's been a while, but what I think I meant that this was a poor man's $shutdown(). Before $shutdown() was implemented, we were special-casing $rootScope.$destroy() to do destroy the application (because $browser.$$applicationDestroyed() was private). But now that $shutdown() is a public service, people wanting to destroy the app can (and should) use that (instead of destroying $rootScope.

It doesn't seem too harmful though, so we might as well keep it to avoid the breaking change.

@gkalpak
Copy link
Member

gkalpak commented Jun 25, 2016

Generally LGTM!

I have left a couple minor comments (mainly related to cleaning up $rootElement).
I think there are also a couple more things to clean up (or take care of in a way that they will get cleaned up automatically):

  • Cancel (or at least ignore when completed) pending XHR/JSONP requests.
  • Clean up JSONP callbacks (stored on angular.windows.callbacks)[*].
  • Clean up listeners added with addEventListener on form/ngForm elements.
  • Clean up listeners on $document (e.g. .on('visibilitychange')).

*: I just realized that bootstrapping multiple apps on the same page, could mess up the JSONP callbacks, since each app will generate the same callback IDs.

@lgalfaso
Copy link
Contributor Author

Cancel (or at least ignore when completed) pending XHR/JSONP requests.

I think that if we ignore $digest on destroyed scopes (I know this is a BC), this should be enough

Clean up JSONP callbacks (stored on angular.windows.callbacks)[*].

Oh, another part that we do not keep track from what app each callback comes... anyhow the same solution to ignore $digest should work

Clean up listeners added with addEventListener on form/ngForm elements.

We should be fine here as we trigger $destroy

Clean up listeners on $document (e.g. .on('visibilitychange')).

We should be fine here as we trigger $destroy

@gkalpak
Copy link
Member

gkalpak commented Jun 28, 2016

Cancel (or at least ignore when completed) pending XHR/JSONP requests.

I think that if we ignore $digest on destroyed scopes (I know this is a BC), this should be enough

We already keep track of the pending requests in $http (although only the configs). How about we start keeping track of active XHRs instances as well and have $http register with $shutdownProvider to abort them on shut-down?

Clean up JSONP callbacks (stored on angular.windows.callbacks)[*].

Oh, another part that we do not keep track from what app each callback comes... anyhow the same solution to ignore $digest should work

Now that we have a $jsonpCallbacks service, we could keep track of the added callback IDs and have $jsonpCallbacks delete them on shut-down.

Clean up listeners added with addEventListener on form/ngForm elements.

We should be fine here as we trigger $destroy

Good point. Missed that 😃

Clean up listeners on $document (e.g. .on('visibilitychange')).

We should be fine here as we trigger $destroy

Are we calling $destroy on $document?

rootElement.dealoc();
} else {
rootElement.find('*').removeData();
rootElement.removeData();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not enough as it won't remove event listeners. How about:

jqLite.cleanData(rootElement.find('*'));
jqLite.cleanData(rootElement);

or something analogous to @gkalpak's code but using cleanData if we want to not break ng-non-bindable-contained AngularJS apps.

This, BTW, made me notice that jqLite's implementation of removeData is really broken. It should only remove user-set data and not touch the event handlers storage at all. I reported #15869 to fix that in 1.7.

}, delay || 0);
pendingDeferIds[timeoutId] = true;
} else {
timeoutId = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this line?

});
self.interval.repeatFns.sort(function(a,b) { return a.nextTime - b.nextTime;});

return self.interval.nextRepeatId++;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please separate this into two instructions; ease of reading is more important than being shorter, especially in mocks code.

var task = self.interval.repeatFns[0];
task.fn();
task.nextTime += task.delay;
self.interval.repeatFns.sort(function(a,b) { return a.nextTime - b.nextTime;});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? The queue is sorted during callback registration already.

@@ -1671,6 +1671,110 @@ describe('angular', function() {
});
});

describe('shutdown', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test that event handlers are removed as well (current code doesn't do that when jQuery is used).

@mgol
Copy link
Member

mgol commented Jun 20, 2018

This is a pretty big change which has a potential of introducing regressions. Considering that and taking into account the fact that we're getting close to the LTS phase when only a few kinds of issues get fixed (like security ones) we've decided to not pursue it further.

@mgol mgol closed this Jun 20, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants