Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/extension/src/iframe-vat-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export const makeIframeVatWorker = (
id: vatHtmlId,
testId: 'ocap-iframe',
});
const port = await getPort(newWindow);
const port = await getPort((message, transfer) =>
newWindow.postMessage(message, '*', transfer),
);

return [port, newWindow];
},
Expand Down
5 changes: 4 additions & 1 deletion packages/extension/src/iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ main().catch(console.error);
* The main function for the iframe.
*/
async function main(): Promise<void> {
const port = await receiveMessagePort();
const port = await receiveMessagePort(
(listener) => addEventListener('message', listener),
(listener) => removeEventListener('message', listener),
);
const stream = new MessagePortDuplexStream<
StreamEnvelope,
StreamEnvelopeReply
Expand Down
32 changes: 23 additions & 9 deletions packages/streams/src/message-channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ describe.concurrent('initializeMessageChannel', () => {
const postMessageSpy = vi.spyOn(targetWindow, 'postMessage');
// We intentionally let this one go. It will never settle.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
initializeMessageChannel(targetWindow as unknown as Window);
initializeMessageChannel((message, transfer) =>
targetWindow.postMessage(message, '*', transfer),
);

expect(postMessageSpy).toHaveBeenCalledOnce();
expect(postMessageSpy).toHaveBeenCalledWith(
Expand All @@ -33,8 +35,8 @@ describe.concurrent('initializeMessageChannel', () => {
}) => {
const targetWindow = new JSDOM().window;
const postMessageSpy = vi.spyOn(targetWindow, 'postMessage');
const messageChannelP = initializeMessageChannel(
targetWindow as unknown as Window,
const messageChannelP = initializeMessageChannel((message, transfer) =>
targetWindow.postMessage(message, '*', transfer),
);

// @ts-expect-error Wrong types for window.postMessage()
Expand All @@ -61,8 +63,8 @@ describe.concurrent('initializeMessageChannel', () => {
async (unexpectedMessage, { expect }) => {
const targetWindow = new JSDOM().window;
const postMessageSpy = vi.spyOn(targetWindow, 'postMessage');
const messageChannelP = initializeMessageChannel(
targetWindow as unknown as Window,
const messageChannelP = initializeMessageChannel((message, transfer) =>
targetWindow.postMessage(message, '*', transfer),
);

// @ts-expect-error Wrong types for window.postMessage()
Expand Down Expand Up @@ -105,7 +107,10 @@ describe('receiveMessagePort', () => {
});

it('receives and acknowledges a message port', async ({ expect }) => {
const messagePortP = receiveMessagePort();
const messagePortP = receiveMessagePort(
(listener) => addEventListener('message', listener),
(listener) => removeEventListener('message', listener),
);

const { port2 } = new MessageChannel();
const portPostMessageSpy = vi.spyOn(port2, 'postMessage');
Expand All @@ -130,7 +135,10 @@ describe('receiveMessagePort', () => {
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');

const messagePortP = receiveMessagePort();
const messagePortP = receiveMessagePort(
(listener) => addEventListener('message', listener),
(listener) => removeEventListener('message', listener),
);

const { port2 } = new MessageChannel();
window.dispatchEvent(
Expand Down Expand Up @@ -167,7 +175,10 @@ describe('receiveMessagePort', () => {
])(
'ignores message events with unexpected data dispatched on window: %#',
async (unexpectedMessage, { expect }) => {
const messagePortP = receiveMessagePort();
const messagePortP = receiveMessagePort(
(listener) => addEventListener('message', listener),
(listener) => removeEventListener('message', listener),
);

const { port2 } = new MessageChannel();
const portPostMessageSpy = vi.spyOn(port2, 'postMessage');
Expand All @@ -191,7 +202,10 @@ describe('receiveMessagePort', () => {
it.for([{}, { ports: [] }, { ports: [{}, {}] }])(
'ignores message events with unexpected ports dispatched on window: %#',
async (unexpectedPorts, { expect }) => {
const messagePortP = receiveMessagePort();
const messagePortP = receiveMessagePort(
(listener) => addEventListener('message', listener),
(listener) => removeEventListener('message', listener),
);

const { port2 } = new MessageChannel();
const portPostMessageSpy = vi.spyOn(port2, 'postMessage');
Expand Down
61 changes: 35 additions & 26 deletions packages/streams/src/message-channel.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/**
* This module establishes a simple protocol for creating a MessageChannel between a
* window and one of its iframes, as follows:
* 1. The parent window creates an iframe and appends it to the DOM. The iframe must be
* loaded and the `contentWindow` property must be accessible.
* 2. The iframe calls `receiveMessagePort()` on startup in one of its scripts. The script
* element in question should not have the `async` attribute.
* 3. The parent window calls `initializeMessageChannel()` which sends a message port to
* the iframe. When the returned promise resolves, the parent window and the iframe have
* established a message channel.
* This module establishes a simple protocol for creating a MessageChannel between two
* realms, as follows:
* 1. The sending realm asserts that the receiving realm is ready to receive messages,
* either by creating the realm itself (for example, by appending an iframe to the DOM),
* or via some other means.
* 2. The receiving realm calls `receiveMessagePort()` on startup in one of its scripts.
* The script element in question should not have the `async` attribute.
* 3. The sending realm calls `initializeMessageChannel()` which sends a message port to
* the receiving realm. When the returned promise resolves, the sending realm and the
* receiving realm have established a message channel.
*
* @module MessageChannel utilities
*/
Expand Down Expand Up @@ -37,17 +38,18 @@ const isAckMessage = (value: unknown): value is AcknowledgeMessage =>
isObject(value) && value.type === MessageType.Acknowledge;

/**
* Creates a message channel and sends one of the ports to the target window. The iframe
* associated with the target window must be loaded, and it must have called
* {@link receiveMessagePort} to receive the remote message port. Rejects if the first
* message received over the channel is not an {@link AcknowledgeMessage}.
* Creates a message channel and sends one of the ports to the receiving realm. The
* realm must be loaded, and it must have called {@link receiveMessagePort} to
* receive the remote message port. Rejects if the first message received over the
* channel is not an {@link AcknowledgeMessage}.
*
* @param targetWindow - The iframe window to send the message port to.
* @returns A promise that resolves with the local message port, once the target window
* has acknowledged its receipt of the remote port.
* @param postMessage - A bound method for posting a message to the receiving realm.
* Must be able to transfer a message port.
* @returns A promise that resolves with the local message port, once the receiving
* realm has acknowledged its receipt of the remote port.
*/
export async function initializeMessageChannel(
targetWindow: Window,
postMessage: (message: unknown, transfer: Transferable[]) => void,
): Promise<MessagePort> {
const { port1, port2 } = new MessageChannel();

Expand All @@ -71,7 +73,7 @@ export async function initializeMessageChannel(
const initMessage: InitializeMessage = {
type: MessageType.Initialize,
};
targetWindow.postMessage(initMessage, '*', [port2]);
postMessage(initMessage, [port2]);

return promise
.catch((error) => {
Expand All @@ -81,31 +83,38 @@ export async function initializeMessageChannel(
.finally(() => (port1.onmessage = null));
}

type Listener = (message: MessageEvent) => void;

/**
* Receives a message port from the parent window, and sends an {@link AcknowledgeMessage}
* Receives a message port from the sending realm, and sends an {@link AcknowledgeMessage}
* over the port. Should be called in a script _without_ the `async` attribute on startup.
* The parent window must call {@link initializeMessageChannel} to send the message port
* after this iframe has loaded. Ignores any message events dispatched on the local
* `window` that are not an {@link InitializeMessage}.
* The sending realm must call {@link initializeMessageChannel} to send the message port
* after this realm has loaded. Ignores any message events dispatched on the local
* realm that are not an {@link InitializeMessage}.
*
* @param addListener - A bound method to add a message event listener to the sending realm.
* @param removeListener - A bound method to remove a message event listener from the sending realm.
* @returns A promise that resolves with a message port that can be used to communicate
* with the parent window.
* with the sending realm.
*/
export async function receiveMessagePort(): Promise<MessagePort> {
export async function receiveMessagePort(
addListener: (listener: Listener) => void,
removeListener: (listener: Listener) => void,
): Promise<MessagePort> {
const { promise, resolve } = makePromiseKit<MessagePort>();

const listener = (message: MessageEvent): void => {
if (!isInitMessage(message)) {
return;
}
window.removeEventListener('message', listener);
removeListener(listener);

const port = message.ports[0] as MessagePort;
const ackMessage: AcknowledgeMessage = { type: MessageType.Acknowledge };
port.postMessage(ackMessage);
resolve(port);
};

window.addEventListener('message', listener);
addListener(listener);
return promise;
}