'use strict' import bsock = require('bsock'); import * as T from './Types'; import * as I from './Interfaces'; /** * Utility function to strip parameters like "a = 3" of their defaults * @param str The parameter to modify */ function stripAfterEquals(str:string):string{ return str.split("=")[0] } /** * A websocket-on-steroids with built-in RPC capabilities */ export class RPCSocket implements I.Socket{ private socket: I.Socket /** * * @param port Port to connect to * @param server Server address * @param tls @default false use TLS */ constructor(public port:number, private server: string, private tls: boolean = false){ Object.defineProperty(this, 'socket', {value: undefined, writable: true}) } /** * Hooks a handler to a function name. Use {@link call} to trigger it. * @param name The function name to listen on * @param handler The handler to attach */ public hook(name: T.Name, handler: (...args:any[]) => any | Promise){ return this.socket.hook(name, handler) } /** * Removes a {@link hook} listener by name. * @param name The function name */ public unhook(name: T.Name){ return this.socket.unhook(name) } /** * Attach a listener to error or close events * @param type 'error' or 'close' * @param f The listener to attach */ public on(type: "error" | "close", f: (e?: any) => void){ return this.socket.on(type, f) } /** * Destroys the socket */ public destroy(){ return this.socket.destroy() } /** * Closes the socket. It may attempt to reconnect. */ public close(){ return this.socket.close() } /** * Trigger a hooked handler on the server * @param rpcname The function to call * @param args other arguments */ public async call (rpcname: T.Name, ...args: T.Any[]) : Promise{ return await this.socket.call.apply(this.socket, [rpcname, ...args]) } /** * An alternative to call that does not wait for confirmation and doesn't return a value. * @param rpcname The function to call * @param args other arguments */ public async fire(rpcname: T.Name, ...args: T.Any[]) : Promise{ await this.socket.fire.apply(this.socket, [rpcname, ...args]) } /** * Connects to the server and attaches available RPCs to this object */ public async connect(){ this.socket = await bsock.connect(this.port, this.server, this.tls) const info:T.ExtendedRpcInfo[] = await this.info() info.forEach(i => { let f: any switch (i.type) { case 'Call': f = this.callGenerator(i.uniqueName, i.argNames) break case 'Hook': f = this.hookGenerator(i.uniqueName, i.argNames) break } if(this[i.owner] == null) this[i.owner] = {} this[i.owner][i.name] = f this[i.owner][i.name].bind(this) }) } /** * Get a list of available RPCs from the server */ public async info(){ return await this.socket.call('info') } /** * Utility {@link AsyncFunction} generator * @param fnName The function name * @param fnArgs A string-list of parameters */ private callGenerator(fnName: T.Name, fnArgs:T.Arg[]): T.AsyncFunction{ const headerArgs = fnArgs.join(",") const argParams = fnArgs.map(stripAfterEquals).join(",") return eval( '( () => async ('+headerArgs+') => { return await this.socket.call("'+fnName+'", '+argParams+')} )()' ) } /** * Utility {@link HookFunction} generator * @param fnName The function name * @param fnArgs A string-list of parameters */ private hookGenerator(fnName: T.Name, fnArgs:T.Arg[]): T.HookFunction{ const headerArgs = fnArgs.join(",") const argParams = fnArgs.map(stripAfterEquals).join(",") return eval( `( () => async (`+headerArgs+(headerArgs.length!==0?",":"")+` callback) => { const r = await this.socket.call("`+fnName+`", `+argParams+`) if(r.result === 'Success'){ this.socket.hook(r.uuid, callback) } return r } )()` ) } }