'use strict' import { PromiseIOClient } from './PromiseIO/Client' import * as T from './Types'; import * as I from './Interfaces'; import { stripAfterEquals, appendComma } from './Utils'; /** * A websocket-on-steroids with built-in RPC capabilities */ export class RPCSocket implements I.Socket{ static async makeSocket(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise> { const socket = new RPCSocket(port, server, conf) return await socket.connect(sesame) } private socket: I.Socket private handlers : { [name in string]: T.AnyFunction[] } = { error: [], close: [] } private hooks : {[name in string]: T.AnyFunction} = {} /** * * @param port Port to connect to * @param server Server address * @param tls @default false use TLS */ constructor(public port:number, private server: string, private conf:T.SocketConf = { tls: 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: string, handler: (...args:any[]) => any | Promise){ if(!this.socket){ this.hooks[name] = handler }else{ this.socket.hook(name, handler) } } /** * 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 bind(name: string, handler: (...args:any[]) => any | Promise){ if(!this.socket){ this.hooks[name] = handler }else{ this.socket.bind(name, handler) } } /** * Removes a {@link hook} listener by name. * @param name The function name */ public unhook(name: string){ if(!this.socket){ delete this.hooks[name] }else{ 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: string, f: T.AnyFunction){ if(!this.socket){ if(!this.handlers[type]) this.handlers[type] = [] this.handlers[type].push(f) }else{ this.socket.on(type, f) } } /** * Emit a LOCAL event * @param eventName The event name to emit under * @param data The data the event carries */ public emit(eventName:string, data:any){ if(!this.socket) return this.socket.emit(eventName, data) } /** * Closes the socket. It may attempt to reconnect. */ public close(){ if(!this.socket) 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: string, ...args: any[]) : Promise{ if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") try{ const val = await this.socket.call.apply(this.socket, [rpcname, ...args]) return val }catch(e){ this.emit('error', e) throw e } } /** * 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: string, ...args: any[]) : Promise{ if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") await this.socket.fire.apply(this.socket, [rpcname, ...args]) } /** * Connects to the server and attaches available RPCs to this object */ public async connect( sesame?: string ) : Promise> { try{ this.socket = await PromiseIOClient.connect(this.port, this.server, /*this.conf.tls?this.conf.tls:false*/) }catch(e){ this.handlers['error'].forEach(h => h(e)) throw e } Object.entries(this.handlers).forEach(([k,v])=>{ v.forEach(h => this.socket.on(k, h)) }) Object.entries(this.hooks).forEach((kv: [string, T.AnyFunction]) => { this.socket.hook(kv[0], kv[1]) }) const info:T.ExtendedRpcInfo[] = await this.info(sesame) info.forEach(i => { let f: any switch (i.type) { case 'Call': f = this.callGenerator(i.uniqueName, i.argNames, sesame) break case 'Hook': f = this.frontEndHookGenerator(i.uniqueName, i.argNames, sesame) break } if(this[i.owner] == null) this[i.owner] = {} this[i.owner][i.name] = f this[i.owner][i.name].bind(this) }) return > (this as any) } /** * Get a list of available RPCs from the server */ public async info(sesame?:string){ if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") return await this.socket.call('info', sesame) } /** * Utility {@link AsyncFunction} generator * @param fnName The function name * @param fnArgs A string-list of parameters */ private callGenerator(fnName: string, fnArgs:string[], sesame?:string): T.AnyFunction{ const headerArgs = fnArgs.join(",") const argParams = fnArgs.map(stripAfterEquals).join(",") sesame = appendComma(sesame) return eval(`async (${headerArgs}) => { return await this.call("${fnName}", ${sesame} ${argParams}) }`) } /** * Utility {@link HookFunction} generator * @param fnName The function name * @param fnArgs A string-list of parameters */ private frontEndHookGenerator(fnName: string, fnArgs:string[], sesame?:string): T.HookFunction{ if(sesame) fnArgs.shift() let headerArgs = fnArgs.join(",") const argParams = fnArgs.map(stripAfterEquals).join(",") sesame = appendComma(sesame, true) headerArgs = fnArgs.length>0?headerArgs+",":headerArgs return eval( ` async (${headerArgs} callback) => { const r = await this.call("${fnName}", ${sesame} ${argParams}) try{ if(r){ if(r.uuid){ callback['destroy'] = () => { this.socket.unhook(r.uuid) } this.socket.hook(r.uuid, callback) } return r.return }else{ throw new Error("Empty response") } }catch(e){ throw e } }`) } }