Some prep for IPC events

master
WolverinDEV 2021-01-22 13:34:43 +01:00
parent e760a4760d
commit 16ad080df9
16 changed files with 525 additions and 226 deletions

View File

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

View File

@ -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<Events, T = keyof Events> {
readonly type: T;
as<T extends keyof Events>() : 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<SingletonEvents, "singletone-instance"> {
static readonly instance = new SingletonEvent();
export type EventMap<P> = {
[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<T extends keyof SingletonEvents>() : SingletonEvents[T] { return; }
export type Event<P extends EventMap<P>, T extends keyof P> = {
readonly type: T,
as<S extends T>(target: S) : Event<P, S>;
asUnchecked<S extends T>(target: S) : Event<P, S>;
asAnyUnchecked<S extends keyof P>(target: S) : Event<P, S>;
/**
* 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<P extends EventMap<P>, T extends keyof P>(type: T, payload?: P[T]) : Event<P, T> {
if(payload) {
let event = payload as any as Event<P, T>;
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<Events extends { [key: string]: any } = { [key: string]: any }> {
@ -43,16 +101,25 @@ export interface EventSender<Events extends { [key: string]: any } = { [key: str
fire_react<T extends keyof Events>(event_type: T, data?: Events[T], callback?: () => void);
}
const event_annotation_key = guid();
export class Registry<Events extends { [key: string]: any } = { [key: string]: any }> implements EventSender<Events> {
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<Events extends { [key: string]: any } = { [key: string]: any }> implements EventSender<Events> {
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<Events>[]} = {};
private eventHandlerObjects: {
object: any,
handlers: {[key: string]: ((event) => void)[]}
}[] = [];
private debugPrefix = undefined;
private warnUnhandledEvents = false;
@ -63,7 +130,14 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
private pendingReactCallbacksFrame: number = 0;
constructor() {
this.registryUuid = "evreg_data_" + guid();
this.registryUniqueId = "evreg_data_" + guid();
}
destroy() {
Object.values(this.persistentEventHandler).forEach(handlers => 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<Events extends { [key: string]: any } = { [key: string]: a
enableWarnUnhandledEvents() { this.warnUnhandledEvents = true; }
disableWarnUnhandledEvents() { this.warnUnhandledEvents = false; }
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => 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<T extends keyof Events>(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<Events>) => 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<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
one(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => 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<T extends keyof Events>(handler: (event?) => void);
off<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void);
off(event: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => 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<Events>) => void) {
(this.handler[null as any] || []).remove(handler);
fire_later<T extends keyof Events>(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<T extends keyof Events>(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<T extends keyof Events>(event: T | T[], handler: (event: Event<Events, T>) => 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<T extends keyof Events>(event: T | T[], handler: (event: Event<Events, T>) => 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<Events, keyof Events>) => void);
off<T extends keyof Events>(events: T | T[], handler: (event: Event<Events, T>) => 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<Events, keyof Events>) => void): () => void {
this.genericEventHandler.push(handler);
return () => this.genericEventHandler.remove(handler);
}
offAll(handler: (event: Event<Events, keyof Events>) => 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<Events extends { [key: string]: any } = { [key: string]: a
useEffect(() => {});
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<EOther, T extends keyof Events & keyof EOther>(target: EventSender<Events>) {
(this.connections[null as any] || (this.connections[null as any] = [])).push(target as any);
}
connect<EOther, T extends (keyof Events & keyof EOther)>(events: T | T[], target: EventSender<EOther>) {
for(const event of Array.isArray(events) ? events : [events])
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
}
disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: EventSender<Events>) {
for(const event of Array.isArray(events) ? events : [events])
(this.connections[event as string] || []).remove(target as any);
}
disconnectAll<EOther>(target: EventSender<EOther>) {
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<T extends keyof Events>(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<Events, keyof Events>) {
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<T extends keyof Events>(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<T extends keyof Events>(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<Events extends { [key: string]: any } = { [key: string]: a
});
}
destroy() {
this.handler = {};
this.connections = {};
this.eventHandlerObjects = [];
}
register_handler(handler: any, parentClasses?: boolean) {
if(typeof handler !== "object")
registerHandler(handler: any, parentClasses?: boolean) {
if(typeof handler !== "object") {
throw "event handler must be an object";
}
const proto = Object.getPrototypeOf(handler);
if(typeof proto !== "object")
if(typeof handler[this.registryUniqueId] !== "undefined") {
throw "event handler already registered";
}
const prototype = Object.getPrototypeOf(handler);
if(typeof prototype !== "object") {
throw "event handler must have a prototype";
}
let currentPrototype = proto;
let registered_events = {};
const data = handler[this.registryUniqueId] = {
registeredHandler: {}
} as EventHandlerRegisterData;
let currentPrototype = prototype;
do {
Object.getOwnPropertyNames(currentPrototype).forEach(function_name => {
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<EventTypes>(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<ObjectClass = React.Component<any, any>, 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<ObjectClass = React.Component<any, any>, 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);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -409,8 +409,9 @@ function initializeController(events: Registry<NotificationSettingsEvents>) {
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 = {

View File

@ -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<ErrorBoundaryState> {

View File

@ -55,10 +55,13 @@ export abstract class EventControllerBase<Type extends "controller" | "popout">
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<Type extends "controller" | "popout">
}
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 = {};

View File

@ -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 = {};
}

View File

@ -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 = [];
}

View File

@ -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<Variables extends UiVariableMap> extends UiVariableProvider<Variables> {
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<Variables> {
return {
ipcChannelId: this.ipcChannelId
};
}
}
export type IpcVariableDescriptor<Variables extends UiVariableMap> = {
readonly ipcChannelId: string
}
let editTokenIndex = 0;
class IpcUiVariableConsumer<Variables extends UiVariableMap> extends UiVariableConsumer<Variables> {
readonly description: IpcVariableDescriptor<Variables>;
private broadcastChannel: BroadcastChannel;
private editListener: {[key: string]: { resolve: () => void, reject: (error) => void }};
constructor(description: IpcVariableDescriptor<Variables>) {
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> | 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<Variables extends UiVariableMap>() : IpcUiVariableProvider<Variables> {
return new IpcUiVariableProvider();
}
export function createIpcUiVariableConsumer<Variables extends UiVariableMap>(description: IpcVariableDescriptor<Variables>) : IpcUiVariableConsumer<Variables> {
return new IpcUiVariableConsumer<Variables>(description);
}

View File

@ -56,5 +56,5 @@ export function createLocalUiVariables<Variables extends UiVariableMap>() : [UiV
const provider = new LocalUiVariableProvider();
const consumer = new LocalUiVariableConsumer(provider);
provider.setConsumer(consumer);
return [provider, consumer];
return [provider as any, consumer as any];
}

View File

@ -34,7 +34,7 @@ class VideoViewer {
this.connection = connection;
this.events = new Registry<VideoViewerEvents>();
this.events.register_handler(this);
this.events.registerHandler(this);
this.plugin = connection.getPluginCmdRegistry().getPluginHandler<W2GPluginCmdHandler>(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();

View File

@ -423,11 +423,11 @@ export class W2GPluginCmdHandler extends PluginCmdHandler {
private handleLocalWatcherEvent(event: Event<W2GWatcherEvents, "notify_watcher_url_changed" | "notify_watcher_status_changed" | "notify_destroyed">) {
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":

View File

@ -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) {