emberjs / ember.js

Ember.js - A JavaScript framework for creating ambitious web applications
https://emberjs.com
MIT License
22.47k stars 4.21k forks source link

Router.map callbacks hung off Router constructor #13840

Open jasonmit opened 8 years ago

jasonmit commented 8 years ago

Router.map hangs the callback functions it receives within Router.constructor.dslCallbacks. In a traditional client-side rendered Ember-world, this is harmless.

However, when you take into account FastBoot, this becomes unsafe if you call Router.map multiple times within your application those functions can leak across instances. A particular use-case is we registered routes dynamically depending on user state and we do this inside of an instance-initializer.

This isn't a terribly uncommon pattern; example is someone who wants to register routes in the language of the user. This is likely the place where that would happen.

Where this becomes a problem in FastBoot is each time a request is made the callback is pushed to a property on the constructor and then enumerated over and invoked. So for each request, a new callback is pushed into the array of callbacks and no clean up takes place (a memory leak and unsafe where functions from previous app instances are invoked).

https://github.com/emberjs/ember.js/blob/master/packages/ember-routing/lib/system/router.js#L89-L96

Are the lines in question and so I was wondering if anyone would be opened to me moving these callbacks elsewhere, such as the application instance. This will have larger ramifications, so would like to gather interest or feedback first.

nathanhammond commented 8 years ago

Yep, definitely an issue. This is made more frustrating by the many layers everything here has to go through in order to function the way Ember would like. If you can come up with a quick patch I would be happy to review it and we would likely land it. Depending upon the complexity/invasiveness the core team will decide what branch (canary, beta, release) to target. Probably a late addition to the 2.8.0-beta.X series of which the first one is planned for next week.

If it becomes too ridiculous the RouterDSL is on my list of things to rework after finishing route-recognizer.

jasonmit commented 8 years ago

@nathanhammond right now I don't have a good idea on how I could patch it while keeping the API intact. I was thinking of perhaps exposing a new API for registering callbacks that are concatted with constructor.dslCallbacks during _initRouterJs.

Let me know if you have any ideas and feel free to ping me on Slack.

nathanhammond commented 8 years ago

Everything in that area should be considered private API. I don't believe we need to maintain API consistency in addressing this bug. (Somebody else please chime in and correct me if I'm wrong.)

The only addon I know of which really abuses this portion of the router is one of mine, ember-route-alias, and even I don't touch it.

jasonmit commented 8 years ago

I meant to say I don't know of a way of resolving this without implementing a new API for registering routes designed to be used at runtime instead of going through the constructor method. I can keep the existing API intact but the new way might look similar to exporting a function from router.js.

nathanhammond commented 8 years ago

You could change the way we save off those references such that they're captured in a closure that is only accessible during that run? I think that should be possible.

jasonmit commented 8 years ago

We spoke offline, the outcome was to spike two possible solutions and discuss.

jasonmit commented 8 years ago

/cc @wycats

Based on our discussion today. Feel free to add your input since you floated a few ideas, the core of the issue being the need for Router.map's callback to handle conditional logic and a similar concept of needs.

jasonmit commented 8 years ago

This is what I've ended up with to remove the need of calling Router.map inside of an instance initializer.

https://github.com/jasonmit/ember-routermap-inject-service

It's obviously touching private APIs, and I'd be happy to land something like this in the API if others feel it might be useful.

Another option might be exposing a service that enables registering routes at app runtime safely. This would end up replacing Router.map.

// app/services/router.js
export default EmberRouter.extend({
  session: inject.service().
  map() {
    let session = this.get('session');

    if (session.get('isAdmin')) {
      this.route('admin', { path: '/' });
    } else {
      this.route('home', { path: '/' });
    }

    this.route('about', { path: '/' + session.translate('urls.about') });
    this.route('contact', { path: '/' + session.translate('urls.contact') });
  }
});
nathanhammond commented 8 years ago

So what you've done is make sure that each invocation is identical, allowing Ember to continue to hang its callback functions off of Router.constructor.dslCallbacks. I feel like this is a good workaround, but doesn't solve the underlying problem.

I feel like we should go ahead and fix this at the fundamental level of making sure we clean up after ourselves instead of working around it. @jasonmit did you attempt to head down that path as well before going with the addition to the RouterDSL prototype?

jasonmit commented 8 years ago

@nathanhammond no it certainly doesn't fix the issue at the core, it does though eliminate having to invoke Router.map with the app instance in scope though. I did chat with @wycats and what he said was basically the ability to needs avoids surfacing this bug.

That said, the way I would fix this at the core boils down to never hanging the callback off the constructor for later access. Instead, just resolve and invoke a module which exports a function, the Router.map callback, per-instance of the app that is booted. If this approach is fine with everyone, I'd be happy to send a PR.

nathanhammond commented 8 years ago

@jasonmit I'd like to see that!

rwjblue commented 8 years ago

FWIW, we use that same pattern for engines. ember-engines uses addon/routes.js that just exports the callback.

jasonmit commented 8 years ago

@rwjblue noticed this while researching the bug. I'll be sure to piggyback off that work versus reinventing the wheel.

pixelhandler commented 6 years ago

@jasonmit @rwjblue @nathanhammond is this still an issue, perhaps we should close; what do you think?

jasonmit commented 6 years ago

@pixelhandler issue is still active

wycats commented 6 years ago

@tomdale I agree that this is still an active issue, and relevant to FastBoot. Any thoughts?

rwjblue commented 5 years ago

We need to add an assertion when Router.map is called after an ApplicationInstance instance is created.