From 773957fe7cdf10f09d8195c83bd8b61d1478992e Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:49:39 -0500 Subject: [PATCH] feat(streams): Generalize message channel init to abstract execution contexts. --- packages/extension/src/iframe-vat-worker.ts | 4 +- packages/extension/src/iframe.ts | 5 +- packages/streams/src/message-channel.test.ts | 32 +++++++--- packages/streams/src/message-channel.ts | 61 +++++++++++--------- 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/packages/extension/src/iframe-vat-worker.ts b/packages/extension/src/iframe-vat-worker.ts index 9d52be12e..400e149e6 100644 --- a/packages/extension/src/iframe-vat-worker.ts +++ b/packages/extension/src/iframe-vat-worker.ts @@ -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]; }, diff --git a/packages/extension/src/iframe.ts b/packages/extension/src/iframe.ts index a244c7aaf..f930f967a 100644 --- a/packages/extension/src/iframe.ts +++ b/packages/extension/src/iframe.ts @@ -10,7 +10,10 @@ main().catch(console.error); * The main function for the iframe. */ async function main(): Promise { - const port = await receiveMessagePort(); + const port = await receiveMessagePort( + (listener) => addEventListener('message', listener), + (listener) => removeEventListener('message', listener), + ); const stream = new MessagePortDuplexStream< StreamEnvelope, StreamEnvelopeReply diff --git a/packages/streams/src/message-channel.test.ts b/packages/streams/src/message-channel.test.ts index d05d2ab6b..7af64a9a6 100644 --- a/packages/streams/src/message-channel.test.ts +++ b/packages/streams/src/message-channel.test.ts @@ -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( @@ -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() @@ -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() @@ -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'); @@ -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( @@ -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'); @@ -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'); diff --git a/packages/streams/src/message-channel.ts b/packages/streams/src/message-channel.ts index 48a79311d..bbcc14c74 100644 --- a/packages/streams/src/message-channel.ts +++ b/packages/streams/src/message-channel.ts @@ -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 */ @@ -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 { const { port1, port2 } = new MessageChannel(); @@ -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) => { @@ -81,24 +83,31 @@ 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 { +export async function receiveMessagePort( + addListener: (listener: Listener) => void, + removeListener: (listener: Listener) => void, +): Promise { const { promise, resolve } = makePromiseKit(); 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 }; @@ -106,6 +115,6 @@ export async function receiveMessagePort(): Promise { resolve(port); }; - window.addEventListener('message', listener); + addListener(listener); return promise; }