123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- import * as uuidv4 from "uuid/v4"
-
- import * as T from "./Types";
- import * as I from "./Interfaces";
- import { Socket } from "socket.io"
-
- /**
- * 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<any, any>, owner: string, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction): T.RpcInfo => {
- switch (typeof rpc) {
- case "object":
- if (rpc['call']) {
- const _rpc: T.CallRPC<any, any> = rpc
- 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.close() } : rpc['call'], // check & remove sesame
- }
- } else {
- const _rpc: T.HookRPC<any, any> = rpc
- 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 serverSocket The websocket (implementation: socket.io) to hook on
- * @param exporter The exporter
- * @param makeUnique @default true Attach a suffix to RPC names
- */
- export function rpcHooker(serverSocket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] {
- const owner = exporter.name
- const RPCs = typeof exporter.RPCs === "function" ? exporter.RPCs() : exporter.RPCs
-
- return RPCs
- .map(rpc => rpcToRpcinfo(serverSocket, 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(serverSocket) : info.call
- serverSocket.hook(ret.uniqueName, callGenerator(info.name, serverSocket, 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(',')
-
- const callStr = `async (${args}) => {
- try{
- return await rpcFunction(${argsStr})
- }catch(e){
- errorHandler($__socket__$)(e, rpcName, [${args}])
- }
- }`
-
- return eval(callStr);
- }
-
- /**
- * 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<any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction, injectSocket?: boolean): T.HookInfo['generator'] => {
- let argsArr = extractArgs(rpc.hook)
- argsArr.pop() //remove callback param
-
- let callArgs = argsArr.join(',')
- const args = sesameFn ? (['sesame', ...argsArr].join(','))
- : callArgs
-
- callArgs = appendComma(callArgs, false)
-
- const hookStr = `
- ($__socket__$) => async (${args}) => {
- try{
- if(sesameFn && !sesameFn(sesame)) return
- const uuid = uuidv4()
- const res = await rpc.hook(${callArgs} (...cbargs) => {
- ${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``}
- $__socket__$.call.apply($__socket__$, [uuid, ...cbargs])
- })
- ${rpc.onDestroy ? `$__socket__$.bind(uuid, () => {
- rpc.onDestroy(res, rpc)
- })` : ``}
- return {'uuid': uuid, 'return': res}
- }catch(e){
- //can throw to pass exception to client or swallow to keep it local
- errorHandler($__socket__$)(e, ${rpc.name}, [${args}])
- }
- }`
-
- return eval(hookStr)
- }
-
- const makeError = (callName: string) => new Error(`Call not found: ${callName}. ; Zone: <root> ; 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
- fn = (fn = String(f)).substr(0, fn.indexOf(")")).substr(fn.indexOf("(") + 1)
- return fn !== "" ? fn.split(',') : []
- }
-
-
- 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, turnToString = true): string {
- if (turnToString)
- return s ? `'${s}',` : ""
- return s ? `${s},` : ""
- }
-
-
- /**
- * Typescript incorrectly omits the function.name attribute for MethodDeclaration.
- * This was supposedly fixed (https://github.com/microsoft/TypeScript/issues/5611) but it still is the case.
- * This function sets the name value for all object members that are functions.
- */
- export function fixNames(o: Object): void {
- Object.keys(o).forEach(key => {
- if (typeof o[key] === 'function' && !o[key].name) {
- Object.defineProperty(o[key], 'name', {
- value: key
- })
- }
- })
- }
-
- /**
- * Transforms a socket.io instance into one conforming to I.Socket
- * @param socket A socket.io socket
- */
- export const makePioSocket = (socket: any): I.Socket => {
- return <I.Socket>{
- id: socket.id,
- bind: (name: string, listener: T.PioBindListener) => socket.on(name, (...args: any) => listener.apply(null, args)),
-
- hook: (name: string, listener: T.PioHookListener) => {
- const args = extractArgs(listener)
- let argNames
- let restParam = args.find(e => e.includes('...'))
- if (!restParam) {
- argNames = [...args, '...$__args__$'].join(',')
- restParam = '$__args__$'
- } else {
- argNames = [...args].join(',')
- restParam = restParam.replace('...', '')
- }
-
- const decoratedListener = eval(`(() => async (${argNames}) => {
- const __ack__ = ${restParam}.pop()
- try{
- const response = await listener.apply(null, [${argNames}])
- __ack__(response)
- }catch(e){
- __ack__({
- ...e,
- stack: e.stack,
- message: e.message,
- name: e.name,
- })
- }
- })()`)
- socket.on(name, decoratedListener)
- },
-
- call: (name: string, ...args: any) => {
- return new Promise((res, rej) => {
- const params: any = [name, ...args, (resp) => {
- if (isError(resp)) {
- const err = new Error()
- err.stack = resp.stack
- err.name = resp.name
- err.message = resp.message
- return rej(err)
- }
- res(resp)
- }]
- socket.emit.apply(socket, params)
- })
- },
-
- fire: (name: string, ...args: any) => new Promise((res, rej) => {
- const params: any = [name, ...args]
- socket.emit.apply(socket, params)
- res()
- }),
-
- unhook: (name: string, listener?: T.AnyFunction) => {
- if (listener) {
- socket.removeListener(name, listener)
- } else {
- socket.removeAllListeners(name)
- }
- },
-
- on: (...args) => socket.on.apply(socket, args),
- emit: (...args) => socket.emit.apply(socket, args),
- close: () => {
- socket.disconnect(true)
- }
- }
- }
-
- export const isError = function (e) {
- return e && e.stack && e.message && typeof e.stack === 'string'
- && typeof e.message === 'string';
- }
|