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.4KB

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