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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. 'use strict'
  2. import bsock = require('bsock');
  3. import * as T from './Types';
  4. import * as I from './Interfaces';
  5. import { stripAfterEquals, appendComma } from './Utils';
  6. /**
  7. * A websocket-on-steroids with built-in RPC capabilities
  8. */
  9. export class RPCSocket implements I.Socket{
  10. static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise<RPCSocket & T> {
  11. const socket = new RPCSocket(port, server, conf)
  12. return await socket.connect<T>(sesame)
  13. }
  14. private socket: I.Socket
  15. private closeHandlers: T.CloseHandler[] = []
  16. private errorHandlers: T.ErrorHandler[] = []
  17. private hooks : {[name in string]: T.AnyFunction} = {}
  18. /**
  19. *
  20. * @param port Port to connect to
  21. * @param server Server address
  22. * @param tls @default false use TLS
  23. */
  24. constructor(public port:number, private server: string, private conf:T.SocketConf = { tls: false }){
  25. Object.defineProperty(this, 'socket', {value: undefined, writable: true})
  26. }
  27. /**
  28. * Hooks a handler to a function name. Use {@link call} to trigger it.
  29. * @param name The function name to listen on
  30. * @param handler The handler to attach
  31. */
  32. public hook(name: string, handler: (...args:any[]) => any | Promise<any>){
  33. if(!this.socket){
  34. this.hooks[name] = handler
  35. }else{
  36. this.socket.hook(name, handler)
  37. }
  38. }
  39. /**
  40. * Removes a {@link hook} listener by name.
  41. * @param name The function name
  42. */
  43. public unhook(name: string){
  44. if(!this.socket){
  45. delete this.hooks[name]
  46. }else{
  47. this.socket.unhook(name)
  48. }
  49. }
  50. /**
  51. * Attach a listener to error or close events
  52. * @param type 'error' or 'close'
  53. * @param f The listener to attach
  54. */
  55. public on<T extends "error" | "close">(type: T, f: T.HandlerType[T]){
  56. if(!this.socket){
  57. switch(type){
  58. case "error": this.errorHandlers.push(<T.HandlerType['error']> f); break;
  59. case "close": this.closeHandlers.push(<T.HandlerType['close']> f); break;
  60. default: throw new Error('socket.on only supports ´error´ and ´close´ as first parameter. Got: ´'+type+'´')
  61. }
  62. }else{
  63. this.socket.on(type, f)
  64. }
  65. }
  66. /**
  67. * Destroys the socket
  68. */
  69. public destroy(){
  70. if(!this.socket) return;
  71. this.socket.destroy()
  72. }
  73. /**
  74. * Closes the socket. It may attempt to reconnect.
  75. */
  76. public close(){
  77. if(!this.socket) return;
  78. this.socket.close()
  79. }
  80. /**
  81. * Trigger a hooked handler on the server
  82. * @param rpcname The function to call
  83. * @param args other arguments
  84. */
  85. public async call (rpcname: string, ...args: any[]) : Promise<any>{
  86. if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
  87. return await this.socket.call.apply(this.socket, [rpcname, ...args])
  88. }
  89. /**
  90. * An alternative to call that does not wait for confirmation and doesn't return a value.
  91. * @param rpcname The function to call
  92. * @param args other arguments
  93. */
  94. public async fire(rpcname: string, ...args: any[]) : Promise<void>{
  95. if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
  96. await this.socket.fire.apply(this.socket, [rpcname, ...args])
  97. }
  98. /**
  99. * Connects to the server and attaches available RPCs to this object
  100. */
  101. public async connect<T extends T.RPCInterface= T.RPCInterface>( sesame?: string ) : Promise<RPCSocket & T>{
  102. this.socket = await bsock.connect(this.port, this.server, this.conf.tls?this.conf.tls:false)
  103. this.errorHandlers.forEach(h => this.socket.on('error', h))
  104. this.closeHandlers.forEach(h => this.socket.on('close', h))
  105. Object.entries(this.hooks).forEach((kv: [string, T.AnyFunction]) => {
  106. this.socket.hook(kv[0], kv[1])
  107. })
  108. const info:T.ExtendedRpcInfo[] = await this.info()
  109. info.forEach(i => {
  110. let f: any
  111. switch (i.type) {
  112. case 'Call':
  113. f = this.callGenerator(i.uniqueName, i.argNames, sesame)
  114. break
  115. case 'Hook':
  116. f = this.frontEndHookGenerator(i.uniqueName, i.argNames, sesame)
  117. break
  118. }
  119. if(this[i.owner] == null)
  120. this[i.owner] = {}
  121. this[i.owner][i.name] = f
  122. this[i.owner][i.name].bind(this)
  123. })
  124. return <RPCSocket & T.RPCInterface<T>> (this as any)
  125. }
  126. /**
  127. * Get a list of available RPCs from the server
  128. */
  129. public async info(){
  130. if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
  131. return await this.socket.call('info')
  132. }
  133. /**
  134. * Utility {@link AsyncFunction} generator
  135. * @param fnName The function name
  136. * @param fnArgs A string-list of parameters
  137. */
  138. private callGenerator(fnName: string, fnArgs:string[], sesame?:string): T.AnyFunction{
  139. const headerArgs = fnArgs.join(",")
  140. const argParams = fnArgs.map(stripAfterEquals).join(",")
  141. sesame = appendComma(sesame)
  142. return eval(`async (${headerArgs}) => { return await this.socket.call("${fnName}", ${sesame} ${argParams})}`)
  143. }
  144. /**
  145. * Utility {@link HookFunction} generator
  146. * @param fnName The function name
  147. * @param fnArgs A string-list of parameters
  148. */
  149. private frontEndHookGenerator(fnName: string, fnArgs:string[], sesame?:string): T.HookFunction{
  150. if(sesame)
  151. fnArgs.shift()
  152. let headerArgs = fnArgs.join(",")
  153. const argParams = fnArgs.map(stripAfterEquals).join(",")
  154. sesame = appendComma(sesame, true)
  155. headerArgs = fnArgs.length>0?headerArgs+",":headerArgs
  156. return eval( `
  157. async (${headerArgs} callback) => {
  158. const r = await this.socket.call("${fnName}", ${sesame} ${argParams})
  159. if(r && r.result === 'Success'){
  160. this.socket.hook(r.uuid, callback)
  161. }
  162. return r
  163. }`)
  164. }
  165. }