Commit c72257a9 authored by Robert Knight's avatar Robert Knight

Remove `onConnect` method from Bridge

Simplify the Bridge class by removing the `onConnect` API and replacing
current calls with alternative approaches.

The `onConnect` method was a way to register a callback that would be invoked
after a test RPC call over a newly-connected channel succeeded.

The purpose of this method was not that clear however and its existence
suggested that it is necessary to wait for the callback to be invoked before
making RPC requests on the new channel. This is not the case, as the underlying
transport buffers messages until the receiver is ready to consume them. Therefore
RPC requests can be made as soon as `createChannel` returns.

The current uses of `onConnect` were replaced as follows:

 1. The host frame used it to be notified when the sidebar application has
    loaded. This has been replaced with an explicit `ready` RPC call.

 2. The sidebar waited for this callback before querying metadata for a
    newly-connected guest. It now sends a metadata query to the guest
    immediately when it receives the guest port from the host frame. This saves
    an unnecessary round-trip between the host and guest. In future we can
    optimize metadata querying more by having the guest _push_ metadata to
    the sidebar rather than waiting for the sidebar to query it.
parent fd3d8660
...@@ -189,7 +189,7 @@ export default class Sidebar { ...@@ -189,7 +189,7 @@ export default class Sidebar {
this._notifyOfLayoutChange(false); this._notifyOfLayoutChange(false);
this._setupSidebarEvents(); this._setupSidebarEvents();
this._sidebarRPC.onConnect(() => { this._sidebarRPC.on('ready', () => {
// Show the UI // Show the UI
if (this.iframeContainer) { if (this.iframeContainer) {
this.iframeContainer.style.display = ''; this.iframeContainer.style.display = '';
......
...@@ -106,7 +106,6 @@ describe('Guest', () => { ...@@ -106,7 +106,6 @@ describe('Guest', () => {
createChannel: sinon.stub(), createChannel: sinon.stub(),
destroy: sinon.stub(), destroy: sinon.stub(),
on: sinon.stub(), on: sinon.stub(),
onConnect: sinon.stub(),
}; };
fakeBridges.push(bridge); fakeBridges.push(bridge);
return bridge; return bridge;
......
...@@ -72,8 +72,7 @@ describe('Sidebar', () => { ...@@ -72,8 +72,7 @@ describe('Sidebar', () => {
* when the sidebar has loaded and is ready. * when the sidebar has loaded and is ready.
*/ */
const connectSidebarApp = () => { const connectSidebarApp = () => {
const callback = sidebarBridge().onConnect.getCall(0).args[0]; emitSidebarEvent('ready');
callback();
}; };
const createSidebar = (config = {}) => { const createSidebar = (config = {}) => {
...@@ -116,7 +115,6 @@ describe('Sidebar', () => { ...@@ -116,7 +115,6 @@ describe('Sidebar', () => {
createChannel: sinon.stub(), createChannel: sinon.stub(),
destroy: sinon.stub(), destroy: sinon.stub(),
on: sinon.stub(), on: sinon.stub(),
onConnect: sinon.stub(),
}; };
fakeBridges.push(bridge); fakeBridges.push(bridge);
return bridge; return bridge;
......
...@@ -19,12 +19,6 @@ export class Bridge { ...@@ -19,12 +19,6 @@ export class Bridge {
this.links = []; this.links = [];
/** @type {Record<string, (...args: any[]) => void>} */ /** @type {Record<string, (...args: any[]) => void>} */
this.channelListeners = {}; this.channelListeners = {};
/** @type {Array<(channel: PortRPC) => void>} */
this.onConnectListeners = [];
// `connect` is registered with a callback so it triggers a `postMessage`
// response from the reciprocal port.
this.on('connect', cb => cb());
} }
/** /**
...@@ -48,22 +42,7 @@ export class Bridge { ...@@ -48,22 +42,7 @@ export class Bridge {
*/ */
createChannel(port) { createChannel(port) {
const channel = new PortRPC(port, this.channelListeners); const channel = new PortRPC(port, this.channelListeners);
let connected = false;
const ready = () => {
if (connected) {
return;
}
connected = true;
this.onConnectListeners.forEach(cb => cb(channel));
};
// Fire off a connection attempt
channel.call('connect', ready);
// Store the newly created channel in our collection
this.links.push(channel); this.links.push(channel);
return channel; return channel;
} }
...@@ -157,14 +136,4 @@ export class Bridge { ...@@ -157,14 +136,4 @@ export class Bridge {
this.channelListeners[method] = listener; this.channelListeners[method] = listener;
return this; return this;
} }
/**
* Add a listener to be called upon a new connection.
*
* @param {(channel: PortRPC) => void} listener
*/
onConnect(listener) {
this.onConnectListeners.push(listener);
return this;
}
} }
...@@ -194,59 +194,6 @@ describe('shared/bridge', () => { ...@@ -194,59 +194,6 @@ describe('shared/bridge', () => {
}); });
}); });
describe('#onConnect', () => {
// Simulate a Bridge attached to the other end of a channel receiving
// the `connect` RPC request and handling it using the `connect` handler
// registered by the Bridge.
const runConnectHandler = channel => {
const connectCall = channel.call
.getCalls()
.find(call => call.firstArg === 'connect');
// Invoke the `connect` handler. Here we're invoking it on `channel` but
// in the actual app this would be called on the counterpart channel in
// the other frame.
channel.methods.connect(...connectCall.args.slice(1));
};
it('runs callbacks when channel connects', () => {
const onConnectCallback = sinon.stub();
bridge.onConnect(onConnectCallback);
const channel = createChannel();
runConnectHandler(channel);
assert.calledWith(onConnectCallback, channel);
});
it('allows multiple callbacks to be registered', () => {
const onConnectCallback1 = sinon.stub();
const onConnectCallback2 = sinon.stub();
bridge.onConnect(onConnectCallback1);
bridge.onConnect(onConnectCallback2);
const channel = createChannel();
runConnectHandler(channel);
assert.calledWith(onConnectCallback1, channel);
assert.calledWith(onConnectCallback2, channel);
});
it('only invokes `onConnect` callback once', () => {
const onConnectCallback = sinon.stub();
bridge.onConnect(onConnectCallback);
const channel = createChannel();
runConnectHandler(channel);
runConnectHandler(channel);
assert.calledOnce(onConnectCallback);
});
});
describe('#destroy', () => describe('#destroy', () =>
it('destroys all opened channels', () => { it('destroys all opened channels', () => {
const channel1 = bridge.createChannel(); const channel1 = bridge.createChannel();
......
...@@ -24,34 +24,6 @@ describe('PortRPC-Bridge integration', () => { ...@@ -24,34 +24,6 @@ describe('PortRPC-Bridge integration', () => {
clock.restore(); clock.restore();
}); });
describe('establishing a connection', () => {
it('should invoke Bridge `onConnect` callbacks after connecting', async () => {
const bridge = createBridge();
const reciprocalBridge = createBridge();
reciprocalBridge.on('method', cb => cb(null));
let callbackCount = 0;
const callback = () => {
++callbackCount;
};
bridge.onConnect(callback);
bridge.onConnect(callback); // allows multiple callbacks to be registered
reciprocalBridge.onConnect(callback);
const channel = bridge.createChannel(port1);
const reciprocalChannel = reciprocalBridge.createChannel(port2);
await bridge.call('method');
assert.equal(callbackCount, 3);
// Additional calls to the RPC `connect` method are ignored
await channel.call('connect');
await reciprocalChannel.call('connect');
await bridge.call('method');
assert.equal(callbackCount, 3);
});
});
describe('sending and receiving RPC messages', () => { describe('sending and receiving RPC messages', () => {
it('should invoke Bridge method handler on every channel when calling a RPC method', async () => { it('should invoke Bridge method handler on every channel when calling a RPC method', async () => {
const bridge = createBridge(); const bridge = createBridge();
......
...@@ -250,32 +250,30 @@ export class FrameSyncService { ...@@ -250,32 +250,30 @@ export class FrameSyncService {
this._guestRPC.on('closeSidebar', () => { this._guestRPC.on('closeSidebar', () => {
this._hostRPC.call('closeSidebar'); this._hostRPC.call('closeSidebar');
}); });
}
/** /**
* Query the guest in a frame for the URL and metadata of the document that * Query the guest 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. * is currently loaded and add the result to the set of connected frames.
* *
* @param {PortRPC} channel * @param {PortRPC} channel
*/ */
const addFrame = channel => { _addFrame(channel) {
// Synchronize highlight visibility in this guest with the sidebar's controls. // Synchronize highlight visibility in this guest with the sidebar's controls.
channel.call('setHighlightsVisible', this._highlightsVisible); channel.call('setHighlightsVisible', this._highlightsVisible);
channel.call('getDocumentInfo', (err, info) => { channel.call('getDocumentInfo', (err, info) => {
if (err) { if (err) {
channel.destroy(); channel.destroy();
return; return;
} }
this._store.connectFrame({ this._store.connectFrame({
id: info.frameIdentifier, id: info.frameIdentifier,
metadata: info.metadata, metadata: info.metadata,
uri: info.uri, uri: info.uri,
});
}); });
}; });
this._guestRPC.onConnect(addFrame);
} }
/** /**
...@@ -311,6 +309,7 @@ export class FrameSyncService { ...@@ -311,6 +309,7 @@ export class FrameSyncService {
// Create channel for sidebar-host communication. // Create channel for sidebar-host communication.
const hostPort = await this._portFinder.discover('host'); const hostPort = await this._portFinder.discover('host');
this._hostRPC.createChannel(hostPort); this._hostRPC.createChannel(hostPort);
this._hostRPC.call('ready');
// 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 => {
...@@ -322,7 +321,8 @@ export class FrameSyncService { ...@@ -322,7 +321,8 @@ export class FrameSyncService {
type: 'offer', type: 'offer',
}) })
) { ) {
this._guestRPC.createChannel(ports[0]); const channel = this._guestRPC.createChannel(ports[0]);
this._addFrame(channel);
} }
}); });
} }
......
...@@ -60,6 +60,7 @@ describe('FrameSyncService', () => { ...@@ -60,6 +60,7 @@ describe('FrameSyncService', () => {
let fakeAnnotationsService; let fakeAnnotationsService;
let fakeBridges; let fakeBridges;
let fakePortFinder; let fakePortFinder;
let fakeStore; let fakeStore;
let fakeWindow; let fakeWindow;
...@@ -67,20 +68,31 @@ describe('FrameSyncService', () => { ...@@ -67,20 +68,31 @@ describe('FrameSyncService', () => {
let hostPort; let hostPort;
let sidebarPort; let sidebarPort;
// Hook to prepare channels created by `Bridge.createChannel` before they are
// returned to the service.
let setupChannel;
beforeEach(() => { beforeEach(() => {
fakeAnnotationsService = { create: sinon.stub() }; fakeAnnotationsService = { create: sinon.stub() };
fakeBridges = []; fakeBridges = [];
setupChannel = null;
FakeBridge = sinon.stub().callsFake(() => { FakeBridge = sinon.stub().callsFake(() => {
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const bridge = { const bridge = {
call: sinon.stub(), call: sinon.stub(),
createChannel: sinon.stub(), createChannel: sinon.stub().callsFake(() => {
const rpc = {
call: sinon.stub(),
destroy: sinon.stub(),
};
setupChannel?.(rpc);
return rpc;
}),
emit: emitter.emit.bind(emitter), emit: emitter.emit.bind(emitter),
on: emitter.on.bind(emitter), on: emitter.on.bind(emitter),
onConnect: function (listener) {
emitter.on('connect', listener);
},
}; };
fakeBridges.push(bridge); fakeBridges.push(bridge);
return bridge; return bridge;
...@@ -160,6 +172,25 @@ describe('FrameSyncService', () => { ...@@ -160,6 +172,25 @@ describe('FrameSyncService', () => {
guestBridge().emit(event, ...args); guestBridge().emit(event, ...args);
} }
/**
* Simulate a new guest frame connecting to the sidebar.
*
* @return {MessagePort} - The port that was sent to the sidebar
*/
async function connectGuest() {
const { port1 } = new MessageChannel();
hostPort.postMessage(
{
frame1: 'guest',
frame2: 'sidebar',
type: 'offer',
},
[port1]
);
await delay(0);
return port1;
}
describe('#connect', () => { describe('#connect', () => {
it('discovers and connects to the host frame', async () => { it('discovers and connects to the host frame', async () => {
await frameSync.connect(); await frameSync.connect();
...@@ -167,21 +198,16 @@ describe('FrameSyncService', () => { ...@@ -167,21 +198,16 @@ describe('FrameSyncService', () => {
assert.calledWith(hostBridge().createChannel, sidebarPort); assert.calledWith(hostBridge().createChannel, sidebarPort);
}); });
it('connects to new guests when they are ready', async () => { it('notifies the host frame that the sidebar is ready to be displayed', async () => {
const { port1 } = new MessageChannel(); await frameSync.connect();
frameSync.connect(); assert.calledWith(hostBridge().call, 'ready');
hostPort.postMessage( });
{
frame1: 'guest',
frame2: 'sidebar',
type: 'offer',
},
[port1]
);
await delay(0);
assert.calledWith(guestBridge().createChannel, port1); it('connects to new guests', async () => {
frameSync.connect();
const port = await connectGuest();
assert.calledWith(guestBridge().createChannel, port);
}); });
}); });
...@@ -419,25 +445,19 @@ describe('FrameSyncService', () => { ...@@ -419,25 +445,19 @@ describe('FrameSyncService', () => {
}); });
context('when a new frame connects', () => { context('when a new frame connects', () => {
let frameInfo;
let fakeChannel;
beforeEach(() => { beforeEach(() => {
fakeChannel = {
call: sinon.spy((name, callback) => {
if (name === 'getDocumentInfo') {
callback(null, frameInfo);
}
}),
destroy: sinon.stub(),
};
frameSync.connect(); frameSync.connect();
}); });
it("adds the page's metadata to the frames list", () => { it("adds the page's metadata to the frames list", async () => {
frameInfo = fixtures.htmlDocumentInfo; const frameInfo = fixtures.htmlDocumentInfo;
setupChannel = rpc => {
rpc.call.withArgs('getDocumentInfo').callsFake((method, callback) => {
callback(null, frameInfo);
});
};
emitGuestEvent('connect', fakeChannel); await connectGuest();
assert.calledWith(fakeStore.connectFrame, { assert.calledWith(fakeStore.connectFrame, {
id: frameInfo.frameIdentifier, id: frameInfo.frameIdentifier,
...@@ -446,27 +466,35 @@ describe('FrameSyncService', () => { ...@@ -446,27 +466,35 @@ describe('FrameSyncService', () => {
}); });
}); });
it('closes the channel and does not add frame to store if getting document info fails', () => { it('closes the channel and does not add frame to store if getting document info fails', async () => {
fakeChannel.call = (name, callback) => { let channel;
if (name === 'getDocumentInfo') { setupChannel = rpc => {
callback('Something went wrong'); rpc.call.withArgs('getDocumentInfo').callsFake((method, callback) => {
} callback('Error getting document info');
});
channel = rpc;
}; };
emitGuestEvent('connect', fakeChannel); await connectGuest();
assert.called(fakeChannel.destroy); assert.ok(channel);
assert.called(channel.destroy);
assert.notCalled(fakeStore.connectFrame); assert.notCalled(fakeStore.connectFrame);
}); });
it("synchronizes highlight visibility in the guest with the sidebar's controls", () => { it("synchronizes highlight visibility in the guest with the sidebar's controls", async () => {
let channel;
setupChannel = rpc => {
channel = rpc;
};
emitHostEvent('setHighlightsVisible', true); emitHostEvent('setHighlightsVisible', true);
emitGuestEvent('connect', fakeChannel); await connectGuest();
assert.calledWith(fakeChannel.call, 'setHighlightsVisible', true); assert.calledWith(channel.call, 'setHighlightsVisible', true);
emitHostEvent('setHighlightsVisible', false); emitHostEvent('setHighlightsVisible', false);
emitGuestEvent('connect', fakeChannel); await connectGuest();
assert.calledWith(fakeChannel.call, 'setHighlightsVisible', false); assert.calledWith(channel.call, 'setHighlightsVisible', false);
}); });
}); });
......
...@@ -137,6 +137,11 @@ export type SidebarToGuestEvent = ...@@ -137,6 +137,11 @@ export type SidebarToGuestEvent =
* Events that the sidebar sends to the host * Events that the sidebar sends to the host
*/ */
export type SidebarToHostEvent = export type SidebarToHostEvent =
/**
* The sidebar notifies the host that it has loaded and is ready to be displayed.
*/
| 'ready'
/** /**
* The sidebar relays to the host to close the sidebar. * The sidebar relays to the host to close the sidebar.
*/ */
......
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