-
Notifications
You must be signed in to change notification settings - Fork 204
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
Convert host <-> guest communication to use MessageChannel #3611
Conversation
This commit implements the initial transition of communication between guest/host frames and the sidebar to use `MessageChannel` as the underlying transport instead of `window.postMessage`, building on previous changes to the `Bridge` and `RPC` classes. This change aligns with an ongoing to plan to use `MessageChannel` instead of `window.postMessage`, and also resolves an issue where guest frames may fail to connect to the sidebar if the annotator code loads in the guest before the sidebar application has finished loading. This initial version has several limitations compared to what is planned for the final version of the new inter-frame communication system: - It only supports guest frames which are the same frame as the host or a direct, same-origin child. - Sidebar <-> host communication relies on the host frame also being a guest frame. In order for the host frame to receive messages from the sidebar, it must run the logic to establish sidebar <-> guest communication. In other words, it is not possible to have a host frame which cannot also be annotated. - The only supported roles for frames are sidebar and host/guest. There is no separate role for notebook frames for use in notebook <-> sidebar communication. Making this change involved replacing the protocol used by guest frames and the sidebar to discover each other. The new one is simpler than the previous one and works as follows: 1. When the sidebar application starts up it notifies the parent frame that it is ready to connect to guests via a `hypothesisSidebarReady` message. 2. Guest frames wait for this notification to be received before connecting to the sidebar. When they connect, they create a `MessageChannel` and send one port to the sidebar via a `hypothesisGuestReady` message and use the other port locally. 3. When the sidebar receives a `hypothesisGuestReady` message it creates a channel to communicate with the guest.
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.
The approach suggested here is sensible transition. @robertknight has well identified all the limitations
A left a few comments.
Use the existing `MessageEvent.ports` property to access transferred ports instead of adding an extra property to the message itself.
Codecov Report
@@ Coverage Diff @@
## master #3611 +/- ##
=======================================
Coverage 98.61% 98.61%
=======================================
Files 211 211
Lines 7735 7743 +8
Branches 1758 1761 +3
=======================================
+ Hits 7628 7636 +8
Misses 107 107
Continue to review full report at Codecov.
|
Without this call the test doesn't execute the code paths it intends to test.
Thanks for the feedback @esanzgar. I removed the This is now ready for review. |
The previous code assumed that the `window.parent.__hypothesis` access would return undefined. This assumption was wrong. Instead it triggers an error. Handle this error and display a more informative message if a guest cannot connect to a sidebar.
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.
It looks good.
I left a couple of very minor suggestions.
@@ -71,26 +69,25 @@ export class CrossFrame { | |||
* Returns a promise that resolves once the connection has been established. | |||
* | |||
* @param {Window} frame - The window containing the sidebar application |
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.
Optionally:
* @param {Window} frame - The window containing the sidebar application | |
* @param {Window} sidebarFrame - The window containing the sidebar application |
src/annotator/index.js
Outdated
try { | ||
sidebarWindow = /** @type {HypothesisWindow} */ (window.parent).__hypothesis | ||
?.sidebarWindow; |
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.
try { | |
sidebarWindow = /** @type {HypothesisWindow} */ (window.parent).__hypothesis | |
?.sidebarWindow; | |
try { | |
// Only `guest` frame/s that are direct children of the `host` frame are currently supported | |
sidebarWindow = /** @type {HypothesisWindow} */ (window.parent).__hypothesis | |
?.sidebarWindow; |
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've added a comment here. More importantly looking at this again made be realize there was a bug where the assignment in the try
block did not check that sidebarWindow
was not already assigned.
origin: 'ORIGIN', | ||
token: 'TOKEN', | ||
}); | ||
cf.connectToSidebar(sidebarFrame); |
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 would suggest to add the origin
parameter as well for consistency.
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.
Will do.
src/annotator/test/sidebar-test.js
Outdated
const sidebar = createSidebar(); | ||
|
||
// Check `sidebar.ready` is not resolved before `hypothesisSidebarReady` | ||
// message is received. | ||
assert.equal( | ||
await Promise.race([sidebar.ready, Promise.resolve('not-ready')]), | ||
'not-ready' | ||
); | ||
|
||
window.dispatchEvent( | ||
new MessageEvent('message', { | ||
data: { type: 'hypothesisSidebarReady' }, | ||
}) | ||
); | ||
|
||
return sidebar.ready; |
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 found this test a bit contrive. What about this alternative?
const sidebar = createSidebar(); | |
// Check `sidebar.ready` is not resolved before `hypothesisSidebarReady` | |
// message is received. | |
assert.equal( | |
await Promise.race([sidebar.ready, Promise.resolve('not-ready')]), | |
'not-ready' | |
); | |
window.dispatchEvent( | |
new MessageEvent('message', { | |
data: { type: 'hypothesisSidebarReady' }, | |
}) | |
); | |
return sidebar.ready; | |
const sidebar = createSidebar(); | |
window.dispatchEvent( | |
new MessageEvent('message', { | |
data: { type: 'hypothesisSidebarReady' }, | |
}) | |
); | |
await sidebar.ready; | |
assert(true); |
Or maybe:
const sidebar = createSidebar(); | |
// Check `sidebar.ready` is not resolved before `hypothesisSidebarReady` | |
// message is received. | |
assert.equal( | |
await Promise.race([sidebar.ready, Promise.resolve('not-ready')]), | |
'not-ready' | |
); | |
window.dispatchEvent( | |
new MessageEvent('message', { | |
data: { type: 'hypothesisSidebarReady' }, | |
}) | |
); | |
return sidebar.ready; | |
const sidebar = createSidebar(); | |
let counter = 0; | |
const promise = sidebar.ready.then(() => ++counter); | |
window.dispatchEvent( | |
new MessageEvent('message', { | |
data: { type: 'hypothesisSidebarReady' }, | |
}) | |
); | |
await promise; | |
assert.equal(counter, 1); |
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.
The Promise.race
part is an attempt to check that the sidebar.ready
Promise is not already resolved. JavaScript intentionally doesn't provide APIs to inspect Promise state, so the best we can do is race it against a Promise that is immediately resolved.
const promise = sidebar.ready.then(() => ++counter);
Promises can only be resolved once, so unless sidebar.ready
were re-assigned to a different Promise, counter
can never have a value that is more than one here.
We don't want to connect to the sidebar in the parent frame if the current frame has one.
The test happened to work without it, but the parameter is ostensibly required.
After #3611 there is no more need to support `window.postMessage` on `RPC` and `Bridge` classes.
After #3611 there is no more need to support `window.postMessage` on `RPC` and `Bridge` classes.
After #3611 there is no more need to support `window.postMessage` on `RPC` and `Bridge` classes.
The current inter-frame communication doesn't work if an annotatable (guest) iframe is from a different origin than the host frame (see #3611 (comment)). This will be fixed in a more comprehensive overhaul of the inter-face communication (see #3533). Meanwhile, I add a scenario into the local dev server where the annotatable iframe is from a different origin than the host frame. For this, I needed to spawn an additional dev server at port 3002): ``` [11:32:50] Dev web server started at http://localhost:3000/ [11:32:50] Dev web server started at http://localhost:3002/ ``` Close #3629
The current inter-frame communication doesn't work if an annotatable (guest) iframe is from a different origin than the host frame (see #3611 (comment)). This will be fixed in a more comprehensive overhaul of the inter-face communication (see #3533). Meanwhile, I add a scenario into the local dev server where the annotatable iframe is from an origin different than the host frame. For this, I needed to spawn an additional dev server at port 3002): ``` [11:32:50] Dev web server started at http://localhost:3000/ [11:32:50] Dev web server started at http://localhost:3002/ ``` Close #3629
The current inter-frame communication doesn't work if an annotatable (guest) iframe is from a different origin than the host frame (see #3611 (comment)). This will be fixed in a more comprehensive overhaul of the inter-face communication (see #3533). Meanwhile, I add a scenario into the local dev server where the annotatable iframe is from an origin different than the host frame. For this, I needed to spawn an additional dev server at port 3002): ``` [11:32:50] Dev web server started at http://localhost:3000/ [11:32:50] Dev web server started at http://localhost:3002/ ``` Close #3629
The current inter-frame communication doesn't work if an annotatable (guest) iframe is from a different origin than the host frame (see #3611 (comment)). This will be fixed in a more comprehensive overhaul of the inter-face communication (see #3533). Meanwhile, I add a scenario into the local dev server where the annotatable iframe is from an origin different than the host frame. For this, I needed to spawn an additional dev server at port 3002): ``` [11:32:50] Dev web server started at http://localhost:3000/ [11:32:50] Dev web server started at http://localhost:3002/ ``` Close #3629
This commit implements the initial transition of communication between
guest/host frames and the sidebar to use
MessageChannel
as theunderlying transport instead of
window.postMessage
, building onprevious changes to the
Bridge
andRPC
classes.This change aligns with an ongoing to plan to use
MessageChannel
instead of
window.postMessage
, and also resolves an issue where guestframes may fail to connect to the sidebar if the annotator code loads in
the guest before the sidebar application has finished loading.
This initial version has several limitations compared to what is planned
for the final version of the new inter-frame communication system:
a direct, same-origin child.
guest frame. In order for the host frame to receive messages from the
sidebar, it must run the logic to establish sidebar <-> guest communication.
In other words, it is not possible to have a host frame which cannot
also be annotated.
is no separate role for notebook frames for use in notebook <->
sidebar communication.
Making this change involved replacing the protocol used by guest frames
and the sidebar to discover each other. The new one is a minimal protocol
which is intended to be temporary and replaced with a different one in future
that addresses the above limitations (see
shared/communicator.js
in this draft).it is ready to connect to guests via a
hypothesisSidebarReady
message.
connecting to the sidebar. When they connect, they create a
MessageChannel
and send one port to the sidebar via ahypothesisGuestReady
message and use the other port locally.hypothesisGuestReady
message it createsa channel to communicate with the guest.