Commit 7a0131b3 authored by Robert Knight's avatar Robert Knight

Modernize RPC client/server class.

Modernize the `RPC` class in `src/shared/frame-rpc` that serves as both
the client and server for RPC requests between frames.

This code was originally imported from
https://github.com/substack/frame-rpc but now that we've adopted it, it
makes sense to modernize it to match the conventions and requirements of
the rest of our code.

 - Convert `frame-rpc.js` to modern syntax
 - Update JSDoc documentation for methods and class
 - Convert `RPC` class to a named rather than default export
 - Inline the `apply` method into the `call` method, since only `call`
   was used externally
 - Remove unused option for `methods` constructor argument to be a
   function
 - Rename `src` and `dest` public fields to the more explicit
   `sourceFrame` and `destFrame`
parent f1d43291
import RPC from './frame-rpc';
import { RPC } from './frame-rpc';
/**
* The Bridge service sets up a channel between frames and provides an events
......
/* eslint-disable */
/*
This module was adapted from `index.js` in https://github.com/substack/frame-rpc.
/** This software is released under the MIT license:
This software is released under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
......@@ -21,106 +22,144 @@
SOFTWARE.
*/
/**
* This is a modified copy of index.js from
* https://github.com/substack/frame-rpc (see git log for the modifications),
* upstream license above.
*/
const VERSION = '1.0.0';
/**
* @constructor
* @param {Window} src
* @param {Window} dst
* @param {string} origin
* @param {(Object<string, ((any) => any)>) | ((any) => any)} methods
* Format of messages sent between frames.
*
* See https://github.com/substack/frame-rpc#protocol
*
* @typedef RequestMessage
* @prop {number} sequence
* @prop {string} method
* @prop {any[]} arguments
*
* @typedef ResponseMessage
* @prop {number} response
* @prop {any[]} arguments
*
* @typedef {RequestMessage|ResponseMessage} Message
*/
export default function RPC(src, dst, origin, methods) {
const self = this;
this.src = src;
this.dst = dst;
if (origin === '*') {
this.origin = '*';
} else {
const uorigin = new URL(origin);
this.origin = uorigin.protocol + '//' + uorigin.host;
}
/**
* Class for making RPC requests between frames.
*
* Code adapted from https://github.com/substack/frame-rpc.
*/
export class RPC {
/**
* Create an RPC client for sending RPC requests from `sourceFrame` to
* `destFrame`.
*
* @param {Window} sourceFrame
* @param {Window} destFrame
* @param {string} origin - Origin of destination frame
* @param {Record<string, (...args: any[]) => any>} methods - Map of method
* name to method handler
*/
constructor(sourceFrame, destFrame, origin, methods) {
this.sourceFrame = sourceFrame;
this.destFrame = destFrame;
this._sequence = 0;
this._callbacks = {};
if (origin === '*') {
this.origin = '*';
} else {
const uorigin = new URL(origin);
this.origin = uorigin.protocol + '//' + uorigin.host;
}
this._onmessage = function (ev) {
if (self._destroyed) return;
if (self.dst !== ev.source) return;
if (self.origin !== '*' && ev.origin !== self.origin) return;
if (!ev.data || typeof ev.data !== 'object') return;
if (ev.data.protocol !== 'frame-rpc') return;
if (!Array.isArray(ev.data.arguments)) return;
self._handle(ev.data);
};
this.src.addEventListener('message', this._onmessage);
this._methods =
(typeof methods === 'function' ? methods(this) : methods) || {};
}
this._sequence = 0;
this._callbacks = {};
RPC.prototype.destroy = function () {
this._destroyed = true;
this.src.removeEventListener('message', this._onmessage);
};
/** @param {MessageEvent} event */
this._onmessage = event => {
// Validate message sender and format.
if (
this._destroyed ||
this.destFrame !== event.source ||
(this.origin !== '*' && event.origin !== this.origin) ||
!event.data ||
typeof event.data !== 'object' ||
event.data.protocol !== 'frame-rpc' ||
!Array.isArray(event.data.arguments)
) {
return;
}
this._handle(event.data);
};
this.sourceFrame.addEventListener('message', this._onmessage);
this._methods = methods;
}
/**
* @param {string} method
*/
RPC.prototype.call = function (method) {
const args = [].slice.call(arguments, 1);
return this.apply(method, args);
};
/**
* Disconnect the RPC channel. After this is invoked no further method calls
* will be received.
*/
destroy() {
this._destroyed = true;
this.sourceFrame.removeEventListener('message', this._onmessage);
}
/**
* @param {string} method
* @param {any[]} args
*/
RPC.prototype.apply = function (method, args) {
if (this._destroyed) return;
const seq = this._sequence++;
if (typeof args[args.length - 1] === 'function') {
this._callbacks[seq] = args[args.length - 1];
args = args.slice(0, -1);
/**
* Send an RPC request to the destination frame.
*
* If the final argument in `args` is a function, it is treated as a callback
* which is invoked with the response.
*
* @param {string} method
* @param {any[]} args
*/
call(method, ...args) {
if (this._destroyed) {
return;
}
const seq = this._sequence++;
if (typeof args[args.length - 1] === 'function') {
this._callbacks[seq] = args[args.length - 1];
args = args.slice(0, -1);
}
this.destFrame.postMessage(
{
protocol: 'frame-rpc',
version: VERSION,
sequence: seq,
method: method,
arguments: args,
},
this.origin
);
}
this.dst.postMessage(
{
protocol: 'frame-rpc',
version: VERSION,
sequence: seq,
method: method,
arguments: args,
},
this.origin
);
};
RPC.prototype._handle = function (msg) {
const self = this;
if (self._destroyed) return;
if (msg.hasOwnProperty('method')) {
if (!this._methods.hasOwnProperty(msg.method)) return;
const args = msg.arguments.concat(function () {
self.dst.postMessage(
{
protocol: 'frame-rpc',
version: VERSION,
response: msg.sequence,
arguments: [].slice.call(arguments),
},
self.origin
);
});
this._methods[msg.method].apply(this._methods, args);
} else if (msg.hasOwnProperty('response')) {
const cb = this._callbacks[msg.response];
delete this._callbacks[msg.response];
if (cb) cb.apply(null, msg.arguments);
/**
* @param {Message} msg
*/
_handle(msg) {
if (this._destroyed) {
return;
}
if ('method' in msg) {
if (!this._methods.hasOwnProperty(msg.method)) {
return;
}
/** @param {any[]} args */
const callback = (...args) => {
this.destFrame.postMessage(
{
protocol: 'frame-rpc',
version: VERSION,
response: msg.sequence,
arguments: args,
},
this.origin
);
};
this._methods[msg.method].call(this._methods, ...msg.arguments, callback);
} else if ('response' in msg) {
const cb = this._callbacks[msg.response];
delete this._callbacks[msg.response];
if (cb) {
cb.apply(null, msg.arguments);
}
}
}
};
}
import Bridge from '../bridge';
import RPC from '../frame-rpc';
import { RPC } from '../frame-rpc';
describe('shared.bridge', function () {
describe('shared/bridge', function () {
const sandbox = sinon.createSandbox();
let bridge;
let createChannel;
......@@ -26,8 +26,8 @@ describe('shared.bridge', function () {
describe('#createChannel', function () {
it('creates a new channel with the provided options', function () {
const channel = createChannel();
assert.equal(channel.src, window);
assert.equal(channel.dst, fakeWindow);
assert.equal(channel.sourceFrame, window);
assert.equal(channel.destFrame, fakeWindow);
assert.equal(channel.origin, 'http://example.com');
});
......
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