Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix validation stage redirection behaviour. #197

Conversation

alexspeller
Copy link
Contributor

@alexspeller alexspeller commented Oct 28, 2016

This has been broken forever AFAIK, and is surprising to a lot of people. In
fact, even the ember guides recommend using this.transitionTo for redirecting

This in fact does not work. If you use transitionTo as a redirect strategy,
then you get an extra history entry if you hit the route as the first route in
your app. This breaks the back button, as the back button takes you to the route
with the redirect, then immediately redirects back to the page you're on.
Maybe you can go back if you hammer back button really quickly, but you have to
hammer it loads super quick. Not a good UX.

replaceWith works if you use it to redirect from a route that's your
initial transition. However if you use it to redirect and you hit that route
from some way after the initial app load, then at the point that the
replaceWith is called, you are still on the same URL. For example, if you are on
/ and you click a link to /foo, which in it's model hook redirects to /bar
using replaceWith, at the time replaceWith is called, your current url is /.

This means / entry gets removed from your history entirely. Clicking back
will take you back to whatever page you were on before /, which often isn't
even your app, maybe it's google or something. This breaks the back button
again.

This commit should do the correct thing in all cases, allowing replaceWith and
transitionTo outside of redirects as specified by the developer but only allowing
transitionTo or replaceWith in redirects in a way that doesn't break the back
button.

@alexspeller
Copy link
Contributor Author

@stefanpenner you can retire ember-redirect-to now :)

Copy link
Contributor

@trentmwillis trentmwillis left a comment

Choose a reason for hiding this comment

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

Some minor things, but overall this seems solid. I appreciate the detail put into it.

// in the initial transition also need to know if they are part of the
// initial transition
newTransition.isCausedByInitialTransition = this.activeTransition.isCausedByInitialTransition|| this.activeTransition.sequence === 0;
newTransition.isCausedByAbortingTransition = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we set these in the Transition constructor? At the very least, we should set them to undefined in the constructor to avoid mutating the shape of the transition object.

// is actually part of the first transition or not. Any further redirects
// in the initial transition also need to know if they are part of the
// initial transition
newTransition.isCausedByInitialTransition = this.activeTransition.isCausedByInitialTransition|| this.activeTransition.sequence === 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: Space before the ||

router.replaceURL(url);
} else {
router.updateURL(url);
if (urlMethod === 'replace' && !aborting) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unsure how others will feel about this, but I'd prefer doing:

if (initial) {
} else if (urlMethod === 'replace' && !aborting) {
} else {
}

Instead of the nesting you currently have.

@@ -3327,6 +3329,672 @@ test("intermediateTransitionTo() forces an immediate intermediate transition tha
counterAt(7, "original transition promise resolves");
});


test("Calling transitionTo during initial transition in validation hook should use replaceURL", function(assert) {
Transition.currentSequence = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Tangential to this PR, we should probably store this on the Router instance if possible.

@alexspeller alexspeller force-pushed the fix-redirecting-in-validation-hooks branch from fbe33a7 to e93f8ef Compare October 29, 2016 12:47
@@ -63,6 +63,13 @@ function getTransitionByIntent(intent, isIntermediate) {

// Abort and usurp any previously active transition.
if (this.activeTransition) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved to the transition constructor now

@alexspeller
Copy link
Contributor Author

@trentmwillis thanks for detailed review, I made some improvements based on your comments, including moving currentSequence to router - that was bugging me too and it was a minor change

Copy link
Contributor

@trentmwillis trentmwillis left a comment

Choose a reason for hiding this comment

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

Thanks for the updates. This looks good to me, much easier to follow now I think.

!transition.isCausedByAbortingTransition
);


Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: Remove the extra line here

@rwjblue
Copy link
Collaborator

rwjblue commented Oct 29, 2016

@alexspeller - What happens to folks that are already working around these bugs? I think that existing workarounds continue to work fine (but are just not needed), but I'd like to confirm.

@alexspeller
Copy link
Contributor Author

@rwjblue correct, this makes no difference to anyone using workarounds. This patch ensures that the right thing is always done. If the right thing is being done already there is no difference

@alexspeller alexspeller force-pushed the fix-redirecting-in-validation-hooks branch from e93f8ef to bf4d075 Compare October 29, 2016 23:28
@rwjblue
Copy link
Collaborator

rwjblue commented Oct 30, 2016

@nathanhammond - r?

Copy link
Contributor

@nathanhammond nathanhammond left a comment

Choose a reason for hiding this comment

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

A few small things and one thing which needs discussion. I'm looking forward to retiring @stefanpenner's fix.

@@ -16,7 +16,7 @@ import { trigger, slice, log, promiseLabel } from './utils';
@param {Object} error
@private
*/
function Transition(router, intent, state, error) {
function Transition(router, intent, state, error, previousTransition) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Must we pass the entire transition forward? It appears that we only need to identify if this is the initial transition. As it stands it seems like this might be closing over a lot of transitions which would keep them dormant in memory longer than we care for.

(Need somebody more familiar to confirm/deny, just a quick thought.)

Copy link
Contributor Author

@alexspeller alexspeller Oct 30, 2016

Choose a reason for hiding this comment

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

The transition isn't needed beyond setting two boolean properties in the constructor. Now my understanding is that because I'm not saving any reference to the transition after the constructor, it will be GC'ed the same place as it was before. Maybe someone with more js VM / GC knowledge can confirm

Copy link
Collaborator

Choose a reason for hiding this comment

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

I tend to agree with @alexspeller here, but might make sense for @stefanpenner / @krisselden / other @tildeio/ember-core folks to sanity check also...

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree that this doesn't affect retention.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@alexspeller confirm, you aren't retaining it so it does not affect GC

Copy link
Collaborator

Choose a reason for hiding this comment

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

@alexspeller I spoke too soon, there is a closure to this context being created.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for reviewing / double checking @krisselden.

@nathanhammond - Are you happy to move forward with that confirmation, or should we do more tests (or refactor anyways)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

on more detailed review, it looks like transition is the only property that would be added to the closure. It is a little weird to do this in a condition.

I'd likely make a function that took transitionas an arg that returned a error handler. This limits the scope and shape of the closure context.

transitionTo(router, '/foo');

assert.equal(url, '/baz');
assert.equal(bazModelCount, 2, 'Baz model should be called thrice');
Copy link
Contributor

Choose a reason for hiding this comment

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

Something is wrong on this line, unsure whether it's the message or the expectation.


router.replaceURL = function(replaceURL) {
url = replaceURL;
assert.ok(false, "replaceURL should not be used");
Copy link
Contributor

Choose a reason for hiding this comment

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

I love how you're partially following ye ol' @tomhuda style of "English? double quotes. Code? single quotes." :trollface:

If you want to map all quote behavior in a specific way that might be worth doing. I didn't review the non-diff so I don't know how consistent it is currently.

assert.equal(updateUrlCount, 2, 'updateURL should be used for subsequent transition');
assert.equal(url, '/baz');
assert.equal(bazModelCount, 2, 'Baz model should be called once');
assert.equal(fooModelCount, 1, 'Foo model should not be called');
Copy link
Contributor

Choose a reason for hiding this comment

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

This one and the line below it need messaging adjustments.

@alexspeller alexspeller force-pushed the fix-redirecting-in-validation-hooks branch from bf4d075 to e97244f Compare October 30, 2016 19:40
@alexspeller
Copy link
Contributor Author

Fixed test labelling, think I've addressed saving previous transition issue, not addressing quoting style currently

@@ -58,7 +70,7 @@ function Transition(router, intent, state, error) {
this.pivotHandler = handlerInfo.handler;
}

this.sequence = Transition.currentSequence++;
this.sequence = router.currentSequence++;
this.promise = state.resolve(checkForAbort, this)['catch'](function(result) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Pretty sure this function catches the transition in the closure.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ya, could be (stupid VM's doing smart things). Without looking terribly deeply here, it seems that we might be able to avoid using closure scope stuff (and avoid generating a new function for each transition created too), by elevating this to a private prototype method (and moving checkForAbort) and calling it from the constructor..

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still concerned about this accidentally trolling us in the future. One of us won't be paying attention and will merge a PR which accidentally again captures things in a closure. In general I'm hesitant to pass around "huge" (by reference) objects.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm. We definitely need to figure out the flags to know which method to use, do you have suggestions that would allow us to accomplish both objectives?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually right now I'm just being a jerk and identifying problems without coming up with proposed solutions. 😛 Addressing this one problem (for example, as you proposed) and then adding a comment to remind us to look out for this behavior is probably just fine.

I'd still like some GC/VM wizards to chime in on anything else we may have missed...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So to be clear, right now the best thing for me to do is to refactor that function in the transition constructor to a prototype method, so that it doesn't risk closing over the arguments passed in to the transition constructor?

FWIW I hadn't thought that it would close over variables that you don't explicitly use in the function body, however that's based on chrome's behaviour and not a deep understanding of the various js VMs ember supports

Copy link
Contributor

Choose a reason for hiding this comment

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

That function is lexically scoped in a place where it can reach the previous transition which means it can't be cleaned up until that property is reset.

Yes, we need to refactor it such that the definition of this section happens outside of the constructor. This will have the bonus win of being more performant.

@alexspeller alexspeller force-pushed the fix-redirecting-in-validation-hooks branch from e97244f to 22a2afd Compare October 31, 2016 21:43
@alexspeller
Copy link
Contributor Author

@krisselden I believe this is what you meant, avoiding the creation of a closure in the constructor now

@rwjblue @nathanhammond @trentmwillis AFAIK this should be ready for merge based on the feedback given above, let me know if there's anything else 😄

Copy link
Collaborator

@rwjblue rwjblue left a comment

Choose a reason for hiding this comment

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

Thanks for working through that with us @alexspeller (lots of learning for me on this one too).

@nathanhammond - r?

This has been broken forever AFAIK, and is surprising to a lot of people. In
fact, even the ember guides recommend [using `this.transitionTo` for redirecting](https://guides.emberjs.com/v2.9.0/routing/redirection/)

This is in fact broken. If you use `transitionTo` as a redirect strategy,
then you get an extra history entry if you hit the route as the first route in
your app. This breaks the back button, as the back button takes you to the route
with the redirect, then immediately redirects back to the page you're on.
Maybe you can go back if you hammer back button really quickly, but you have to
hammer it loads super quick. Not a good UX.

`replaceWith` works if you use it to redirect from a route that's your
initial transition. However if you use it to redirect and you hit that route
from some way _after_ the initial app load, then at the point that the
replaceWith is called, you are still on the same URL. For example, if you are on
`/` and you click a link to `/foo`, which in it's model hook redirects to `/bar`
using `replaceWith`, at the time replaceWith is called, your current url is `/`.

This means `/` entry gets removed from your history entirely. Clicking back
will take you back to whatever page you were on before `/`, which often isn't
event your app, maybe it's google or something. This breaks the back button
again.

This commit should do the correct thing in all cases, allowing replaceWith and
transitionTo outside of redirects as specified by the developer but only allowing
transitionTo or replaceWith in redirects in a way that doesn't break the back
button.
@alexspeller alexspeller force-pushed the fix-redirecting-in-validation-hooks branch from 22a2afd to 3723643 Compare October 31, 2016 23:15
@nathanhammond
Copy link
Contributor

@alexspeller Thank you for keeping this thing moving with all of the deep JS internals review. I'm grateful for the detailed attention you paid to this and for getting this solved at the appropriate layer.

@krisselden I always learn things when you show up on threads, I appreciate your detailed analysis of what's going on in JS Engine land both in this thread and on Slack.

This change now appears to be rock solid, let land it! 🎉

@nathanhammond nathanhammond merged commit 9172bc8 into tildeio:master Nov 1, 2016
alexspeller added a commit to alexspeller/router.js that referenced this pull request Apr 17, 2017
So I fixed this in tildeio#198. But it was broken again by tildeio#197 and the tests didn't
catch that. So I've improved the tests and fixed it again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants