ultrawidify/src/ext/module/EventBus.ts

239 lines
7.0 KiB
TypeScript
Raw Normal View History

import { EventBusCommand, EventBusContext, EventBusMessage } from '@src/common/interfaces/EventBusMessage.interface';
import { IframeTunnelPayload } from '@src/common/interfaces/IframeTunnelPayload.interface';
import Comms from '@src/ext/module/comms/Comms';
import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import CommsServer from '@src/ext/module/comms/CommsServer';
2021-10-26 22:19:41 +02:00
2021-10-26 22:19:41 +02:00
export default class EventBus {
2026-01-07 01:42:37 +01:00
private name: string;
private uuid = crypto.randomUUID();
2026-01-07 01:42:37 +01:00
2021-10-26 22:19:41 +02:00
private commands: { [x: string]: EventBusCommand[]} = {};
private comms?: CommsClient | CommsServer;
private commsOrigin: CommsOrigin;
private lastExecutedCommandIds: string[] = new Array(32);
private lastExecutedCommandIndex: number = 0;
private disableTunnel: boolean = false;
private popupContext: any = {};
private iframeForwardingList: {iframe: any, fn: (action, payload, context?) => void}[] = [];
// private uiUri = window.location.href;
constructor(options?: {isUWServer?: boolean, name?: string, commsOrigin?: CommsOrigin}) {
2023-07-10 22:00:53 +02:00
if (!options?.isUWServer) {
this.setupIframeTunnelling();
}
this.name = options?.name ?? '(unnamed EventBus)';
this.commsOrigin = options?.commsOrigin;
}
setupPopupTunnelWorkaround(context: EventBusContext): void {
this.disableTunnel = true;
this.popupContext = context;
}
//#region lifecycle
destroy() {
this.commands = null;
this.destroyIframeTunnelling();
}
//#endregion
setComms(comms: CommsClient | CommsServer) {
this.comms = comms;
}
2021-10-26 22:19:41 +02:00
subscribe(commandString: string, command: EventBusCommand) {
if (!this.commands[commandString]) {
this.commands[commandString] = [command];
} else {
this.commands[commandString].push(command);
2022-05-06 00:22:35 +02:00
}
}
2021-10-26 22:19:41 +02:00
subscribeMulti(commands: {[commandString: string]: EventBusCommand}, source?: any) {
for (const key in commands) {
this.subscribe(
key,
{
...commands[key],
source: source ?? commands[key].source
}
);
}
}
/**
* Removes all commands from a given source
* @param source
*/
unsubscribeAll(source: any) {
for (const commandString in this.commands) {
this.commands[commandString] = this.commands[commandString].filter(x => x.source !== source);
2021-10-26 22:19:41 +02:00
}
}
forwardToIframe(iframe: any, fn: (action: string, payload: any, context?: EventBusContext) => void) {
this.cancelIframeForwarding(iframe);
this.iframeForwardingList.push({iframe, fn});
}
cancelIframeForwarding(iframe: any) {
const existingForwarding = this.iframeForwardingList.findIndex((x: any) => x.iframe === iframe);
if (existingForwarding !== -1) {
this.iframeForwardingList.splice(existingForwarding, 1);
}
}
send(command: string, commandData: any, context: EventBusContext = {}) {
if (context.visitedBusses?.includes(this.uuid)) {
return;
}
if (context.commandId && this.lastExecutedCommandIds.includes(context.commandId)) {
return;
}
context.visitedBusses = [...context.visitedBusses ?? [], this.uuid];
// execute commands we have subscriptions for
2022-07-31 01:12:54 +02:00
if (this.commands?.[command]) {
for (const eventBusCommand of this.commands[command]) {
eventBusCommand.function(commandData, context);
2022-07-31 01:12:54 +02:00
}
}
if (context.commandId) {
const i = this.lastExecutedCommandIndex++ % this.lastExecutedCommandIds.length;
this.lastExecutedCommandIds[i] = context.commandId;
}
// preventing messages from flowing back to their original senders is
// CommsServer's job. EventBus does not have enough data for this decision.
// We do, however, have enough data to prevent backflow of messages that
// crossed CommsServer once already.
if (!context.commandId) {
context.commandId = crypto.randomUUID();
}
2025-10-08 19:39:55 +02:00
if (
this.comms
&& context?.origin !== CommsOrigin.Server
&& !context?.borderCrossings?.commsServer
) {
try {
this.comms.sendMessage({command, config: commandData, context}, context);
} catch (e) {
if (command !== 'reload-required') {
// We shouldn't let reload-required command to trigger new reload-required commands.
this.send('reload-required', {});
2025-10-08 19:39:55 +02:00
}
}
};
// call forwarding functions if they exist
for (const forwarding of this.iframeForwardingList) {
forwarding.fn(
command,
commandData,
{
...context,
borderCrossings: {
...context?.borderCrossings,
// iframe: true // we actually no longer check this prop, we should instead rely on visitedBusses
}
}
);
2022-07-31 01:12:54 +02:00
}
this.sendToTunnel(command, commandData, context);
2021-10-26 22:19:41 +02:00
2022-07-31 01:12:54 +02:00
if (context?.stopPropagation) {
return;
2021-10-26 22:19:41 +02:00
}
}
//#endregion
2021-10-26 22:19:41 +02:00
/**
* Send, but intended for sending commands from iframe to content scripts
* @param command
* @param config
*/
private sendToTunnel(command: string, config: any, context: EventBusContext = {}) {
if (!context.visitedBusses) {
console.error('Visited busses is missing from contextn. This is illegal.');
return;
}
context.visitedBusses = [...context.visitedBusses ?? [], this.uuid];
if (!this.disableTunnel && typeof window !== 'undefined') {
window.parent.postMessage(
{
action: 'uw-bus-tunnel',
payload: {command, config, context} as EventBusMessage
},
'*'
);
} else {
// because iframe UI components get reused in the popup, we
// also need to set up a detour because the tunnel is closed
// in the popup
if (this.comms) {
2025-10-08 19:39:55 +02:00
try {
this.comms.sendMessage(
{
command,
config,
context: {
...this.popupContext,
...context
}
},
this.popupContext
);
2025-10-08 19:39:55 +02:00
} catch (e) {
if (command !== 'reload-required') {
this.send('reload-required', {}, context);
2025-10-08 19:39:55 +02:00
}
}
}
}
}
//#region iframe tunnelling
private setupIframeTunnelling() {
// forward messages coming from iframe tunnels
window.addEventListener('message', this);
}
private destroyIframeTunnelling() {
window.removeEventListener('message', this);
}
/**
* Handles 'message' events (formerly handleIframeMessage)
* @param event
* @returns
*/
handleEvent(event: any) {
2026-01-07 01:42:37 +01:00
if (event.data?.action !== 'uw-bus-tunnel') {
return;
}
const payload = event.data.payload as EventBusMessage;
console.info(this.name, 'received message from iframe. command:', payload);
if (!payload.context?.visitedBusses) {
console.warn('Received iframe message without context. Doing nothing in order to avoid infinite loop. Event:', event);
return;
}
if (payload.context?.visitedBusses?.includes(this.uuid)) {
return;
}
this.send(payload.command, payload.config, payload.context);
}
//#endregion
2021-10-26 22:19:41 +02:00
}