Better ErrorHandler and more tests
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rpclibrary",
|
"name": "rpclibrary",
|
||||||
"version": "1.5.3",
|
"version": "1.6.0",
|
||||||
"description": "rpclibrary is a websocket on steroids!",
|
"description": "rpclibrary is a websocket on steroids!",
|
||||||
"main": "./js/Index.js",
|
"main": "./js/Index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
+4
-7
@@ -42,18 +42,15 @@ export class RPCServer<
|
|||||||
|
|
||||||
this.errorHandler = (socket:I.Socket) => (error:any) => {
|
this.errorHandler = (socket:I.Socket) => (error:any) => {
|
||||||
if(conf.errorHandler) conf.errorHandler(socket, error)
|
if(conf.errorHandler) conf.errorHandler(socket, error)
|
||||||
socket.destroy();
|
else throw error
|
||||||
console.error("Caught websocket error", String(error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeHandler = (socket:I.Socket) => {
|
this.closeHandler = (socket:I.Socket) => {
|
||||||
if(!conf.closeHandler) console.log("Connection on port "+socket.port+" closing")
|
if(conf.closeHandler) conf.closeHandler(socket)
|
||||||
else conf.closeHandler(socket)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectionHandler = (socket:I.Socket) => {
|
this.connectionHandler = (socket:I.Socket) => {
|
||||||
if(!conf.connectionHandler) console.log("New connection on port "+socket.port)
|
if(conf.connectionHandler) conf.connectionHandler(socket)
|
||||||
else conf.connectionHandler(socket)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let badRPC
|
let badRPC
|
||||||
@@ -86,7 +83,7 @@ export class RPCServer<
|
|||||||
protected initRPCs(socket:I.Socket){
|
protected initRPCs(socket:I.Socket){
|
||||||
socket.hook('info', () => rpcInfos)
|
socket.hook('info', () => rpcInfos)
|
||||||
const rpcInfos:T.ExtendedRpcInfo[] = [
|
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 T from './Types';
|
||||||
import * as I from './Interfaces';
|
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
|
* 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 ErrorHandler = (socket:I.Socket, error:any) => void
|
||||||
export type CloseHandler = (socket:I.Socket) => void
|
export type CloseHandler = (socket:I.Socket) => void
|
||||||
export type SesameFunction = (sesame : string) => boolean
|
export type SesameFunction = (sesame : string) => boolean
|
||||||
|
export type ExceptionHandling = 'local' | 'remote'
|
||||||
export type SesameConf = {
|
export type SesameConf = {
|
||||||
sesame?: string | SesameFunction
|
sesame?: string | SesameFunction
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ export type ServerConf = {
|
|||||||
errorHandler?: ErrorHandler
|
errorHandler?: ErrorHandler
|
||||||
closeHandler?: CloseHandler
|
closeHandler?: CloseHandler
|
||||||
visibility?: Visibility
|
visibility?: Visibility
|
||||||
|
exceptionHandling?: ExceptionHandling
|
||||||
} & SesameConf
|
} & SesameConf
|
||||||
|
|
||||||
export type SocketConf = {
|
export type SocketConf = {
|
||||||
|
|||||||
+64
-30
@@ -10,8 +10,7 @@ import { SubscriptionResponse } from "./Types";
|
|||||||
* @param owner The owning RPC group's name
|
* @param owner The owning RPC group's name
|
||||||
* @throws Error on RPC without name property
|
* @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){
|
switch (typeof rpc){
|
||||||
case "object":
|
case "object":
|
||||||
if(rpc['call']){
|
if(rpc['call']){
|
||||||
@@ -20,10 +19,10 @@ export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner
|
|||||||
argNames: extractArgs(rpc['call']),
|
argNames: extractArgs(rpc['call']),
|
||||||
type: "Call",
|
type: "Call",
|
||||||
name: rpc.name,
|
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{
|
}else{
|
||||||
const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc, sesame)
|
const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc, errorHandler, sesame)
|
||||||
return {
|
return {
|
||||||
owner: owner,
|
owner: owner,
|
||||||
argNames: extractArgs(generator(undefined)),
|
argNames: extractArgs(generator(undefined)),
|
||||||
@@ -45,7 +44,7 @@ RPC did not provide a name.
|
|||||||
argNames: extractArgs(rpc),
|
argNames: extractArgs(rpc),
|
||||||
type: "Call",
|
type: "Call",
|
||||||
name: rpc.name,
|
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)
|
throw new Error("Bad socketIORPC type "+ typeof rpc)
|
||||||
@@ -57,62 +56,97 @@ RPC did not provide a name.
|
|||||||
* @param exporter The exporter
|
* @param exporter The exporter
|
||||||
* @param makeUnique @default true Attach a suffix to RPC names
|
* @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 owner = exporter.name
|
||||||
const RPCs = [...exporter.exportRPCs()]
|
const RPCs = [...exporter.exportRPCs()]
|
||||||
|
|
||||||
|
|
||||||
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, sesame))
|
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, errorHandler, sesame))
|
||||||
.map(info => {
|
.map(info => {
|
||||||
const suffix = makeUnique?"-"+uuidv4().substr(0,4):""
|
const suffix = makeUnique?"-"+uuidv4().substr(0,4):""
|
||||||
const ret:any = info
|
const ret:any = info
|
||||||
ret.uniqueName = info.name+suffix
|
ret.uniqueName = info.name+suffix
|
||||||
|
|
||||||
|
let rpcFunction
|
||||||
|
|
||||||
switch(info.type){
|
if(info.type === 'Hook')
|
||||||
case "Hook":
|
rpcFunction = info.generator(socket)
|
||||||
socket.hook(ret.uniqueName, info.generator(socket))
|
else
|
||||||
break;
|
rpcFunction = info.call
|
||||||
case "Call":
|
|
||||||
socket.hook(ret.uniqueName, info.call)
|
socket.hook(ret.uniqueName, callGenerator(socket, rpcFunction, errorHandler))
|
||||||
break;
|
|
||||||
}
|
|
||||||
socket.on('close', () => socket.unhook(info.name))
|
|
||||||
return ret
|
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
|
* Utility function to generate {@link HookFunction} from a RPC for backend
|
||||||
* @param rpc The RPC to transform
|
* @param rpc The RPC to transform
|
||||||
* @returns A {@link HookFunction}
|
* @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)
|
const argsArr = extractArgs(rpc.hook)
|
||||||
argsArr.pop() //remove 'callback' from the end
|
argsArr.pop() //remove 'callback' from the end
|
||||||
const argsStr = argsArr.join(',')
|
const argsStr = argsArr.join(',')
|
||||||
|
|
||||||
if(sesameFn){
|
if(sesameFn){
|
||||||
const args = ['sesame', ...argsArr].join(',')
|
const args = ['sesame', ...argsArr].join(',')
|
||||||
const f = eval(`(socket) => async (`+args+`) => {
|
return eval(`(socket) => async (`+args+`) => {
|
||||||
if(!sesameFn(sesame)) return
|
try{
|
||||||
const res = await rpc.hook(`+argsStr+(argsStr.length!==0?',':'')+` (...cbargs) => {
|
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)
|
if(rpc.onCallback) rpc.onCallback.apply({}, cbargs)
|
||||||
socket.call.apply(socket, [res.uuid, ...cbargs])
|
socket.call.apply(socket, [res.uuid, ...cbargs])
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}`)
|
}catch(e){
|
||||||
return f
|
errorHandler(socket, e)
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}`)
|
}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
* Extract a string list of parameters from a function
|
||||||
|
|||||||
+86
-35
@@ -2,6 +2,8 @@ import { describe, it, Func } from "mocha";
|
|||||||
|
|
||||||
import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index'
|
import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index'
|
||||||
import * as uuidv4 from "uuid/v4"
|
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)}
|
const add = (...args:number[]) => {return args.reduce((a,b)=>a+b, 0)}
|
||||||
function makeServer(){
|
function makeServer(){
|
||||||
@@ -284,8 +286,23 @@ describe('Sesame should unlock the socket', () => {
|
|||||||
const sock = new RPCSocket(21004, "localhost")
|
const sock = new RPCSocket(21004, "localhost")
|
||||||
sock.connect<SesameTestIfc>( /* no sesame */).then(async (c) => {
|
sock.connect<SesameTestIfc>( /* no sesame */).then(async (c) => {
|
||||||
c.test.checkCandy().then(d => {
|
c.test.checkCandy().then(d => {
|
||||||
if(d === candy)
|
done()
|
||||||
done("should not be able to get candy")
|
}).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()
|
done()
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
sock.destroy()
|
sock.destroy()
|
||||||
@@ -300,7 +317,7 @@ describe('Sesame should unlock the socket', () => {
|
|||||||
}
|
}
|
||||||
}).then(d => {
|
}).then(d => {
|
||||||
if(d.result !== 'Success')
|
if(d.result !== 'Success')
|
||||||
done('expected valid response')
|
done('unexpected valid response')
|
||||||
|
|
||||||
client.test.checkCandy()
|
client.test.checkCandy()
|
||||||
})
|
})
|
||||||
@@ -326,42 +343,76 @@ describe('Sesame should unlock the socket', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
|
||||||
class myServer{
|
|
||||||
server = new RPCServer(21004, [ {
|
describe('Error handling', ()=>{
|
||||||
name: 'createUser' as 'createUser',
|
|
||||||
exportRPCs: () => [{
|
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',
|
name: 'createUser' as 'createUser',
|
||||||
call: this.createUser
|
exportRPCs: () => [{
|
||||||
}]
|
name: 'createUser' as 'createUser',
|
||||||
}
|
call: 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 sock = new RPCSocket(21004, 'localhost')
|
||||||
let client = await sock.connect()
|
sock.connect().then((cli) => {
|
||||||
client["createUser"]["createUser"]({
|
cli["createUser"]["createUser"]({
|
||||||
a:'a',
|
a:'a',
|
||||||
b:'b'
|
b:'b'
|
||||||
}).then(console.log)
|
})
|
||||||
|
.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