TeaWeb/shared/js/ipc/MethodProxy.ts

217 lines
7.5 KiB
TypeScript

import {LogCategory, logDebug, logInfo, logWarn} from "../log";
import {BasicIPCHandler, ChannelMessage, IPCChannel} from "../ipc/BrowserIPC";
import {guid} from "../crypto/uid";
import {tr} from "tc-shared/i18n/localize";
export interface MethodProxyInvokeData {
method_name: string;
arguments: any[];
promise_id: string;
}
export interface MethodProxyResultData {
promise_id: string;
result: any;
success: boolean;
}
export interface MethodProxyCallback {
promise: Promise<any>;
promise_id: string;
resolve: (object: any) => any;
reject: (object: any) => any;
}
export type MethodProxyConnectParameters = {
channel_id: string;
client_id: string;
}
export abstract class MethodProxy {
readonly ipc_handler: BasicIPCHandler;
private _ipc_channel: IPCChannel;
private _ipc_parameters: MethodProxyConnectParameters;
private readonly _local: boolean;
private readonly _slave: boolean;
private _connected: boolean;
private _proxied_methods: {[key: string]:() => Promise<any>} = {};
private _proxied_callbacks: {[key: string]:MethodProxyCallback} = {};
protected constructor(ipc_handler: BasicIPCHandler, connect_params?: MethodProxyConnectParameters) {
this.ipc_handler = ipc_handler;
this._ipc_parameters = connect_params;
this._connected = false;
this._slave = typeof(connect_params) !== "undefined";
this._local = typeof(connect_params) !== "undefined" && connect_params.channel_id === "local" && connect_params.client_id === "local";
}
protected setup() {
if(this._local) {
this._connected = true;
this.on_connected();
} else {
if(this._slave)
this._ipc_channel = this.ipc_handler.createChannel(this._ipc_parameters.client_id, this._ipc_parameters.channel_id);
else
this._ipc_channel = this.ipc_handler.createChannel();
this._ipc_channel.messageHandler = this._handle_message.bind(this);
if(this._slave)
this._ipc_channel.sendMessage("initialize", {});
}
}
protected finalize() {
if(!this._local) {
if(this._connected)
this._ipc_channel.sendMessage("finalize", {});
this.ipc_handler.deleteChannel(this._ipc_channel);
this._ipc_channel = undefined;
}
for(const promise of Object.values(this._proxied_callbacks))
promise.reject("disconnected");
this._proxied_callbacks = {};
this._connected = false;
this.on_disconnected();
}
protected register_method<R>(method: (...args: any[]) => Promise<R> | string) {
let method_name: string;
if(typeof method === "function") {
logDebug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name);
method_name = method.name;
} else {
logDebug(LogCategory.IPC, tr("Registering method proxy for %s"), method);
method_name = method;
}
if(!this[method_name])
throw "method is missing in current object";
this._proxied_methods[method_name] = this[method_name];
if(!this._local) {
this[method_name] = (...args: any[]) => {
if(!this._connected)
return Promise.reject("not connected");
const proxy_callback = {
promise_id: guid()
} as MethodProxyCallback;
this._proxied_callbacks[proxy_callback.promise_id] = proxy_callback;
proxy_callback.promise = new Promise((resolve, reject) => {
proxy_callback.resolve = resolve;
proxy_callback.reject = reject;
});
this._ipc_channel.sendMessage("invoke", {
promise_id: proxy_callback.promise_id,
arguments: [...args],
method_name: method_name
} as MethodProxyInvokeData);
return proxy_callback.promise;
}
}
}
private _handle_message(remote_id: string, boradcast: boolean, message: ChannelMessage) {
if(message.type === "finalize") {
this._handle_finalize();
} else if(message.type === "initialize") {
this._handle_remote_callback(remote_id);
} else if(message.type === "invoke") {
this._handle_invoke(message.data);
} else if(message.type === "result") {
this._handle_result(message.data);
}
}
private _handle_finalize() {
this.on_disconnected();
this.finalize();
this._connected = false;
}
private _handle_remote_callback(remote_id: string) {
if(!this._ipc_channel.targetClientId) {
if(this._slave)
throw "initialize wrong state!";
this._ipc_channel.targetClientId = remote_id; /* now we're able to send messages */
this.on_connected();
this._ipc_channel.sendMessage("initialize", true);
} else {
if(!this._slave)
throw "initialize wrong state!";
this.on_connected();
}
this._connected = true;
}
private _send_result(promise_id: string, success: boolean, message: any) {
this._ipc_channel.sendMessage("result", {
promise_id: promise_id,
result: message,
success: success
} as MethodProxyResultData);
}
private _handle_invoke(data: MethodProxyInvokeData) {
if(this._proxied_methods[data.method_name])
throw "we could not invoke a local proxied method!";
if(!this[data.method_name]) {
this._send_result(data.promise_id, false, "missing method");
return;
}
try {
logInfo(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments);
const promise = this[data.method_name](...data.arguments);
promise.then(result => {
logInfo(LogCategory.IPC, tr("Result: %o"), result);
this._send_result(data.promise_id, true, result);
}).catch(error => {
this._send_result(data.promise_id, false, error);
});
} catch(error) {
this._send_result(data.promise_id, false, error);
return;
}
}
private _handle_result(data: MethodProxyResultData) {
if(!this._proxied_callbacks[data.promise_id]) {
logWarn(LogCategory.IPC, tr("Received proxy method result for unknown promise"));
return;
}
const callback = this._proxied_callbacks[data.promise_id];
delete this._proxied_callbacks[data.promise_id];
if(data.success)
callback.resolve(data.result);
else
callback.reject(data.result);
}
generate_connect_parameters() : MethodProxyConnectParameters {
if(this._slave)
throw "only masters can generate connect parameters!";
if(!this._ipc_channel)
throw "please call setup() before";
return {
channel_id: this._ipc_channel.channelId,
client_id: this.ipc_handler.getLocalAddress()
};
}
is_slave() { return this._local || this._slave; } /* the popout modal */
is_master() { return this._local || !this._slave; } /* the host (teaweb application) */
protected abstract on_connected();
protected abstract on_disconnected();
}