Browse Source

Better ErrorHandler and more tests

master
peter 4 years ago
parent
commit
c4e280077b
6 changed files with 156 additions and 78 deletions
  1. 1
    1
      package.json
  2. 4
    7
      src/Backend.ts
  3. 2
    7
      src/Frontend.ts
  4. 2
    1
      src/Types.ts
  5. 64
    30
      src/Utils.ts
  6. 83
    32
      test/Test.ts

+ 1
- 1
package.json View File

1
 {
1
 {
2
   "name": "rpclibrary",
2
   "name": "rpclibrary",
3
-  "version": "1.5.3",
3
+  "version": "1.6.0",
4
   "description": "rpclibrary is a websocket on steroids!",
4
   "description": "rpclibrary is a websocket on steroids!",
5
   "main": "./js/Index.js",
5
   "main": "./js/Index.js",
6
   "repository": {
6
   "repository": {

+ 4
- 7
src/Backend.ts View File

42
 
42
 
43
         this.errorHandler = (socket:I.Socket) => (error:any) => { 
43
         this.errorHandler = (socket:I.Socket) => (error:any) => { 
44
             if(conf.errorHandler) conf.errorHandler(socket, error)
44
             if(conf.errorHandler) conf.errorHandler(socket, error)
45
-            socket.destroy(); 
46
-            console.error("Caught websocket error", String(error)) 
45
+            else throw error
47
         }
46
         }
48
         
47
         
49
         this.closeHandler = (socket:I.Socket) => { 
48
         this.closeHandler = (socket:I.Socket) => { 
50
-            if(!conf.closeHandler) console.log("Connection on port "+socket.port+" closing") 
51
-            else conf.closeHandler(socket)
49
+            if(conf.closeHandler) conf.closeHandler(socket)
52
         }
50
         }
53
         
51
         
54
         this.connectionHandler = (socket:I.Socket) => { 
52
         this.connectionHandler = (socket:I.Socket) => { 
55
-            if(!conf.connectionHandler) console.log("New connection on port "+socket.port)
56
-            else conf.connectionHandler(socket)
53
+            if(conf.connectionHandler) conf.connectionHandler(socket)
57
         }
54
         }
58
 
55
 
59
         let badRPC 
56
         let badRPC 
86
     protected initRPCs(socket:I.Socket){
83
     protected initRPCs(socket:I.Socket){
87
         socket.hook('info', () => rpcInfos)
84
         socket.hook('info', () => rpcInfos)
88
         const rpcInfos:T.ExtendedRpcInfo[] = [
85
         const rpcInfos:T.ExtendedRpcInfo[] = [
89
-            ...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter, this.sesame))
86
+            ...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter, this.errorHandler, this.sesame))
90
         ]
87
         ]
91
     }
88
     }
92
 
89
 

+ 2
- 7
src/Frontend.ts View File

4
 
4
 
5
 import * as T from './Types'; 
5
 import * as T from './Types'; 
6
 import * as I from './Interfaces';
6
 import * as I from './Interfaces';
7
+import { stripAfterEquals } from './Utils';
8
+
7
 
9
 
8
-/**
9
- * Utility function to strip parameters like "a = 3" of their defaults
10
- * @param str The parameter to modify
11
- */
12
-function stripAfterEquals(str:string):string{
13
-    return str.split("=")[0]
14
-}
15
 
10
 
16
 /**
11
 /**
17
  * A websocket-on-steroids with built-in RPC capabilities
12
  * A websocket-on-steroids with built-in RPC capabilities

+ 2
- 1
src/Types.ts View File

8
 export type ErrorHandler = (socket:I.Socket, error:any) => void
8
 export type ErrorHandler = (socket:I.Socket, error:any) => void
9
 export type CloseHandler = (socket:I.Socket) =>  void
9
 export type CloseHandler = (socket:I.Socket) =>  void
10
 export type SesameFunction = (sesame : string) => boolean
10
 export type SesameFunction = (sesame : string) => boolean
11
-
11
+export type ExceptionHandling = 'local' | 'remote'
12
 export type SesameConf = {
12
 export type SesameConf = {
13
     sesame?: string | SesameFunction
13
     sesame?: string | SesameFunction
14
 }
14
 }
17
     errorHandler?: ErrorHandler
17
     errorHandler?: ErrorHandler
18
     closeHandler?: CloseHandler
18
     closeHandler?: CloseHandler
19
     visibility?: Visibility
19
     visibility?: Visibility
20
+    exceptionHandling?: ExceptionHandling
20
 } & SesameConf
21
 } & SesameConf
21
 
22
 
22
 export type SocketConf = {
23
 export type SocketConf = {

+ 64
- 30
src/Utils.ts View File

10
  * @param owner The owning RPC group's name
10
  * @param owner The owning RPC group's name
11
  * @throws Error on RPC without name property
11
  * @throws Error on RPC without name property
12
  */
12
  */
13
-export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner: string, sesame?:T.SesameFunction):T.RpcInfo => {
14
-    
13
+export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner: string, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction):T.RpcInfo => {
15
     switch (typeof rpc){
14
     switch (typeof rpc){
16
         case  "object":
15
         case  "object":
17
             if(rpc['call']){
16
             if(rpc['call']){
20
                     argNames: extractArgs(rpc['call']),
19
                     argNames: extractArgs(rpc['call']),
21
                     type: "Call",
20
                     type: "Call",
22
                     name: rpc.name,
21
                     name: rpc.name,
23
-                    call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); throw new Error('Bad sesame')}:rpc['call'], // check & remove sesame 
22
+                    call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); throw makeError(rpc.name)}:rpc['call'], // check & remove sesame 
24
                 }
23
                 }
25
             }else{
24
             }else{
26
-                const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc, sesame)
25
+                const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc, errorHandler, sesame)
27
                 return {
26
                 return {
28
                     owner: owner,
27
                     owner: owner,
29
                     argNames: extractArgs(generator(undefined)),
28
                     argNames: extractArgs(generator(undefined)),
45
                 argNames: extractArgs(rpc),
44
                 argNames: extractArgs(rpc),
46
                 type: "Call",
45
                 type: "Call",
47
                 name: rpc.name,
46
                 name: rpc.name,
48
-                call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args)}:rpc, // check & remove sesame 
47
+                call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args); throw makeError(rpc.name)}:rpc, // check & remove sesame 
49
             }
48
             }
50
     }
49
     }
51
     throw new Error("Bad socketIORPC type "+ typeof rpc)
50
     throw new Error("Bad socketIORPC type "+ typeof rpc)
57
  * @param exporter The exporter
56
  * @param exporter The exporter
58
  * @param makeUnique @default true Attach a suffix to RPC names
57
  * @param makeUnique @default true Attach a suffix to RPC names
59
  */
58
  */
60
-export function rpcHooker<SubResT = {}>(socket: I.Socket, exporter:I.RPCExporter<any, any, SubResT>, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{
59
+export function rpcHooker<SubResT = {}>(socket: I.Socket, exporter:I.RPCExporter<any, any, SubResT>, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{
61
     const owner = exporter.name
60
     const owner = exporter.name
62
     const RPCs = [...exporter.exportRPCs()]
61
     const RPCs = [...exporter.exportRPCs()]
63
 
62
 
64
 
63
 
65
-    return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, sesame))
64
+    return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, errorHandler, sesame))
66
     .map(info => {
65
     .map(info => {
67
         const suffix = makeUnique?"-"+uuidv4().substr(0,4):""
66
         const suffix = makeUnique?"-"+uuidv4().substr(0,4):""
68
         const ret:any = info
67
         const ret:any = info
69
         ret.uniqueName = info.name+suffix
68
         ret.uniqueName = info.name+suffix
70
 
69
 
70
+        let rpcFunction
71
 
71
 
72
-        switch(info.type){
73
-            case "Hook":
74
-                socket.hook(ret.uniqueName, info.generator(socket))
75
-                break;
76
-            case "Call":
77
-                socket.hook(ret.uniqueName, info.call)
78
-                break;
79
-        }
80
-        socket.on('close', () => socket.unhook(info.name))
72
+        if(info.type === 'Hook')
73
+            rpcFunction = info.generator(socket)
74
+        else
75
+            rpcFunction = info.call
76
+
77
+        socket.hook(ret.uniqueName, callGenerator(socket, rpcFunction, errorHandler))
81
         return ret
78
         return ret
82
     })
79
     })
83
 }
80
 }
81
+
82
+/**
83
+ * Decorate an RPC with the error handler
84
+ * @param rpcFunction the function to decorate
85
+ */
86
+const callGenerator = (socket: I.Socket, rpcFunction : T.AnyFunction, errorHandler: T.ErrorHandler) : T.AnyFunction => {
87
+    const argsArr = extractArgs(rpcFunction)
88
+    const args = argsArr.join(',')
89
+    const argsStr = argsArr.map(stripAfterEquals).join(',')
90
+
91
+    return eval(`async (`+args+`) => {
92
+        try{
93
+            return await rpcFunction(`+argsStr+`)
94
+        }catch(e){
95
+            errorHandler(socket)(e)
96
+        }
97
+    }`)
98
+}
99
+
100
+/**
101
+ * Utility function to strip parameters like "a = 3" of their defaults
102
+ * @param str The parameter to modify
103
+ */
104
+export function stripAfterEquals(str:string):string{
105
+    return str.split("=")[0]
106
+}
107
+
84
 /**
108
 /**
85
  * Utility function to generate {@link HookFunction} from a RPC for backend
109
  * Utility function to generate {@link HookFunction} from a RPC for backend
86
  * @param rpc The RPC to transform
110
  * @param rpc The RPC to transform
87
  * @returns A {@link HookFunction}
111
  * @returns A {@link HookFunction}
88
  */
112
  */
89
-const hookGenerator = (rpc:T.HookRPC<any, any, any>, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => { 
113
+const hookGenerator = (rpc:T.HookRPC<any, any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => { 
90
     const argsArr = extractArgs(rpc.hook)
114
     const argsArr = extractArgs(rpc.hook)
91
     argsArr.pop() //remove 'callback' from the end
115
     argsArr.pop() //remove 'callback' from the end
92
     const argsStr = argsArr.join(',')
116
     const argsStr = argsArr.join(',')
93
 
117
 
94
     if(sesameFn){
118
     if(sesameFn){
95
         const args = ['sesame', ...argsArr].join(',')
119
         const args = ['sesame', ...argsArr].join(',')
96
-        const f =  eval(`(socket) => async (`+args+`) => {
97
-            if(!sesameFn(sesame)) return
98
-            const res = await rpc.hook(`+argsStr+(argsStr.length!==0?',':'')+` (...cbargs) => {
99
-                if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
100
-                socket.call.apply(socket, [res.uuid, ...cbargs])
101
-            })
102
-            return res
120
+        return eval(`(socket) => async (`+args+`) => {
121
+            try{
122
+                if(!sesameFn(sesame)) return
123
+                const res = await rpc.hook(`+argsStr+(argsStr.length!==0?',':'')+` (...cbargs) => {
124
+                    if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
125
+                    socket.call.apply(socket, [res.uuid, ...cbargs])
126
+                })
127
+                return res
128
+            }catch(e){
129
+                errorHandler(socket, e)
130
+            }
103
         }`)
131
         }`)
104
-        return f
105
     }
132
     }
106
     const args = argsArr.join(',')
133
     const args = argsArr.join(',')
107
     return eval(`(socket) => async (`+args+`) => {
134
     return eval(`(socket) => async (`+args+`) => {
108
-        const res = await rpc.hook(`+args+(args.length!==0?',':'')+` (...cbargs) => {
109
-            if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
110
-            socket.call.apply(socket, [res.uuid, ...cbargs])
111
-        })
112
-        return res
135
+        try{
136
+            const res = await rpc.hook(`+args+(args.length!==0?',':'')+` (...cbargs) => {
137
+                if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
138
+                socket.call.apply(socket, [res.uuid, ...cbargs])
139
+            })
140
+            return res
141
+        }catch(e){
142
+            errorHandler(socket, e)
143
+        }
113
     }`)
144
     }`)
114
 }
145
 }
115
 
146
 
147
+const makeError = (callName: string) => {
148
+    return new Error("Unhandled Promise rejection: Call not found: "+callName+". ; Zone: <root> ; Task: Promise.then ; Value: Error: Call not found: "+callName)
149
+}
116
 
150
 
117
 /**
151
 /**
118
  * Extract a string list of parameters from a function
152
  * Extract a string list of parameters from a function

+ 83
- 32
test/Test.ts View File

2
 
2
 
3
 import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index'
3
 import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index'
4
 import * as uuidv4 from "uuid/v4"
4
 import * as uuidv4 from "uuid/v4"
5
+import { doesNotReject } from "assert";
6
+import { Socket } from "dgram";
5
 
7
 
6
 const add = (...args:number[]) => {return args.reduce((a,b)=>a+b, 0)}
8
 const add = (...args:number[]) => {return args.reduce((a,b)=>a+b, 0)}
7
 function makeServer(){
9
 function makeServer(){
284
         const sock = new RPCSocket(21004, "localhost")
286
         const sock = new RPCSocket(21004, "localhost")
285
         sock.connect<SesameTestIfc>( /* no sesame */).then(async (c) => {
287
         sock.connect<SesameTestIfc>( /* no sesame */).then(async (c) => {
286
             c.test.checkCandy().then(d => {
288
             c.test.checkCandy().then(d => {
287
-                if(d === candy)
288
-                    done("should not be able to get candy")
289
+                done()
290
+            }).catch(e => {
291
+                //console.log("EXPECTED CLIENT EXCEPTION", String(e));
292
+                done()
293
+            }).finally(() => {
294
+                sock.destroy()
295
+            })
296
+        })
297
+    })
298
+
299
+    it('should fail with wrong sesame', (done) => {
300
+        const sock = new RPCSocket(21004, "localhost")
301
+        sock.connect<SesameTestIfc>('abasd').then(async (c) => {
302
+            c.test.checkCandy().then(d => {
303
+                done("should not be able to get candy")
304
+            }).catch(e => {
305
+                //console.log("EXPECTED CLIENT EXCEPTION", String(e));
289
                 done()
306
                 done()
290
             }).finally(() => {
307
             }).finally(() => {
291
                 sock.destroy()
308
                 sock.destroy()
300
             }
317
             }
301
         }).then(d => {
318
         }).then(d => {
302
             if(d.result !== 'Success')
319
             if(d.result !== 'Success')
303
-                done('expected valid response')
320
+                done('unexpected valid response')
304
 
321
 
305
             client.test.checkCandy()
322
             client.test.checkCandy()
306
         })
323
         })
326
     })
343
     })
327
 })
344
 })
328
 
345
 
329
-/*
330
-class myServer{
331
-    server = new RPCServer(21004, [ {
332
-        name: 'createUser' as 'createUser',
333
-        exportRPCs: () => [{
334
-            name: 'createUser' as 'createUser',
335
-            call: this.createUser
336
-        }]
337
-    }
338
-    ])
339
 
346
 
340
-    createUser = async( user: {a:any,b:any}) => {
341
-        console.log(user)
342
-        return user
347
+
348
+describe('Error handling', ()=>{
349
+    
350
+    let createUser = async( user: {a:any,b:any}) => {
351
+        throw new Error("BAD BAD BAD")
343
     }
352
     }
344
-}
345
 
353
 
346
-describe('Should pass the createUser edge case', ()=>{
347
-    let server
354
+    it("RPC throws on client without handler", (done)=>{
355
+        let server = new RPCServer(21004, [ {
356
+            name: 'createUser' as 'createUser',
357
+            exportRPCs: () => [{
358
+                name: 'createUser' as 'createUser',
359
+                call: createUser
360
+        }]}], {
348
 
361
 
349
-    before(()=>{
350
-        server = new myServer()
351
-    })
362
+        })
352
 
363
 
353
-    after(()=>{
354
-        server.server.destroy()
364
+        let sock = new RPCSocket(21004, 'localhost')
365
+        sock.connect().then((cli) => {
366
+            cli["createUser"]["createUser"]({
367
+                a:'a',
368
+                b:'b'
369
+            })
370
+            .then(r => {
371
+                if(r != null)
372
+                done("UNEXPECTED RESULT " + r)
373
+            })
374
+            .catch((e) => {
375
+                //console.log("EXPECTED CLIENT EXCEPTION", String(e));
376
+                done()
377
+            })
378
+            .finally(() => {
379
+                sock.destroy()
380
+                server.destroy()
381
+            })
382
+        })
355
     })
383
     })
356
 
384
 
357
-    it("should work", async ()=>{
385
+    it("RPC throws server with handler", (done)=>{
386
+        let server = new RPCServer(21004, [ {
387
+            name: 'createUser' as 'createUser',
388
+            exportRPCs: () => [{
389
+                name: 'createUser' as 'createUser',
390
+                call: createUser
391
+        }]}], {
392
+            errorHandler: (socket, e) => {
393
+                //console.log("EXPECTED SERVER EXCEPTION", String(e));
394
+                done()
395
+            }
396
+        })
397
+
358
         let sock = new RPCSocket(21004, 'localhost')
398
         let sock = new RPCSocket(21004, 'localhost')
359
-        let client = await sock.connect()
360
-        client["createUser"]["createUser"]({
361
-            a:'a',
362
-            b:'b'
363
-        }).then(console.log)
399
+        sock.connect().then((cli) => {
400
+            cli["createUser"]["createUser"]({
401
+                a:'a',
402
+                b:'b'
403
+            })
404
+            .then(r => {
405
+                if(r != null)
406
+                done("UNEXPECTED RESULT " + r)
407
+            })
408
+            .catch((e) => {
409
+                done("UNEXPECTED CLIENT ERROR " + e)
410
+                done(e)
411
+            })
412
+            .finally(() => {
413
+                sock.destroy()
414
+                server.destroy()
415
+            })
416
+        })
364
     })
417
     })
365
-    
366
 })
418
 })
367
-*/

Loading…
Cancel
Save