From 16ad080df9eb2e7fa684d604989033375f7d9260 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Fri, 22 Jan 2021 13:34:43 +0100 Subject: [PATCH] Some prep for IPC events --- shared/js/ConnectionHandler.ts | 4 +- shared/js/events.ts | 466 ++++++++++-------- shared/js/file/LocalAvatars.ts | 7 +- shared/js/file/RemoteAvatars.ts | 8 +- .../side/ChannelConversationController.ts | 4 +- .../side/PrivateConversationController.ts | 4 +- shared/js/ui/modal/settings/Notifications.tsx | 5 +- shared/js/ui/react-elements/ErrorBoundary.tsx | 1 + .../external-modal/IPCMessage.ts | 6 +- shared/js/ui/tree/Controller.tsx | 4 +- shared/js/ui/tree/RendererDataProvider.tsx | 4 +- shared/js/ui/utils/IpcVariable.ts | 174 +++++++ shared/js/ui/utils/LocalVariable.ts | 2 +- shared/js/video-viewer/Controller.ts | 4 +- shared/js/video-viewer/W2GPlugin.ts | 4 +- .../connection/LegacySupportVoiceBridge.ts | 54 +- 16 files changed, 525 insertions(+), 226 deletions(-) create mode 100644 shared/js/ui/utils/IpcVariable.ts diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 85c0ccc7..84dcf64c 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -240,7 +240,7 @@ export class ConnectionHandler { this.localClient = new LocalClientEntry(this); this.localClient.channelTree = this.channelTree; - this.events_.register_handler(this); + this.events_.registerHandler(this); this.pluginCmdRegistry.registerHandler(new W2GPluginCmdHandler()); this.events_.fire("notify_handler_initialized"); @@ -1073,7 +1073,7 @@ export class ConnectionHandler { } destroy() { - this.events_.unregister_handler(this); + this.events_.unregisterHandler(this); this.cancelAutoReconnect(true); this.pluginCmdRegistry?.destroy(); diff --git a/shared/js/events.ts b/shared/js/events.ts index 722d58c0..a41b71e6 100644 --- a/shared/js/events.ts +++ b/shared/js/events.ts @@ -1,25 +1,83 @@ -import {LogCategory, logTrace, logWarn} from "./log"; +import {LogCategory, logTrace} from "./log"; import {guid} from "./crypto/uid"; import * as React from "react"; import {useEffect} from "react"; import {unstable_batchedUpdates} from "react-dom"; import { tr } from "./i18n/localize"; -export interface Event { - readonly type: T; - as() : Events[T]; -} +export type EventPayloadObject = { + [key: string]: EventPayload +} | { + [key: number]: EventPayload +}; -export interface SingletonEvents { - "singletone-instance": never; -} +export type EventPayload = string | number | bigint | null | undefined | EventPayloadObject; -export class SingletonEvent implements Event { - static readonly instance = new SingletonEvent(); +export type EventMap

= { + [K in keyof P]: EventPayloadObject & { + /* prohibit the type attribute on the highest layer (used to identify the event type) */ + type?: never + } +}; - readonly type = "singletone-instance"; - private constructor() { } - as() : SingletonEvents[T] { return; } +export type Event

, T extends keyof P> = { + readonly type: T, + + as(target: S) : Event; + asUnchecked(target: S) : Event; + asAnyUnchecked(target: S) : Event; + + /** + * Return an object containing only the event payload specific key value pairs. + */ + extractPayload() : P[T]; +} & P[T]; + +namespace EventHelper { + /** + * Turn the payload object into a bus event object + * @param payload + */ + /* May inline this somehow? A function call seems to be 3% slower */ + export function createEvent

, T extends keyof P>(type: T, payload?: P[T]) : Event { + if(payload) { + let event = payload as any as Event; + event.as = as; + event.asUnchecked = asUnchecked; + event.asAnyUnchecked = asUnchecked; + event.extractPayload = extractPayload; + return event; + } else { + return { + type, + as, + asUnchecked, + asAnyUnchecked: asUnchecked, + extractPayload + } as any; + } + } + + function extractPayload() { + const result = Object.assign({}, this); + delete result["as"]; + delete result["asUnchecked"]; + delete result["asAnyUnchecked"]; + delete result["extractPayload"]; + return result; + } + + function as(target) { + if(this.type !== target) { + throw "Mismatching event type. Expected: " + target + ", Got: " + this.type; + } + + return this; + } + + function asUnchecked() { + return this; + } } export interface EventSender { @@ -43,16 +101,25 @@ export interface EventSender(event_type: T, data?: Events[T], callback?: () => void); } -const event_annotation_key = guid(); -export class Registry implements EventSender { - private readonly registryUuid; +export type EventDispatchType = "sync" | "later" | "react"; + +export interface EventConsumer { + handleEvent(mode: EventDispatchType, type: string, data: any); +} + +interface EventHandlerRegisterData { + registeredHandler: {[key: string]: ((event) => void)[]} +} + +const kEventAnnotationKey = guid(); +export class Registry implements EventSender { + protected readonly registryUniqueId; + + protected persistentEventHandler: { [key: string]: ((event) => void)[] } = {}; + protected oneShotEventHandler: { [key: string]: ((event) => void)[] } = {}; + protected genericEventHandler: ((event) => void)[] = []; + protected consumer: EventConsumer[] = []; - private handler: {[key: string]: ((event) => void)[]} = {}; - private connections: {[key: string]: EventSender[]} = {}; - private eventHandlerObjects: { - object: any, - handlers: {[key: string]: ((event) => void)[]} - }[] = []; private debugPrefix = undefined; private warnUnhandledEvents = false; @@ -63,7 +130,14 @@ export class Registry handlers.splice(0, handlers.length)); + Object.values(this.oneShotEventHandler).forEach(handlers => handlers.splice(0, handlers.length)); + this.genericEventHandler.splice(0, this.genericEventHandler.length); + this.consumer.splice(0, this.consumer.length); } enableDebug(prefix: string) { this.debugPrefix = prefix || "---"; } @@ -72,70 +146,107 @@ export class Registry(event: T, handler: (event?: Events[T] & Event) => void) : () => void; - on(events: (keyof Events)[], handler: (event?: Event) => void) : () => void; - on(events, handler) : () => void { - if(!Array.isArray(events)) - events = [events]; - - handler[this.registryUuid] = { - singleshot: false - }; - for(const event of events) { - const handlers = this.handler[event] || (this.handler[event] = []); - handlers.push(handler); + fire(eventType: T, data?: Events[T], overrideTypeKey?: boolean) { + if(this.debugPrefix) { + logTrace(LogCategory.EVENT_REGISTRY, tr("[%s] Trigger event: %s"), this.debugPrefix, eventType); } - return () => this.off(events, handler); - } - onAll(handler: (event?: Event) => void) : () => void { - handler[this.registryUuid] = { - singleshot: false - }; - (this.handler[null as any] || (this.handler[null as any] = [])).push(handler); - return () => this.offAll(handler); - } - - /* one */ - one(event: T, handler: (event?: Events[T] & Event) => void) : () => void; - one(events: (keyof Events)[], handler: (event?: Event) => void) : () => void; - one(events, handler) : () => void { - if(!Array.isArray(events)) - events = [events]; - - for(const event of events) { - const handlers = this.handler[event] || (this.handler[event] = []); - - handler[this.registryUuid] = { singleshot: true }; - handlers.push(handler); - } - return () => this.off(events, handler); - } - - off(handler: (event?) => void); - off(event: T, handler: (event?: Events[T] & Event) => void); - off(event: (keyof Events)[], handler: (event?: Event) => void); - off(handler_or_events, handler?) { - if(typeof handler_or_events === "function") { - for(const key of Object.keys(this.handler)) - this.handler[key].remove(handler_or_events); - } else { - if(!Array.isArray(handler_or_events)) - handler_or_events = [handler_or_events]; - - for(const event of handler_or_events) { - const handlers = this.handler[event]; - if(handlers) handlers.remove(handler); + if(typeof data === "object" && 'type' in data && !overrideTypeKey) { + if((data as any).type !== eventType) { + debugger; + throw tr("The keyword 'type' is reserved for the event type and should not be passed as argument"); } } + + for(const consumer of this.consumer) { + consumer.handleEvent("sync", eventType as string, data); + } + + this.doInvokeEvent(EventHelper.createEvent(eventType, data)); } - offAll(handler: (event?: Event) => void) { - (this.handler[null as any] || []).remove(handler); + fire_later(eventType: T, data?: Events[T], callback?: () => void) { + if(!this.pendingAsyncCallbacksTimeout) { + this.pendingAsyncCallbacksTimeout = setTimeout(() => this.invokeAsyncCallbacks()); + this.pendingAsyncCallbacks = []; + } + this.pendingAsyncCallbacks.push({ type: eventType, data: data, callback: callback }); + + for(const consumer of this.consumer) { + consumer.handleEvent("later", eventType as string, data); + } } + fire_react(eventType: T, data?: Events[T], callback?: () => void) { + if(!this.pendingReactCallbacks) { + this.pendingReactCallbacksFrame = requestAnimationFrame(() => this.invokeReactCallbacks()); + this.pendingReactCallbacks = []; + } + + this.pendingReactCallbacks.push({ type: eventType, data: data, callback: callback }); + + for(const consumer of this.consumer) { + consumer.handleEvent("react", eventType as string, data); + } + } + + on(event: T | T[], handler: (event: Event) => void) : () => void; + on(events, handler) : () => void { + if(!Array.isArray(events)) { + events = [events]; + } + + for(const event of events as string[]) { + const persistentHandler = this.persistentEventHandler[event] || (this.persistentEventHandler[event] = []); + persistentHandler.push(handler); + } + + return () => this.off(events, handler); + } + + one(event: T | T[], handler: (event: Event) => void) : () => void; + one(events, handler) : () => void { + if(!Array.isArray(events)) { + events = [events]; + } + + for(const event of events as string[]) { + const persistentHandler = this.oneShotEventHandler[event] || (this.oneShotEventHandler[event] = []); + persistentHandler.push(handler); + } + + return () => this.off(events, handler); + } + + off(handler: (event: Event) => void); + off(events: T | T[], handler: (event: Event) => void); + off(handlerOrEvents, handler?) { + if(typeof handlerOrEvents === "function") { + this.offAll(handler); + } else if(typeof handlerOrEvents === "string") { + if(this.persistentEventHandler[handlerOrEvents]) { + this.persistentEventHandler[handlerOrEvents].remove(handler); + } + + if(this.oneShotEventHandler[handlerOrEvents]) { + this.oneShotEventHandler[handlerOrEvents].remove(handler); + } + } else if(Array.isArray(handlerOrEvents)) { + handlerOrEvents.forEach(handler_or_event => this.off(handler_or_event, handler)); + } + } + + onAll(handler: (event: Event) => void): () => void { + this.genericEventHandler.push(handler); + return () => this.genericEventHandler.remove(handler); + } + + offAll(handler: (event: Event) => void) { + Object.values(this.persistentEventHandler).forEach(persistentHandler => persistentHandler.remove(handler)); + Object.values(this.oneShotEventHandler).forEach(oneShotHandler => oneShotHandler.remove(handler)); + this.genericEventHandler.remove(handler); + } - /* special helper methods for react components */ /** * @param event * @param handler @@ -147,97 +258,42 @@ export class Registry {}); return; } - const handlers = this.handler[event as any] || (this.handler[event as any] = []); + + const handlers = this.persistentEventHandler[event as any] || (this.persistentEventHandler[event as any] = []); useEffect(() => { handlers.push(handler); return () => { - handlers.remove(handler); + const index = handlers.findIndex(handler); + if(index !== -1) { + handlers.splice(index, 1); + } }; }, reactEffectDependencies); } - connectAll(target: EventSender) { - (this.connections[null as any] || (this.connections[null as any] = [])).push(target as any); - } - - connect(events: T | T[], target: EventSender) { - for(const event of Array.isArray(events) ? events : [events]) - (this.connections[event as string] || (this.connections[event as string] = [])).push(target as any); - } - - disconnect(events: T | T[], target: EventSender) { - for(const event of Array.isArray(events) ? events : [events]) - (this.connections[event as string] || []).remove(target as any); - } - - disconnectAll(target: EventSender) { - this.connections[null as any]?.remove(target as any); - for(const event of Object.keys(this.connections)) - this.connections[event].remove(target as any); - } - - fire(event_type: T, data?: Events[T], overrideTypeKey?: boolean) { - if(this.debugPrefix) - logTrace(LogCategory.EVENT_REGISTRY, tr("[%s] Trigger event: %s"), this.debugPrefix, event_type); - - if(typeof data === "object" && 'type' in data && !overrideTypeKey) { - if((data as any).type !== event_type) { - debugger; - throw tr("The keyword 'type' is reserved for the event type and should not be passed as argument"); + private doInvokeEvent(event: Event) { + const oneShotHandler = this.oneShotEventHandler[event.type]; + if(oneShotHandler) { + delete this.oneShotEventHandler[event.type]; + for(const handler of oneShotHandler) { + handler(event); } } - const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, { - type: event_type, - as: function () { return this; } - }); - this.fire_event(event_type, event); - } + for(const handler of this.persistentEventHandler[event.type] || []) { + handler(event); + } - private fire_event(type: keyof Events, data: any) { + for(const handler of this.genericEventHandler) { + handler(event); + } + /* let invokeCount = 0; - - const typedHandler = this.handler[type as string] || []; - const generalHandler = this.handler[null as string] || []; - for(const handler of [...generalHandler, ...typedHandler]) { - handler(data); - invokeCount++; - - const regData = handler[this.registryUuid]; - if(typeof regData === "object" && regData.singleshot) - this.handler[type as string].remove(handler); /* FIXME: General single shot? */ - } - - const typedConnections = this.connections[type as string] || []; - const generalConnections = this.connections[null as string] || []; - for(const evhandler of [...generalConnections, ...typedConnections]) { - if('fire_event' in evhandler) - /* evhandler is an event registry as well. We don't have to check for any inappropriate keys */ - (evhandler as any).fire_event(type, data); - else - evhandler.fire(type, data); - invokeCount++; - } if(this.warnUnhandledEvents && invokeCount === 0) { - logWarn(LogCategory.EVENT_REGISTRY, tr("Event handler (%s) triggered event %s which has no consumers."), this.debugPrefix, type); + logWarn(LogCategory.EVENT_REGISTRY, tr("Event handler (%s) triggered event %s which has no consumers."), this.debugPrefix, event.type); } - } - - fire_later(event_type: T, data?: Events[T], callback?: () => void) { - if(!this.pendingAsyncCallbacksTimeout) { - this.pendingAsyncCallbacksTimeout = setTimeout(() => this.invokeAsyncCallbacks()); - this.pendingAsyncCallbacks = []; - } - this.pendingAsyncCallbacks.push({ type: event_type, data: data, callback: callback }); - } - - fire_react(event_type: T, data?: Events[T], callback?: () => void) { - if(!this.pendingReactCallbacks) { - this.pendingReactCallbacksFrame = requestAnimationFrame(() => this.invokeReactCallbacks()); - this.pendingReactCallbacks = []; - } - this.pendingReactCallbacks.push({ type: event_type, data: data, callback: callback }); + */ } private invokeAsyncCallbacks() { @@ -286,68 +342,84 @@ export class Registry { - if(function_name === "constructor") + Object.getOwnPropertyNames(currentPrototype).forEach(functionName => { + if(functionName === "constructor") { return; + } - if(typeof proto[function_name] !== "function") + if(typeof prototype[functionName] !== "function") { return; + } - if(typeof proto[function_name][event_annotation_key] !== "object") + if(typeof prototype[functionName][kEventAnnotationKey] !== "object") { return; + } - const event_data = proto[function_name][event_annotation_key]; - const ev_handler = event => proto[function_name].call(handler, event); - for(const event of event_data.events) { - registered_events[event] = registered_events[event] || []; - registered_events[event].push(ev_handler); - this.on(event, ev_handler); + const eventData = prototype[functionName][kEventAnnotationKey]; + const eventHandler = event => prototype[functionName].call(handler, event); + for(const event of eventData.events) { + const registeredHandler = data.registeredHandler[event] || (data.registeredHandler[event] = []); + registeredHandler.push(eventHandler); + + this.on(event, eventHandler); } }); - if(!parentClasses) + if(!parentClasses) { break; + } } while ((currentPrototype = Object.getPrototypeOf(currentPrototype))); - if(Object.keys(registered_events).length === 0) { - logWarn(LogCategory.EVENT_REGISTRY, tr("No events found in event handler which has been registered.")); - return; - } - - this.eventHandlerObjects.push({ - handlers: registered_events, - object: handler - }); } - unregister_handler(handler: any) { - const data = this.eventHandlerObjects.find(e => e.object === handler); - if(!data) return; + unregisterHandler(handler: any) { + if(typeof handler !== "object") { + throw "event handler must be an object"; + } - this.eventHandlerObjects.remove(data); + if(typeof handler[this.registryUniqueId] === "undefined") { + throw "event handler not registered"; + } - for(const key of Object.keys(data.handlers)) { - for(const evhandler of data.handlers[key]) { - this.off(evhandler); + const data = handler[this.registryUniqueId] as EventHandlerRegisterData; + delete handler[this.registryUniqueId]; + + for(const event of Object.keys(data.registeredHandler)) { + for(const handler of data.registeredHandler[event]) { + this.off(event, handler); } } } + + registerConsumer(consumer: EventConsumer) : () => void { + const allConsumer = this.consumer; + allConsumer.push(consumer); + + return () => allConsumer.remove(consumer); + } + + unregisterConsumer(consumer: EventConsumer) { + this.consumer.remove(consumer); + } } export type RegistryMap = {[key: string]: any /* can't use Registry here since the template parameter is missing */ }; @@ -355,11 +427,11 @@ export type RegistryMap = {[key: string]: any /* can't use Registry here since t export function EventHandler(events: (keyof EventTypes) | (keyof EventTypes)[]) { return function (target: any, propertyKey: string, - descriptor: PropertyDescriptor) { + _descriptor: PropertyDescriptor) { if(typeof target[propertyKey] !== "function") throw "Invalid event handler annotation. Expected to be on a function type."; - target[propertyKey][event_annotation_key] = { + target[propertyKey][kEventAnnotationKey] = { events: Array.isArray(events) ? events : [events] }; } @@ -374,7 +446,7 @@ export function ReactEventHandler, Event constructor.prototype.componentDidMount = function() { const registry = registry_callback(this); if(!registry) throw "Event registry returned for an event object is invalid"; - registry.register_handler(this); + registry.registerHandler(this); if(typeof didMount === "function") { didMount.call(this, arguments); @@ -386,7 +458,7 @@ export function ReactEventHandler, Event const registry = registry_callback(this); if(!registry) throw "Event registry returned for an event object is invalid"; try { - registry.unregister_handler(this); + registry.unregisterHandler(this); } catch (error) { console.warn("Failed to unregister event handler: %o", error); } diff --git a/shared/js/file/LocalAvatars.ts b/shared/js/file/LocalAvatars.ts index a1a9736d..b8cddc2e 100644 --- a/shared/js/file/LocalAvatars.ts +++ b/shared/js/file/LocalAvatars.ts @@ -29,6 +29,7 @@ import {IPCChannel} from "../ipc/BrowserIPC"; import {ConnectionHandler} from "../ConnectionHandler"; import {ErrorCode} from "../connection/ErrorCode"; import {server_connections} from "tc-shared/ConnectionManager"; +import {EventDispatchType} from "tc-shared/events"; /* FIXME: Retry avatar download after some time! */ @@ -424,8 +425,10 @@ class LocalAvatarManagerFactory extends AbstractAvatarManagerFactory { subscribedAvatars.push({ avatar: avatar, remoteAvatarId: avatarId, - unregisterCallback: avatar.events.onAll(event => { - this.ipcChannel.sendMessage("avatar-event", { handlerId: handlerId, avatarId: avatarId, event: event }, remoteId); + unregisterCallback: avatar.events.registerConsumer({ + handleEvent(mode: EventDispatchType, type: string, payload: any) { + this.ipcChannel.sendMessage("avatar-event", { handlerId: handlerId, avatarId: avatarId, type, payload }, remoteId); + } }) }); diff --git a/shared/js/file/RemoteAvatars.ts b/shared/js/file/RemoteAvatars.ts index e0f5b469..8d8fd771 100644 --- a/shared/js/file/RemoteAvatars.ts +++ b/shared/js/file/RemoteAvatars.ts @@ -142,11 +142,11 @@ class RemoteAvatarManager extends AbstractAvatarManager { avatar.updateStateFromRemote(data.state, data.stateData); } - handleAvatarEvent(data: any) { - const avatar = this.knownAvatars.find(e => e.avatarId === data.avatarId); + handleAvatarEvent(type: string, payload: any) { + const avatar = this.knownAvatars.find(e => e.avatarId === payload.avatarId); if(!avatar) return; - avatar.events.fire(data.event.type, data.event, true); + avatar.events.fire(type as any, payload, true); } } @@ -211,7 +211,7 @@ class RemoteAvatarManagerFactory extends AbstractAvatarManagerFactory { manager?.handleAvatarLoadCallback(message.data); } else if(message.type === "avatar-event") { const manager = this.manager[message.data.handlerId]; - manager?.handleAvatarEvent(message.data); + manager?.handleAvatarEvent(message.data.type, message.data.payload); } } } diff --git a/shared/js/ui/frames/side/ChannelConversationController.ts b/shared/js/ui/frames/side/ChannelConversationController.ts index 7fd11ce9..9b017237 100644 --- a/shared/js/ui/frames/side/ChannelConversationController.ts +++ b/shared/js/ui/frames/side/ChannelConversationController.ts @@ -36,14 +36,14 @@ export class ChannelConversationController extends AbstractConversationControlle }); */ - this.uiEvents.register_handler(this, true); + this.uiEvents.registerHandler(this, true); } destroy() { this.connectionListener.forEach(callback => callback()); this.connectionListener = []; - this.uiEvents.unregister_handler(this); + this.uiEvents.unregisterHandler(this); super.destroy(); } diff --git a/shared/js/ui/frames/side/PrivateConversationController.ts b/shared/js/ui/frames/side/PrivateConversationController.ts index 239e57bf..714b3e31 100644 --- a/shared/js/ui/frames/side/PrivateConversationController.ts +++ b/shared/js/ui/frames/side/PrivateConversationController.ts @@ -52,14 +52,14 @@ export class PrivateConversationController extends AbstractConversationControlle this.connectionListener = []; this.listenerConversation = {}; - this.uiEvents.register_handler(this, true); + this.uiEvents.registerHandler(this, true); this.uiEvents.enableDebug("private-conversations"); } destroy() { /* listenerConversation will be cleaned up via the listenerManager callbacks */ - this.uiEvents.unregister_handler(this); + this.uiEvents.unregisterHandler(this); super.destroy(); } diff --git a/shared/js/ui/modal/settings/Notifications.tsx b/shared/js/ui/modal/settings/Notifications.tsx index 9cd7a78e..74cc8f62 100644 --- a/shared/js/ui/modal/settings/Notifications.tsx +++ b/shared/js/ui/modal/settings/Notifications.tsx @@ -409,8 +409,9 @@ function initializeController(events: Registry) { let filter = undefined; events.on(["query_events", "action_set_filter"], event => { - if (event.type === "action_set_filter") - filter = event.as<"action_set_filter">().filter; + if (event.type === "action_set_filter") { + filter = event.asUnchecked("action_set_filter").filter; + } const groupMapper = (group: EventGroup) => { const result = { diff --git a/shared/js/ui/react-elements/ErrorBoundary.tsx b/shared/js/ui/react-elements/ErrorBoundary.tsx index 1b962ebb..e3022f7f 100644 --- a/shared/js/ui/react-elements/ErrorBoundary.tsx +++ b/shared/js/ui/react-elements/ErrorBoundary.tsx @@ -28,6 +28,7 @@ export class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { /* TODO: Some kind of logging? */ + console.error(error); } static getDerivedStateFromError() : Partial { diff --git a/shared/js/ui/react-elements/external-modal/IPCMessage.ts b/shared/js/ui/react-elements/external-modal/IPCMessage.ts index 909c2077..6b9bc4e6 100644 --- a/shared/js/ui/react-elements/external-modal/IPCMessage.ts +++ b/shared/js/ui/react-elements/external-modal/IPCMessage.ts @@ -55,10 +55,13 @@ export abstract class EventControllerBase this.localEventReceiver = {}; this.localRegistries = registries; + /* FIXME: Modals no longer use RegistryMap instead they should use IPCRegistryDescription */ + /* for(const key of Object.keys(this.localRegistries)) { this.localEventReceiver[key] = this.createEventReceiver(key); this.localRegistries[key].connectAll(this.localEventReceiver[key]); } + */ } private createEventReceiver(key: string) : EventSender { @@ -141,7 +144,8 @@ export abstract class EventControllerBase } protected destroyIPC() { - Object.keys(this.localRegistries).forEach(key => this.localRegistries[key].disconnectAll(this.localEventReceiver[key])); + /* FIXME: See above */ + //Object.keys(this.localRegistries).forEach(key => this.localRegistries[key].disconnectAll(this.localEventReceiver[key])); this.ipcChannel = undefined; this.ipcRemoteId = undefined; this.eventFiredListeners = {}; diff --git a/shared/js/ui/tree/Controller.tsx b/shared/js/ui/tree/Controller.tsx index e4a614b8..079b83c3 100644 --- a/shared/js/ui/tree/Controller.tsx +++ b/shared/js/ui/tree/Controller.tsx @@ -247,7 +247,7 @@ class ChannelTreeController { this.channelTree.client.groups.events.on("notify_groups_received", this.groupsReceivedListener); this.initializeServerEvents(this.channelTree.server); - this.channelTree.events.register_handler(this); + this.channelTree.events.registerHandler(this); if(this.channelTree.channelsInitialized) { this.handleChannelListReceived(); @@ -261,7 +261,7 @@ class ChannelTreeController { this.channelTree.client.groups.events.off("notify_groups_received", this.groupsReceivedListener); this.finalizeEvents(this.channelTree.server); - this.channelTree.events.unregister_handler(this); + this.channelTree.events.unregisterHandler(this); Object.values(this.eventListeners).forEach(callbacks => callbacks.forEach(callback => callback())); this.eventListeners = {}; } diff --git a/shared/js/ui/tree/RendererDataProvider.tsx b/shared/js/ui/tree/RendererDataProvider.tsx index 28ef4042..2fdaf7fe 100644 --- a/shared/js/ui/tree/RendererDataProvider.tsx +++ b/shared/js/ui/tree/RendererDataProvider.tsx @@ -338,7 +338,7 @@ export class RDPChannelTree { } initialize() { - this.events.register_handler(this); + this.events.registerHandler(this); const events = this.registeredEventHandlers; @@ -466,7 +466,7 @@ export class RDPChannelTree { document.removeEventListener("focusout", this.documentDragStopListener); document.removeEventListener("mouseout", this.documentDragStopListener); - this.events.unregister_handler(this); + this.events.unregisterHandler(this); this.registeredEventHandlers.forEach(callback => callback()); this.registeredEventHandlers = []; } diff --git a/shared/js/ui/utils/IpcVariable.ts b/shared/js/ui/utils/IpcVariable.ts new file mode 100644 index 00000000..c7898083 --- /dev/null +++ b/shared/js/ui/utils/IpcVariable.ts @@ -0,0 +1,174 @@ +import {UiVariableConsumer, UiVariableMap, UiVariableProvider} from "tc-shared/ui/utils/Variable"; +import {guid} from "tc-shared/crypto/uid"; +import {LogCategory, logWarn} from "tc-shared/log"; + +class IpcUiVariableProvider extends UiVariableProvider { + readonly ipcChannelId: string; + private broadcastChannel: BroadcastChannel; + + constructor() { + super(); + + this.ipcChannelId = "teaspeak-ipc-vars-" + guid(); + this.broadcastChannel = new BroadcastChannel(this.ipcChannelId); + this.broadcastChannel.onmessage = event => this.handleIpcMessage(event.data, event.source, event.origin); + } + + destroy() { + super.destroy(); + + if(this.broadcastChannel) { + this.broadcastChannel.onmessage = undefined; + this.broadcastChannel.onmessageerror = undefined; + this.broadcastChannel.close(); + } + + this.broadcastChannel = undefined; + } + + protected doSendVariable(variable: string, customData: any, value: any) { + console.error("Sending variable: %o", variable); + this.broadcastChannel.postMessage({ + type: "notify", + + variable, + customData, + value + }); + } + + private handleIpcMessage(message: any, source: MessageEventSource | null, origin: string) { + if(message.type === "edit") { + const token = message.token; + const sendResult = (error?: any) => { + if(source) { + // @ts-ignore + source.postMessage({ + type: "edit-result", + token, + error + }); + } else { + this.broadcastChannel.postMessage({ + type: "edit-result", + token, + error + }); + } + } + + try { + const result = this.doEditVariable(message.variable, message.customData, message.newValue); + if(result instanceof Promise) { + result.then(sendResult) + .catch(error => { + logWarn(LogCategory.GENERAL, tr("Failed to edit variable %s: %o"), message.variable, error); + sendResult(tr("invoke error")); + }); + } else { + sendResult(); + } + } catch (error) { + logWarn(LogCategory.GENERAL, tr("Failed to edit variable %s: %o"), message.variable, error); + sendResult(tr("invoke error")); + } + } else if(message.type === "query") { + this.sendVariable(message.variable, message.customData, true); + } + } + + generateConsumerDescription() : IpcVariableDescriptor { + return { + ipcChannelId: this.ipcChannelId + }; + } +} + +export type IpcVariableDescriptor = { + readonly ipcChannelId: string +} + +let editTokenIndex = 0; +class IpcUiVariableConsumer extends UiVariableConsumer { + readonly description: IpcVariableDescriptor; + private broadcastChannel: BroadcastChannel; + private editListener: {[key: string]: { resolve: () => void, reject: (error) => void }}; + + constructor(description: IpcVariableDescriptor) { + super(); + this.description = description; + this.editListener = {}; + + this.broadcastChannel = new BroadcastChannel(this.description.ipcChannelId); + this.broadcastChannel.onmessage = event => this.handleIpcMessage(event.data, event.source); + } + + destroy() { + super.destroy(); + + if(this.broadcastChannel) { + this.broadcastChannel.onmessage = undefined; + this.broadcastChannel.onmessageerror = undefined; + this.broadcastChannel.close(); + } + + this.broadcastChannel = undefined; + + Object.values(this.editListener).forEach(listener => listener.reject(tr("consumer destroyed"))); + this.editListener = {}; + } + + protected doEditVariable(variable: string, customData: any, newValue: any): Promise | void { + const token = "t" + ++editTokenIndex; + + return new Promise((resolve, reject) => { + this.broadcastChannel.postMessage({ + type: "edit", + token, + variable, + customData, + newValue + }); + + this.editListener[token] = { + reject, + resolve + } + }); + } + + protected doRequestVariable(variable: string, customData: any) { + this.broadcastChannel.postMessage({ + type: "query", + variable, + customData, + }); + } + + private handleIpcMessage(message: any, _source: MessageEventSource | null) { + if(message.type === "notify") { + console.error("Received notify %s", message.variable); + this.notifyRemoteVariable(message.variable, message.customData, message.value); + } else if(message.type === "edit-result") { + const payload = this.editListener[message.token]; + if(!payload) { + return; + } + delete this.editListener[message.token]; + + if(typeof message.error !== "undefined") { + payload.reject(message.error); + } else { + payload.resolve(); + } + } + } +} + +export function createIpcUiVariableProvider() : IpcUiVariableProvider { + return new IpcUiVariableProvider(); +} + +export function createIpcUiVariableConsumer(description: IpcVariableDescriptor) : IpcUiVariableConsumer { + return new IpcUiVariableConsumer(description); +} \ No newline at end of file diff --git a/shared/js/ui/utils/LocalVariable.ts b/shared/js/ui/utils/LocalVariable.ts index e53c53a4..c7a5e95c 100644 --- a/shared/js/ui/utils/LocalVariable.ts +++ b/shared/js/ui/utils/LocalVariable.ts @@ -56,5 +56,5 @@ export function createLocalUiVariables() : [UiV const provider = new LocalUiVariableProvider(); const consumer = new LocalUiVariableConsumer(provider); provider.setConsumer(consumer); - return [provider, consumer]; + return [provider as any, consumer as any]; } \ No newline at end of file diff --git a/shared/js/video-viewer/Controller.ts b/shared/js/video-viewer/Controller.ts index dd60aeb4..757dd3f0 100644 --- a/shared/js/video-viewer/Controller.ts +++ b/shared/js/video-viewer/Controller.ts @@ -34,7 +34,7 @@ class VideoViewer { this.connection = connection; this.events = new Registry(); - this.events.register_handler(this); + this.events.registerHandler(this); this.plugin = connection.getPluginCmdRegistry().getPluginHandler(W2GPluginCmdHandler.kPluginChannel); if(!this.plugin) { @@ -57,7 +57,7 @@ class VideoViewer { this.plugin.setLocalPlayerClosed(); this.events.fire("notify_destroy"); - this.events.unregister_handler(this); + this.events.unregisterHandler(this); this.modal.destroy(); this.events.destroy(); diff --git a/shared/js/video-viewer/W2GPlugin.ts b/shared/js/video-viewer/W2GPlugin.ts index f60c7037..d65e74b9 100644 --- a/shared/js/video-viewer/W2GPlugin.ts +++ b/shared/js/video-viewer/W2GPlugin.ts @@ -423,11 +423,11 @@ export class W2GPluginCmdHandler extends PluginCmdHandler { private handleLocalWatcherEvent(event: Event) { switch (event.type) { case "notify_watcher_url_changed": - this.events.fire("notify_following_url", { newUrl: event.as<"notify_watcher_url_changed">().newVideo }); + this.events.fire("notify_following_url", { newUrl: event.asUnchecked("notify_watcher_url_changed").newVideo }); break; case "notify_watcher_status_changed": - this.events.fire("notify_following_watcher_status", { newStatus: event.as<"notify_watcher_status_changed">().newStatus }); + this.events.fire("notify_following_watcher_status", { newStatus: event.asUnchecked("notify_watcher_status_changed").newStatus }); break; case "notify_destroyed": diff --git a/web/app/connection/LegacySupportVoiceBridge.ts b/web/app/connection/LegacySupportVoiceBridge.ts index d9d4da8d..9e979802 100644 --- a/web/app/connection/LegacySupportVoiceBridge.ts +++ b/web/app/connection/LegacySupportVoiceBridge.ts @@ -8,7 +8,7 @@ import {RecorderProfile} from "tc-shared/voice/RecorderProfile"; import {VoiceClient} from "tc-shared/voice/VoiceClient"; import {WhisperSession, WhisperTarget} from "tc-shared/voice/VoiceWhisper"; import {AbstractServerConnection, ConnectionStatistics} from "tc-shared/connection/ConnectionBase"; -import {Registry} from "tc-shared/events"; +import {EventDispatchType, Registry} from "tc-shared/events"; import {VoicePlayerEvents, VoicePlayerLatencySettings, VoicePlayerState} from "tc-shared/voice/VoicePlayer"; import { tr } from "tc-shared/i18n/localize"; import {RtpVoiceConnection} from "tc-backend/web/voice/Connection"; @@ -21,6 +21,7 @@ class ProxiedVoiceClient implements VoiceClient { private volume: number; private latencySettings: VoicePlayerLatencySettings | undefined; + private eventDisconnect: () => void; constructor(clientId: number) { this.clientId = clientId; @@ -30,14 +31,36 @@ class ProxiedVoiceClient implements VoiceClient { } setHandle(handle: VoiceClient | undefined) { - this.handle?.events.disconnectAll(this.events); + if(this.eventDisconnect) { + this.eventDisconnect(); + this.eventDisconnect = undefined; + } this.handle = handle; if(this.latencySettings) { this.handle?.setLatencySettings(this.latencySettings); } this.handle?.setVolume(this.volume); - this.handle?.events.connectAll(this.events); + if(this.handle) { + const targetEvents = this.events; + this.eventDisconnect = this.handle.events.registerConsumer({ + handleEvent(mode: EventDispatchType, type: string, data: any) { + switch (mode) { + case "later": + targetEvents.fire_later(type as any, data); + break; + + case "react": + targetEvents.fire_react(type as any, data); + break; + + case "sync": + targetEvents.fire(type as any, data); + break; + } + } + }); + } } abortReplay() { @@ -86,6 +109,7 @@ export class LegacySupportVoiceBridge extends AbstractVoiceConnection { private readonly oldVoiceBridge: VoiceConnection; private activeBridge: AbstractVoiceConnection; + private disconnectEvents: () => void; private encoderCodec: number; private currentRecorder: RecorderProfile; @@ -108,11 +132,31 @@ export class LegacySupportVoiceBridge extends AbstractVoiceConnection { e.setHandle(undefined); } }); - this.activeBridge?.events.disconnectAll(this.events); + if(this.disconnectEvents) { + this.disconnectEvents(); + this.disconnectEvents = undefined; + } this.activeBridge = type === "old" ? this.oldVoiceBridge : type === "new" ? this.newVoiceBride : undefined; if(this.activeBridge) { - this.activeBridge.events.connectAll(this.events); + const targetEvents = this.events; + this.disconnectEvents = this.activeBridge.events.registerConsumer({ + handleEvent(mode: EventDispatchType, type: string, data: any) { + switch (mode) { + case "later": + targetEvents.fire_later(type as any, data); + break; + + case "react": + targetEvents.fire_react(type as any, data); + break; + + case "sync": + targetEvents.fire(type as any, data); + break; + } + } + }); this.registeredClients.forEach(e => { if(!e.handle) {