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,6 +1,6 @@
1 1
 {
2 2
   "name": "rpclibrary",
3
-  "version": "1.5.3",
3
+  "version": "1.6.0",
4 4
   "description": "rpclibrary is a websocket on steroids!",
5 5
   "main": "./js/Index.js",
6 6
   "repository": {

+ 4
- 7
src/Backend.ts View File

@@ -42,18 +42,15 @@ export class RPCServer<
42 42
 
43 43
         this.errorHandler = (socket:I.Socket) => (error:any) => { 
44 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 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 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 56
         let badRPC 
@@ -86,7 +83,7 @@ export class RPCServer<
86 83
     protected initRPCs(socket:I.Socket){
87 84
         socket.hook('info', () => rpcInfos)
88 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,14 +4,9 @@ import bsock = require('bsock');
4 4
 
5 5
 import * as T from './Types'; 
6 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 12
  * A websocket-on-steroids with built-in RPC capabilities

+ 2
- 1
src/Types.ts View File

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

+ 64
- 30
src/Utils.ts View File

@@ -10,8 +10,7 @@ import { SubscriptionResponse } from "./Types";
10 10
  * @param owner The owning RPC group's name
11 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 14
     switch (typeof rpc){
16 15
         case  "object":
17 16
             if(rpc['call']){
@@ -20,10 +19,10 @@ export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner
20 19
                     argNames: extractArgs(rpc['call']),
21 20
                     type: "Call",
22 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 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 26
                 return {
28 27
                     owner: owner,
29 28
                     argNames: extractArgs(generator(undefined)),
@@ -45,7 +44,7 @@ RPC did not provide a name.
45 44
                 argNames: extractArgs(rpc),
46 45
                 type: "Call",
47 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 50
     throw new Error("Bad socketIORPC type "+ typeof rpc)
@@ -57,62 +56,97 @@ RPC did not provide a name.
57 56
  * @param exporter The exporter
58 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 60
     const owner = exporter.name
62 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 65
     .map(info => {
67 66
         const suffix = makeUnique?"-"+uuidv4().substr(0,4):""
68 67
         const ret:any = info
69 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 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 109
  * Utility function to generate {@link HookFunction} from a RPC for backend
86 110
  * @param rpc The RPC to transform
87 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 114
     const argsArr = extractArgs(rpc.hook)
91 115
     argsArr.pop() //remove 'callback' from the end
92 116
     const argsStr = argsArr.join(',')
93 117
 
94 118
     if(sesameFn){
95 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 133
     const args = argsArr.join(',')
107 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 152
  * Extract a string list of parameters from a function

+ 83
- 32
test/Test.ts View File

@@ -2,6 +2,8 @@ import { describe, it, Func } from "mocha";
2 2
 
3 3
 import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index'
4 4
 import * as uuidv4 from "uuid/v4"
5
+import { doesNotReject } from "assert";
6
+import { Socket } from "dgram";
5 7
 
6 8
 const add = (...args:number[]) => {return args.reduce((a,b)=>a+b, 0)}
7 9
 function makeServer(){
@@ -284,8 +286,23 @@ describe('Sesame should unlock the socket', () => {
284 286
         const sock = new RPCSocket(21004, "localhost")
285 287
         sock.connect<SesameTestIfc>( /* no sesame */).then(async (c) => {
286 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 306
                 done()
290 307
             }).finally(() => {
291 308
                 sock.destroy()
@@ -300,7 +317,7 @@ describe('Sesame should unlock the socket', () => {
300 317
             }
301 318
         }).then(d => {
302 319
             if(d.result !== 'Success')
303
-                done('expected valid response')
320
+                done('unexpected valid response')
304 321
 
305 322
             client.test.checkCandy()
306 323
         })
@@ -326,42 +343,76 @@ describe('Sesame should unlock the socket', () => {
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 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