Skip to content

feat (Multiple Route Matches): Allow multiple route matches via a shouldHandle property#12

Merged
jamesopti merged 3 commits into
masterfrom
james/multiple_matching_routes
Oct 25, 2018
Merged

feat (Multiple Route Matches): Allow multiple route matches via a shouldHandle property#12
jamesopti merged 3 commits into
masterfrom
james/multiple_matching_routes

Conversation

@jamesopti
Copy link
Copy Markdown

@jamesopti jamesopti commented Oct 24, 2018

Summary

Enable multiple routes to match via the match regex property.
Use shouldHandle to break the tie.

shouldHandle is an optional property of a route which expects a function which returns a Promise
If that Promise resolves without explicitly passing false, the route will be considered a match.

The PromiseOrderedFirst utility is written to ensure deterministic behavior for a given set of matched routes regardless of how long any given match's shouldHandle promise takes to resolve.

Multiple Match Example Use Cases

Case 1 - All matches use shouldHandle

If data.type is basketball, the first route will be used
If data.type is football, the second route will be used
Else the catchall route will be used

{
    match: '/route123',
    shouldHandle: () => {
      return FetchData().then(data => data.type === 'basketball')
    },
    handle: [
      ...
    ]
},
{
    match: '/route123',
    shouldHandle: () => {
      return FetchData().then(data => data.type === 'football')
    },
    handle: [
      ...
    ]
}

Case 2 - Only some matches use shouldHandle

If data.type is basketball, the first route will be used
Else the second route will be used

{
    match: '/route123',
    shouldHandle: () => {
      return FetchData().then(data => data.type === 'basketball')
    },
    handle: [
      ...
    ]
},
{
    match: '/route123',
    handle: [
      ...
    ]
},

Case 3 - More than one shouldHandle matches

The first route will always be used.

{
    match: '/route123',
    handle: [
      ...
    ]
},
{
    match: '/route123',
    shouldHandle: () => {
      return FetchData().then(data => data.type === 'football')
    },
    handle: [
      ...
    ]
}

Test Plan

Unit tests added to cover the following scenarios:

List of route matches that look like:

  • [ <no shouldHandle>, <no shouldHandle>, ]
  • [ <shouldHandle rejected immediately>, <shouldHandle resolved immediately> ]
  • [ <shouldHandle rejected after delay>, <shouldHandle resolved immediately> ]
  • [ <shouldHandle resolved after delay>, <shouldHandle resolved immediately> ]

@jamesopti jamesopti force-pushed the james/multiple_matching_routes branch 2 times, most recently from bcd1674 to c158f42 Compare October 24, 2018 19:32
Copy link
Copy Markdown

@quintondang quintondang left a comment

Choose a reason for hiding this comment

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

thank you so much for doing this @jamesopti! Mostly super minor questions. overall really good

Comment thread src/fns.js Outdated
return new Promise((res, rej) => {
if (routes.length === 0) {
rej('No routes match');
} else if (routes.length === 1) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

do we care about the case if someone accidentally adds a singular route with a shouldHandle fn? i.e. if they thought their route was a match but wasn't, or if they wanted to gate it anyways even if its a singular route. Otherwise this would fail silently and take all users to the route

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hmmmmm, thats an interesting catch!

I added this as a shortcut, but I think you're probably right.

shouldHandle should be respected as a secondary matching criteria if specified, even if there is only one match.

Will update.

Comment thread src/utils.js
.catch(res => {
promiseToStateMap[index] = { state: "rejected", res, index };
})
.then(compute, compute);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

we want to compute twice? or am I misunderstanding this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No, this just means we invoke compute on resolve or on reject of the promise.

Signature: .then(<onFulfilled>, <onRejected>)

This is a safer alternative/fallback to .finally(<onFulfilled or rejected>) because .finally is only supported starting in Chrome 63.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ohhh right thanks for the explanation! Did not realize that about finally.

Comment thread test/Router.spec.js
* This is useful to control test timing where our
* router resolves routes using Promises (micro-task timing)
* More on micro-tasks vs macro-tasks:
* https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

very interesting read thanks!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

NP! See here for where Nikhil and I learned about this timing behavior.

https://github.com/optimizely/client-js/blob/devel/src/plugins/change_attribute/index.js#L175-L196

Comment thread test/Router.spec.js Outdated
]);
});

it('should execute the first route in the list', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

update this to second route in the list

Comment thread test/Router.spec.js
.then(() => {
sinon.assert.calledOnce(handlerSpy2)
sinon.assert.notCalled(handlerSpy1)
sinon.assert.callOrder(shouldHandleSpy2, shouldHandleSpy1, handlerSpy2)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nice! didn't know about callOrder

Comment thread test/Router.spec.js Outdated
.then(() => {
sinon.assert.calledOnce(onRouteCompleteSpy)
sinon.assert.callOrder(spy1, spy2, onRouteCompleteSpy)
var args = onRouteCompleteSpy.firstCall.args[0]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: update these vars to consts but wont block on this

Copy link
Copy Markdown
Author

@jamesopti jamesopti 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 quick review @quintondang !

Updated for the single route case and added some more tests.

Comment thread src/fns.js Outdated
return new Promise((res, rej) => {
if (routes.length === 0) {
rej('No routes match');
} else if (routes.length === 1) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hmmmmm, thats an interesting catch!

I added this as a shortcut, but I think you're probably right.

shouldHandle should be respected as a secondary matching criteria if specified, even if there is only one match.

Will update.

Comment thread src/utils.js
.catch(res => {
promiseToStateMap[index] = { state: "rejected", res, index };
})
.then(compute, compute);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No, this just means we invoke compute on resolve or on reject of the promise.

Signature: .then(<onFulfilled>, <onRejected>)

This is a safer alternative/fallback to .finally(<onFulfilled or rejected>) because .finally is only supported starting in Chrome 63.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally

Comment thread test/Router.spec.js
* This is useful to control test timing where our
* router resolves routes using Promises (micro-task timing)
* More on micro-tasks vs macro-tasks:
* https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

NP! See here for where Nikhil and I learned about this timing behavior.

https://github.com/optimizely/client-js/blob/devel/src/plugins/change_attribute/index.js#L175-L196

@jamesopti jamesopti force-pushed the james/multiple_matching_routes branch from 0c65ead to 443d5a5 Compare October 24, 2018 23:07
@jamesopti jamesopti force-pushed the james/multiple_matching_routes branch from 443d5a5 to d40ae0d Compare October 24, 2018 23:14
@jamesopti jamesopti removed their assignment Oct 24, 2018
@jamesopti jamesopti requested a review from quintondang October 25, 2018 01:23
Copy link
Copy Markdown

@quintondang quintondang left a comment

Choose a reason for hiding this comment

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

lgtm!! one smal question

Comment thread src/fns.js
).then(({index}) => res(routes[index]));
}
});
return utils
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

will this rej if no routes match?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yup. Theres a test for that too.

PromiseOrderedFirst will iterate through the (empty) list of promises and since it wont find any pending or resolved ones, it will reject on first attempt.

@jamesopti jamesopti merged commit 6ddb4ab into master Oct 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants