Commit d72c4b1d authored by Robert Knight's avatar Robert Knight

Send frame identifier for guests as part of connection setup

Guest frames pass an identifier for their associated browser frame to the
sidebar. The sidebar in turn uses this identifier as a key to refer to different
guests in various places. This identifier used to be sent as part of the
`documentInfoChanged` message after a guest <-> sidebar connection is
established, rather than as part of the initial connection metadata. As a result
the sidebar had to invent a temporary ID for the new guest, which was used until
it learned the real ID.

This commit simplifies the picture by adding a `sourceId` attribute to frame
connection setup messages. This replaces the `frameIdentifier` in the
`documentInfoChanged` message, and allows the sidebar to know the final ID of
the guest as soon as it connects.
parent 098b06a4
...@@ -192,6 +192,7 @@ export class Guest { ...@@ -192,6 +192,7 @@ export class Guest {
this._portFinder = new PortFinder({ this._portFinder = new PortFinder({
hostFrame: this._hostFrame, hostFrame: this._hostFrame,
source: 'guest', source: 'guest',
sourceId: this._frameIdentifier ?? undefined,
}); });
this.features = new FeatureFlags(); this.features = new FeatureFlags();
...@@ -316,7 +317,6 @@ export class Guest { ...@@ -316,7 +317,6 @@ export class Guest {
return { return {
uri: normalizeURI(uri), uri: normalizeURI(uri),
metadata, metadata,
frameIdentifier: this._frameIdentifier,
}; };
} }
......
...@@ -1354,7 +1354,6 @@ describe('Guest', () => { ...@@ -1354,7 +1354,6 @@ describe('Guest', () => {
title: 'Test title', title: 'Test title',
documentFingerprint: 'test-fingerprint', documentFingerprint: 'test-fingerprint',
}, },
frameIdentifier: null,
}); });
}); });
...@@ -1371,7 +1370,6 @@ describe('Guest', () => { ...@@ -1371,7 +1370,6 @@ describe('Guest', () => {
metadata: { metadata: {
title: 'Page 1', title: 'Page 1',
}, },
frameIdentifier: null,
}); });
sidebarRPCCall.resetHistory(); sidebarRPCCall.resetHistory();
...@@ -1386,7 +1384,6 @@ describe('Guest', () => { ...@@ -1386,7 +1384,6 @@ describe('Guest', () => {
metadata: { metadata: {
title: 'Page 2', title: 'Page 2',
}, },
frameIdentifier: null,
}); });
}); });
......
export { PortFinder } from './port-finder'; export { PortFinder } from './port-finder';
export { PortProvider } from './port-provider'; export { PortProvider } from './port-provider';
export { PortRPC, installPortCloseWorkaroundForSafari } from './port-rpc'; export { PortRPC, installPortCloseWorkaroundForSafari } from './port-rpc';
export { isMessageEqual } from './port-util'; export { isMessage, isMessageEqual } from './port-util';
export type { Message } from './port-util';
...@@ -25,12 +25,14 @@ export class PortFinder { ...@@ -25,12 +25,14 @@ export class PortFinder {
/** /**
* @param {object} options * @param {object} options
* @param {Exclude<Frame, 'host'>} options.source - the role of this frame * @param {Exclude<Frame, 'host'>} options.source - the role of this frame
* @param {string} [options.sourceId] - Identifier for this frame
* @param {Window} options.hostFrame - the frame where the `PortProvider` is * @param {Window} options.hostFrame - the frame where the `PortProvider` is
* listening for messages. * listening for messages.
*/ */
constructor({ hostFrame, source }) { constructor({ hostFrame, source, sourceId }) {
this._hostFrame = hostFrame; this._hostFrame = hostFrame;
this._source = source; this._source = source;
this._sourceId = sourceId;
this._listeners = new ListenerCollection(); this._listeners = new ListenerCollection();
} }
...@@ -69,6 +71,7 @@ export class PortFinder { ...@@ -69,6 +71,7 @@ export class PortFinder {
frame2: target, frame2: target,
type: 'request', type: 'request',
requestId, requestId,
sourceId: this._sourceId,
}, },
'*' '*'
); );
......
...@@ -144,7 +144,7 @@ export class PortProvider { ...@@ -144,7 +144,7 @@ export class PortProvider {
return; return;
} }
const { frame1, frame2, requestId } = data; const { frame1, frame2, requestId, sourceId } = data;
const channel = /** @type {Channel} */ (`${frame1}-${frame2}`); const channel = /** @type {Channel} */ (`${frame1}-${frame2}`);
if (!isSourceWindow(source)) { if (!isSourceWindow(source)) {
...@@ -183,7 +183,11 @@ export class PortProvider { ...@@ -183,7 +183,11 @@ export class PortProvider {
? this._sidebarHostChannel ? this._sidebarHostChannel
: new MessageChannel(); : new MessageChannel();
const message = { frame1, frame2, type: 'offer', requestId }; // The message that is sent to the target frame that the source wants to
// connect to, as well as the source frame requesting the connection.
// Each message is accompanied by a port for the appropriate end of the
// connection.
const message = { frame1, frame2, type: 'offer', requestId, sourceId };
// If the source window has an opaque origin [1], `event.origin` will be // If the source window has an opaque origin [1], `event.origin` will be
// the string "null". This is not a legal value for the `targetOrigin` // the string "null". This is not a legal value for the `targetOrigin`
......
/** /**
* These types are the used in by `PortProvider` and `PortFinder` for the * Message sent by `PortProvider` and `PortFinder` to establish a
* inter-frame discovery and communication processes. * MessageChannel-based connection between two frames.
* *
* @typedef {'guest'|'host'|'notebook'|'sidebar'} Frame * @typedef {'guest'|'host'|'notebook'|'sidebar'} Frame
* *
* @typedef Message * @typedef Message
* @prop {Frame} frame1 * @prop {Frame} frame1 - Role of the source frame
* @prop {Frame} frame2 * @prop {Frame} frame2 - Role of the target frame
* @prop {'offer'|'request'} type * @prop {'offer'|'request'} type - Message type. "request" messages are sent
* @prop {string} requestId - ID of the request. Used to associate `offer` * by the source frame to the host frame to request a connection. "offer"
* responses with requests and enable PortProvider to ignore re-sent requests. * messages are sent from the host frame back to the source frame and also
* to the target frame, accompanied by a MessagePort.
* @prop {string} requestId - ID of the request. Used to associate "offer"
* messages with their corresponding "request" messages.
* @prop {string} [sourceId] - Identifier for the source frame. This is useful
* in cases where multiple source frames with a given role may connect to
* the same destination frame.
*/ */
/** /**
......
...@@ -16,8 +16,8 @@ describe('PortFinder', () => { ...@@ -16,8 +16,8 @@ describe('PortFinder', () => {
let portFinder; let portFinder;
let portFinders; let portFinders;
function createPortFinder(source = frame1) { function createPortFinder(source = frame1, sourceId) {
const instance = new PortFinder({ hostFrame: window, source }); const instance = new PortFinder({ hostFrame: window, source, sourceId });
portFinders.push(instance); portFinders.push(instance);
return instance; return instance;
} }
...@@ -99,6 +99,29 @@ describe('PortFinder', () => { ...@@ -99,6 +99,29 @@ describe('PortFinder', () => {
}) })
); );
it('sends port request to host frame', async () => {
const clock = sinon.useFakeTimers();
try {
portFinder = createPortFinder('guest', 'guest-id');
portFinder.discover('sidebar');
clock.tick(POLLING_INTERVAL_FOR_PORT);
assert.calledWith(
window.postMessage,
{
frame1: 'guest',
frame2: 'sidebar',
type: 'request',
requestId,
sourceId: 'guest-id',
},
'*'
);
} finally {
clock.restore();
}
});
[ [
{ source: 'guest', target: 'host' }, { source: 'guest', target: 'host' },
{ source: 'guest', target: 'sidebar' }, { source: 'guest', target: 'sidebar' },
...@@ -144,7 +167,13 @@ describe('PortFinder', () => { ...@@ -144,7 +167,13 @@ describe('PortFinder', () => {
assert.callCount(window.postMessage, expectedCalls); assert.callCount(window.postMessage, expectedCalls);
assert.alwaysCalledWithExactly( assert.alwaysCalledWithExactly(
window.postMessage, window.postMessage,
{ frame1, frame2: target, type: 'request', requestId }, {
frame1,
frame2: target,
type: 'request',
requestId,
sourceId: undefined,
},
'*' '*'
); );
......
...@@ -158,6 +158,7 @@ describe('PortProvider', () => { ...@@ -158,6 +158,7 @@ describe('PortProvider', () => {
frame2: 'host', frame2: 'host',
type: 'request', type: 'request',
requestId: 'abcdef', requestId: 'abcdef',
sourceId: undefined,
}; };
await sendPortFinderRequest({ await sendPortFinderRequest({
...@@ -178,6 +179,7 @@ describe('PortProvider', () => { ...@@ -178,6 +179,7 @@ describe('PortProvider', () => {
frame2: 'sidebar', frame2: 'sidebar',
type: 'request', type: 'request',
requestId: 'abcdef', requestId: 'abcdef',
sourceId: undefined,
}; };
await sendPortFinderRequest({ data, origin: 'null' }); await sendPortFinderRequest({ data, origin: 'null' });
...@@ -193,6 +195,7 @@ describe('PortProvider', () => { ...@@ -193,6 +195,7 @@ describe('PortProvider', () => {
frame2: 'sidebar', frame2: 'sidebar',
type: 'request', type: 'request',
requestId: 'abcdef', requestId: 'abcdef',
sourceId: undefined,
}; };
for (let i = 0; i < 4; ++i) { for (let i = 0; i < 4; ++i) {
...@@ -228,6 +231,7 @@ describe('PortProvider', () => { ...@@ -228,6 +231,7 @@ describe('PortProvider', () => {
frame2: 'sidebar', frame2: 'sidebar',
type: 'request', type: 'request',
requestId: 'ghijkl', requestId: 'ghijkl',
sourceId: 'test-frame',
}; };
await sendPortFinderRequest({ await sendPortFinderRequest({
data, data,
......
...@@ -2,11 +2,17 @@ import debounce from 'lodash.debounce'; ...@@ -2,11 +2,17 @@ import debounce from 'lodash.debounce';
import shallowEqual from 'shallowequal'; import shallowEqual from 'shallowequal';
import { ListenerCollection } from '../../shared/listener-collection'; import { ListenerCollection } from '../../shared/listener-collection';
import { PortFinder, PortRPC, isMessageEqual } from '../../shared/messaging'; import {
PortFinder,
PortRPC,
isMessage,
isMessageEqual,
} from '../../shared/messaging';
import { isReply, isPublic } from '../helpers/annotation-metadata'; import { isReply, isPublic } from '../helpers/annotation-metadata';
import { watch } from '../util/watch'; import { watch } from '../util/watch';
/** /**
* @typedef {import('../../shared/messaging').Message} Message
* @typedef {import('../../types/annotator').AnnotationData} AnnotationData * @typedef {import('../../types/annotator').AnnotationData} AnnotationData
* @typedef {import('../../types/annotator').DocumentMetadata} DocumentMetadata * @typedef {import('../../types/annotator').DocumentMetadata} DocumentMetadata
* @typedef {import('../../types/api').Annotation} Annotation * @typedef {import('../../types/api').Annotation} Annotation
...@@ -19,7 +25,6 @@ import { watch } from '../util/watch'; ...@@ -19,7 +25,6 @@ import { watch } from '../util/watch';
/** /**
* @typedef DocumentInfo * @typedef DocumentInfo
* @prop {string|null} frameIdentifier
* @prop {string} uri * @prop {string} uri
* @prop {DocumentMetadata} metadata * @prop {DocumentMetadata} metadata
*/ */
...@@ -120,7 +125,6 @@ export class FrameSyncService { ...@@ -120,7 +125,6 @@ export class FrameSyncService {
* @type {Map<string|null, PortRPC<GuestToSidebarEvent, SidebarToGuestEvent>>} * @type {Map<string|null, PortRPC<GuestToSidebarEvent, SidebarToGuestEvent>>}
*/ */
this._guestRPC = new Map(); this._guestRPC = new Map();
this._nextGuestId = 0;
/** /**
* Tags of annotations that are currently loaded into guest frames. * Tags of annotations that are currently loaded into guest frames.
...@@ -256,21 +260,13 @@ export class FrameSyncService { ...@@ -256,21 +260,13 @@ export class FrameSyncService {
* Set up a connection to a new guest frame. * Set up a connection to a new guest frame.
* *
* @param {MessagePort} port - Port for communicating with the guest * @param {MessagePort} port - Port for communicating with the guest
* @param {string|null} sourceId - Identifier for the guest frame
*/ */
_connectGuest(port) { _connectGuest(port, sourceId) {
/** @type {PortRPC<GuestToSidebarEvent, SidebarToGuestEvent>} */ /** @type {PortRPC<GuestToSidebarEvent, SidebarToGuestEvent>} */
const guestRPC = new PortRPC(); const guestRPC = new PortRPC();
// Add guest RPC to map with a temporary ID until we learn the real ID. this._guestRPC.set(sourceId, guestRPC);
//
// We need to add the guest to the map immediately so that any notifications
// sent from this service to all guests, before we learn the real frame ID,
// are sent to this new guest.
++this._nextGuestId;
let frameIdentifier = /** @type {string|null} */ (
`temp-${this._nextGuestId}`
);
this._guestRPC.set(frameIdentifier, guestRPC);
// Update document metadata for this guest. We currently assume that the // Update document metadata for this guest. We currently assume that the
// guest will make this call once after it connects. To handle updates // guest will make this call once after it connects. To handle updates
...@@ -280,13 +276,8 @@ export class FrameSyncService { ...@@ -280,13 +276,8 @@ export class FrameSyncService {
'documentInfoChanged', 'documentInfoChanged',
/** @param {DocumentInfo} info */ /** @param {DocumentInfo} info */
info => { info => {
this._guestRPC.delete(frameIdentifier);
frameIdentifier = info.frameIdentifier;
this._guestRPC.set(frameIdentifier, guestRPC);
this._store.connectFrame({ this._store.connectFrame({
id: info.frameIdentifier, id: sourceId,
metadata: info.metadata, metadata: info.metadata,
uri: info.uri, uri: info.uri,
}); });
...@@ -297,12 +288,12 @@ export class FrameSyncService { ...@@ -297,12 +288,12 @@ export class FrameSyncService {
// a certain time frame. // a certain time frame.
guestRPC.on('close', () => { guestRPC.on('close', () => {
const frame = this._store.frames().find(f => f.id === frameIdentifier); const frame = this._store.frames().find(f => f.id === sourceId);
if (frame) { if (frame) {
this._store.destroyFrame(frame); this._store.destroyFrame(frame);
} }
guestRPC.destroy(); guestRPC.destroy();
this._guestRPC.delete(frameIdentifier); this._guestRPC.delete(sourceId);
}); });
// A new annotation, note or highlight was created in the frame // A new annotation, note or highlight was created in the frame
...@@ -487,14 +478,20 @@ export class FrameSyncService { ...@@ -487,14 +478,20 @@ export class FrameSyncService {
// Listen for guests connecting to the sidebar. // Listen for guests connecting to the sidebar.
this._listeners.add(hostPort, 'message', event => { this._listeners.add(hostPort, 'message', event => {
const { data, ports } = event; const { data, ports } = event;
const message = /** @type {unknown|Message} */ (data);
if (!isMessage(message)) {
return;
}
if ( if (
isMessageEqual(data, { isMessageEqual(message, {
frame1: 'guest', frame1: 'guest',
frame2: 'sidebar', frame2: 'sidebar',
type: 'offer', type: 'offer',
}) })
) { ) {
this._connectGuest(ports[0]); this._connectGuest(ports[0], message.sourceId ?? null);
} }
}); });
} }
......
...@@ -34,9 +34,6 @@ const fixtures = { ...@@ -34,9 +34,6 @@ const fixtures = {
metadata: { metadata: {
link: [], link: [],
}, },
// This should match the guest frame ID from `framesListEntry`.
frameIdentifier: 'abc',
}, },
}; };
...@@ -191,9 +188,10 @@ describe('FrameSyncService', () => { ...@@ -191,9 +188,10 @@ describe('FrameSyncService', () => {
/** /**
* Simulate a new guest frame connecting to the sidebar. * Simulate a new guest frame connecting to the sidebar.
* *
* @param {string} [frameId] - Guest frame ID, or `undefined` for main frame
* @return {MessagePort} - The port that was sent to the sidebar * @return {MessagePort} - The port that was sent to the sidebar
*/ */
async function connectGuest() { async function connectGuest(frameId) {
const { port1 } = new MessageChannel(); const { port1 } = new MessageChannel();
hostPort.postMessage( hostPort.postMessage(
{ {
...@@ -201,6 +199,7 @@ describe('FrameSyncService', () => { ...@@ -201,6 +199,7 @@ describe('FrameSyncService', () => {
frame2: 'sidebar', frame2: 'sidebar',
type: 'offer', type: 'offer',
requestId: 'abc', requestId: 'abc',
sourceId: frameId,
}, },
[port1] [port1]
); );
...@@ -268,18 +267,16 @@ describe('FrameSyncService', () => { ...@@ -268,18 +267,16 @@ describe('FrameSyncService', () => {
// Connect two guests, one representing the main frame and one representing // Connect two guests, one representing the main frame and one representing
// an iframe. // an iframe.
await connectGuest(); await connectGuest();
await connectGuest(); await connectGuest('iframe');
const mainGuestRPC = fakePortRPCs[1]; const mainGuestRPC = fakePortRPCs[1];
const iframeGuestRPC = fakePortRPCs[2]; const iframeGuestRPC = fakePortRPCs[2];
mainGuestRPC.emit('documentInfoChanged', { mainGuestRPC.emit('documentInfoChanged', {
frameIdentifier: null,
uri: mainFrameAnn.uri, uri: mainFrameAnn.uri,
}); });
iframeGuestRPC.emit('documentInfoChanged', { iframeGuestRPC.emit('documentInfoChanged', {
frameIdentifier: 'iframe',
uri: iframeAnn.uri, uri: iframeAnn.uri,
}); });
...@@ -310,8 +307,6 @@ describe('FrameSyncService', () => { ...@@ -310,8 +307,6 @@ describe('FrameSyncService', () => {
// what happens in VitalSource for example. // what happens in VitalSource for example.
await connectGuest(); await connectGuest();
emitGuestEvent('documentInfoChanged', { emitGuestEvent('documentInfoChanged', {
frameIdentifier: 'iframe',
// Note that URI does not match annotation URI. The backend can still return // Note that URI does not match annotation URI. The backend can still return
// the annotation for this frame based on URI equivalence information. // the annotation for this frame based on URI equivalence information.
uri: 'https://publisher.com/books/1234/chapter1.html', uri: 'https://publisher.com/books/1234/chapter1.html',
...@@ -573,14 +568,16 @@ describe('FrameSyncService', () => { ...@@ -573,14 +568,16 @@ describe('FrameSyncService', () => {
frameSync.connect(); frameSync.connect();
}); });
it("adds the page's metadata to the frames list", async () => { it('adds guest frame details to the store', async () => {
const frameInfo = fixtures.htmlDocumentInfo; const frameInfo = fixtures.htmlDocumentInfo;
await connectGuest(); const frameId = 'test-frame';
await connectGuest(frameId);
emitGuestEvent('documentInfoChanged', frameInfo); emitGuestEvent('documentInfoChanged', frameInfo);
assert.deepEqual(fakeStore.frames(), [ assert.deepEqual(fakeStore.frames(), [
{ {
id: frameInfo.frameIdentifier, id: frameId,
metadata: frameInfo.metadata, metadata: frameInfo.metadata,
uri: frameInfo.uri, uri: frameInfo.uri,
...@@ -642,7 +639,6 @@ describe('FrameSyncService', () => { ...@@ -642,7 +639,6 @@ describe('FrameSyncService', () => {
await connectGuest(); await connectGuest();
emitGuestEvent('documentInfoChanged', { emitGuestEvent('documentInfoChanged', {
frameIdentifier: 'abc',
uri: 'http://example.org', uri: 'http://example.org',
}); });
emitGuestEvent('close'); emitGuestEvent('close');
......
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