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.

Frontend.ts 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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.FrontEndHandlerType['close'][] = []
  16. private errorHandlers: T.FrontEndHandlerType['error'][] = []
  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.FrontEndHandlerType[T]){
  56. if(!this.socket){
  57. switch(type){
  58. case "error": this.errorHandlers.push(<T.FrontEndHandlerType['error']> f); break;
  59. case "close": this.closeHandlers.push(<T.FrontEndHandlerType['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. * Emit a LOCAL event
  68. * @param eventName The event name to emit under
  69. * @param data The data the event carries
  70. */
  71. public emit(eventName:string, data:any){
  72. if(!this.socket) return
  73. this.socket.emit(eventName, data)
  74. }
  75. /**
  76. * Destroys the socket
  77. */
  78. public destroy(){
  79. if(!this.socket) return;
  80. this.socket.destroy()
  81. }
  82. /**
  83. * Closes the socket. It may attempt to reconnect.
  84. */
  85. public close(){
  86. if(!this.socket) return;
  87. this.socket.close()
  88. }
  89. /**
  90. * Trigger a hooked handler on the server
  91. * @param rpcname The function to call
  92. * @param args other arguments
  93. */
  94. public async call (rpcname: string, ...args: any[]) : Promise<any>{
  95. if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
  96. try{
  97. return await this.socket.call.apply(this.socket, [rpcname, ...args])
  98. }catch(e){
  99. this.emit('error', e)
  100. throw e
  101. }
  102. }
  103. /**
  104. * An alternative to call that does not wait for confirmation and doesn't return a value.
  105. * @param rpcname The function to call
  106. * @param args other arguments
  107. */
  108. public async fire(rpcname: string, ...args: any[]) : Promise<void>{
  109. if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
  110. await this.socket.fire.apply(this.socket, [rpcname, ...args])
  111. }
  112. /**
  113. * Connects to the server and attaches available RPCs to this object
  114. */
  115. public async connect<T extends T.RPCInterface= T.RPCInterface>( sesame?: string ) : Promise<RPCSocket & T>{
  116. this.socket = await bsock.connect(this.port, this.server, this.conf.tls?this.conf.tls:false)
  117. this.errorHandlers.forEach(h => this.socket.on('error', h))
  118. this.closeHandlers.forEach(h => this.socket.on('close', h))
  119. Object.entries(this.hooks).forEach((kv: [string, T.AnyFunction]) => {
  120. this.socket.hook(kv[0], kv[1])
  121. })
  122. const info:T.ExtendedRpcInfo[] = await this.info()
  123. info.forEach(i => {
  124. let f: any
  125. switch (i.type) {
  126. case 'Call':
  127. f = this.callGenerator(i.uniqueName, i.argNames, sesame)
  128. break
  129. case 'Hook':
  130. f = this.frontEndHookGenerator(i.uniqueName, i.argNames, sesame)
  131. break
  132. }
  133. if(this[i.owner] == null)
  134. this[i.owner] = {}
  135. this[i.owner][i.name] = f
  136. this[i.owner][i.name].bind(this)
  137. })
  138. return <RPCSocket & T.RPCInterface<T>> (this as any)
  139. }
  140. /**
  141. * Get a list of available RPCs from the server
  142. */
  143. public async info(){
  144. if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
  145. return await this.socket.call('info')
  146. }
  147. /**
  148. * Utility {@link AsyncFunction} generator
  149. * @param fnName The function name
  150. * @param fnArgs A string-list of parameters
  151. */
  152. private callGenerator(fnName: string, fnArgs:string[], sesame?:string): T.AnyFunction{
  153. const headerArgs = fnArgs.join(",")
  154. const argParams = fnArgs.map(stripAfterEquals).join(",")
  155. sesame = appendComma(sesame)
  156. return eval(`async (${headerArgs}) => {
  157. return await this.socket.call("${fnName}", ${sesame} ${argParams})
  158. }`)
  159. }
  160. /**
  161. * Utility {@link HookFunction} generator
  162. * @param fnName The function name
  163. * @param fnArgs A string-list of parameters
  164. */
  165. private frontEndHookGenerator(fnName: string, fnArgs:string[], sesame?:string): T.HookFunction{
  166. if(sesame)
  167. fnArgs.shift()
  168. let headerArgs = fnArgs.join(",")
  169. const argParams = fnArgs.map(stripAfterEquals).join(",")
  170. sesame = appendComma(sesame, true)
  171. headerArgs = fnArgs.length>0?headerArgs+",":headerArgs
  172. return eval( `
  173. async (${headerArgs} callback) => {
  174. const r = await this.socket.call("${fnName}", ${sesame} ${argParams})
  175. if(r && r.result === 'Success'){
  176. this.socket.hook(r.uuid, callback)
  177. }
  178. return r
  179. }`)
  180. }
  181. }