-
Notifications
You must be signed in to change notification settings - Fork 268
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
Enable cross tabs communication to sync authState #478
Conversation
expect(instance._emitEventsForCrossTabsStorageUpdate).not.toHaveBeenCalled(); | ||
}); | ||
|
||
describe('_emitEventsForCrossTabsStorageUpdate', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to test to make sure that we don't cause extra calls? I don't see anything in the code that's wrong, but a test making sure that an emitted event doesn't cause a chain of events continuously bouncing between tabs would help future-proof the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra calls may happen, as we still want to keep autoRenew running in each single tab. Then we handle it in AuthStateManager to throttle the emitted events by comparing the state and cancelablePromise. https://github.com/okta/okta-auth-js/pull/484/files#diff-c87977be240b146c8cc1ab3c11a6cc66R71
Also, the events just triggered by storage change, but it will not trigger another storage change, then we should be save from the infinite loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, yes, extra calls may happen. I'm just saying we should test to make sure that if only one call is expected (because artificial test situation) only one call happens.
That helps make sure we don't shoot ourselves in the foot later (as you say, currently it won't happen. I want to make sure later changes don't break that assumption).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with the future-proof
point. I just don't feel it's easy to simulate the multiple tabs env in unit test, if we just trigger the function by emitting a StorageEvent, I don't think it can help too much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that's more involved than I was thinking. More of:
- test that when we make a change, we only emit exactly one event, not more than one
- test that when we get an event that doesn't require a change, we don't emit an event
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StorageEvent can only be triggered outside the current tab, maybe I can try wdio
as Aaron suggested.
test that when we get an event that doesn't require a change, we don't emit an event
yep 👍 , looks like more tests are needed.
test/spec/fingerprint.js
Outdated
expect(name).toEqual('message'); | ||
listener = fn; | ||
// expect(name).toEqual('message'); | ||
listeners.message = fn; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be
if (name === 'message') {
listeners.message = fn;
}|
At a high level, I'm not sure we should call it "cross tab communication" since this will also keep multiple instances within a tab in sync, if they have tokens in memory (such as authState). It's handling "storage events", if this storage is shared between tabs then it will affect instances in multiple tabs. But even within a single tab using sessionStorage, this logic still applies. |
Will it? I don't believe the event will be received by other code in the same tab that emits the code. |
lib/TokenManager.ts
Outdated
Object.keys(newTokens).forEach(key => { | ||
const oldToken = oldTokens[key]; | ||
const newToken = newTokens[key]; | ||
if (JSON.stringify(oldToken) !== JSON.stringify(newTokens)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did you mean newToken
(instead of newTokens
) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching it, dammit auto-completion...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why I don't use auto-completion, and make boutique, artisanal typos...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, if the above didn't trigger a test failure, that signals a test we should add...
@@ -117,14 +142,23 @@ function setExpireEventTimeoutAll(sdk, tokenMgmtRef, storage) { | |||
} | |||
} | |||
|
|||
function add(sdk, tokenMgmtRef, storage, key, token: Token) { | |||
var tokenStorage = storage.getStorage(); | |||
function resetExpireEventTimeoutAll(sdk, tokenMgmtRef, storage) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a comment explaining what clearing and re-adding the timeouts does
lib/TokenManager.ts
Outdated
// LocalStorage cross tabs update is not synced in IE, set a 1s timer to read latest value | ||
// https://stackoverflow.com/questions/24077117/localstorage-in-win8-1-ie11-does-not-synchronize | ||
if (isIE11OrLess()) { | ||
setTimeout(() => emitEventsAndResetExpireEventTimeouts(), 1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned what may happen within that 1 second. Will we be emitting adding/removed events that could clobber more recent, authoritative values? Should we check that the current value in storage is the newToken?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I am thinking how about attach a timestamp to the emitted events, if by any chance an old event comes to updateAuthState, just ignore it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ smart catch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need more test coverage on this.
I would like to see how this works with authState
: Calling tokenManager.add() should not cause more than one authState event.
I would also like to see testing around this ie11 delay, lets think of all the edges that could be exposed by waiting 1 second.
Our wdio E2E framework does support multiple tabs. I would like to see two app instances started in separate tabs and verify that the cross tab communication is working as we expect.
lib/AuthStateManager.ts
Outdated
}); | ||
} | ||
|
||
getAuthState(): AuthState { | ||
return this._authState; | ||
} | ||
|
||
updateAuthState({ event, key, token }: UpdateAuthStateOptions = {}): void { | ||
updateAuthState({ event, key, token, timestamp }: UpdateAuthStateOptions = { timestamp: Date.now() }): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think this function should take any parameters or options. None of these parameters should affect the output. Is the output state a function of an event or a key? All of these params (except for timestamp) appear to be here for logging purposes. I would prefer this type of logging was done outside of this method. If an event handler has been fired, that handler can log a message before calling updateAuthState
. updateAuthState
itself should not take any parameters or return any value, since it should calculate the same state no matter who calls it nor however frequently it is called. Two identical calls in a row should produce the same output state.
Adding time-based logic with timestamp and Date.now() greatly increases the complexity and breaks the assumption of repeatability / idempotence since two subsequent calls with the same parameters would have different results. With the time logic, each call causes an update to internal state.
Calling this method should not produce side-effects other than emitting a state change event with the calculated state object, which is a function of stored token values. It should not matter when I call the method, the current state will always be a reflection of current storage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can still track the timestamp
, but instead of tracking it in updateAuthState
, we can manage it when AuthStateManager gets the events, then make the decision there.
In this way, the internal evaluation can still benefit from the tracked timestamp, and the external evaluation won't break the assumption of repeatability/idempotence.
37f19fb
to
3ceea90
Compare
}; | ||
if (preToken) { | ||
assert(JSON.stringify(preToken) === JSON.stringify(tabTokenMap[handle])); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should there be an else {
here? We only need/want to set preToken
once, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should not really matter, any unequal will fail the test.
test/e2e/specs/crossTabs.js
Outdated
|
||
it('should update tokens cross tabs', async () => { | ||
// // login in the latest opened tab | ||
// await loginPopup(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comments can be removed?
lib/AuthStateManager.ts
Outdated
this._setLogOptions({ event: 'added', key, token }); | ||
// "added" event is emitted in both add and renew process | ||
// Only listen on "added" event to update auth state | ||
const shouldUpdateAuthState = (timestamp: number): boolean => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually need this logic? If updateAuthState is called repeatedly or unnecessarily it should not cause any negative side-effects. If it produces an identical state to the current state it will not emit a change, so will it actually matter if an "older" event was the reason it is being called?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so will it actually matter if an "older" event was the reason it is being called
Yes, the logic here is to handle the 1s delay hack for IE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah its a hack to call the events later, but does that actually cause a problem? And if it doesn't cause a problem why do we need extra logic to suppress the calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than the cross tabs update, each tab is still running autoRenew process by itself. In this case, the delayed event will override the value from the tab's own autoRenew result if it comes later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand "override the value". Isn't it safe to call updateAuthState
at any time? It will read tokens from storage and, if these are different than what is currently in the authState, then it will emit a change event. If there is no change in the state, then nothing is emitted and it is essentially a no-op. If 1 second after an auto renew, the values in storage have changed, then we want this change event, right?
lib/TokenManager.ts
Outdated
|
||
// LocalStorage cross tabs update is not synced in IE, set a 1s timer to read latest value | ||
// https://stackoverflow.com/questions/24077117/localstorage-in-win8-1-ie11-does-not-synchronize | ||
if (isIE11OrLess()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about having this function always called in a setTimeout, but have the time value be configurable. We can set it by default to 0 unless IE11 where we set it by default to 1000. But we could also manually set it to a value in our test/sample apps to verify that the logic works as we expect without needing to fake IE11
b788356
to
a9263dd
Compare
Codecov Report
@@ Coverage Diff @@
## master #478 +/- ##
==========================================
+ Coverage 90.83% 91.00% +0.17%
==========================================
Files 33 32 -1
Lines 1767 1834 +67
Branches 391 406 +15
==========================================
+ Hits 1605 1669 +64
- Misses 162 165 +3
Continue to review full report at Codecov.
|
lib/TokenManager.ts
Outdated
@@ -303,6 +405,10 @@ export class TokenManager { | |||
options.storage = 'cookie'; | |||
} | |||
|
|||
if (isIE11OrLess()) { | |||
options._storageEventDelay = 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be a default if running on IE11, it should not override the value we have set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove timestamp logic on events, then 🚀
Changes:
updateAuthState()