Commit f18218f7 authored by Robert Knight's avatar Robert Knight

Unload client from iframes when it is unloaded in the host frame

When the client is unloaded in a frame H, ensure that it is also unloaded from
guest frames whose host frame is H. This enables toggling the browser
extension's active state to work properly in VitalSource Bookshelf and other
pages which have annotation-enabled iframes.

This works by having the `Guest` instance in the iframe listen for the existing
"close" message from its connection to the host frame, and relaying that to the
entry point via a "hostDisconnected" event, which then handles it in the same
way as if the client had been unloaded directly in the guest frame.
parent 93fb3f68
import { TinyEmitter } from 'tiny-emitter';
import { ListenerCollection } from '../shared/listener-collection'; import { ListenerCollection } from '../shared/listener-collection';
import { PortFinder, PortRPC } from '../shared/messaging'; import { PortFinder, PortRPC } from '../shared/messaging';
import { generateHexString } from '../shared/random'; import { generateHexString } from '../shared/random';
...@@ -119,7 +121,7 @@ export type GuestConfig = { ...@@ -119,7 +121,7 @@ export type GuestConfig = {
* each frame connects to the sidebar and host frames as part of its * each frame connects to the sidebar and host frames as part of its
* initialization. * initialization.
*/ */
export class Guest implements Annotator, Destroyable { export class Guest extends TinyEmitter implements Annotator, Destroyable {
public element: HTMLElement; public element: HTMLElement;
/** Ranges of the current text selection. */ /** Ranges of the current text selection. */
...@@ -193,6 +195,8 @@ export class Guest implements Annotator, Destroyable { ...@@ -193,6 +195,8 @@ export class Guest implements Annotator, Destroyable {
config: GuestConfig = {}, config: GuestConfig = {},
hostFrame: Window = window hostFrame: Window = window
) { ) {
super();
this.element = element; this.element = element;
this._hostFrame = hostFrame; this._hostFrame = hostFrame;
this._highlightsVisible = false; this._highlightsVisible = false;
...@@ -401,6 +405,8 @@ export class Guest implements Annotator, Destroyable { ...@@ -401,6 +405,8 @@ export class Guest implements Annotator, Destroyable {
} }
}); });
this._hostRPC.on('close', () => this.emit('hostDisconnected'));
// Discover and connect to the host frame. All RPC events must be // Discover and connect to the host frame. All RPC events must be
// registered before creating the channel. // registered before creating the channel.
const hostPort = await this._portFinder.discover('host'); const hostPort = await this._portFinder.discover('host');
......
...@@ -50,6 +50,12 @@ const sidebarLinkElement = document.querySelector( ...@@ -50,6 +50,12 @@ const sidebarLinkElement = document.querySelector(
function init() { function init() {
const annotatorConfig = getConfig('annotator') as GuestConfig & InjectConfig; const annotatorConfig = getConfig('annotator') as GuestConfig & InjectConfig;
let resolveUnloadRequested = () => {};
const unloadRequested = new Promise<void>(resolve => {
resolveUnloadRequested = resolve;
});
sidebarLinkElement.addEventListener('destroy', resolveUnloadRequested);
const hostFrame = annotatorConfig.subFrameIdentifier ? window.parent : window; const hostFrame = annotatorConfig.subFrameIdentifier ? window.parent : window;
const destroyables = [] as Destroyable[]; const destroyables = [] as Destroyable[];
...@@ -88,12 +94,18 @@ function init() { ...@@ -88,12 +94,18 @@ function init() {
document.body, document.body,
annotatorConfig annotatorConfig
); );
// Create the guest that handles creating annotations and displaying highlights. // Create the guest that handles creating annotations and displaying highlights.
const guest = new Guest(document.body, annotatorConfig, hostFrame); const guest = new Guest(document.body, annotatorConfig, hostFrame);
// When the client is unloaded in the host frame, also unload it from any
// connected iframes.
guest.on('hostDisconnected', resolveUnloadRequested);
destroyables.push(hypothesisInjector, guest); destroyables.push(hypothesisInjector, guest);
} }
sidebarLinkElement.addEventListener('destroy', () => { unloadRequested.then(() => {
destroyables.forEach(instance => instance.destroy()); destroyables.forEach(instance => instance.destroy());
// Remove all the `<link>`, `<script>` and `<style>` elements added to the // Remove all the `<link>`, `<script>` and `<style>` elements added to the
......
...@@ -1363,6 +1363,16 @@ describe('Guest', () => { ...@@ -1363,6 +1363,16 @@ describe('Guest', () => {
}); });
}); });
it('emits "hostDisconnected" event when host frame closes connection with guest', () => {
const guest = createGuest();
const hostDisconnected = sinon.stub();
guest.on('hostDisconnected', hostDisconnected);
emitHostEvent('close');
assert.called(hostDisconnected);
});
it('discovers and creates a channel for communication with the sidebar', async () => { it('discovers and creates a channel for communication with the sidebar', async () => {
const { port1 } = new MessageChannel(); const { port1 } = new MessageChannel();
fakePortFinder.discover.resolves(port1); fakePortFinder.discover.resolves(port1);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment