From 5e2d9a2fbcf2cda7c058c7fb78a52a08f6beef46 Mon Sep 17 00:00:00 2001 From: hensm Date: Wed, 27 Apr 2022 16:17:33 +0100 Subject: [PATCH] Replace eventMessageChannel with clearer implementation --- ext/src/cast/contentBridge.ts | 18 +++--- ext/src/cast/eventMessageChannel.ts | 79 ------------------------ ext/src/cast/eventMessaging.ts | 93 +++++++++++++++++++++++++++++ ext/src/cast/export.ts | 28 +++++---- ext/src/cast/index.ts | 4 +- ext/src/cast/sdk/Session.ts | 6 +- ext/src/cast/sdk/index.ts | 12 ++-- ext/src/lib/TypedMessagePort.ts | 2 +- 8 files changed, 132 insertions(+), 110 deletions(-) delete mode 100644 ext/src/cast/eventMessageChannel.ts create mode 100644 ext/src/cast/eventMessaging.ts diff --git a/ext/src/cast/contentBridge.ts b/ext/src/cast/contentBridge.ts index 7a9b157..cbc9b28 100644 --- a/ext/src/cast/contentBridge.ts +++ b/ext/src/cast/contentBridge.ts @@ -1,21 +1,25 @@ "use strict"; -import { onMessageResponse, sendMessage } from "./eventMessageChannel"; +import eventMessaging from "./eventMessaging"; import messaging, { Message } from "../messaging"; // Message port to background script export const backgroundPort = messaging.connect({ name: "cast" }); -const forwardToCast = (message: Message) => sendMessage(message); -const forwardToMain = (message: Message) => backgroundPort.postMessage(message); +const forwardToPage = (message: Message) => { + eventMessaging.extension.sendMessage(message); +}; +const forwardToMain = (message: Message) => { + backgroundPort.postMessage(message); +}; // Add message listeners -backgroundPort.onMessage.addListener(forwardToCast); -const listener = onMessageResponse(forwardToMain); +backgroundPort.onMessage.addListener(forwardToPage); +eventMessaging.extension.addListener(forwardToMain); // Remove listeners backgroundPort.onDisconnect.addListener(() => { - backgroundPort.onMessage.removeListener(forwardToCast); - listener.disconnect(); + backgroundPort.onMessage.removeListener(forwardToPage); + eventMessaging.extension.addListener(forwardToMain); }); diff --git a/ext/src/cast/eventMessageChannel.ts b/ext/src/cast/eventMessageChannel.ts deleted file mode 100644 index 0204e76..0000000 --- a/ext/src/cast/eventMessageChannel.ts +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; - -import { Message } from "../messaging"; - -type ListenerFunc = (message: Message) => void; - -export interface ListenerObject { - disconnect(): void; -} - -export function onMessage(listener: ListenerFunc): ListenerObject { - function on__castMessage(ev: CustomEvent) { - listener(JSON.parse(ev.detail)); - - /** - * TODO: - * Figure out a way to handle and stop propagation of this - * event to hide it from page scripts. - * Currently the event handler is set after the page loads the - * cast API, allowing pages set handlers before this script, - * intercept the event, and cancel it. - */ - ev.stopPropagation(); - } - - // @ts-ignore - document.addEventListener("__castMessage", on__castMessage, true); - - return { - disconnect() { - // @ts-ignore - document.removeEventListener( - "__castMessage", - on__castMessage, - true - ); - } - }; -} - -export function sendMessageResponse(message: Message) { - const event = new CustomEvent("__castMessageResponse", { - detail: JSON.stringify(message) - }); - - document.dispatchEvent(event); -} - -export function onMessageResponse(listener: ListenerFunc): ListenerObject { - function on__castMessageResponse(ev: CustomEvent) { - listener(JSON.parse(ev.detail)); - } - - // @ts-ignore - document.addEventListener( - "__castMessageResponse", - on__castMessageResponse, - true - ); - - return { - disconnect() { - // @ts-ignore - document.removeEventListener( - "__castMessageResponse", - on__castMessageResponse, - true - ); - } - }; -} - -export function sendMessage(message: Message) { - const event = new CustomEvent("__castMessage", { - detail: JSON.stringify(message) - }); - - document.dispatchEvent(event); -} diff --git a/ext/src/cast/eventMessaging.ts b/ext/src/cast/eventMessaging.ts new file mode 100644 index 0000000..b6f937a --- /dev/null +++ b/ext/src/cast/eventMessaging.ts @@ -0,0 +1,93 @@ +"use strict"; + +import logger from "../lib/logger"; +import { TypedEventTarget } from "../lib/TypedEventTarget"; +import { Message } from "../messaging"; + +type EventMessengerListener = (message: Message) => void; + +/** + * Messenger class for cross-context messages via CustomEvent. + * + * Supplied with an incoming and outgoing event name, it provides a + * message channel from content scripts to page scripts provided that + * the opposite event names are used with instances on either side. + * + * Note: + * Extending EventTarget seems to cause issues with dispatching custom + * events in WebExtension content scripts (sandbox issue?), so custom + * addListener/removeListener methods are used instead. + */ +abstract class EventMessenger { + private listeners = new Set(); + + constructor( + private incomingMessageEventName: string, + private outgoingMessageEventName: string + ) { + // @ts-ignore + document.addEventListener( + this.incomingMessageEventName, + (ev: CustomEvent) => { + for (const listener of this.listeners) { + listener(JSON.parse(ev.detail)); + } + } + ); + } + + addListener(listener: EventMessengerListener) { + this.listeners.add(listener); + } + removeListener(listener: EventMessengerListener) { + this.listeners.delete(listener); + } + + sendMessage(message: Message) { + document.dispatchEvent( + new CustomEvent(this.outgoingMessageEventName, { + detail: JSON.stringify(message) + }) + ); + } +} + +const EV_TO_PAGE = "__castMessage"; +const EV_FROM_PAGE = "__castMessageResponse"; + +export class PageEventMessenger extends EventMessenger { + constructor() { + super(EV_TO_PAGE, EV_FROM_PAGE); + } +} +export class ExtensionEventMessenger extends EventMessenger { + constructor() { + super(EV_FROM_PAGE, EV_TO_PAGE); + } +} + +// Ensure only one instance of the type initially created is used +let messenger: EventMessenger; +function getMessenger(messengerType: { new (): EventMessenger }) { + if (!messenger) { + messenger = new messengerType(); + } else if (!(messenger instanceof messengerType)) { + throw logger.error( + "Requested messenger does not match type of instantiated messenger!" + ); + } + + return messenger; +} + +export default { + /** Event messenger for page scripts. */ + get page() { + return getMessenger(PageEventMessenger); + }, + + /** Event messenger for extension content scripts. */ + get extension() { + return getMessenger(ExtensionEventMessenger); + } +}; diff --git a/ext/src/cast/export.ts b/ext/src/cast/export.ts index c405e44..8f259b2 100644 --- a/ext/src/cast/export.ts +++ b/ext/src/cast/export.ts @@ -1,18 +1,20 @@ "use strict"; -import { Message } from "../messaging"; +import messaging, { Message } from "../messaging"; import { BridgeInfo } from "../lib/bridge"; import { TypedMessagePort } from "../lib/TypedMessagePort"; -import { - onMessage, - onMessageResponse, - sendMessage -} from "./eventMessageChannel"; - import CastSDK from "./sdk"; +import { PageEventMessenger, ExtensionEventMessenger } from "./eventMessaging"; + +// Create messengers manually instead of relying on getters +const eventMessaging = { + page: new PageEventMessenger(), + extension: new ExtensionEventMessenger() +}; + let initializedBridgeInfo: BridgeInfo; let initializedBackgroundPort: MessagePort; @@ -57,14 +59,14 @@ export function ensureInit(): Promise> { const message = ev.data as Message; // Send message to cast instance - sendMessage(message); + eventMessaging.extension.sendMessage(message); handleIncomingMessageToCast(message); }; // cast instance -> bridge - onMessageResponse(message => { - channel.port1.postMessage(message); - }); + eventMessaging.extension.addListener(message => + channel.port1.postMessage(message) + ); } else { /** * Import reference to message port created by contentBridge. @@ -85,7 +87,9 @@ export function ensureInit(): Promise> { }; // Handle cast messages - onMessage(handleIncomingMessageToCast); + eventMessaging.page.addListener(message => + handleIncomingMessageToCast(message) + ); } function handleIncomingMessageToCast(message: Message) { diff --git a/ext/src/cast/index.ts b/ext/src/cast/index.ts index aac3361..486911d 100644 --- a/ext/src/cast/index.ts +++ b/ext/src/cast/index.ts @@ -4,7 +4,7 @@ import logger from "../lib/logger"; import { loadScript } from "../lib/utils"; import { CAST_FRAMEWORK_SCRIPT_URL } from "./endpoints"; -import { onMessage } from "./eventMessageChannel"; +import eventMessaging from "./eventMessaging"; import CastSDK from "./sdk"; @@ -40,7 +40,7 @@ if (document.currentScript) { } } -onMessage(async message => { +eventMessaging.page.addListener(async message => { switch (message.subject) { case "cast:initialized": { bridgeInfo = message.data; diff --git a/ext/src/cast/sdk/Session.ts b/ext/src/cast/sdk/Session.ts index 121156a..116eeda 100644 --- a/ext/src/cast/sdk/Session.ts +++ b/ext/src/cast/sdk/Session.ts @@ -4,7 +4,7 @@ import { v4 as uuid } from "uuid"; import logger from "../../lib/logger"; -import { sendMessageResponse } from "../eventMessageChannel"; +import eventMessaging from "../eventMessaging"; import { ErrorCallback, @@ -201,7 +201,7 @@ export default class Session { return new Promise((resolve, reject) => { const messageId = uuid(); - sendMessageResponse({ + eventMessaging.page.sendMessage({ subject: "bridge:sendCastReceiverMessage", data: { sessionId: this.sessionId, @@ -271,7 +271,7 @@ export default class Session { ) { const messageId = uuid(); - sendMessageResponse({ + eventMessaging.page.sendMessage({ subject: "bridge:sendCastSessionMessage", data: { sessionId: this.sessionId, diff --git a/ext/src/cast/sdk/index.ts b/ext/src/cast/sdk/index.ts index 237dd0c..803ac3d 100644 --- a/ext/src/cast/sdk/index.ts +++ b/ext/src/cast/sdk/index.ts @@ -8,7 +8,7 @@ import { } from "../../types"; import { ErrorCallback, SuccessCallback } from "../types"; -import { onMessage, sendMessageResponse } from "../eventMessageChannel"; +import eventMessaging from "../eventMessaging"; import { AutoJoinPolicy, @@ -125,7 +125,7 @@ export default class { timeout = new Timeout(); constructor() { - onMessage(this.#onMessage.bind(this)); + eventMessaging.page.addListener(this.#onMessage.bind(this)); } #sendSessionRequest( @@ -136,7 +136,7 @@ export default class { listener(createReceiver(receiverDevice), ReceiverAction.CAST); } - sendMessageResponse({ + eventMessaging.page.sendMessage({ subject: "bridge:createCastSession", data: { appId: sessionRequest.appId, @@ -158,7 +158,7 @@ export default class { */ case "cast:sessionCreated": { // Notify background to close UI - sendMessageResponse({ + eventMessaging.page.sendMessage({ subject: "main:closeReceiverSelector" }); @@ -397,7 +397,7 @@ export default class { this.#apiConfig = apiConfig; - sendMessageResponse({ + eventMessaging.page.sendMessage({ subject: "main:initializeCast", data: { appId: this.#apiConfig.sessionRequest.appId } }); @@ -465,7 +465,7 @@ export default class { } } else { // Open receiver selector UI - sendMessageResponse({ + eventMessaging.page.sendMessage({ subject: "main:selectReceiver", data: { sessionRequest: this.#sessionRequest } }); diff --git a/ext/src/lib/TypedMessagePort.ts b/ext/src/lib/TypedMessagePort.ts index de317ae..867e0c8 100644 --- a/ext/src/lib/TypedMessagePort.ts +++ b/ext/src/lib/TypedMessagePort.ts @@ -5,5 +5,5 @@ */ export interface TypedMessagePort extends MessagePort { postMessage(message: T, transfer: Transferable[]): void; - postMessage(message: T, options?: PostMessageOptions): void; + postMessage(message: T, options?: StructuredSerializeOptions): void; }