Some prep for IPC events
parent
e760a4760d
commit
16ad080df9
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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))
|
||||
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);
|
||||
}
|
||||
|
||||
for(const event of events as string[]) {
|
||||
const persistentHandler = this.persistentEventHandler[event] || (this.persistentEventHandler[event] = []);
|
||||
persistentHandler.push(handler);
|
||||
}
|
||||
|
||||
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<T extends keyof Events>(event: T | T[], handler: (event: Event<Events, T>) => void) : () => void;
|
||||
one(events, handler) : () => void {
|
||||
if(!Array.isArray(events))
|
||||
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);
|
||||
}
|
||||
|
||||
for(const event of events as string[]) {
|
||||
const persistentHandler = this.oneShotEventHandler[event] || (this.oneShotEventHandler[event] = []);
|
||||
persistentHandler.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);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
offAll(handler: (event?: Event<Events>) => void) {
|
||||
(this.handler[null as any] || []).remove(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
|
||||
});
|
||||
unregisterHandler(handler: any) {
|
||||
if(typeof handler !== "object") {
|
||||
throw "event handler must be an object";
|
||||
}
|
||||
|
||||
unregister_handler(handler: any) {
|
||||
const data = this.eventHandlerObjects.find(e => e.object === handler);
|
||||
if(!data) return;
|
||||
if(typeof handler[this.registryUniqueId] === "undefined") {
|
||||
throw "event handler not registered";
|
||||
}
|
||||
|
||||
this.eventHandlerObjects.remove(data);
|
||||
const data = handler[this.registryUniqueId] as EventHandlerRegisterData;
|
||||
delete handler[this.registryUniqueId];
|
||||
|
||||
for(const key of Object.keys(data.handlers)) {
|
||||
for(const evhandler of data.handlers[key]) {
|
||||
this.off(evhandler);
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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 = {};
|
||||
}
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue