You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Utils.ts 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import * as uuidv4 from "uuid/v4"
  2. import * as T from "./Types";
  3. import * as I from "./Interfaces";
  4. import { Socket } from "socket.io"
  5. import { CALL_NOT_FOUND, DESTROY_PREFIX, RPC_BAD_TYPE, RPC_NO_NAME } from "./Strings";
  6. import { DeserializerFactory } from "./Decorator";
  7. /**
  8. * Translate an RPC to RPCInfo for serialization.
  9. * @param rpc The RPC to transform
  10. * @param owner The owning RPC group's name
  11. * @param errorHandler The error handler to invoke when something goes wrong
  12. * @param sesame optional sesame phrase to prepend before all RPC arguments
  13. * @throws Error on RPC without name property
  14. */
  15. export const rpcToRpcinfo = (socket: I.Socket, rpc: T.RPC<any, any>, owner: string, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction): T.RpcInfo => {
  16. switch (typeof rpc) {
  17. case "object":
  18. if (rpc['call']) {
  19. const _rpc: T.CallRPC<any, any> = rpc
  20. return {
  21. owner: owner,
  22. argNames: extractArgs(rpc['call']),
  23. type: "Call",
  24. name: rpc.name,
  25. call: sesame ? async ($__sesame__$, ...args) => { if (sesame($__sesame__$)) return await rpc['call'].apply({}, args); socket.close() } : rpc['call'], // check & remove sesame
  26. }
  27. } else {
  28. const _rpc: T.HookRPC<any, any> = rpc
  29. const generator = hookGenerator(_rpc, errorHandler, sesame)
  30. return {
  31. owner: owner,
  32. argNames: extractArgs(generator(undefined)),
  33. type: "Hook",
  34. name: _rpc.name,
  35. generator: generator,
  36. }
  37. }
  38. case "function":
  39. if (!rpc.name) throw new Error(RPC_NO_NAME(rpc.toString()))
  40. return {
  41. owner: owner,
  42. argNames: extractArgs(rpc),
  43. type: "Call",
  44. name: rpc.name,
  45. call: sesame ? async ($__sesame__$, ...args) => { if (sesame($__sesame__$)) return await rpc.apply({}, args); throw makeError(rpc.name) } : rpc, // check & remove sesame
  46. }
  47. }
  48. throw new Error(RPC_BAD_TYPE(typeof rpc))
  49. }
  50. /**
  51. * Utility function to apply the RPCs of an {@link RPCExporter}.
  52. * @param socket The websocket (implementation: socket.io) to hook on
  53. * @param exporter The exporter
  54. * @param makeUnique @default true Attach a suffix to RPC names
  55. */
  56. export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] {
  57. const owner = exporter.name
  58. const RPCs = typeof exporter.RPCs === "function" ? exporter.RPCs() : exporter.RPCs
  59. return RPCs
  60. .map(rpc => rpcToRpcinfo(socket, rpc, owner, errorHandler, sesame))
  61. .map(info => {
  62. const suffix = makeUnique ? "-" + uuidv4().substr(0, 4) : ""
  63. const ret: any = info
  64. ret.uniqueName = info.name + suffix
  65. let rpcFunction = info.type === 'Hook' ? info.generator(socket) : info.call
  66. socket.hook(ret.uniqueName, callGenerator(info.name, socket, rpcFunction, errorHandler))
  67. return ret
  68. })
  69. }
  70. /**
  71. * Decorate an RPC with the error handler
  72. * @param rpcFunction the function to decorate
  73. */
  74. const callGenerator = (rpcName: string, $__socket__$: I.Socket, rpcFunction: T.GenericFunction, errorHandler: T.ErrorHandler): T.GenericFunction => {
  75. const argsArr = extractArgs(rpcFunction)
  76. const args = argsArr.join(',')
  77. const argsStr = argsArr.map(stripAfterEquals).join(',')
  78. const deserializer = DeserializerFactory
  79. const callStr = `async (${args}) => {
  80. try{
  81. const res = await rpcFunction(${argsStr})
  82. return deserializer.makeDeserializable(res)
  83. }catch(e){
  84. errorHandler($__socket__$, e, rpcName, [${args}])
  85. }
  86. }`
  87. return eval(callStr);
  88. }
  89. /**
  90. * Utility function to strip parameters like "a = 3" of their defaults
  91. * @param str The parameter to modify
  92. */
  93. export function stripAfterEquals(str: string): string {
  94. return str.split("=")[0]
  95. }
  96. /**
  97. * Utility function to generate {@link HookFunction} from a RPC for backend
  98. * @param rpc The RPC to transform
  99. * @returns A {@link HookFunction}
  100. */
  101. const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction, injectSocket?: boolean): T.HookInfo['generator'] => {
  102. const deserializer = DeserializerFactory
  103. let argsArr = extractArgs(rpc.hook)
  104. argsArr.shift()//remove callback param
  105. let callArgs = argsArr.join(',')
  106. const args = sesameFn ? (['sesame', ...argsArr].join(','))
  107. : callArgs
  108. callArgs = appendComma(callArgs, false)
  109. const destroy_prefix = DESTROY_PREFIX
  110. const hookStr = `
  111. ($__socket__$) => async (${args}) => {
  112. try{
  113. if(sesameFn && !sesameFn(sesame)) return
  114. const uuid = uuidv4()
  115. const res = await rpc.hook((...cbargs) => {
  116. ${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``}
  117. cbargs = cbargs.map(deserializer.makeDeserializable)
  118. $__socket__$.call.apply($__socket__$, [uuid, ...cbargs])
  119. },${callArgs})
  120. ${rpc.onDestroy ? `$__socket__$.bind(destroy_prefix+uuid, () => {
  121. rpc.onDestroy(res, rpc)
  122. })` : ``}
  123. return {'uuid': uuid, 'return': res}
  124. }catch(e){
  125. //can throw to pass exception to client or swallow to keep it local
  126. errorHandler($__socket__$, e, ${rpc.name}, [${args}])
  127. }
  128. }`
  129. return eval(hookStr)
  130. }
  131. const makeError = (callName: string) => new Error(CALL_NOT_FOUND(callName))
  132. /**
  133. * Extract a string list of parameters from a function
  134. * @param f The source function
  135. */
  136. const extractArgs = (f: Function): string[] => {
  137. let fn: string
  138. fn = (fn = String(f)).substring(0, fn.indexOf(")")).substring(fn.indexOf("(") + 1)
  139. return fn !== "" ? fn.split(',') : []
  140. }
  141. export function makeSesameFunction(sesame: T.SesameFunction | string): T.SesameFunction {
  142. if (typeof sesame === 'function') {
  143. return sesame
  144. }
  145. return (testSesame: string) => {
  146. return testSesame === sesame
  147. }
  148. }
  149. export function appendComma(s?: string, turnToString = true): string {
  150. if (turnToString)
  151. return s ? `'${s}',` : ""
  152. return s ? `${s},` : ""
  153. }
  154. /**
  155. * Typescript incorrectly omits the function.name attribute for MethodDeclaration.
  156. * This was supposedly fixed (https://github.com/microsoft/TypeScript/issues/5611) but it still is the case.
  157. * This function sets the name value for all object members that are functions.
  158. */
  159. export function fixNames(o: Object): void {
  160. Object.keys(o).forEach(key => {
  161. if (typeof o[key] === 'function' && !o[key].name) {
  162. Object.defineProperty(o[key], 'name', {
  163. value: key
  164. })
  165. }
  166. })
  167. }
  168. /**
  169. * Transforms a socket.io instance into one conforming to I.Socket
  170. * @param socket A socket.io socket
  171. */
  172. export const makePioSocket = (socket: any): I.Socket => {
  173. return <I.Socket>{
  174. id: socket.id,
  175. bind: (name: string, listener: T.PioBindListener) => {
  176. socket.on(name, (...args: any) => listener.apply(null, args))
  177. },
  178. hook: (name: string, listener: T.PioHookListener) => {
  179. const args = extractArgs(listener)
  180. let argNames
  181. let restParam = args.find(e => e.includes('...'))
  182. if (!restParam) {
  183. argNames = [...args, '...$__args__$'].join(',')
  184. restParam = '$__args__$'
  185. } else {
  186. argNames = [...args].join(',')
  187. restParam = restParam.replace('...', '')
  188. }
  189. const decoratedListener = eval(`(() => async (${argNames}) => {
  190. const __ack__ = ${restParam}.pop()
  191. try{
  192. const response = await listener.apply(null, [${argNames}])
  193. __ack__(response)
  194. }catch(e){
  195. __ack__({
  196. ...e,
  197. stack: e.stack,
  198. message: e.message,
  199. name: e.name,
  200. })
  201. }
  202. })()`)
  203. socket.on(name, decoratedListener)
  204. },
  205. call: (name: string, ...args: any) => {
  206. return new Promise((res, rej) => {
  207. const params: any = [name, ...args, (resp) => {
  208. if (isError(resp)) {
  209. const err = new Error()
  210. err.stack = resp.stack
  211. err.name = resp.name
  212. err.message = resp.message
  213. return rej(err)
  214. }
  215. res(resp)
  216. }]
  217. socket.emit.apply(socket, params)
  218. })
  219. },
  220. fire: (name: string, ...args: any) => new Promise((res, rej) => {
  221. const params: any = [name, ...args]
  222. socket.emit.apply(socket, params)
  223. res(undefined)
  224. }),
  225. unhook: (name: string, listener?: T.GenericFunction) => {
  226. if (listener) {
  227. socket.removeListener(name, listener)
  228. } else {
  229. socket.removeAllListeners(name)
  230. }
  231. },
  232. on: (...args) => socket.on.apply(socket, args),
  233. emit: (...args) => socket.emit.apply(socket, args),
  234. close: () => {
  235. socket.disconnect(true)
  236. }
  237. }
  238. }
  239. export const isError = function (e) {
  240. return e && e.stack && e.message && typeof e.stack === 'string'
  241. && typeof e.message === 'string';
  242. }