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

Prevent context matching if the oldContext is null #242

Merged
merged 4 commits into from
Mar 7, 2018

Conversation

@rwjblue
Copy link
Collaborator

rwjblue commented Feb 24, 2018

Why is it not possible to test (can you explain)?

@nathanhammond
Copy link
Contributor Author

nathanhammond commented Feb 24, 2018

This comment was wrong. To trigger the issue the route actually needs to be processed asynchronously. The faked-in asynchronous behavior isn't enough to trigger it.

So, I would need to modify this line like so:

diff --git a/tests/router_test.js b/tests/router_test.js
index 07b2d69..cb3bafd 100644
--- a/tests/router_test.js
+++ b/tests/router_test.js
@@ -28,7 +28,15 @@ var scenarios = [
     getHandler: function(name) {
       // Treat 'loading' route transitions are synchronous
       var handler = handlers[name] || (handlers[name] = {});
-      return name === 'loading' ? handler : Promise.resolve(handler);
+      if (name === 'loading') {
+        return handler;
+      } else {
+        return new Promise(function(resolve) {
+          setTimeout(function() {
+            resolve(handler);
+          }, 1);
+        });
+      }
     },
     getSerializer: function(name) {
       return serializers && serializers[name];

And then approximately every async test fails since it is now running out of turn compared to the test suite.

@nathanhammond
Copy link
Contributor Author

@nathanhammond
Copy link
Contributor Author

nathanhammond commented Feb 24, 2018

The problem with the old code is that it was simply comparing equality of the context. But the context could be unset for both old (if it is a loading transition) and new (always).

We need to check to see if there actually is a context, and if that context is shared. Only in that case is it a valid assumption to copy over params.

if (result.names.length > 0 && newHandlerInfo.context === oldContext) {
if (
result.names.length > 0 &&
!!oldContext &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should be specific here I think. For example, a 0 or ’’ (empty string) are probably fine...

@nathanhammond
Copy link
Contributor Author

The bug is triggered by this process:

  1. Generate a new TransitionState.
  2. Start that TransitionState becoming resolved.
  3. Before that transition has resolved (thus the async requirement for failure)...
  4. ... attempt to check if the destination route is active using isActiveIntent.
  5. Feed in the partially-completed transition to the "private" _state argument so that we compare against the ongoing transition.
  6. This will trigger an attempt to read the context on oldHandlerInfo before it is assigned.
  7. Then, because oldHandlerInfo (which isn't even finished being processed yet) doesn't have a context, it assigns oldContext to null.
  8. This matches the prototype value from newHandlerInfo (obviously, sharing a prototype).
  9. Then we clobber newHandlerInfo's params because we didn't count on this race condition.

@nathanhammond
Copy link
Contributor Author

(Travis lost its mind. Restart the failing test one to see it actually fail.)

@rwjblue
Copy link
Collaborator

rwjblue commented Feb 27, 2018

Restarted a few times, seems to not fail?

if (result.names.length > 0 && newHandlerInfo.context === oldContext) {
if (
result.names.length > 0 &&
oldHandlerInfo.hasOwnProperty('context') &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does adding this guard allow us to delete this

// this is bonkers, we require that `context` be set on on the
// HandlerInfo prototype to null because the checks in
// `NamedTransitionIntent.prototype.applyToHandlers` here
// https://github.com/tildeio/router.js/blob/v1.2.8/lib/router/transition-intent/named-transition-intent.js#L76-L81
// check of `oldHandlerInfo.context === newHandlerInfo.context` and assumes
// that the params _must_ match also in that case.
//
// The only reason `oldHandlerInfo.context` and `newHandlerInfo.context` did not
// match in prior versions is because if the context isn't set yet (on newHandlerInfo)
// is because it inherits the `null` from the prototype vs `undefined` (on
// the oldHandlerInfo).
//
// A future refactoring should remove that conditional, and fix the hand full of
// failing tests.
HandlerInfo.prototype.context = null;

If we can do that, we don't need the hasOwnProperty check here...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're still going to need hasOwnProperty for the scenario where: oldHandlerInfo.context === undefined && newHandlerInfo.context === undefined because neither explicitly set it.

It's also theoretically possible that use as a microlib would set context to a "meaningful" undefined value on the instance.

Regardless, I believe that hasOwnProperty is the correct check here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(See step 7 in #242 (comment))

@@ -1594,6 +1594,27 @@ scenarios.forEach(function(scenario) {
showPost: showPostHandler,
};

// Check for mid-transition correctness.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you move this into a test of its own? Seems odd to just be an extra assertion in a somewhat unrelated test...

@nathanhammond nathanhammond force-pushed the patch-1 branch 2 times, most recently from 28979ce to cb0b43f Compare March 7, 2018 18:47
@nathanhammond
Copy link
Contributor Author

  • Test extracted.
  • Removal of the "default" context from the prototype is added as an optional separate commit.

I opted to never explicitly set the context on a handler unless we actually received a context. Since we always compare identity but could have a primitive show up (undefined) we need to use hasOwnProperty to distinguish between an undefined context and an unset context.

@rwjblue rwjblue merged commit 4b82317 into tildeio:master Mar 7, 2018
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.

2 participants