From 251cba56d9103c9ac38f9b98b69ee953ce23111e Mon Sep 17 00:00:00 2001 From: Melonai Date: Wed, 14 Jul 2021 19:36:29 +0200 Subject: Started better typing for message handler --- .../src/network/channel/messages/event_handler.ts | 153 +++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 assets/src/network/channel/messages/event_handler.ts (limited to 'assets/src/network/channel/messages/event_handler.ts') diff --git a/assets/src/network/channel/messages/event_handler.ts b/assets/src/network/channel/messages/event_handler.ts new file mode 100644 index 0000000..749f289 --- /dev/null +++ b/assets/src/network/channel/messages/event_handler.ts @@ -0,0 +1,153 @@ +import type { Channel } from "phoenix"; +import type { + AnyMessage, + EventName, + MessageForEvent, + TokenizedMessage, +} from "./messages"; + +// The single handler for all events, which is used to dispatch to the correct +// handler for each event and token. +// Every event can only have either one single handler, or multiple handlers for different tokens. +export type EventHandler = { + [EN in EventName]?: MessageHandler>; +}; + +// A handler for a specific event and message. +// A message handler can either be a single handler or can have multiple handlers for different tokens +type MessageHandler = + // A single handler + | { type: "single"; handler: Handler } + // A group of handlers for different tokens + // Can only be used for messages which have a "token" field. + | (M extends TokenizedMessage + ? { + type: "token"; + handler: TokenHandler; + } + : never); + +// A map of token to handler for a specific event. +export type TokenHandler = Map>; + +// A function which handles a message for a specific event. +export type Handler = (message?: M) => void; + +// A function that unregisters a single event handler. +export type Unregister = () => void; + +// Adds a single handler for a specific event. +export function registerHandler( + eventHandler: EventHandler, + channel: Channel, + event: M["event_name"], + handler: Handler +) { + const messageHandler: MessageHandler = { + type: "single", + handler, + }; + + let unregisterChannelEvent: Unregister | null = null; + if (typeof eventHandler[event] === "undefined") { + // Register a new event handler, since this is the first handler for this event + unregisterChannelEvent = registerNewEvent( + channel, + messageHandler, + event + ); + } else { + throw new Error("Event already has a handler attached to it."); + } + + // TODO: Check if we there is a possibility for TS to accept this. + // Technically this should work, because TS is afraid of the generic type parameter + // not matching the message type of the handler. But it should match, since + // the handler should accept only the exact message type from which the event_name was derived. + // @ts-ignore + eventHandler[event] = messageHandler; + + // This could cause problems if we would allow to redefine the handler, + // as that would cause the Unsubscribe function to no longer apply to this specific handler, + // but as we throw an error on redefinition, this should be fine. + return () => { + delete eventHandler[event]; + + // If we registered a new event on the channel, we need to unregister it + if (unregisterChannelEvent !== null) { + unregisterChannelEvent(); + } + }; +} + +export function registerHandlerForSpecificToken( + eventHandler: EventHandler, + channel: Channel, + event: M["event_name"], + token: string, + handler: Handler +): Unregister { + if (typeof eventHandler[event] === "undefined") { + // TODO: Same as above, this should probably be valid. + // @ts-ignore + eventHandler[event] = { + type: "token", + handler: new Map>(), + }; + // @ts-ignore + registerNewEvent(channel, eventHandler[event], event); + } else { + if (eventHandler[event].type === "single") { + throw new Error("Event already has a handler attached to it."); + } + } + + // @ts-ignore This shoudl be valid, as we derive the event name from the message type. + const tokenHandler: TokenHandler = eventHandler[event].handler; + tokenHandler.set(token, handler); + + return () => { + // Unregister the handler for this specific token + tokenHandler.delete(token); + // If there are no more handlers for this event, we can unregister the event + if (tokenHandler.size === 0) { + // We should technically use the ref here, but we don't yet have a way to get it, as the event + // could have been registered outside of this function call. + channel.off(event); + } + }; +} + +// Adds a callback for a new event +function registerNewEvent( + channel: Channel, + messageHandler: MessageHandler, + event: M["event_name"] +): Unregister { + const callback = (data: M) => { + onEvent(messageHandler, data); + }; + + const ref = channel.on(event, callback); + + return () => channel.off(event, ref); +} + +function onEvent( + messageHandler: MessageHandler, + message: M +) { + if (messageHandler.type === "token") { + const token: string = message["token"]; + + const handler = messageHandler.handler.get(token); + + if (typeof handler !== "undefined") { + (handler as Handler)(message); + } else { + console.warn(`Received message for unknown token: ${token}`); + } + } else { + messageHandler.handler(message); + } +} -- cgit 1.4.1