Improved the external modal support
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import {ipcRenderer, IpcRendererEvent, remote} from "electron";
|
||||
import {tr} from "tc-shared/i18n/localize";
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {ProxiedClass, ProxyInterface} from "./Definitions";
|
||||
|
||||
export class ObjectProxyClient<ObjectType extends ProxyInterface<ObjectType>> {
|
||||
private readonly ipcChannel: string;
|
||||
private readonly handleIPCMessageBinding;
|
||||
|
||||
private eventInvokers: {[key: string]: { fireEvent: (type: string, ...args: any) => void }} = {};
|
||||
|
||||
constructor(ipcChannel: string) {
|
||||
this.ipcChannel = ipcChannel;
|
||||
this.handleIPCMessageBinding = this.handleIPCMessage.bind(this);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
ipcRenderer.on(this.ipcChannel, this.handleIPCMessageBinding);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
ipcRenderer.off(this.ipcChannel, this.handleIPCMessageBinding);
|
||||
/* TODO: Destroy all client instances? */
|
||||
}
|
||||
|
||||
async createNewInstance() : Promise<ObjectType & ProxiedClass<ObjectType>> {
|
||||
let object = {
|
||||
objectId: undefined as string
|
||||
};
|
||||
|
||||
const result = await ipcRenderer.invoke(this.ipcChannel, "create");
|
||||
if(result.status !== "success") {
|
||||
if(result.status === "error") {
|
||||
throw result.message || tr("failed to create a new instance");
|
||||
} else {
|
||||
throw tr("failed to create a new object instance ({})", result.status);
|
||||
}
|
||||
}
|
||||
object.objectId = result.instanceId;
|
||||
|
||||
const ipcChannel = this.ipcChannel;
|
||||
|
||||
const events = this.generateEvents(object.objectId);
|
||||
return new Proxy(object, {
|
||||
get(target, key: PropertyKey) {
|
||||
if(key === "ownerWindowId") {
|
||||
return remote.getCurrentWindow().id;
|
||||
} else if(key === "instanceId") {
|
||||
return object.objectId;
|
||||
} else if(key === "destroy") {
|
||||
return () => {
|
||||
ipcRenderer.invoke(ipcChannel, "destroy", target.objectId);
|
||||
events.destroy();
|
||||
};
|
||||
} else if(key === "events") {
|
||||
return events;
|
||||
} else if(key === "then" || key === "catch") {
|
||||
/* typescript for some reason has an issue if then and catch return anything */
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (...args: any) => ipcRenderer.invoke(ipcChannel, "invoke", target.objectId, key, ...args);
|
||||
},
|
||||
|
||||
set(): boolean {
|
||||
throw "class is a ready only interface";
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
|
||||
private generateEvents(objectId: string) : { destroy() } {
|
||||
const eventInvokers = this.eventInvokers;
|
||||
const registeredEvents = {};
|
||||
|
||||
eventInvokers[objectId] = {
|
||||
fireEvent(event: string, ...args: any) {
|
||||
if(typeof registeredEvents[event] === "undefined")
|
||||
return;
|
||||
|
||||
try {
|
||||
registeredEvents[event](...args);
|
||||
} catch (error) {
|
||||
logError(LogCategory.IPC, tr("Failed to invoke event %s on %s: %o"), event, objectId, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return new Proxy({ }, {
|
||||
set(target, key: PropertyKey, value: any): boolean {
|
||||
registeredEvents[key] = value;
|
||||
return true;
|
||||
},
|
||||
|
||||
get(target, key: PropertyKey): any {
|
||||
if(key === "destroy") {
|
||||
return () => delete eventInvokers[objectId];
|
||||
} else if(typeof registeredEvents[key] === "function") {
|
||||
return () => { throw tr("events can only be invoked via IPC") };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
|
||||
private handleIPCMessage(event: IpcRendererEvent, ...args: any[]) {
|
||||
const actionType = args[0];
|
||||
|
||||
if(actionType === "notify-event") {
|
||||
const invoker = this.eventInvokers[args[1]];
|
||||
if(typeof invoker !== "object") {
|
||||
logWarn(LogCategory.IPC, tr("Received event %s for unknown object instance on channel %s"), args[2], args[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
invoker.fireEvent(args[2], ...args.slice(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export type ProxiedEvents<EventObject> = {
|
||||
[Q in keyof EventObject]: EventObject[Q] extends (...args: any) => void ? (...args: Parameters<EventObject[Q]>) => void : never
|
||||
}
|
||||
|
||||
export type FunctionalInterface<ObjectType> = {
|
||||
[P in keyof ObjectType]: ObjectType[P] extends (...args: any) => Promise<any> ? (...args: any) => Promise<any> :
|
||||
P extends "events" ? ObjectType[P] extends ProxiedEvents<ObjectType[P]> ? ProxiedEvents<ObjectType[P]> : never : never
|
||||
};
|
||||
|
||||
export type ProxiedClassProperties = { instanceId: string, ownerWindowId: number, events: any };
|
||||
|
||||
export type ProxyInterface<ObjectType> = FunctionalInterface<ObjectType>;
|
||||
export type ProxyClass<ObjectType> = { new(props: ProxiedClassProperties): ProxyInterface<ObjectType> & ProxiedClass<ObjectType> };
|
||||
|
||||
export abstract class ProxiedClass<Interface extends { events?: ProxiedEvents<Interface["events"]> }> {
|
||||
public readonly ownerWindowId: number;
|
||||
public readonly instanceId: string;
|
||||
|
||||
public readonly events: ProxiedEvents<Interface["events"]>;
|
||||
|
||||
public constructor(props: ProxiedClassProperties) {
|
||||
this.ownerWindowId = props.ownerWindowId;
|
||||
this.instanceId = props.instanceId;
|
||||
this.events = props.events;
|
||||
}
|
||||
|
||||
public destroy() {}
|
||||
}
|
||||
|
||||
export function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import {BrowserWindow, ipcMain, IpcMainEvent} from "electron";
|
||||
import {generateUUID, ProxiedClass, ProxyClass, ProxyInterface} from "./Definitions";
|
||||
|
||||
export class ObjectProxyServer<ObjectType extends ProxyInterface<ObjectType>> {
|
||||
private readonly ipcChannel: string;
|
||||
private readonly klass: ProxyClass<ObjectType>;
|
||||
private readonly instances: { [key: string]: ProxyInterface<ObjectType> & ProxiedClass<ObjectType> } = {};
|
||||
|
||||
private readonly handleIPCMessageBinding;
|
||||
|
||||
constructor(ipcChannel: string, klass: ProxyClass<ObjectType>) {
|
||||
this.klass = klass;
|
||||
this.ipcChannel = ipcChannel;
|
||||
|
||||
this.handleIPCMessageBinding = this.handleIPCMessage.bind(this);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
ipcMain.handle(this.ipcChannel, this.handleIPCMessageBinding);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
ipcMain.removeHandler(this.ipcChannel);
|
||||
}
|
||||
|
||||
private async handleIPCMessage(event: IpcMainEvent, ...args: any[]) {
|
||||
const actionType = args[0];
|
||||
|
||||
if(actionType === "create") {
|
||||
let instance: ProxiedClass<ObjectType> & ProxyInterface<ObjectType>;
|
||||
try {
|
||||
const instanceId = generateUUID();
|
||||
instance = new this.klass({
|
||||
ownerWindowId: event.sender.id,
|
||||
instanceId: instanceId,
|
||||
events: this.generateEventProxy(instanceId, event.sender.id)
|
||||
});
|
||||
this.instances[instance.instanceId] = instance;
|
||||
} catch (error) {
|
||||
event.returnValue = { "status": "error", message: "create-error" };
|
||||
return;
|
||||
}
|
||||
|
||||
return { "status": "success", instanceId: instance.instanceId };
|
||||
} else {
|
||||
const instance = this.instances[args[1]];
|
||||
|
||||
if(!instance) {
|
||||
throw "instance-unknown";
|
||||
}
|
||||
|
||||
if(actionType === "destroy") {
|
||||
delete this.instances[args[1]];
|
||||
instance.destroy();
|
||||
} else if(actionType === "invoke") {
|
||||
if(typeof instance[args[2]] !== "function") {
|
||||
throw "function-unknown";
|
||||
}
|
||||
|
||||
return instance[args[2]](...args.slice(3));
|
||||
} else {
|
||||
console.warn("Received an invalid action: %s", actionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private generateEventProxy(instanceId: string, owningWindowId: number) : {} {
|
||||
const ipcChannel = this.ipcChannel;
|
||||
return new Proxy({ }, {
|
||||
get(target: { }, event: PropertyKey, receiver: any): any {
|
||||
return (...args: any) => {
|
||||
const window = BrowserWindow.fromId(owningWindowId);
|
||||
if(!window) return;
|
||||
|
||||
window.webContents.send(ipcChannel, "notify-event", instanceId, event, ...args);
|
||||
}
|
||||
},
|
||||
set(): boolean {
|
||||
throw "the events are read only for the implementation";
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {ProxiedClass} from "./Definitions";
|
||||
import {ObjectProxyClient} from "./Client";
|
||||
import {ObjectProxyServer} from "./Server";
|
||||
|
||||
interface TextModal {
|
||||
readonly events: {
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
sayHi() : Promise<void>;
|
||||
}
|
||||
|
||||
class TextModalImpl extends ProxiedClass<TextModal> implements TextModal {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
async sayHi(): Promise<void> {
|
||||
this.events.onHide();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let server = new ObjectProxyServer<TextModal>("", TextModalImpl);
|
||||
let client = new ObjectProxyClient<TextModal>("");
|
||||
|
||||
const instance = await client.createNewInstance();
|
||||
await instance.sayHi();
|
||||
instance.events.onHide = () => {};
|
||||
}
|
||||
Reference in New Issue
Block a user