Better ErrorHandler and more tests
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rpclibrary",
|
||||
"version": "1.5.3",
|
||||
"version": "1.6.0",
|
||||
"description": "rpclibrary is a websocket on steroids!",
|
||||
"main": "./js/Index.js",
|
||||
"repository": {
|
||||
|
||||
+4
-7
@@ -42,18 +42,15 @@ export class RPCServer<
|
||||
|
||||
this.errorHandler = (socket:I.Socket) => (error:any) => {
|
||||
if(conf.errorHandler) conf.errorHandler(socket, error)
|
||||
socket.destroy();
|
||||
console.error("Caught websocket error", String(error))
|
||||
else throw error
|
||||
}
|
||||
|
||||
this.closeHandler = (socket:I.Socket) => {
|
||||
if(!conf.closeHandler) console.log("Connection on port "+socket.port+" closing")
|
||||
else conf.closeHandler(socket)
|
||||
if(conf.closeHandler) conf.closeHandler(socket)
|
||||
}
|
||||
|
||||
this.connectionHandler = (socket:I.Socket) => {
|
||||
if(!conf.connectionHandler) console.log("New connection on port "+socket.port)
|
||||
else conf.connectionHandler(socket)
|
||||
if(conf.connectionHandler) conf.connectionHandler(socket)
|
||||
}
|
||||
|
||||
let badRPC
|
||||
@@ -86,7 +83,7 @@ export class RPCServer<
|
||||
protected initRPCs(socket:I.Socket){
|
||||
socket.hook('info', () => rpcInfos)
|
||||
const rpcInfos:T.ExtendedRpcInfo[] = [
|
||||
...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter, this.sesame))
|
||||
...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter, this.errorHandler, this.sesame))
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+2
-7
@@ -4,14 +4,9 @@ import bsock = require('bsock');
|
||||
|
||||
import * as T from './Types';
|
||||
import * as I from './Interfaces';
|
||||
import { stripAfterEquals } from './Utils';
|
||||
|
||||
|
||||
/**
|
||||
* Utility function to strip parameters like "a = 3" of their defaults
|
||||
* @param str The parameter to modify
|
||||
*/
|
||||
function stripAfterEquals(str:string):string{
|
||||
return str.split("=")[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* A websocket-on-steroids with built-in RPC capabilities
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ export type ConnectionHandler = (socket:I.Socket) => void
|
||||
export type ErrorHandler = (socket:I.Socket, error:any) => void
|
||||
export type CloseHandler = (socket:I.Socket) => void
|
||||
export type SesameFunction = (sesame : string) => boolean
|
||||
|
||||
export type ExceptionHandling = 'local' | 'remote'
|
||||
export type SesameConf = {
|
||||
sesame?: string | SesameFunction
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export type ServerConf = {
|
||||
errorHandler?: ErrorHandler
|
||||
closeHandler?: CloseHandler
|
||||
visibility?: Visibility
|
||||
exceptionHandling?: ExceptionHandling
|
||||
} & SesameConf
|
||||
|
||||
export type SocketConf = {
|
||||
|
||||
+64
-30
@@ -10,8 +10,7 @@ import { SubscriptionResponse } from "./Types";
|
||||
* @param owner The owning RPC group's name
|
||||
* @throws Error on RPC without name property
|
||||
*/
|
||||
export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner: string, sesame?:T.SesameFunction):T.RpcInfo => {
|
||||
|
||||
export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner: string, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction):T.RpcInfo => {
|
||||
switch (typeof rpc){
|
||||
case "object":
|
||||
if(rpc['call']){
|
||||
@@ -20,10 +19,10 @@ export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner
|
||||
argNames: extractArgs(rpc['call']),
|
||||
type: "Call",
|
||||
name: rpc.name,
|
||||
call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); throw new Error('Bad sesame')}:rpc['call'], // check & remove sesame
|
||||
call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); throw makeError(rpc.name)}:rpc['call'], // check & remove sesame
|
||||
}
|
||||
}else{
|
||||
const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc, sesame)
|
||||
const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc, errorHandler, sesame)
|
||||
return {
|
||||
owner: owner,
|
||||
argNames: extractArgs(generator(undefined)),
|
||||
@@ -45,7 +44,7 @@ RPC did not provide a name.
|
||||
argNames: extractArgs(rpc),
|
||||
type: "Call",
|
||||
name: rpc.name,
|
||||
call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args)}:rpc, // check & remove sesame
|
||||
call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args); throw makeError(rpc.name)}:rpc, // check & remove sesame
|
||||
}
|
||||
}
|
||||
throw new Error("Bad socketIORPC type "+ typeof rpc)
|
||||
@@ -57,62 +56,97 @@ RPC did not provide a name.
|
||||
* @param exporter The exporter
|
||||
* @param makeUnique @default true Attach a suffix to RPC names
|
||||
*/
|
||||
export function rpcHooker<SubResT = {}>(socket: I.Socket, exporter:I.RPCExporter<any, any, SubResT>, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{
|
||||
export function rpcHooker<SubResT = {}>(socket: I.Socket, exporter:I.RPCExporter<any, any, SubResT>, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{
|
||||
const owner = exporter.name
|
||||
const RPCs = [...exporter.exportRPCs()]
|
||||
|
||||
|
||||
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, sesame))
|
||||
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, errorHandler, sesame))
|
||||
.map(info => {
|
||||
const suffix = makeUnique?"-"+uuidv4().substr(0,4):""
|
||||
const ret:any = info
|
||||
ret.uniqueName = info.name+suffix
|
||||
|
||||
let rpcFunction
|
||||
|
||||
switch(info.type){
|
||||
case "Hook":
|
||||
socket.hook(ret.uniqueName, info.generator(socket))
|
||||
break;
|
||||
case "Call":
|
||||
socket.hook(ret.uniqueName, info.call)
|
||||
break;
|
||||
}
|
||||
socket.on('close', () => socket.unhook(info.name))
|
||||
if(info.type === 'Hook')
|
||||
rpcFunction = info.generator(socket)
|
||||
else
|
||||
rpcFunction = info.call
|
||||
|
||||
socket.hook(ret.uniqueName, callGenerator(socket, rpcFunction, errorHandler))
|
||||
return ret
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate an RPC with the error handler
|
||||
* @param rpcFunction the function to decorate
|
||||
*/
|
||||
const callGenerator = (socket: I.Socket, rpcFunction : T.AnyFunction, errorHandler: T.ErrorHandler) : T.AnyFunction => {
|
||||
const argsArr = extractArgs(rpcFunction)
|
||||
const args = argsArr.join(',')
|
||||
const argsStr = argsArr.map(stripAfterEquals).join(',')
|
||||
|
||||
return eval(`async (`+args+`) => {
|
||||
try{
|
||||
return await rpcFunction(`+argsStr+`)
|
||||
}catch(e){
|
||||
errorHandler(socket)(e)
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to strip parameters like "a = 3" of their defaults
|
||||
* @param str The parameter to modify
|
||||
*/
|
||||
export function stripAfterEquals(str:string):string{
|
||||
return str.split("=")[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to generate {@link HookFunction} from a RPC for backend
|
||||
* @param rpc The RPC to transform
|
||||
* @returns A {@link HookFunction}
|
||||
*/
|
||||
const hookGenerator = (rpc:T.HookRPC<any, any, any>, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => {
|
||||
const hookGenerator = (rpc:T.HookRPC<any, any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => {
|
||||
const argsArr = extractArgs(rpc.hook)
|
||||
argsArr.pop() //remove 'callback' from the end
|
||||
const argsStr = argsArr.join(',')
|
||||
|
||||
if(sesameFn){
|
||||
const args = ['sesame', ...argsArr].join(',')
|
||||
const f = eval(`(socket) => async (`+args+`) => {
|
||||
if(!sesameFn(sesame)) return
|
||||
const res = await rpc.hook(`+argsStr+(argsStr.length!==0?',':'')+` (...cbargs) => {
|
||||
return eval(`(socket) => async (`+args+`) => {
|
||||
try{
|
||||
if(!sesameFn(sesame)) return
|
||||
const res = await rpc.hook(`+argsStr+(argsStr.length!==0?',':'')+` (...cbargs) => {
|
||||
if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
|
||||
socket.call.apply(socket, [res.uuid, ...cbargs])
|
||||
})
|
||||
return res
|
||||
}catch(e){
|
||||
errorHandler(socket, e)
|
||||
}
|
||||
}`)
|
||||
}
|
||||
const args = argsArr.join(',')
|
||||
return eval(`(socket) => async (`+args+`) => {
|
||||
try{
|
||||
const res = await rpc.hook(`+args+(args.length!==0?',':'')+` (...cbargs) => {
|
||||
if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
|
||||
socket.call.apply(socket, [res.uuid, ...cbargs])
|
||||
})
|
||||
return res
|
||||
}`)
|
||||
return f
|
||||
}
|
||||
const args = argsArr.join(',')
|
||||
return eval(`(socket) => async (`+args+`) => {
|
||||
const res = await rpc.hook(`+args+(args.length!==0?',':'')+` (...cbargs) => {
|
||||
if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
|
||||
socket.call.apply(socket, [res.uuid, ...cbargs])
|
||||
})
|
||||
return res
|
||||
}catch(e){
|
||||
errorHandler(socket, e)
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
const makeError = (callName: string) => {
|
||||
return new Error("Unhandled Promise rejection: Call not found: "+callName+". ; Zone: <root> ; Task: Promise.then ; Value: Error: Call not found: "+callName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a string list of parameters from a function
|
||||
|
||||
+89
-38
@@ -2,6 +2,8 @@ import { describe, it, Func } from "mocha";
|
||||
|
||||
import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index'
|
||||
import * as uuidv4 from "uuid/v4"
|
||||
import { doesNotReject } from "assert";
|
||||
import { Socket } from "dgram";
|
||||
|
||||
const add = (...args:number[]) => {return args.reduce((a,b)=>a+b, 0)}
|
||||
function makeServer(){
|
||||
@@ -284,8 +286,23 @@ describe('Sesame should unlock the socket', () => {
|
||||
const sock = new RPCSocket(21004, "localhost")
|
||||
sock.connect<SesameTestIfc>( /* no sesame */).then(async (c) => {
|
||||
c.test.checkCandy().then(d => {
|
||||
if(d === candy)
|
||||
done("should not be able to get candy")
|
||||
done()
|
||||
}).catch(e => {
|
||||
//console.log("EXPECTED CLIENT EXCEPTION", String(e));
|
||||
done()
|
||||
}).finally(() => {
|
||||
sock.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail with wrong sesame', (done) => {
|
||||
const sock = new RPCSocket(21004, "localhost")
|
||||
sock.connect<SesameTestIfc>('abasd').then(async (c) => {
|
||||
c.test.checkCandy().then(d => {
|
||||
done("should not be able to get candy")
|
||||
}).catch(e => {
|
||||
//console.log("EXPECTED CLIENT EXCEPTION", String(e));
|
||||
done()
|
||||
}).finally(() => {
|
||||
sock.destroy()
|
||||
@@ -300,7 +317,7 @@ describe('Sesame should unlock the socket', () => {
|
||||
}
|
||||
}).then(d => {
|
||||
if(d.result !== 'Success')
|
||||
done('expected valid response')
|
||||
done('unexpected valid response')
|
||||
|
||||
client.test.checkCandy()
|
||||
})
|
||||
@@ -326,42 +343,76 @@ describe('Sesame should unlock the socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
class myServer{
|
||||
server = new RPCServer(21004, [ {
|
||||
name: 'createUser' as 'createUser',
|
||||
exportRPCs: () => [{
|
||||
name: 'createUser' as 'createUser',
|
||||
call: this.createUser
|
||||
}]
|
||||
}
|
||||
])
|
||||
|
||||
createUser = async( user: {a:any,b:any}) => {
|
||||
console.log(user)
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
describe('Should pass the createUser edge case', ()=>{
|
||||
let server
|
||||
|
||||
before(()=>{
|
||||
server = new myServer()
|
||||
})
|
||||
|
||||
after(()=>{
|
||||
server.server.destroy()
|
||||
})
|
||||
|
||||
it("should work", async ()=>{
|
||||
let sock = new RPCSocket(21004, 'localhost')
|
||||
let client = await sock.connect()
|
||||
client["createUser"]["createUser"]({
|
||||
a:'a',
|
||||
b:'b'
|
||||
}).then(console.log)
|
||||
})
|
||||
describe('Error handling', ()=>{
|
||||
|
||||
let createUser = async( user: {a:any,b:any}) => {
|
||||
throw new Error("BAD BAD BAD")
|
||||
}
|
||||
|
||||
it("RPC throws on client without handler", (done)=>{
|
||||
let server = new RPCServer(21004, [ {
|
||||
name: 'createUser' as 'createUser',
|
||||
exportRPCs: () => [{
|
||||
name: 'createUser' as 'createUser',
|
||||
call: createUser
|
||||
}]}], {
|
||||
|
||||
})
|
||||
|
||||
let sock = new RPCSocket(21004, 'localhost')
|
||||
sock.connect().then((cli) => {
|
||||
cli["createUser"]["createUser"]({
|
||||
a:'a',
|
||||
b:'b'
|
||||
})
|
||||
.then(r => {
|
||||
if(r != null)
|
||||
done("UNEXPECTED RESULT " + r)
|
||||
})
|
||||
.catch((e) => {
|
||||
//console.log("EXPECTED CLIENT EXCEPTION", String(e));
|
||||
done()
|
||||
})
|
||||
.finally(() => {
|
||||
sock.destroy()
|
||||
server.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("RPC throws server with handler", (done)=>{
|
||||
let server = new RPCServer(21004, [ {
|
||||
name: 'createUser' as 'createUser',
|
||||
exportRPCs: () => [{
|
||||
name: 'createUser' as 'createUser',
|
||||
call: createUser
|
||||
}]}], {
|
||||
errorHandler: (socket, e) => {
|
||||
//console.log("EXPECTED SERVER EXCEPTION", String(e));
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
let sock = new RPCSocket(21004, 'localhost')
|
||||
sock.connect().then((cli) => {
|
||||
cli["createUser"]["createUser"]({
|
||||
a:'a',
|
||||
b:'b'
|
||||
})
|
||||
.then(r => {
|
||||
if(r != null)
|
||||
done("UNEXPECTED RESULT " + r)
|
||||
})
|
||||
.catch((e) => {
|
||||
done("UNEXPECTED CLIENT ERROR " + e)
|
||||
done(e)
|
||||
})
|
||||
.finally(() => {
|
||||
sock.destroy()
|
||||
server.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
*/
|
||||
Reference in New Issue
Block a user