306 lines
No EOL
9.8 KiB
TypeScript
306 lines
No EOL
9.8 KiB
TypeScript
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<Variables extends UiVariableMap> extends UiVariableProvider<Variables> {
|
|
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<Variables> {
|
|
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<Variables extends UiVariableMap> = {
|
|
readonly ipcChannelId: string
|
|
}
|
|
|
|
let editTokenIndex = 0;
|
|
|
|
class IpcUiVariableConsumer<Variables extends UiVariableMap> extends UiVariableConsumer<Variables> {
|
|
readonly description: IpcVariableDescriptor<Variables>;
|
|
private readonly bundleMaxSize: number;
|
|
|
|
private broadcastChannel: BroadcastChannel;
|
|
private editListener: {[key: string]: { resolve: () => void, reject: (error) => void }};
|
|
|
|
private enqueuedMessages: any[];
|
|
|
|
constructor(description: IpcVariableDescriptor<Variables>) {
|
|
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> | 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<Variables extends UiVariableMap>() : IpcUiVariableProvider<Variables> {
|
|
return new IpcUiVariableProvider();
|
|
}
|
|
|
|
export function createIpcUiVariableConsumer<Variables extends UiVariableMap>(description: IpcVariableDescriptor<Variables>) : IpcUiVariableConsumer<Variables> {
|
|
return new IpcUiVariableConsumer<Variables>(description);
|
|
} |