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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import * as uuidv4 from "uuid/v4"
  2. import * as T from "./Types";
  3. import * as I from "./Interfaces";
  4. /**
  5. * Translate an RPC to RPCInfo for serialization.
  6. * @param rpc The RPC to transform
  7. * @param owner The owning RPC group's name
  8. * @param errorHandler The error handler to invoke when something goes wrong
  9. * @param sesame optional sesame phrase to prepend before all RPC arguments
  10. * @throws Error on RPC without name property
  11. */
  12. export const rpcToRpcinfo = (socket: I.Socket, rpc : T.RPC<any, any>, owner: string, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction):T.RpcInfo => {
  13. switch (typeof rpc){
  14. case "object":
  15. if(rpc['call']){
  16. return {
  17. owner: owner,
  18. argNames: extractArgs(rpc['call']),
  19. type: "Call",
  20. name: rpc.name,
  21. call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); socket.destroy()}:rpc['call'], // check & remove sesame
  22. }
  23. }else{
  24. const generator = hookGenerator(<T.HookRPC<any, any>>rpc, errorHandler, sesame)
  25. return {
  26. owner: owner,
  27. argNames: extractArgs(generator(undefined)),
  28. type: "Hook",
  29. name: rpc.name,
  30. generator: generator,
  31. }
  32. }
  33. case "function":
  34. if(!rpc.name) throw new Error(`
  35. RPC did not provide a name.
  36. \nUse 'funtion name(..){ .. }' syntax instead.
  37. \n
  38. \n<------------OFFENDING RPC:
  39. \n${rpc.toString()}
  40. \n>------------OFFENDING RPC`)
  41. return {
  42. owner : owner,
  43. argNames: extractArgs(rpc),
  44. type: "Call",
  45. name: rpc.name,
  46. call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args); throw makeError(rpc.name)}:rpc, // check & remove sesame
  47. }
  48. }
  49. throw new Error("Bad socketIORPC type "+ typeof rpc)
  50. }
  51. /**
  52. * Utility function to apply the RPCs of an {@link RPCExporter}.
  53. * @param socket The websocket (implementation: bsock) to hook on
  54. * @param exporter The exporter
  55. * @param makeUnique @default true Attach a suffix to RPC names
  56. */
  57. export function rpcHooker(socket: I.Socket, exporter:I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{
  58. const owner = exporter.name
  59. const RPCs = exporter.exportRPCs()
  60. return RPCs.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)
  66. : info.call
  67. socket.hook(ret.uniqueName, callGenerator(info.name, socket, rpcFunction, errorHandler))
  68. return ret
  69. })
  70. }
  71. /**
  72. * Decorate an RPC with the error handler
  73. * @param rpcFunction the function to decorate
  74. */
  75. const callGenerator = (rpcName : string, socket: I.Socket, rpcFunction : T.AnyFunction, errorHandler: T.ErrorHandler) : T.AnyFunction => {
  76. const argsArr = extractArgs(rpcFunction)
  77. const args = argsArr.join(',')
  78. const argsStr = argsArr.map(stripAfterEquals).join(',')
  79. return eval(`async (`+args+`) => {
  80. try{
  81. return await rpcFunction(`+argsStr+`)
  82. }catch(e){
  83. errorHandler(socket)(e, rpcName, [`+args+`])
  84. }
  85. }`)
  86. }
  87. /**
  88. * Utility function to strip parameters like "a = 3" of their defaults
  89. * @param str The parameter to modify
  90. */
  91. export function stripAfterEquals(str:string):string{
  92. return str.split("=")[0]
  93. }
  94. /**
  95. * Utility function to generate {@link HookFunction} from a RPC for backend
  96. * @param rpc The RPC to transform
  97. * @returns A {@link HookFunction}
  98. */
  99. const hookGenerator = (rpc:T.HookRPC<any, any>, /*not unused!*/ errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => {
  100. let argsArr = extractArgs(rpc.hook)
  101. argsArr.pop() //remove 'callback' from the end
  102. let callArgs = argsArr.join(',')
  103. const args = sesameFn?(['sesame', ...argsArr].join(','))
  104. :callArgs
  105. callArgs = appendComma(callArgs, false)
  106. //note rpc.hook is the associated RPC, not a socket.hook
  107. return eval(`
  108. (clientSocket) => async (${args}) => {
  109. try{
  110. if(sesameFn && !sesameFn(sesame)) return
  111. const uuid = uuidv4()
  112. const res = await rpc.hook(${callArgs} (...cbargs) => {
  113. if(rpc.onCallback){
  114. rpc.onCallback.apply({}, cbargs)
  115. }
  116. clientSocket.call.apply(clientSocket, [uuid, ...cbargs])
  117. })
  118. if(rpc.onClose){
  119. clientSocket.on('close', () => rpc.onClose(res, rpc))
  120. }
  121. return {'uuid': uuid, 'return': res}
  122. }catch(e){
  123. //can throw to pass exception to client or swallow to keep it local
  124. errorHandler(clientSocket)(e, ${rpc.name}, [${args}])
  125. }
  126. }`)
  127. }
  128. const makeError = (callName: string) => {
  129. return new Error("Call not found: "+callName+". ; Zone: <root> ; Task: Promise.then ; Value: Error: Call not found: "+callName)
  130. }
  131. /**
  132. * Extract a string list of parameters from a function
  133. * @param f The source function
  134. */
  135. const extractArgs = (f:Function):string[] => {
  136. let fn:string
  137. fn = (fn = String(f)).substr(0, fn.indexOf(")")).substr(fn.indexOf("(")+1)
  138. return fn!==""?fn.split(',') : []
  139. }
  140. export function makeSesameFunction (sesame : T.SesameFunction | string) : T.SesameFunction {
  141. if(typeof sesame === 'function'){
  142. return sesame
  143. }
  144. return (testSesame : string) => {
  145. return testSesame === sesame
  146. }
  147. }
  148. export function appendComma(s?:string, turnToString = true):string{
  149. if(turnToString)
  150. return s?`'${s}',`:""
  151. return s?`${s},`:""
  152. }
  153. /**
  154. * Typescript incorrectly omits the function.name attribute for MethodDeclaration.
  155. * This was supposedly fixed (https://github.com/microsoft/TypeScript/issues/5611) but it still is the case.
  156. * This function sets the name value for all object members that are functions.
  157. */
  158. export function fixNames(o:Object):void{
  159. Object.keys(o).forEach(key => {
  160. if(typeof o[key] === 'function' && !o[key].name){
  161. Object.defineProperty(o[key], 'name', {
  162. value: key
  163. })
  164. }
  165. })
  166. }