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

Use separate channels for guest / host messages from sidebar #3807

Merged
merged 2 commits into from
Oct 7, 2021

Conversation

robertknight
Copy link
Member

Use separate channels for sending messages to guests vs the host in the
sidebar, even for the common case when there is only one guest and it is
the same frame as the host.

This change makes it clear for readers which part of the annotator code
is intended to handle a particular message from the sidebar. It is also
a step towards supporting host frames that are not guests. This will be
needed in ebook readers where the host frame provides the navigation UI
and contains the frame displaying the book content, but should not be
annotatable itself.

  • Remove the bridge service in the sidebar. The frameSync service
    now provides the entry point for other services/components to make RPC calls
    to the host or guest frames. Currently the only use case is
    sending notifications to the host via FrameSyncService.notifyHost.

  • Create separate Bridge instances in FrameSyncService for sidebar
    <-> guest and sidebar <-> host communication. The sidebar <-> guest
    bridge works the same as before. The sidebar <-> host bridge
    is established by having FrameSyncService create a MessageChannel
    when sending the hypothesisSidebarReady notification to the host.

    The sidebar / host frames then add respective ports of this channel
    to a Bridge instance.

  • Change the various existing RPC calls between frames to use either
    the guest <-> sidebar or host <-> sidebar communication channels as
    appropriate

@robertknight robertknight requested a review from esanzgar October 6, 2021 10:47
@codecov
Copy link

codecov bot commented Oct 6, 2021

Codecov Report

Merging #3807 (711ca92) into master (806f0f8) will decrease coverage by 0.02%.
The diff coverage is 96.66%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3807      +/-   ##
==========================================
- Coverage   99.01%   98.98%   -0.03%     
==========================================
  Files         212      212              
  Lines        7796     7802       +6     
  Branches     1757     1757              
==========================================
+ Hits         7719     7723       +4     
- Misses         77       79       +2     
Impacted Files Coverage Δ
src/sidebar/services/frame-sync.js 98.16% <94.28%> (-1.84%) ⬇️
src/annotator/sidebar.js 98.10% <100.00%> (+<0.01%) ⬆️
src/sidebar/components/HypothesisApp.js 100.00% <100.00%> (ø)
src/sidebar/components/TopBar.js 100.00% <100.00%> (ø)
src/sidebar/components/UserMenu.js 100.00% <100.00%> (ø)
src/sidebar/services/features.js 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 806f0f8...711ca92. Read the comment docs.

Use separate channels for sending messages to guests vs the host in the
sidebar, even for the common case when there is only one guest and it is
the same frame as the host.

This change makes it clear for readers which part of the annotator code
is intended to handle a particular message from the sidebar. It is also
a step towards supporting host frames that are not guests. This will be
needed in ebook readers where the host frame provides the navigation UI
and contains the frame displaying the book content, but should not be
annotatble itself.

 - Remove the `bridge` service in the sidebar. The `frameSync` service
   now provides the entry point for other services/components to make RPC calls
   to the host or guest frames. Currently the only use case is
   sending notifications to the host via `FrameSyncService.notifyHost`.

 - Create separate `Bridge` instances in `FrameSyncService` for sidebar
   <-> guest and sidebar <-> host communication. The sidebar <-> guest
   bridge works the same as before. The sidebar <-> host bridge
   is established by having `FrameSyncService` create a MessageChannel
   when sending the `hypothesisSidebarReady` notification to the host.

   The sidebar / host frames then add respective ports of this channel
   to a Bridge instance.

 - Change the various existing RPC calls between frames to use either
   the guest <-> sidebar or host <-> sidebar communication channels as
   appropriate
Copy link
Contributor

@esanzgar esanzgar left a comment

Choose a reason for hiding this comment

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

I don't see any problems with the code.

I find a little confusing that the host-sidebar channel is created in the sidebar while the guest-sidebar for the host frame is created in the annotator. I think it would be better to have one frame to be responsible of the creation of all the MessageChannels.

Because it is imposible to reach the the sidebar or the host (except window.parent) from a cross-origin frame I think the host frame is the ideal place to create all the channels (some on demand) and orchestrate the communication.

I planned on an alternative way to send events to specific channels without involving creating multiple Bridges instances. This involved specifying the role of that frame when it is registered and then directing calls to specific frames: this.bridge.call('myevent', {args, to: [annotatables, notebook]}) (to send messages to all the annotatables and notebook frames. (see for example: #3566 (comment))

There is a missing test for this line:
src/sidebar/services/frame-sync.js#L294-L295

@@ -199,8 +202,9 @@ export default class Sidebar {
*/
this.ready = new Promise(resolve => {
this._listeners.add(window, 'message', event => {
const data = /** @type {MessageEvent} */ (event).data;
if (data?.type === 'hypothesisSidebarReady') {
const messageEvent = /** @type {MessageEvent} */ (event);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const messageEvent = /** @type {MessageEvent} */ (event);
const {data, ports} = /** @type {MessageEvent} */ (event);

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to allow for the fact that data may be null/undefined here in case code on the host page has somehow sent an message event with a null data value.

Comment on lines +61 to +62
/** Channel for sidebar <-> guest(s) communication. */
this._guestRPC = new Bridge();
Copy link
Contributor

Choose a reason for hiding this comment

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

Something to consider for the future, maybe we should rename this sidebar-annotatable channel to fully differentiate that it is not the intention to send messages to any guests frame but annotatable frames.

Copy link
Member Author

Choose a reason for hiding this comment

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

To me "guest frame" and "annotatable frame" means the same. What distinction were you thinking of?

inFrame.delete(annot.$tag);
});

if (frames.length > 0) {
if (frames.every(frame => frame.isAnnotationFetchComplete)) {
if (publicAnns === 0 || publicAnns !== prevPublicAnns) {
bridge.call(
this._hostRPC.call(
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, is there something in the host that reacts to the publicAnnotationCountChanged event?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. It supports a feature where the publisher can display the count of annotations on their site. See https://h.readthedocs.io/projects/client/en/latest/publishers/host-page-integration/#cmdoption-arg-data-hypothesis-annotation-count.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, very good to know.

* Query the Hypothesis annotation client in a frame for the URL and metadata
* of the document that is currently loaded and add the result to the set of
* connected frames.
* Query the guest in a frame for the URL and metadata of the document that
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Query the guest in a frame for the URL and metadata of the document that
* Query the annotatable frame for the URL and metadata of the document that

*/
this._setupSyncToFrame = () => {
this._setupSyncToGuests = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
this._setupSyncToGuests = () => {
this._setupSyncToAnnotatables = () => {

* to the sidebar.
*/
this._setupSyncFromFrame = () => {
this._setupSyncFromGuests = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
this._setupSyncFromGuests = () => {
this._setupSyncFromAnnotatables = () => {

Comment on lines +161 to +180
function sendSidebarReadyMessage() {
const channel = new MessageChannel();
const event = new MessageEvent(
'message',
{
data: { type: 'hypothesisSidebarReady' },
},
[channel.port1]
);

window.dispatchEvent(event);

return event;
}

it('creates Bridge for host <-> sidebar RPC calls when `hypothesisSidebarReady` message is received', () => {
createSidebar();
const event = sendSidebarReadyMessage();
assert.calledWith(fakeBridge.createChannel, event.ports[0]);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

An alternative...

Suggested change
function sendSidebarReadyMessage() {
const channel = new MessageChannel();
const event = new MessageEvent(
'message',
{
data: { type: 'hypothesisSidebarReady' },
},
[channel.port1]
);
window.dispatchEvent(event);
return event;
}
it('creates Bridge for host <-> sidebar RPC calls when `hypothesisSidebarReady` message is received', () => {
createSidebar();
const event = sendSidebarReadyMessage();
assert.calledWith(fakeBridge.createChannel, event.ports[0]);
});
function sendSidebarReadyMessage() {
const { port1 } = new MessageChannel();
window.postMessage({ type: 'hypothesisSidebarReady' }, '*', [port1]);
return port1;
}
it('creates Bridge for host <-> sidebar RPC calls when `hypothesisSidebarReady` message is received', async () => {
createSidebar();
const port = sendSidebarReadyMessage();
await delay(0);
assert.calledWith(fakeBridge.createChannel, port);
});

Copy link
Member Author

Choose a reason for hiding this comment

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

I did try something like this initially, but I realized I wasn't sure what the guarantees were around how long/when postMessage delivers its message, so I figured just dispatching the message directly was simpler and "realistic enough" for this case.

@robertknight
Copy link
Member Author

I find a little confusing that the host-sidebar channel is created in the sidebar while the guest-sidebar for the host frame is created in the annotator. I think it would be better to have one frame to be responsible of the creation of all the MessageChannels.

The main goal of this PR is to establish separate channels for sidebar <-> host and sidebar <-> guest communication and to separate which messages need to be sent via which channel. I fully expect we might want to change how and where the creation of the channels happen in future.

Regarding this comment:

Something to consider for the future, maybe we should rename this sidebar-annotatable channel to fully differentiate that it is not the intention to send messages to any guests frame but annotatable frames.

Hmm... we need to use consistent terminology for "a frame which the user can annotate". I thought we had already settled on "guest frame" or "guest" for short, mainly because this means it contains an instance of the Guest class and it contrasts with "host frame" or "host" for short.

@robertknight
Copy link
Member Author

There is a missing test for this line: src/sidebar/services/frame-sync.js#L294-L295

I have added the missing test. I've left the "guest" terminology as-is for the moment. Let's catch up about that tomorrow or Monday as we need to be consistent about it.

@robertknight robertknight merged commit d7a7f09 into master Oct 7, 2021
@robertknight robertknight deleted the split-guest-host-messages branch October 7, 2021 15:43
@esanzgar
Copy link
Contributor

esanzgar commented Oct 8, 2021

I thought we had already settled on "guest frame" or "guest" for short, mainly because this means it contains an instance of the Guest class and it contrasts with "host frame" or "host" for short.

I have no problem referring to annotatable frames as guest frames. The Guest class is instantiated in both the host and annotatable frames, that's probably why I was thinking on guest frames as a superset that refers to both the host frame or annotatable frame/s. That was my interpretation of this sentence from the PR description:

a step towards supporting host frames that are not guests

esanzgar added a commit that referenced this pull request Oct 8, 2021
Follow up of #3807

* reference to the `crossframe` service is inaccurate
* changed default export to a named export, as per team conventions
esanzgar added a commit that referenced this pull request Oct 8, 2021
Follow up of #3807

* reference to the `crossframe` service is inaccurate
* changed default export to a named export, as per team conventions
esanzgar added a commit that referenced this pull request Oct 8, 2021
Follow up of #3807

* reference to the `crossframe` service is inaccurate
* changed default export to a named export, as per team conventions
esanzgar added a commit that referenced this pull request Oct 11, 2021
Follow up of #3807

* reference to the `crossframe` service is inaccurate
* changed default export to a named export, as per team conventions
esanzgar added a commit that referenced this pull request Oct 11, 2021
Follow up of #3807

* reference to the `crossframe` service is inaccurate
* changed default export to a named export, as per team conventions
esanzgar added a commit that referenced this pull request Oct 11, 2021
Follow up of #3807

* reference to the `crossframe` service is inaccurate
* changed default export to a named export, as per team conventions
esanzgar added a commit that referenced this pull request Oct 11, 2021
Follow up of #3807

* reference to the `crossframe` service is inaccurate
* changed default export to a named export, as per team conventions
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.

2 participants