import {UiVariableConsumer, UiVariableMap, UiVariableProvider} from "tc-shared/ui/utils/Variable"; import {guid} from "tc-shared/crypto/uid"; import {LogCategory, logWarn} from "tc-shared/log"; import ReactDOM from "react-dom"; import {Settings, settings} from "tc-shared/settings"; /* * We need to globally bundle all IPC invoke events since * calling setImmediate too often will cause a electron crash with * "async hook stack has become corrupted (actual: 88, expected: 0)". * * WolverinDEV has never experience it by himself but @REDOSS had. */ let ipcInvokeCallbacks: (() => void)[]; function registerInvokeCallback(callback: () => void) { if(Array.isArray(ipcInvokeCallbacks)) { ipcInvokeCallbacks.push(callback); } else { ipcInvokeCallbacks = [ callback ]; setImmediate(() => { const callbacks = ipcInvokeCallbacks; ipcInvokeCallbacks = undefined; for(const callback of callbacks) { callback(); } }); } } function savePostMessage(channel: BroadcastChannel | MessageEventSource, message: any) { try { // @ts-ignore channel.postMessage(message); } catch (error) { if(error instanceof Error) { debugger; console.error(error); return; } throw error; } } export class IpcUiVariableProvider extends UiVariableProvider { readonly ipcChannelId: string; private readonly bundleMaxSize: number; private broadcastChannel: BroadcastChannel; private enqueuedMessages: any[]; constructor() { super(); this.bundleMaxSize = settings.getValue(Settings.KEY_IPC_EVENT_BUNDLE_MAX_SIZE); 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) { this.broadcastIpcMessage({ 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 savePostMessage(source, { type: "edit-result", token, error }); } else { this.broadcastIpcMessage({ 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); } else if(message.type === "bundled") { for(const bundledMessage of message.messages) { this.handleIpcMessage(bundledMessage, source, origin); } } } generateConsumerDescription() : IpcVariableDescriptor { return { ipcChannelId: this.ipcChannelId }; } /** * Send an IPC message. * We bundle messages to improve performance when doing a lot of combined requests. * @param message IPC message to send * @private */ private broadcastIpcMessage(message: any) { if(this.bundleMaxSize <= 0) { savePostMessage(this.broadcastChannel, message); return; } if(Array.isArray(this.enqueuedMessages)) { this.enqueuedMessages.push(message); if(this.enqueuedMessages.length >= this.bundleMaxSize) { this.sendEnqueuedMessages(); } return; } this.enqueuedMessages = [ message ]; registerInvokeCallback(() => this.sendEnqueuedMessages()); } private sendEnqueuedMessages() { if(!this.enqueuedMessages) { return; } savePostMessage(this.broadcastChannel, { type: "bundled", messages: this.enqueuedMessages }); this.enqueuedMessages = undefined; } } export type IpcVariableDescriptor = { readonly ipcChannelId: string } let editTokenIndex = 0; class IpcUiVariableConsumer extends UiVariableConsumer { readonly description: IpcVariableDescriptor; private readonly bundleMaxSize: number; private broadcastChannel: BroadcastChannel; private editListener: {[key: string]: { resolve: () => void, reject: (error) => void }}; private enqueuedMessages: any[]; constructor(description: IpcVariableDescriptor) { super(); this.description = description; this.editListener = {}; this.bundleMaxSize = settings.getValue(Settings.KEY_IPC_EVENT_BUNDLE_MAX_SIZE); this.broadcastChannel = new BroadcastChannel(this.description.ipcChannelId); this.broadcastChannel.onmessage = event => this.handleIpcMessage(event.data, event.source); } destroy() { super.destroy(); if(this.broadcastChannel) { this.broadcastChannel.onmessage = undefined; this.broadcastChannel.onmessageerror = undefined; this.broadcastChannel.close(); } this.broadcastChannel = undefined; Object.values(this.editListener).forEach(listener => listener.reject(tr("consumer destroyed"))); this.editListener = {}; } protected doEditVariable(variable: string, customData: any, newValue: any): Promise | void { const token = "t" + ++editTokenIndex; return new Promise((resolve, reject) => { this.sendIpcMessage({ type: "edit", token, variable, customData, newValue }); this.editListener[token] = { reject, resolve } }); } protected doRequestVariable(variable: string, customData: any) { this.sendIpcMessage({ type: "query", variable, customData, }); } private handleIpcMessage(message: any, source: MessageEventSource | null) { if(message.type === "notify") { 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(); } } else if(message.type === "bundled") { ReactDOM.unstable_batchedUpdates(() => { for(const bundledMessage of message.messages) { this.handleIpcMessage(bundledMessage, source); } }); } } /** * Send an IPC message. * We bundle messages to improve performance when doing a lot of combined requests. * The response will most likely also be bundled. This means that we're only updating react once. * @param message IPC message to send * @private */ private sendIpcMessage(message: any) { if(this.bundleMaxSize <= 0) { savePostMessage(this.broadcastChannel, message); return; } if(Array.isArray(this.enqueuedMessages)) { this.enqueuedMessages.push(message); if(this.enqueuedMessages.length >= this.bundleMaxSize) { this.sendEnqueuedMessages(); } return; } this.enqueuedMessages = [ message ]; registerInvokeCallback(() => this.sendEnqueuedMessages()); } private sendEnqueuedMessages() { if(!this.enqueuedMessages) { return; } savePostMessage(this.broadcastChannel, { type: "bundled", messages: this.enqueuedMessages }); this.enqueuedMessages = undefined; } } export function createIpcUiVariableProvider() : IpcUiVariableProvider { return new IpcUiVariableProvider(); } export function createIpcUiVariableConsumer(description: IpcVariableDescriptor) : IpcUiVariableConsumer { return new IpcUiVariableConsumer(description); }