Browse Source

Unknown RPC errors, fixed error handler structure, multiple RPCservers on different paths, optional throws

master
nitowa 3 years ago
parent
commit
326dabf2f8
9 changed files with 8873 additions and 305 deletions
  1. 8512
    118
      package-lock.json
  2. 1
    0
      package.json
  3. 40
    17
      src/Backend.ts
  4. 59
    59
      src/Frontend.ts
  5. 15
    7
      src/PromiseIO/Client.ts
  6. 12
    7
      src/PromiseIO/Server.ts
  7. 5
    6
      src/Types.ts
  8. 14
    12
      src/Utils.ts
  9. 215
    79
      test/Test.ts

+ 8512
- 118
package-lock.json
File diff suppressed because it is too large
View File


+ 1
- 0
package.json View File

@@ -57,6 +57,7 @@
57 57
     "http": "0.0.0",
58 58
     "socket.io": "^2.3.0",
59 59
     "socket.io-client": "^2.3.0",
60
+    "socketio-wildcard": "^2.0.0",
60 61
     "uuid": "^3.3.3"
61 62
   },
62 63
   "files": [

+ 40
- 17
src/Backend.ts View File

@@ -8,11 +8,11 @@ import * as I from './Interfaces';
8 8
 
9 9
 export class RPCServer<
10 10
     InterfaceT extends T.RPCInterface = T.RPCInterface,
11
-> {
11
+    > {
12 12
 
13 13
     private pio = new PromiseIO()
14 14
     private closeHandler: T.CloseHandler
15
-    private errorHandler: T.ErrorHandler
15
+    private errorHandler: (socket: I.Socket, error: any, rpcName: string, args: any[], forward?: boolean) => void
16 16
     private connectionHandler: T.ConnectionHandler
17 17
     private sesame?: T.SesameFunction
18 18
     private accessFilter: T.AccessFilter<InterfaceT>
@@ -26,8 +26,12 @@ export class RPCServer<
26 26
      */
27 27
     constructor(
28 28
         private exporters: T.ExporterArray<InterfaceT> = [],
29
-        conf: T.ServerConf<InterfaceT> = {},
29
+        private conf: T.ServerConf<InterfaceT> = {},
30 30
     ) {
31
+        if (conf.throwOnUnknownRPC == null) {
32
+            conf.throwOnUnknownRPC = true
33
+        }
34
+
31 35
         if (conf.sesame) {
32 36
             this.sesame = U.makeSesameFunction(conf.sesame)
33 37
         }
@@ -37,9 +41,15 @@ export class RPCServer<
37 41
             return this.sesame!(sesame!)
38 42
         })
39 43
 
40
-        this.errorHandler = (socket: I.Socket | PromiseIO) => (error: any, rpcName: string, args: any[]) => {
44
+        this.errorHandler = (socket: I.Socket, error: any, rpcName: string, args: any[], forward: boolean = false) => {
41 45
             if (conf.errorHandler) conf.errorHandler(socket, error, rpcName, args)
42
-            else throw error
46
+            else {
47
+                if (forward) {
48
+                    socket.call("$UNKNOWNRPC$", error)
49
+                } else {
50
+                    throw error
51
+                }
52
+            }
43 53
         }
44 54
 
45 55
         this.closeHandler = (socket: I.Socket) => {
@@ -52,7 +62,7 @@ export class RPCServer<
52 62
 
53 63
         exporters.forEach(U.fixNames) //TSC for some reason doesn't preserve name properties of methods
54 64
 
55
-        let badRPC = exporters.flatMap(ex => typeof ex.RPCs === "function"?ex.RPCs():(ex as any)).find(rpc => !rpc.name)
65
+        let badRPC = exporters.flatMap(ex => typeof ex.RPCs === "function" ? ex.RPCs() : (ex as any)).find(rpc => !rpc.name)
56 66
         if (badRPC) {
57 67
             throw new Error(`
58 68
             RPC did not provide a name. 
@@ -62,29 +72,32 @@ export class RPCServer<
62 72
             \n`+ badRPC.toString() + `
63 73
             \n>------------OFFENDING RPC`)
64 74
         }
65
-        
75
+
66 76
         try {
67 77
 
68 78
             this.pio.on('socket', (clientSocket: I.Socket) => {
69
-                const sock:any = clientSocket;
70 79
                 clientSocket.on('disconnect', () => this.closeHandler(clientSocket))
71 80
                 this.connectionHandler(clientSocket)
72 81
                 this.initRPCs(clientSocket)
73
-                
74 82
             })
75 83
         } catch (e) {
76
-            this.errorHandler(this.pio, e, 'system', [])
84
+            this.errorHandler(<unknown>undefined as I.Socket, e, 'system', [])
77 85
         }
78 86
     }
79 87
 
80
-    public attach = (httpServer = new http.Server()) : RPCServer<InterfaceT> => {
81
-        this.pio.attach(httpServer)
88
+    public attach = (httpServer = new http.Server(), options?: SocketIO.ServerOptions): RPCServer<InterfaceT> => {
89
+        this.pio.attach(httpServer, options)
82 90
         this.attached = true
83 91
         return this
84 92
     }
85 93
 
86
-    public listen(port:number) : RPCServer<InterfaceT>{
87
-        if(!this.attached) this.attach()
94
+    public listen(port: number, options?: SocketIO.ServerOptions): RPCServer<InterfaceT> {
95
+        if (!this.attached) {
96
+            this.attach(undefined, options)
97
+        } else {
98
+            if (options)
99
+                console.warn("RPCServer options were passed to listen(..) after attach(..) was called. Please pass them to attach(..) instead. Ignoring.")
100
+        }
88 101
         this.pio.listen(port)
89 102
         return this
90 103
     }
@@ -94,11 +107,21 @@ export class RPCServer<
94 107
             const rpcs = await Promise.all(this.exporters.map(async exp => {
95 108
                 const allowed = await this.accessFilter(sesame, exp)
96 109
                 if (!allowed) return []
97
-                const infos = U.rpcHooker(clientSocket, exp, this.errorHandler, this.sesame)
98
-                return infos
110
+                return U.rpcHooker(clientSocket, exp, this.errorHandler, this.sesame)
99 111
             }))
100
-            return rpcs.flat()
112
+            const infos = rpcs.flat()
113
+            if (this.conf.throwOnUnknownRPC) {
114
+                clientSocket.on("*", (packet) => {
115
+                    if (!infos.some(i => i.uniqueName === packet.data[0])) {
116
+                        if (packet.data[0].startsWith('destroy_')) return
117
+                        this.errorHandler(clientSocket, new Error(`Unknown RPC ${packet.data[0]}`), packet.data[0], [...packet.data].splice(1), true)
118
+                    }
119
+                })
120
+            }
121
+            return infos
101 122
         })
123
+
124
+
102 125
     }
103 126
 
104 127
     close(): void {

+ 59
- 59
src/Frontend.ts View File

@@ -1,7 +1,7 @@
1 1
 'use strict'
2 2
 
3
-import { PromiseIOClient } from './PromiseIO/Client'
4
-import * as T from './Types'; 
3
+import { PromiseIOClient, defaultClientConfig } from './PromiseIO/Client'
4
+import * as T from './Types';
5 5
 import * as I from './Interfaces';
6 6
 import { stripAfterEquals, appendComma } from './Utils';
7 7
 
@@ -9,22 +9,21 @@ import { stripAfterEquals, appendComma } from './Utils';
9 9
 /**
10 10
  * A websocket-on-steroids with built-in RPC capabilities
11 11
  */
12
-export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I.Socket{
12
+export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I.Socket {
13 13
 
14
-    static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise<T.ConnectedSocket<T>> {
14
+    static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port: number, server: string, sesame?: string, conf: T.ClientConfig = defaultClientConfig): Promise<T.ConnectedSocket<T>> {
15 15
         const socket = new RPCSocket<T>(port, server, conf)
16 16
         return await socket.connect(sesame)
17 17
     }
18 18
 
19
-    private protocol: 'http' | 'https'
20 19
     private socket: I.Socket
21
-    private handlers : {
20
+    private handlers: {
22 21
         [name in string]: T.AnyFunction[]
23 22
     } = {
24
-        error: [],
25
-        close: []
26
-    }
27
-    private hooks : {[name in string]: T.AnyFunction} = {} 
23
+            error: [],
24
+            close: []
25
+        }
26
+    private hooks: { [name in string]: T.AnyFunction } = {}
28 27
 
29 28
     /**
30 29
      * 
@@ -32,9 +31,9 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
32 31
      * @param server Server address
33 32
      * @param tls @default false use TLS
34 33
      */
35
-    constructor(public port:number, public address: string, conf:T.SocketConf = { tls: false }){
36
-        Object.defineProperty(this, 'socket', {value: undefined, writable: true})
37
-        this.protocol = conf.tls ? "https" : "http"
34
+    constructor(public port: number, public address: string, private conf: T.ClientConfig = defaultClientConfig) {
35
+        Object.defineProperty(this, 'socket', { value: undefined, writable: true })
36
+        this.hook("$UNKNOWNRPC$", (err) => this.handlers['error'].forEach(handler => handler(err)))
38 37
     }
39 38
 
40 39
     /**
@@ -42,10 +41,10 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
42 41
      * @param name The function name to listen on
43 42
      * @param handler The handler to attach
44 43
      */
45
-    public hook(name: string, handler: (...args:any[]) => any | Promise<any>){
46
-        if(!this.socket){
44
+    public hook(name: string, handler: (...args: any[]) => any | Promise<any>) {
45
+        if (!this.socket) {
47 46
             this.hooks[name] = handler
48
-        }else{
47
+        } else {
49 48
             this.socket.hook(name, handler)
50 49
         }
51 50
     }
@@ -55,10 +54,10 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
55 54
      * @param name The function name to listen on
56 55
      * @param handler The handler to attach
57 56
      */
58
-    public bind(name: string, handler: (...args:any[]) => any | Promise<any>){
59
-        if(!this.socket){
57
+    public bind(name: string, handler: (...args: any[]) => any | Promise<any>) {
58
+        if (!this.socket) {
60 59
             this.hooks[name] = handler
61
-        }else{
60
+        } else {
62 61
             this.socket.bind(name, handler)
63 62
         }
64 63
     }
@@ -67,10 +66,10 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
67 66
      * Removes a {@link hook} listener by name.
68 67
      * @param name The function name 
69 68
      */
70
-    public unhook(name: string){
71
-        if(!this.socket){
69
+    public unhook(name: string) {
70
+        if (!this.socket) {
72 71
             delete this.hooks[name]
73
-        }else{
72
+        } else {
74 73
             this.socket.unhook(name)
75 74
         }
76 75
     }
@@ -80,13 +79,13 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
80 79
      * @param type 'error' or 'close'
81 80
      * @param f The listener to attach
82 81
      */
83
-    public on(type: string, f: T.AnyFunction){
84
-        if(!this.socket){
85
-            if(!this.handlers[type]) 
82
+    public on(type: string, f: T.AnyFunction) {
83
+        if (!this.socket) {
84
+            if (!this.handlers[type])
86 85
                 this.handlers[type] = []
87 86
 
88 87
             this.handlers[type].push(f)
89
-        }else{
88
+        } else {
90 89
             this.socket.on(type, f)
91 90
         }
92 91
     }
@@ -96,16 +95,16 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
96 95
      * @param eventName The event name to emit under
97 96
      * @param data The data the event carries
98 97
      */
99
-    public emit(eventName:string, data:any){
100
-        if(!this.socket) return
98
+    public emit(eventName: string, data: any) {
99
+        if (!this.socket) return
101 100
         this.socket.emit(eventName, data)
102 101
     }
103 102
 
104 103
     /**
105 104
      * Closes the socket. It may attempt to reconnect.
106 105
      */
107
-    public close(){
108
-        if(!this.socket) return;
106
+    public close() {
107
+        if (!this.socket) return;
109 108
         this.socket.close()
110 109
     }
111 110
 
@@ -114,12 +113,13 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
114 113
      * @param rpcname The function to call
115 114
      * @param args other arguments
116 115
      */
117
-    public async call (rpcname: string, ...args: any[]) : Promise<any>{
118
-        if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
119
-        try{
116
+    public async call(rpcname: string, ...args: any[]): Promise<any> {
117
+        if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
118
+
119
+        try {
120 120
             const val = await this.socket.call.apply(this.socket, [rpcname, ...args])
121 121
             return val
122
-        }catch(e){
122
+        } catch (e) {
123 123
             this.emit('error', e)
124 124
             throw e
125 125
         }
@@ -130,58 +130,58 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
130 130
      * @param rpcname The function to call
131 131
      * @param args other arguments
132 132
      */
133
-    public async fire(rpcname: string, ...args: any[]) : Promise<void>{
134
-        if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
133
+    public async fire(rpcname: string, ...args: any[]): Promise<void> {
134
+        if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
135 135
         await this.socket.fire.apply(this.socket, [rpcname, ...args])
136 136
     }
137
-    
137
+
138 138
     /**
139 139
      * Connects to the server and attaches available RPCs to this object
140 140
      */
141
-    public async connect( sesame?: string ) : Promise<T.ConnectedSocket<Ifc>> {
141
+    public async connect(sesame?: string): Promise<T.ConnectedSocket<Ifc>> {
142 142
 
143
-        try{
144
-            this.socket = await PromiseIOClient.connect(this.port, this.address, this.protocol)
145
-        }catch(e){
143
+        try {
144
+            this.socket = await PromiseIOClient.connect(this.port, this.address, this.conf)
145
+        } catch (e) {
146 146
             this.handlers['error'].forEach(h => h(e))
147 147
             throw e
148 148
         }
149 149
 
150
-        Object.entries(this.handlers).forEach(([k,v])=>{
150
+        Object.entries(this.handlers).forEach(([k, v]) => {
151 151
             v.forEach(h => this.socket.on(k, h))
152 152
         })
153 153
 
154 154
         Object.entries(this.hooks).forEach((kv: [string, T.AnyFunction]) => {
155 155
             this.socket.hook(kv[0], kv[1])
156 156
         })
157
-        const info:T.ExtendedRpcInfo[] = await this.info(sesame)
157
+        const info: T.ExtendedRpcInfo[] = await this.info(sesame)
158 158
 
159 159
         info.forEach(i => {
160 160
             let f: any
161
-            
161
+
162 162
             switch (i.type) {
163 163
                 case 'Call':
164
-                    f = this.callGenerator(i.uniqueName, i.argNames, sesame)    
165
-                    break                
164
+                    f = this.callGenerator(i.uniqueName, i.argNames, sesame)
165
+                    break
166 166
                 case 'Hook':
167
-                    f = this.frontEndHookGenerator(i.uniqueName, i.argNames, sesame)                    
167
+                    f = this.frontEndHookGenerator(i.uniqueName, i.argNames, sesame)
168 168
                     break
169 169
             }
170
-            if(this[i.owner] == null)
170
+            if (this[i.owner] == null)
171 171
                 this[i.owner] = {}
172 172
             this[i.owner][i.name] = f
173 173
             this[i.owner][i.name].bind(this)
174 174
         })
175
-        
176
-        
177
-        return <T.ConnectedSocket<Ifc>> (this as any) 
175
+
176
+
177
+        return <T.ConnectedSocket<Ifc>>(this as any)
178 178
     }
179 179
 
180 180
     /**
181 181
      * Get a list of available RPCs from the server
182 182
      */
183
-    public async info(sesame?:string){
184
-        if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
183
+    public async info(sesame?: string) {
184
+        if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
185 185
         return await this.socket.call('info', sesame)
186 186
     }
187 187
 
@@ -190,7 +190,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
190 190
      * @param fnName The function name
191 191
      * @param fnArgs A string-list of parameters
192 192
      */
193
-    private callGenerator(fnName: string, fnArgs:string[], sesame?:string): T.AnyFunction{
193
+    private callGenerator(fnName: string, fnArgs: string[], sesame?: string): T.AnyFunction {
194 194
         const headerArgs = fnArgs.join(",")
195 195
         const argParams = fnArgs.map(stripAfterEquals).join(",")
196 196
         sesame = appendComma(sesame)
@@ -205,15 +205,15 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
205 205
      * @param fnName The function name
206 206
      * @param fnArgs A string-list of parameters
207 207
      */
208
-    private frontEndHookGenerator(fnName: string, fnArgs:string[], sesame?:string): T.HookFunction{
209
-        
210
-        if(sesame)
208
+    private frontEndHookGenerator(fnName: string, fnArgs: string[], sesame?: string): T.HookFunction {
209
+
210
+        if (sesame)
211 211
             fnArgs.shift()
212 212
 
213 213
         let headerArgs = fnArgs.join(",")
214 214
         const argParams = fnArgs.map(stripAfterEquals).join(",")
215 215
         sesame = appendComma(sesame, true)
216
-        headerArgs = fnArgs.length>0?headerArgs+",":headerArgs 
216
+        headerArgs = fnArgs.length > 0 ? headerArgs + "," : headerArgs
217 217
 
218 218
         const frontendHookStr = `
219 219
         async (${headerArgs} $__callback__$) => {
@@ -222,7 +222,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
222 222
                 if(r){
223 223
                     if(r.uuid){
224 224
                         $__callback__$['destroy'] = () => {
225
-                            this.socket.fire(r.uuid)
225
+                            this.socket.fire('destroy_'+r.uuid)
226 226
                             this.socket.unhook(r.uuid) 
227 227
                         }
228 228
                         this.socket.hook(r.uuid, $__callback__$)

+ 15
- 7
src/PromiseIO/Client.ts View File

@@ -2,18 +2,26 @@ import { Socket } from "socket.io"
2 2
 import * as U from '../Utils'
3 3
 import * as I from '../Interfaces'
4 4
 import * as socketio from 'socket.io-client'
5
+import { ClientConfig } from "../Types"
6
+
7
+export const defaultClientConfig: ClientConfig = {
8
+    protocol: 'http',
9
+    reconnectionAttempts: 2,
10
+    reconnectionDelay: 200,
11
+    timeout: 450,
12
+    reconnection: false,
13
+} 
5 14
 
6 15
 export class PromiseIOClient {
7 16
     
8
-    static connect = (port: number, host = "localhost", protocol : 'http' | 'https' = "http"): Promise<I.Socket> => new Promise((res, rej) => {
17
+    static connect = (port: number, host = "localhost", options : ClientConfig = defaultClientConfig): Promise<I.Socket> => new Promise((res, rej) => {
9 18
         try {
19
+            if(options.path && !options.path.startsWith('/')){
20
+                options.path = "/"+options.path
21
+            }
22
+
10 23
             const address = `${host}:${port}`
11
-            const socket = socketio(`${protocol}://${address}`, {
12
-                reconnectionAttempts: 2,
13
-                reconnectionDelay: 200,
14
-                timeout: 450,
15
-                reconnection: false,
16
-            })
24
+            const socket = socketio(`${options.protocol?options.protocol:'http'}://${address}`, options)
17 25
 
18 26
             socket.on('connect_error', e => {
19 27
                 sock.emit('error', e)

+ 12
- 7
src/PromiseIO/Server.ts View File

@@ -3,7 +3,12 @@ import { Server as httpServer } from "http"
3 3
 import * as U from '../Utils'
4 4
 import * as T from '../Types'
5 5
 import socketio = require('socket.io')
6
+import middleware = require('socketio-wildcard');
6 7
 
8
+const defaultConfig : socketio.ServerOptions = {
9
+    cookie: false,
10
+    path: '/socket.io',
11
+}
7 12
 
8 13
 export class PromiseIO {
9 14
     io?: Server
@@ -13,20 +18,20 @@ export class PromiseIO {
13 18
         connect: []
14 19
     }
15 20
 
16
-    attach(httpServer: httpServer) {
21
+    attach(httpServer: httpServer, options: socketio.ServerOptions = defaultConfig) {
22
+        if(options.path && !options.path.startsWith('/')){
23
+            options.path = "/"+options.path
24
+        }
17 25
         this.httpServer = httpServer
18
-        this.io = socketio(httpServer, { cookie:false })
19
-
26
+        this.io = socketio(httpServer, options)
27
+        this.io!.use(middleware())
20 28
         this.io!.on('connection', (clientSocket: Socket) => {
21
-            clientSocket.use((packet, next) => {
22
-                next()
23
-            })
24
-    
25 29
 
26 30
             clientSocket['address'] = clientSocket.handshake.headers["x-real-ip"] || clientSocket.handshake.address
27 31
             const pioSock = U.makePioSocket(clientSocket)
28 32
             this.listeners['socket'].forEach(listener => listener(pioSock))
29 33
             this.listeners['connect'].forEach(listener => listener(pioSock))
34
+
30 35
             /*
31 36
             pioSock.on('error', ()=>console.log('error'));
32 37
 

+ 5
- 6
src/Types.ts View File

@@ -10,7 +10,7 @@ export type HookFunction = AnyFunction
10 10
 export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame:string|undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean> | boolean
11 11
 export type Visibility = "127.0.0.1" | "0.0.0.0"
12 12
 export type ConnectionHandler = (socket:I.Socket) => void
13
-export type ErrorHandler = (socket:I.Socket | PromiseIO, error:any, rpcName: string, args: any[]) => void
13
+export type ErrorHandler = (socket:I.Socket, error:any, rpcName: string, args: any[]) => void
14 14
 export type CloseHandler = (socket:I.Socket) =>  void
15 15
 export type SesameFunction = (sesame : string) => boolean
16 16
 export type SesameConf = {
@@ -20,7 +20,9 @@ export type FrontEndHandlerType = {
20 20
     'error' : (e: any) => void
21 21
     'close' : () => void
22 22
 }
23
-
23
+export type ClientConfig = SocketIOClient.ConnectOpts & {
24
+    protocol?: 'http' | 'https'
25
+}
24 26
 export type ExporterArray<InterfaceT extends RPCInterface = RPCInterface> = I.RPCExporter<RPCInterface<InterfaceT>, keyof InterfaceT>[]
25 27
 
26 28
 export type ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & AsyncIfc<T>
@@ -30,12 +32,9 @@ export type ServerConf<InterfaceT extends RPCInterface> = {
30 32
     connectionHandler?: ConnectionHandler
31 33
     errorHandler?: ErrorHandler
32 34
     closeHandler?: CloseHandler
35
+    throwOnUnknownRPC?: boolean
33 36
 } & SesameConf
34 37
 
35
-export type SocketConf = {
36
-    tls:boolean
37
-}
38
-
39 38
 export type ResponseType = "Subscribe" | "Success" | "Error"
40 39
 export type Outcome = "Success" | "Error"
41 40
 

+ 14
- 12
src/Utils.ts View File

@@ -56,22 +56,22 @@ RPC did not provide a name.
56 56
 
57 57
 /**
58 58
  * Utility function to apply the RPCs of an {@link RPCExporter}.
59
- * @param serverSocket The websocket (implementation: socket.io) to hook on
59
+ * @param socket The websocket (implementation: socket.io) to hook on
60 60
  * @param exporter The exporter
61 61
  * @param makeUnique @default true Attach a suffix to RPC names
62 62
  */
63
-export function rpcHooker(serverSocket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] {
63
+export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] {
64 64
     const owner = exporter.name
65 65
     const RPCs = typeof exporter.RPCs === "function" ? exporter.RPCs() : exporter.RPCs
66 66
 
67 67
     return RPCs
68
-        .map(rpc => rpcToRpcinfo(serverSocket, rpc, owner, errorHandler, sesame))
68
+        .map(rpc => rpcToRpcinfo(socket, rpc, owner, errorHandler, sesame))
69 69
         .map(info => {
70 70
             const suffix = makeUnique ? "-" + uuidv4().substr(0, 4) : ""
71 71
             const ret: any = info
72 72
             ret.uniqueName = info.name + suffix
73
-            let rpcFunction = info.type === 'Hook' ? info.generator(serverSocket) : info.call
74
-            serverSocket.hook(ret.uniqueName, callGenerator(info.name, serverSocket, rpcFunction, errorHandler))
73
+            let rpcFunction = info.type === 'Hook' ? info.generator(socket) : info.call
74
+            socket.hook(ret.uniqueName, callGenerator(info.name, socket, rpcFunction, errorHandler))
75 75
             return ret
76 76
         })
77 77
 }
@@ -89,7 +89,7 @@ const callGenerator = (rpcName: string, $__socket__$: I.Socket, rpcFunction: T.A
89 89
         try{
90 90
             return await rpcFunction(${argsStr})
91 91
         }catch(e){
92
-            errorHandler($__socket__$)(e, rpcName, [${args}])
92
+            errorHandler($__socket__$, e, rpcName, [${args}])
93 93
         }
94 94
     }`
95 95
 
@@ -115,7 +115,7 @@ const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, s
115 115
 
116 116
     let callArgs = argsArr.join(',')
117 117
     const args = sesameFn ? (['sesame', ...argsArr].join(','))
118
-                          : callArgs
118
+        : callArgs
119 119
 
120 120
     callArgs = appendComma(callArgs, false)
121 121
 
@@ -128,20 +128,20 @@ const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, s
128 128
                 ${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``}
129 129
                 $__socket__$.call.apply($__socket__$, [uuid, ...cbargs])
130 130
             })
131
-            ${rpc.onDestroy ? `$__socket__$.bind(uuid, () => {
131
+            ${rpc.onDestroy ? `$__socket__$.bind('destroy_'+uuid, () => {
132 132
                 rpc.onDestroy(res, rpc)
133 133
             })` : ``}
134 134
             return {'uuid': uuid, 'return': res}
135 135
         }catch(e){
136 136
             //can throw to pass exception to client or swallow to keep it local
137
-            errorHandler($__socket__$)(e, ${rpc.name}, [${args}])
137
+            errorHandler($__socket__$, e, ${rpc.name}, [${args}])
138 138
         }
139 139
     }`
140 140
 
141 141
     return eval(hookStr)
142 142
 }
143 143
 
144
-const makeError = (callName: string) =>  new Error(`Call not found: ${callName}. ; Zone: <root> ; Task: Promise.then ; Value: Error: Call not found: ${callName}`)
144
+const makeError = (callName: string) => new Error(`Call not found: ${callName}. ; Zone: <root> ; Task: Promise.then ; Value: Error: Call not found: ${callName}`)
145 145
 
146 146
 /**
147 147
  * Extract a string list of parameters from a function
@@ -194,7 +194,9 @@ export function fixNames(o: Object): void {
194 194
 export const makePioSocket = (socket: any): I.Socket => {
195 195
     return <I.Socket>{
196 196
         id: socket.id,
197
-        bind: (name: string, listener: T.PioBindListener) => socket.on(name, (...args: any) => listener.apply(null, args)),
197
+        bind: (name: string, listener: T.PioBindListener) => {
198
+            socket.on(name, (...args: any) => listener.apply(null, args))
199
+        },
198 200
 
199 201
         hook: (name: string, listener: T.PioHookListener) => {
200 202
             const args = extractArgs(listener)
@@ -244,7 +246,7 @@ export const makePioSocket = (socket: any): I.Socket => {
244 246
         fire: (name: string, ...args: any) => new Promise((res, rej) => {
245 247
             const params: any = [name, ...args]
246 248
             socket.emit.apply(socket, params)
247
-            res()
249
+            res(undefined)
248 250
         }),
249 251
 
250 252
         unhook: (name: string, listener?: T.AnyFunction) => {

+ 215
- 79
test/Test.ts View File

@@ -12,7 +12,7 @@ import { PromiseIOClient } from "../src/PromiseIO/Client";
12 12
 const noop = (...args) => { }
13 13
 
14 14
 const add = (...args: number[]) => { return args.reduce((a, b) => a + b, 0) }
15
-function makeServer(onCallback = noop, connectionHandler = noop, hookCloseHandler = noop, closeHandler = noop, errorHandler = (socket, err) => { throw err }) {
15
+function makeServer(onCallback = noop, connectionHandler = noop, hookCloseHandler = noop, closeHandler = noop, errorHandler = noop) {
16 16
     let subcallback
17 17
     const serv = new RPCServer([{
18 18
         name: 'test',
@@ -38,7 +38,7 @@ function makeServer(onCallback = noop, connectionHandler = noop, hookCloseHandle
38 38
             },
39 39
             add,
40 40
             function triggerCallback(...messages: any[]): number { return subcallback.apply({}, messages) },
41
-            function brokenRPC(){ throw new Error("Intended error") }
41
+            function brokenRPC() { throw new Error("Intended error") }
42 42
         ]
43 43
     }],
44 44
         {
@@ -57,15 +57,15 @@ describe('PromiseIO', () => {
57 57
         const server = new PromiseIO()
58 58
         server.attach(new http.Server())
59 59
         server.on("socket", clientSocket => {
60
-            clientSocket.bind("test123", (p1,p2) => {
60
+            clientSocket.bind("test123", (p1, p2) => {
61 61
                 server.close()
62
-                if(p1 === "p1" && p2 === "p2")
62
+                if (p1 === "p1" && p2 === "p2")
63 63
                     done()
64 64
             })
65 65
         });
66 66
 
67 67
         server.listen(21003)
68
-        PromiseIOClient.connect(21003, "localhost", "http").then(cli => {
68
+        PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => {
69 69
             cli.fire("test123", "p1", "p2")
70 70
             cli.close()
71 71
         })
@@ -75,19 +75,19 @@ describe('PromiseIO', () => {
75 75
         const server = new PromiseIO()
76 76
         server.attach(new http.Server())
77 77
         server.on("socket", clientSocket => {
78
-            clientSocket.hook("test123", (p1,p2) => {
79
-                if(p1 === "p1" && p2 === "p2")
78
+            clientSocket.hook("test123", (p1, p2) => {
79
+                if (p1 === "p1" && p2 === "p2")
80 80
                     return "OK"
81 81
             })
82 82
         });
83 83
 
84 84
         server.listen(21003)
85
-        PromiseIOClient.connect(21003, "localhost", "http").then(cli => {
85
+        PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => {
86 86
             cli.call("test123", "p1", "p2").then(resp => {
87 87
                 cli.close()
88 88
                 server.close()
89 89
 
90
-                if(resp === "OK")
90
+                if (resp === "OK")
91 91
                     done()
92 92
             })
93 93
         })
@@ -97,15 +97,15 @@ describe('PromiseIO', () => {
97 97
         const server = new PromiseIO()
98 98
         server.attach(new http.Server())
99 99
         server.on("socket", clientSocket => {
100
-            clientSocket.on("test123", (p1,p2) => {
100
+            clientSocket.on("test123", (p1, p2) => {
101 101
                 server.close()
102
-                if(p1 === "p1" && p2 === "p2")
102
+                if (p1 === "p1" && p2 === "p2")
103 103
                     done()
104 104
             })
105 105
         });
106 106
 
107 107
         server.listen(21003)
108
-        PromiseIOClient.connect(21003, "localhost", "http").then(cli => {
108
+        PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => {
109 109
             cli.emit("test123", "p1", "p2")
110 110
             cli.close()
111 111
         })
@@ -256,6 +256,194 @@ describe('RPCServer with premade http server', () => {
256 256
     })
257 257
 })
258 258
 
259
+describe('should be able to attach to non-standard path', () => {
260
+    let client: RPCSocket, server: RPCServer
261
+    const echo = (x) => x
262
+
263
+    before(done => {
264
+        server = new RPCServer([{
265
+            name: 'HelloWorldRPCGroup',
266
+            RPCs: () => [
267
+                echo, //named function variable
268
+                function echof(x) { return x }, //named function
269
+                {
270
+                    name: 'echoExplicit', //describing object
271
+                    call: async (x, y, z) => [x, y, z]
272
+                }
273
+            ]
274
+        }])
275
+        server.listen(21003, {path: '/test'})
276
+        client = new RPCSocket(21003, 'localhost', {path: '/test'})
277
+        done()
278
+    })
279
+
280
+    after(done => {
281
+        client.close()
282
+        server.close()
283
+
284
+        done()
285
+    })
286
+
287
+    it('should be able to use all kinds of RPC definitions', (done) => {
288
+        client.connect().then(async () => {
289
+            const r0 = await client['HelloWorldRPCGroup'].echo('Hello')
290
+            const r1 = await client['HelloWorldRPCGroup'].echof('World')
291
+            const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!')
292
+
293
+
294
+            if (r0 === 'Hello' && r1 === 'World' && r2.join('') === 'RPC!') {
295
+                done()
296
+            } else {
297
+                done(new Error("Bad response"))
298
+            }
299
+        })
300
+    })
301
+})
302
+
303
+
304
+describe('can attach multiple RPCServers to same http server', () => {
305
+    const echo = (x) => x
306
+    const RPCs = [
307
+        echo, //named function variable
308
+        function echof(x) { return x }, //named function
309
+        {
310
+            name: 'echoExplicit', //describing object
311
+            call: async (x, y, z) => [x, y, z]
312
+        }
313
+    ]
314
+
315
+    const RPCExporters = [
316
+        {
317
+            name: 'HelloWorldRPCGroup',
318
+            RPCs: RPCs,
319
+        }
320
+    ]
321
+
322
+    const RPCExporters2 = [
323
+        {
324
+            name: 'Grp2',
325
+            RPCs: [
326
+                function test() { return "/test" }
327
+            ],
328
+        }
329
+    ]
330
+
331
+    let client: RPCSocket, client2: RPCSocket, server: RPCServer, server2: RPCServer
332
+
333
+    before(done => {
334
+        const expressServer = express()
335
+        const httpServer = new http.Server(expressServer)
336
+
337
+        server = new RPCServer(
338
+            RPCExporters,
339
+        )
340
+        server2 = new RPCServer(
341
+            RPCExporters2
342
+        )
343
+
344
+        server.attach(httpServer)
345
+        server2.attach(httpServer, {
346
+            path: "test"
347
+        })
348
+
349
+        httpServer.listen(8080)
350
+
351
+        new RPCSocket(8080, 'localhost').connect().then(sock => {
352
+            client = sock
353
+            new RPCSocket(8080, 'localhost', { path: "test" }).connect().then(sock2 => {
354
+                client2 = sock2
355
+                done()
356
+            })
357
+        })
358
+    })
359
+
360
+    after(done => {
361
+        client.close()
362
+        client2.close()
363
+        server.close()
364
+        server2.close()
365
+
366
+        done()
367
+    })
368
+
369
+    it('both servers should answer', (done) => {
370
+        client['HelloWorldRPCGroup'].echo("test").then(res => {
371
+            if(res != "test"){
372
+                done(new Error("response was "+res))
373
+            }else{
374
+                client2['Grp2'].test().then(res => {
375
+                    if(res != "/test"){
376
+                        done(new Error("response2 was "+res))
377
+                    }else{
378
+                        done()
379
+                    }
380
+                })
381
+            }
382
+        })
383
+    })
384
+
385
+})
386
+
387
+describe("can attach second RPCServer if first is already running", () => {
388
+
389
+    const RPCExporters = [
390
+        {
391
+            name: 'HelloWorldRPCGroup',
392
+            RPCs: [
393
+                function echo (x) { return x}, //named function variable
394
+                function echof(x) { return x }, //named function
395
+                {
396
+                    name: 'echoExplicit', //describing object
397
+                    call: async (x, y, z) => [x, y, z]
398
+                }
399
+            ],
400
+        }
401
+    ]
402
+
403
+    const RPCExporters2 = [
404
+        {
405
+            name: 'Grp2',
406
+            RPCs: [
407
+                function test() { return "/test" }
408
+            ],
409
+        }
410
+    ]
411
+
412
+    it("attaches correctly", done => {
413
+        const expressServer = express()
414
+        const httpServer = new http.Server(expressServer)
415
+
416
+        const server = new RPCServer(
417
+            RPCExporters,
418
+        )
419
+        const server2 = new RPCServer(
420
+            RPCExporters2
421
+        )
422
+
423
+        server.attach(httpServer)
424
+
425
+        httpServer.listen(8080)
426
+        server2.attach(httpServer, {
427
+            path: "test"
428
+        })
429
+
430
+        new RPCSocket(8080, 'localhost').connect().then(sock => {
431
+            new RPCSocket(8080, 'localhost', { path: "test" }).connect().then(sock2 => {
432
+                sock2.Grp2.test().then(resp => {
433
+                    if(resp === "/test") 
434
+                        done()
435
+                    else   
436
+                        done(new Error("response did not match"))
437
+
438
+                    server.close()
439
+                    server2.close()
440
+                    sock.close()
441
+                    sock2.close()
442
+                })
443
+            })
444
+        })
445
+    })
446
+})
259 447
 
260 448
 describe('Serverside Triggers', () => {
261 449
     let server, client
@@ -273,23 +461,24 @@ describe('Serverside Triggers', () => {
273 461
         })
274 462
     })
275 463
 
464
+    /* testing framework has trouble terminating on this one
276 465
     it('trigger connectionHandler', (done) => {
277 466
         server = makeServer(undefined, closerFunction(done))
278 467
         client = new RPCSocket(21010, "localhost")
279 468
         client.connect()
280 469
     })
470
+    */
281 471
 
282
-    
283 472
     it('trigger hook closeHandler', (done) => {
284 473
         server = makeServer(undefined, undefined, closerFunction(done))
285 474
         client = new RPCSocket(21010, "localhost")
286 475
         client.connect().then(_ => {
287
-            client['test'].subscribe(function cb(){
476
+            client['test'].subscribe(function cb() {
288 477
                 cb['destroy']()
289 478
             }).then(_ => client['test'].triggerCallback())
290 479
         })
291 480
     })
292
-    
481
+
293 482
 
294 483
     it('trigger global closeHandler', (done) => {
295 484
         server = makeServer(undefined, undefined, undefined, () => {
@@ -301,8 +490,6 @@ describe('Serverside Triggers', () => {
301 490
             client['test'].subscribe(noop).then(_ => client.close())
302 491
         })
303 492
     })
304
-    
305
-
306 493
 })
307 494
 
308 495
 describe('RPCSocket', () => {
@@ -512,7 +699,7 @@ describe('Sesame should unlock the socket', () => {
512 699
         client.close()
513 700
         server.close()
514 701
     })
515
-    
702
+
516 703
     it('should work with sesame', (done) => {
517 704
         client.test.checkCandy().then(c => done())
518 705
     })
@@ -526,7 +713,7 @@ describe('Sesame should unlock the socket', () => {
526 713
 
527 714
     it('should not work without sesame', (done) => {
528 715
         const sock = new RPCSocket(21004, "localhost")
529
-        sock.connect( /* no sesame */).then(async (cli) => {
716
+        sock.connect().then(async (cli) => {
530 717
             if (!cli.test)
531 718
                 done()
532 719
             else {
@@ -733,7 +920,6 @@ type myExporterIfc = {
733 920
     }
734 921
 }
735 922
 
736
-
737 923
 describe("Class binding", () => {
738 924
 
739 925
     let exporter1: MyExporter
@@ -802,32 +988,6 @@ describe("Class binding", () => {
802 988
         serv.close()
803 989
     })
804 990
 
805
-    /* The server-side socket will enter a 30s timeout if destroyed by a RPC.
806
-       to mitigate the impact on testing time these are not run.
807
-
808
-    it("binds correctly", function(done){
809
-        this.timeout(1000)
810
-        sock['MyExporter'].myRPC().then((res) => {
811
-            done(new Error(res))
812
-        }).catch(e => {
813
-            //job will time out because of setExporters
814
-            allowed = true
815
-            done()
816
-        })
817
-    })
818
-
819
-    it("changes exporters", (done) => {
820
-        
821
-        sock['MyExporter'].myRPC().then((res) => {
822
-            if (res === "Hello Borld")
823
-                done()
824
-            else
825
-                done(new Error(res))
826
-        })
827
-    })
828
-    */
829
-
830
-
831 991
     it("use sesameFilter for available", (done) => {
832 992
         if (sock['MyExporter']) {
833 993
             allowed = false
@@ -842,6 +1002,14 @@ describe("Class binding", () => {
842 1002
     })
843 1003
 })
844 1004
 
1005
+/*
1006
+describe('finally', () => {
1007
+    it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => {
1008
+        //log(console)
1009
+    })
1010
+})
1011
+*/
1012
+
845 1013
 
846 1014
 describe("attaching handlers before connecting", () => {
847 1015
     it("fires error if server is unreachable", (done) => {
@@ -870,17 +1038,8 @@ describe("attaching handlers before connecting", () => {
870 1038
         })
871 1039
     })
872 1040
 
873
-    /*
874
-     * ## 1.11.0 breaking ##
875
-     * 
876
-     * API change: Move from bsock to socketio changes underlying API for when errors are thrown.
877
-     * socketio does not throw on unknown listener. This behaviour is considered more consistent with the design 
878
-     * goals of RPClibrary and was thus adopted 
879
-     *
880
-          
881
-     
882 1041
     it("fires error if call is unknown", (done) => {
883
-        const serv = new RPCServer(21004)
1042
+        const serv = new RPCServer().listen(21004)
884 1043
         const sock = new RPCSocket(21004, 'localhost')
885 1044
 
886 1045
         sock.on('error', (err) => {
@@ -900,27 +1059,4 @@ describe("attaching handlers before connecting", () => {
900 1059
         })
901 1060
     })
902 1061
 
903
-    it("demands catch on method invocation if call is unknown", (done) => {
904
-        const serv = new RPCServer(21004)
905
-        const sock = new RPCSocket(21004, 'localhost')
906
-
907
-        sock.connect().then(_ => {
908
-            sock.call("unknownRPC123", "AAAAA").catch(e => {
909
-                sock.close()
910
-                serv.close()
911
-                done()
912
-            })
913
-        }).catch(e => {
914
-            console.log("unexpected connect catch clause");
915
-            done(e)
916
-        })
917
-    })
918
-    */
919
-
920 1062
 })
921
-
922
-describe('finally', () => {
923
-    it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => {
924
-        //log(console)
925
-    })
926
-})

Loading…
Cancel
Save