Replace eventMessageChannel with clearer implementation

This commit is contained in:
hensm
2022-04-27 16:17:33 +01:00
parent 234280f5ec
commit 5e2d9a2fbc
8 changed files with 132 additions and 110 deletions

View File

@@ -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);
});

View File

@@ -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);
}

View File

@@ -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<EventMessengerListener>();
constructor(
private incomingMessageEventName: string,
private outgoingMessageEventName: string
) {
// @ts-ignore
document.addEventListener(
this.incomingMessageEventName,
(ev: CustomEvent<string>) => {
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<string>(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);
}
};

View File

@@ -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<TypedMessagePort<Message>> {
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<TypedMessagePort<Message>> {
};
// Handle cast messages
onMessage(handleIncomingMessageToCast);
eventMessaging.page.addListener(message =>
handleIncomingMessageToCast(message)
);
}
function handleIncomingMessageToCast(message: Message) {

View File

@@ -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;

View File

@@ -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<void>((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,

View File

@@ -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 }
});

View File

@@ -5,5 +5,5 @@
*/
export interface TypedMessagePort<T> extends MessagePort {
postMessage(message: T, transfer: Transferable[]): void;
postMessage(message: T, options?: PostMessageOptions): void;
postMessage(message: T, options?: StructuredSerializeOptions): void;
}