import * as uuidv4 from "uuid/v4" import * as T from "./Types"; import * as I from "./Interfaces"; import { SubscriptionResponse } from "./Types"; /** * Translate an RPC to RPCInfo for serialization. * @param rpc The RPC to transform * @param owner The owning RPC group's name * @param errorHandler The error handler to invoke when something goes wrong * @param sesame optional sesame phrase to prepend before all RPC arguments * @throws Error on RPC without name property */ export const rpcToRpcinfo = (socket: I.Socket, rpc : T.RPC, owner: string, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction):T.RpcInfo => { switch (typeof rpc){ case "object": if(rpc['call']){ return { owner: owner, argNames: extractArgs(rpc['call']), type: "Call", name: rpc.name, call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); socket.destroy()}:rpc['call'], // check & remove sesame } }else{ const generator = hookGenerator(>rpc, errorHandler, sesame) return { owner: owner, argNames: extractArgs(generator(undefined)), type: "Hook", name: rpc.name, generator: generator, } } case "function": if(!rpc.name) throw new Error(` RPC did not provide a name. \nUse 'funtion name(..){ .. }' syntax instead. \n \n<------------OFFENDING RPC: \n${rpc.toString()} \n>------------OFFENDING RPC`) return { owner : owner, argNames: extractArgs(rpc), type: "Call", name: rpc.name, call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args); throw makeError(rpc.name)}:rpc, // check & remove sesame } } throw new Error("Bad socketIORPC type "+ typeof rpc) } /** * Utility function to apply the RPCs of an {@link RPCExporter}. * @param socket The websocket (implementation: bsock) to hook on * @param exporter The exporter * @param makeUnique @default true Attach a suffix to RPC names */ export function rpcHooker(socket: I.Socket, exporter:I.RPCExporter, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{ const owner = exporter.name const RPCs = [...exporter.exportRPCs()] return RPCs.map(rpc => rpcToRpcinfo(socket, rpc, owner, errorHandler, sesame)) .map(info => { const suffix = makeUnique?"-"+uuidv4().substr(0,4):"" const ret:any = info ret.uniqueName = info.name+suffix let rpcFunction = info.type === 'Hook'? info.generator(socket) : info.call socket.hook(ret.uniqueName, callGenerator(info.name, socket, rpcFunction, errorHandler)) return ret }) } /** * Decorate an RPC with the error handler * @param rpcFunction the function to decorate */ const callGenerator = (rpcName : string, socket: I.Socket, rpcFunction : T.AnyFunction, errorHandler: T.ErrorHandler) : T.AnyFunction => { const argsArr = extractArgs(rpcFunction) const args = argsArr.join(',') const argsStr = argsArr.map(stripAfterEquals).join(',') return eval(`async (`+args+`) => { try{ return await rpcFunction(`+argsStr+`) }catch(e){ errorHandler(socket)(e, rpcName, [`+args+`]) } }`) } /** * Utility function to strip parameters like "a = 3" of their defaults * @param str The parameter to modify */ export function stripAfterEquals(str:string):string{ return str.split("=")[0] } /** * Utility function to generate {@link HookFunction} from a RPC for backend * @param rpc The RPC to transform * @returns A {@link HookFunction} */ const hookGenerator = (rpc:T.HookRPC, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => { let argsArr = extractArgs(rpc.hook) argsArr.pop() //remove 'callback' from the end let callArgs = argsArr.join(',') const args = sesameFn?(['sesame', ...argsArr].join(',')) :callArgs callArgs = appendComma(callArgs) //note rpc.hook is the associated RPC, not a socket.hook return eval(` (socket) => async (${args}) => { try{ if(sesameFn && !sesameFn(sesame)) return const res = await rpc.hook(${callArgs} (...cbargs) => { if(rpc.onCallback) rpc.onCallback.apply({}, cbargs) socket.call.apply(socket, [res.uuid, ...cbargs]) }) return res }catch(e){ errorHandler(socket)(e, ${rpc.name}, [${args}]) } }`) } const makeError = (callName: string) => { return new Error("Call not found: "+callName+". ; Zone: ; Task: Promise.then ; Value: Error: Call not found: "+callName) } /** * Extract a string list of parameters from a function * @param f The source function */ const extractArgs = (f:Function):string[] => { let fn:string return (fn = String(f)).substr(0, fn.indexOf(")")).substr(fn.indexOf("(")+1).split(",") } /** * Simple utility function to create basic {@link SubscriptionResponse} * @param uuid optional uuid to use, otherwise defaults to uuid/v4 */ export function makeSubResponse(extension:T):SubscriptionResponse & T{ return { result: "Success", uuid: uuidv4(), ...extension } } export function makeSesameFunction (sesame : T.SesameFunction | string) : T.SesameFunction { if(typeof sesame === 'function'){ return sesame } return (testSesame : string) => { return testSesame === sesame } } export function appendComma(s?:string):string{ return s?`'${s}',`:"" }