interface Window { BroadcastChannel: BroadcastChannel; } namespace bipc { export interface BroadcastMessage { timestamp: number; receiver: string; sender: string; type: string; data: any; } function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } interface ProcessQuery { timestamp: number query_id: string; } export interface ProcessQueryResponse { request_timestamp: number request_query_id: string; device_id: string; protocol: number; } export interface CertificateAcceptCallback { request_id: string; } export interface CertificateAcceptSucceeded { } export abstract class BasicIPCHandler { protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000"; protected static readonly PROTOCOL_VERSION = 1; protected unique_id; protected constructor() { } setup() { this.unique_id = uuidv4(); /* lets get an unique identifier */ } abstract send_message(type: string, data: any, target?: string); protected handle_message(message: BroadcastMessage) { console.log("Received: %o", message); if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) { if(message.type == "process-query") { log.debug(LogCategory.IPC, tr("Received a device query from %s."), message.sender); this.send_message("process-query-response", { request_query_id: (message.data).query_id, request_timestamp: (message.data).timestamp, device_id: this.unique_id, protocol: BasicIPCHandler.PROTOCOL_VERSION } as ProcessQueryResponse, message.sender); return; } } else if(message.receiver === this.unique_id) { if(message.type == "process-query-response") { const response: ProcessQueryResponse = message.data; if(this._query_results[response.request_query_id]) this._query_results[response.request_query_id].push(response); else { log.warn(LogCategory.IPC, tr("Received a query response for an unknown request.")); } return; } else if(message.type == "certificate-accept-callback") { const data: CertificateAcceptCallback = message.data; if(!this._cert_accept_callbacks[data.request_id]) { log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID.")); return; } this._cert_accept_callbacks[data.request_id](); delete this._cert_accept_callbacks[data.request_id]; this.send_message("certificate-accept-succeeded", { } as CertificateAcceptSucceeded, message.sender); return; } else if(message.type == "certificate-accept-succeeded") { if(!this._cert_accept_succeeded[message.sender]) { log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback.")); return; } this._cert_accept_succeeded[message.sender](); return; } } } private _query_results: {[key: string]:ProcessQueryResponse[]} = {}; async query_processes(timeout?: number) : Promise { const query_id = uuidv4(); this._query_results[query_id] = []; this.send_message("process-query", { query_id: query_id, timestamp: Date.now() } as ProcessQuery); await new Promise(resolve => setTimeout(resolve, timeout || 250)); const result = this._query_results[query_id]; delete this._query_results[query_id]; return result; } private _cert_accept_callbacks: {[key: string]:(() => any)} = {}; register_certificate_accept_callback(callback: () => any) : string { const id = uuidv4(); this._cert_accept_callbacks[id] = callback; return this.unique_id + ":" + id; } private _cert_accept_succeeded: {[sender: string]:(() => any)} = {}; post_certificate_accpected(id: string, timeout?: number) : Promise { return new Promise((resolve, reject) => { const data = id.split(":"); const timeout_id = setTimeout(() => { delete this._cert_accept_succeeded[data[0]]; clearTimeout(timeout_id); reject("timeout"); }, timeout || 250); this._cert_accept_succeeded[data[0]] = () => { delete this._cert_accept_succeeded[data[0]]; clearTimeout(timeout_id); resolve(); }; this.send_message("certificate-accept-callback", { request_id: data[1] } as CertificateAcceptCallback, data[0]); }) } } class BroadcastChannelIPC extends BasicIPCHandler { private static readonly CHANNEL_NAME = "TeaSpeak-Web"; private channel: BroadcastChannel; constructor() { super(); } setup() { super.setup(); this.channel = new BroadcastChannel(BroadcastChannelIPC.CHANNEL_NAME); this.channel.onmessage = this.on_message.bind(this); this.channel.onmessageerror = this.on_error.bind(this); } private on_message(event: MessageEvent) { if(typeof(event.data) !== "string") { log.warn(LogCategory.IPC, tr("Received message with an invalid type (%s): %o"), typeof(event.data), event.data); return; } let message: BroadcastMessage; try { message = JSON.parse(event.data); } catch(error) { log.error(LogCategory.IPC, tr("Received an invalid encoded message: %o"), event.data); return; } super.handle_message(message); } private on_error(event: MessageEvent) { log.warn(LogCategory.IPC, tr("Received error: %o"), event); } send_message(type: string, data: any, target?: string) { const message: BroadcastMessage = {} as any; message.sender = this.unique_id; message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID; message.timestamp = Date.now(); message.type = type; message.data = data; this.channel.postMessage(JSON.stringify(message)); } } let handler: BasicIPCHandler; export function setup() { if(!supported()) return; /* TODO: test for support */ handler = new BroadcastChannelIPC(); handler.setup(); } export function get_handler() { return handler; } export function supported() { /* ios does not support this */ return typeof(window.BroadcastChannel) !== "undefined"; } }