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

This commit is contained in:
nitowa
2020-12-30 04:01:37 +01:00
parent 1627c12f9d
commit 326dabf2f8
9 changed files with 8874 additions and 306 deletions
+8513 -119
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -57,6 +57,7 @@
"http": "0.0.0", "http": "0.0.0",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"socketio-wildcard": "^2.0.0",
"uuid": "^3.3.3" "uuid": "^3.3.3"
}, },
"files": [ "files": [
+40 -17
View File
@@ -8,11 +8,11 @@ import * as I from './Interfaces';
export class RPCServer< export class RPCServer<
InterfaceT extends T.RPCInterface = T.RPCInterface, InterfaceT extends T.RPCInterface = T.RPCInterface,
> { > {
private pio = new PromiseIO() private pio = new PromiseIO()
private closeHandler: T.CloseHandler private closeHandler: T.CloseHandler
private errorHandler: T.ErrorHandler private errorHandler: (socket: I.Socket, error: any, rpcName: string, args: any[], forward?: boolean) => void
private connectionHandler: T.ConnectionHandler private connectionHandler: T.ConnectionHandler
private sesame?: T.SesameFunction private sesame?: T.SesameFunction
private accessFilter: T.AccessFilter<InterfaceT> private accessFilter: T.AccessFilter<InterfaceT>
@@ -26,8 +26,12 @@ export class RPCServer<
*/ */
constructor( constructor(
private exporters: T.ExporterArray<InterfaceT> = [], private exporters: T.ExporterArray<InterfaceT> = [],
conf: T.ServerConf<InterfaceT> = {}, private conf: T.ServerConf<InterfaceT> = {},
) { ) {
if (conf.throwOnUnknownRPC == null) {
conf.throwOnUnknownRPC = true
}
if (conf.sesame) { if (conf.sesame) {
this.sesame = U.makeSesameFunction(conf.sesame) this.sesame = U.makeSesameFunction(conf.sesame)
} }
@@ -37,9 +41,15 @@ export class RPCServer<
return this.sesame!(sesame!) return this.sesame!(sesame!)
}) })
this.errorHandler = (socket: I.Socket | PromiseIO) => (error: any, rpcName: string, args: any[]) => { this.errorHandler = (socket: I.Socket, error: any, rpcName: string, args: any[], forward: boolean = false) => {
if (conf.errorHandler) conf.errorHandler(socket, error, rpcName, args) if (conf.errorHandler) conf.errorHandler(socket, error, rpcName, args)
else throw error else {
if (forward) {
socket.call("$UNKNOWNRPC$", error)
} else {
throw error
}
}
} }
this.closeHandler = (socket: I.Socket) => { this.closeHandler = (socket: I.Socket) => {
@@ -52,7 +62,7 @@ export class RPCServer<
exporters.forEach(U.fixNames) //TSC for some reason doesn't preserve name properties of methods exporters.forEach(U.fixNames) //TSC for some reason doesn't preserve name properties of methods
let badRPC = exporters.flatMap(ex => typeof ex.RPCs === "function"?ex.RPCs():(ex as any)).find(rpc => !rpc.name) let badRPC = exporters.flatMap(ex => typeof ex.RPCs === "function" ? ex.RPCs() : (ex as any)).find(rpc => !rpc.name)
if (badRPC) { if (badRPC) {
throw new Error(` throw new Error(`
RPC did not provide a name. RPC did not provide a name.
@@ -62,29 +72,32 @@ export class RPCServer<
\n`+ badRPC.toString() + ` \n`+ badRPC.toString() + `
\n>------------OFFENDING RPC`) \n>------------OFFENDING RPC`)
} }
try { try {
this.pio.on('socket', (clientSocket: I.Socket) => { this.pio.on('socket', (clientSocket: I.Socket) => {
const sock:any = clientSocket;
clientSocket.on('disconnect', () => this.closeHandler(clientSocket)) clientSocket.on('disconnect', () => this.closeHandler(clientSocket))
this.connectionHandler(clientSocket) this.connectionHandler(clientSocket)
this.initRPCs(clientSocket) this.initRPCs(clientSocket)
}) })
} catch (e) { } catch (e) {
this.errorHandler(this.pio, e, 'system', []) this.errorHandler(<unknown>undefined as I.Socket, e, 'system', [])
} }
} }
public attach = (httpServer = new http.Server()) : RPCServer<InterfaceT> => { public attach = (httpServer = new http.Server(), options?: SocketIO.ServerOptions): RPCServer<InterfaceT> => {
this.pio.attach(httpServer) this.pio.attach(httpServer, options)
this.attached = true this.attached = true
return this return this
} }
public listen(port:number) : RPCServer<InterfaceT>{ public listen(port: number, options?: SocketIO.ServerOptions): RPCServer<InterfaceT> {
if(!this.attached) this.attach() if (!this.attached) {
this.attach(undefined, options)
} else {
if (options)
console.warn("RPCServer options were passed to listen(..) after attach(..) was called. Please pass them to attach(..) instead. Ignoring.")
}
this.pio.listen(port) this.pio.listen(port)
return this return this
} }
@@ -94,11 +107,21 @@ export class RPCServer<
const rpcs = await Promise.all(this.exporters.map(async exp => { const rpcs = await Promise.all(this.exporters.map(async exp => {
const allowed = await this.accessFilter(sesame, exp) const allowed = await this.accessFilter(sesame, exp)
if (!allowed) return [] if (!allowed) return []
const infos = U.rpcHooker(clientSocket, exp, this.errorHandler, this.sesame) return U.rpcHooker(clientSocket, exp, this.errorHandler, this.sesame)
return infos
})) }))
return rpcs.flat() const infos = rpcs.flat()
if (this.conf.throwOnUnknownRPC) {
clientSocket.on("*", (packet) => {
if (!infos.some(i => i.uniqueName === packet.data[0])) {
if (packet.data[0].startsWith('destroy_')) return
this.errorHandler(clientSocket, new Error(`Unknown RPC ${packet.data[0]}`), packet.data[0], [...packet.data].splice(1), true)
}
})
}
return infos
}) })
} }
close(): void { close(): void {
+59 -59
View File
@@ -1,7 +1,7 @@
'use strict' 'use strict'
import { PromiseIOClient } from './PromiseIO/Client' import { PromiseIOClient, defaultClientConfig } from './PromiseIO/Client'
import * as T from './Types'; import * as T from './Types';
import * as I from './Interfaces'; import * as I from './Interfaces';
import { stripAfterEquals, appendComma } from './Utils'; import { stripAfterEquals, appendComma } from './Utils';
@@ -9,22 +9,21 @@ import { stripAfterEquals, appendComma } from './Utils';
/** /**
* A websocket-on-steroids with built-in RPC capabilities * A websocket-on-steroids with built-in RPC capabilities
*/ */
export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I.Socket{ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I.Socket {
static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise<T.ConnectedSocket<T>> { static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port: number, server: string, sesame?: string, conf: T.ClientConfig = defaultClientConfig): Promise<T.ConnectedSocket<T>> {
const socket = new RPCSocket<T>(port, server, conf) const socket = new RPCSocket<T>(port, server, conf)
return await socket.connect(sesame) return await socket.connect(sesame)
} }
private protocol: 'http' | 'https'
private socket: I.Socket private socket: I.Socket
private handlers : { private handlers: {
[name in string]: T.AnyFunction[] [name in string]: T.AnyFunction[]
} = { } = {
error: [], error: [],
close: [] close: []
} }
private hooks : {[name in string]: T.AnyFunction} = {} private hooks: { [name in string]: T.AnyFunction } = {}
/** /**
* *
@@ -32,9 +31,9 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param server Server address * @param server Server address
* @param tls @default false use TLS * @param tls @default false use TLS
*/ */
constructor(public port:number, public address: string, conf:T.SocketConf = { tls: false }){ constructor(public port: number, public address: string, private conf: T.ClientConfig = defaultClientConfig) {
Object.defineProperty(this, 'socket', {value: undefined, writable: true}) Object.defineProperty(this, 'socket', { value: undefined, writable: true })
this.protocol = conf.tls ? "https" : "http" this.hook("$UNKNOWNRPC$", (err) => this.handlers['error'].forEach(handler => handler(err)))
} }
/** /**
@@ -42,10 +41,10 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param name The function name to listen on * @param name The function name to listen on
* @param handler The handler to attach * @param handler The handler to attach
*/ */
public hook(name: string, handler: (...args:any[]) => any | Promise<any>){ public hook(name: string, handler: (...args: any[]) => any | Promise<any>) {
if(!this.socket){ if (!this.socket) {
this.hooks[name] = handler this.hooks[name] = handler
}else{ } else {
this.socket.hook(name, handler) this.socket.hook(name, handler)
} }
} }
@@ -55,10 +54,10 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param name The function name to listen on * @param name The function name to listen on
* @param handler The handler to attach * @param handler The handler to attach
*/ */
public bind(name: string, handler: (...args:any[]) => any | Promise<any>){ public bind(name: string, handler: (...args: any[]) => any | Promise<any>) {
if(!this.socket){ if (!this.socket) {
this.hooks[name] = handler this.hooks[name] = handler
}else{ } else {
this.socket.bind(name, handler) this.socket.bind(name, handler)
} }
} }
@@ -67,10 +66,10 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* Removes a {@link hook} listener by name. * Removes a {@link hook} listener by name.
* @param name The function name * @param name The function name
*/ */
public unhook(name: string){ public unhook(name: string) {
if(!this.socket){ if (!this.socket) {
delete this.hooks[name] delete this.hooks[name]
}else{ } else {
this.socket.unhook(name) this.socket.unhook(name)
} }
} }
@@ -80,13 +79,13 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param type 'error' or 'close' * @param type 'error' or 'close'
* @param f The listener to attach * @param f The listener to attach
*/ */
public on(type: string, f: T.AnyFunction){ public on(type: string, f: T.AnyFunction) {
if(!this.socket){ if (!this.socket) {
if(!this.handlers[type]) if (!this.handlers[type])
this.handlers[type] = [] this.handlers[type] = []
this.handlers[type].push(f) this.handlers[type].push(f)
}else{ } else {
this.socket.on(type, f) this.socket.on(type, f)
} }
} }
@@ -96,16 +95,16 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param eventName The event name to emit under * @param eventName The event name to emit under
* @param data The data the event carries * @param data The data the event carries
*/ */
public emit(eventName:string, data:any){ public emit(eventName: string, data: any) {
if(!this.socket) return if (!this.socket) return
this.socket.emit(eventName, data) this.socket.emit(eventName, data)
} }
/** /**
* Closes the socket. It may attempt to reconnect. * Closes the socket. It may attempt to reconnect.
*/ */
public close(){ public close() {
if(!this.socket) return; if (!this.socket) return;
this.socket.close() this.socket.close()
} }
@@ -114,12 +113,13 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param rpcname The function to call * @param rpcname The function to call
* @param args other arguments * @param args other arguments
*/ */
public async call (rpcname: string, ...args: any[]) : Promise<any>{ public async call(rpcname: string, ...args: any[]): Promise<any> {
if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
try{
try {
const val = await this.socket.call.apply(this.socket, [rpcname, ...args]) const val = await this.socket.call.apply(this.socket, [rpcname, ...args])
return val return val
}catch(e){ } catch (e) {
this.emit('error', e) this.emit('error', e)
throw e throw e
} }
@@ -130,58 +130,58 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param rpcname The function to call * @param rpcname The function to call
* @param args other arguments * @param args other arguments
*/ */
public async fire(rpcname: string, ...args: any[]) : Promise<void>{ public async fire(rpcname: string, ...args: any[]): Promise<void> {
if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
await this.socket.fire.apply(this.socket, [rpcname, ...args]) await this.socket.fire.apply(this.socket, [rpcname, ...args])
} }
/** /**
* Connects to the server and attaches available RPCs to this object * Connects to the server and attaches available RPCs to this object
*/ */
public async connect( sesame?: string ) : Promise<T.ConnectedSocket<Ifc>> { public async connect(sesame?: string): Promise<T.ConnectedSocket<Ifc>> {
try{ try {
this.socket = await PromiseIOClient.connect(this.port, this.address, this.protocol) this.socket = await PromiseIOClient.connect(this.port, this.address, this.conf)
}catch(e){ } catch (e) {
this.handlers['error'].forEach(h => h(e)) this.handlers['error'].forEach(h => h(e))
throw e throw e
} }
Object.entries(this.handlers).forEach(([k,v])=>{ Object.entries(this.handlers).forEach(([k, v]) => {
v.forEach(h => this.socket.on(k, h)) v.forEach(h => this.socket.on(k, h))
}) })
Object.entries(this.hooks).forEach((kv: [string, T.AnyFunction]) => { Object.entries(this.hooks).forEach((kv: [string, T.AnyFunction]) => {
this.socket.hook(kv[0], kv[1]) this.socket.hook(kv[0], kv[1])
}) })
const info:T.ExtendedRpcInfo[] = await this.info(sesame) const info: T.ExtendedRpcInfo[] = await this.info(sesame)
info.forEach(i => { info.forEach(i => {
let f: any let f: any
switch (i.type) { switch (i.type) {
case 'Call': case 'Call':
f = this.callGenerator(i.uniqueName, i.argNames, sesame) f = this.callGenerator(i.uniqueName, i.argNames, sesame)
break break
case 'Hook': case 'Hook':
f = this.frontEndHookGenerator(i.uniqueName, i.argNames, sesame) f = this.frontEndHookGenerator(i.uniqueName, i.argNames, sesame)
break break
} }
if(this[i.owner] == null) if (this[i.owner] == null)
this[i.owner] = {} this[i.owner] = {}
this[i.owner][i.name] = f this[i.owner][i.name] = f
this[i.owner][i.name].bind(this) this[i.owner][i.name].bind(this)
}) })
return <T.ConnectedSocket<Ifc>> (this as any) return <T.ConnectedSocket<Ifc>>(this as any)
} }
/** /**
* Get a list of available RPCs from the server * Get a list of available RPCs from the server
*/ */
public async info(sesame?:string){ public async info(sesame?: string) {
if(!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first")
return await this.socket.call('info', sesame) return await this.socket.call('info', sesame)
} }
@@ -190,7 +190,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param fnName The function name * @param fnName The function name
* @param fnArgs A string-list of parameters * @param fnArgs A string-list of parameters
*/ */
private callGenerator(fnName: string, fnArgs:string[], sesame?:string): T.AnyFunction{ private callGenerator(fnName: string, fnArgs: string[], sesame?: string): T.AnyFunction {
const headerArgs = fnArgs.join(",") const headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",") const argParams = fnArgs.map(stripAfterEquals).join(",")
sesame = appendComma(sesame) sesame = appendComma(sesame)
@@ -205,15 +205,15 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param fnName The function name * @param fnName The function name
* @param fnArgs A string-list of parameters * @param fnArgs A string-list of parameters
*/ */
private frontEndHookGenerator(fnName: string, fnArgs:string[], sesame?:string): T.HookFunction{ private frontEndHookGenerator(fnName: string, fnArgs: string[], sesame?: string): T.HookFunction {
if(sesame) if (sesame)
fnArgs.shift() fnArgs.shift()
let headerArgs = fnArgs.join(",") let headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",") const argParams = fnArgs.map(stripAfterEquals).join(",")
sesame = appendComma(sesame, true) sesame = appendComma(sesame, true)
headerArgs = fnArgs.length>0?headerArgs+",":headerArgs headerArgs = fnArgs.length > 0 ? headerArgs + "," : headerArgs
const frontendHookStr = ` const frontendHookStr = `
async (${headerArgs} $__callback__$) => { async (${headerArgs} $__callback__$) => {
@@ -222,7 +222,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
if(r){ if(r){
if(r.uuid){ if(r.uuid){
$__callback__$['destroy'] = () => { $__callback__$['destroy'] = () => {
this.socket.fire(r.uuid) this.socket.fire('destroy_'+r.uuid)
this.socket.unhook(r.uuid) this.socket.unhook(r.uuid)
} }
this.socket.hook(r.uuid, $__callback__$) this.socket.hook(r.uuid, $__callback__$)
+15 -7
View File
@@ -2,18 +2,26 @@ import { Socket } from "socket.io"
import * as U from '../Utils' import * as U from '../Utils'
import * as I from '../Interfaces' import * as I from '../Interfaces'
import * as socketio from 'socket.io-client' import * as socketio from 'socket.io-client'
import { ClientConfig } from "../Types"
export const defaultClientConfig: ClientConfig = {
protocol: 'http',
reconnectionAttempts: 2,
reconnectionDelay: 200,
timeout: 450,
reconnection: false,
}
export class PromiseIOClient { export class PromiseIOClient {
static connect = (port: number, host = "localhost", protocol : 'http' | 'https' = "http"): Promise<I.Socket> => new Promise((res, rej) => { static connect = (port: number, host = "localhost", options : ClientConfig = defaultClientConfig): Promise<I.Socket> => new Promise((res, rej) => {
try { try {
if(options.path && !options.path.startsWith('/')){
options.path = "/"+options.path
}
const address = `${host}:${port}` const address = `${host}:${port}`
const socket = socketio(`${protocol}://${address}`, { const socket = socketio(`${options.protocol?options.protocol:'http'}://${address}`, options)
reconnectionAttempts: 2,
reconnectionDelay: 200,
timeout: 450,
reconnection: false,
})
socket.on('connect_error', e => { socket.on('connect_error', e => {
sock.emit('error', e) sock.emit('error', e)
+12 -7
View File
@@ -3,7 +3,12 @@ import { Server as httpServer } from "http"
import * as U from '../Utils' import * as U from '../Utils'
import * as T from '../Types' import * as T from '../Types'
import socketio = require('socket.io') import socketio = require('socket.io')
import middleware = require('socketio-wildcard');
const defaultConfig : socketio.ServerOptions = {
cookie: false,
path: '/socket.io',
}
export class PromiseIO { export class PromiseIO {
io?: Server io?: Server
@@ -13,20 +18,20 @@ export class PromiseIO {
connect: [] connect: []
} }
attach(httpServer: httpServer) { attach(httpServer: httpServer, options: socketio.ServerOptions = defaultConfig) {
if(options.path && !options.path.startsWith('/')){
options.path = "/"+options.path
}
this.httpServer = httpServer this.httpServer = httpServer
this.io = socketio(httpServer, { cookie:false }) this.io = socketio(httpServer, options)
this.io!.use(middleware())
this.io!.on('connection', (clientSocket: Socket) => { this.io!.on('connection', (clientSocket: Socket) => {
clientSocket.use((packet, next) => {
next()
})
clientSocket['address'] = clientSocket.handshake.headers["x-real-ip"] || clientSocket.handshake.address clientSocket['address'] = clientSocket.handshake.headers["x-real-ip"] || clientSocket.handshake.address
const pioSock = U.makePioSocket(clientSocket) const pioSock = U.makePioSocket(clientSocket)
this.listeners['socket'].forEach(listener => listener(pioSock)) this.listeners['socket'].forEach(listener => listener(pioSock))
this.listeners['connect'].forEach(listener => listener(pioSock)) this.listeners['connect'].forEach(listener => listener(pioSock))
/* /*
pioSock.on('error', ()=>console.log('error')); pioSock.on('error', ()=>console.log('error'));
+5 -6
View File
@@ -10,7 +10,7 @@ export type HookFunction = AnyFunction
export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame:string|undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean> | boolean export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame:string|undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean> | boolean
export type Visibility = "127.0.0.1" | "0.0.0.0" export type Visibility = "127.0.0.1" | "0.0.0.0"
export type ConnectionHandler = (socket:I.Socket) => void export type ConnectionHandler = (socket:I.Socket) => void
export type ErrorHandler = (socket:I.Socket | PromiseIO, error:any, rpcName: string, args: any[]) => void export type ErrorHandler = (socket:I.Socket, error:any, rpcName: string, args: 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 SesameConf = { export type SesameConf = {
@@ -20,7 +20,9 @@ export type FrontEndHandlerType = {
'error' : (e: any) => void 'error' : (e: any) => void
'close' : () => void 'close' : () => void
} }
export type ClientConfig = SocketIOClient.ConnectOpts & {
protocol?: 'http' | 'https'
}
export type ExporterArray<InterfaceT extends RPCInterface = RPCInterface> = I.RPCExporter<RPCInterface<InterfaceT>, keyof InterfaceT>[] export type ExporterArray<InterfaceT extends RPCInterface = RPCInterface> = I.RPCExporter<RPCInterface<InterfaceT>, keyof InterfaceT>[]
export type ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & AsyncIfc<T> export type ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & AsyncIfc<T>
@@ -30,12 +32,9 @@ export type ServerConf<InterfaceT extends RPCInterface> = {
connectionHandler?: ConnectionHandler connectionHandler?: ConnectionHandler
errorHandler?: ErrorHandler errorHandler?: ErrorHandler
closeHandler?: CloseHandler closeHandler?: CloseHandler
throwOnUnknownRPC?: boolean
} & SesameConf } & SesameConf
export type SocketConf = {
tls:boolean
}
export type ResponseType = "Subscribe" | "Success" | "Error" export type ResponseType = "Subscribe" | "Success" | "Error"
export type Outcome = "Success" | "Error" export type Outcome = "Success" | "Error"
+14 -12
View File
@@ -56,22 +56,22 @@ RPC did not provide a name.
/** /**
* Utility function to apply the RPCs of an {@link RPCExporter}. * Utility function to apply the RPCs of an {@link RPCExporter}.
* @param serverSocket The websocket (implementation: socket.io) to hook on * @param socket The websocket (implementation: socket.io) to hook on
* @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(serverSocket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] { export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] {
const owner = exporter.name const owner = exporter.name
const RPCs = typeof exporter.RPCs === "function" ? exporter.RPCs() : exporter.RPCs const RPCs = typeof exporter.RPCs === "function" ? exporter.RPCs() : exporter.RPCs
return RPCs return RPCs
.map(rpc => rpcToRpcinfo(serverSocket, rpc, owner, errorHandler, sesame)) .map(rpc => rpcToRpcinfo(socket, 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 = info.type === 'Hook' ? info.generator(serverSocket) : info.call let rpcFunction = info.type === 'Hook' ? info.generator(socket) : info.call
serverSocket.hook(ret.uniqueName, callGenerator(info.name, serverSocket, rpcFunction, errorHandler)) socket.hook(ret.uniqueName, callGenerator(info.name, socket, rpcFunction, errorHandler))
return ret return ret
}) })
} }
@@ -89,7 +89,7 @@ const callGenerator = (rpcName: string, $__socket__$: I.Socket, rpcFunction: T.A
try{ try{
return await rpcFunction(${argsStr}) return await rpcFunction(${argsStr})
}catch(e){ }catch(e){
errorHandler($__socket__$)(e, rpcName, [${args}]) errorHandler($__socket__$, e, rpcName, [${args}])
} }
}` }`
@@ -115,7 +115,7 @@ const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, s
let callArgs = argsArr.join(',') let callArgs = argsArr.join(',')
const args = sesameFn ? (['sesame', ...argsArr].join(',')) const args = sesameFn ? (['sesame', ...argsArr].join(','))
: callArgs : callArgs
callArgs = appendComma(callArgs, false) callArgs = appendComma(callArgs, false)
@@ -128,20 +128,20 @@ const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, s
${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``} ${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``}
$__socket__$.call.apply($__socket__$, [uuid, ...cbargs]) $__socket__$.call.apply($__socket__$, [uuid, ...cbargs])
}) })
${rpc.onDestroy ? `$__socket__$.bind(uuid, () => { ${rpc.onDestroy ? `$__socket__$.bind('destroy_'+uuid, () => {
rpc.onDestroy(res, rpc) rpc.onDestroy(res, rpc)
})` : ``} })` : ``}
return {'uuid': uuid, 'return': res} return {'uuid': uuid, 'return': res}
}catch(e){ }catch(e){
//can throw to pass exception to client or swallow to keep it local //can throw to pass exception to client or swallow to keep it local
errorHandler($__socket__$)(e, ${rpc.name}, [${args}]) errorHandler($__socket__$, e, ${rpc.name}, [${args}])
} }
}` }`
return eval(hookStr) return eval(hookStr)
} }
const makeError = (callName: string) => new Error(`Call not found: ${callName}. ; Zone: <root> ; Task: Promise.then ; Value: Error: Call not found: ${callName}`) const makeError = (callName: string) => new Error(`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
@@ -194,7 +194,9 @@ export function fixNames(o: Object): void {
export const makePioSocket = (socket: any): I.Socket => { export const makePioSocket = (socket: any): I.Socket => {
return <I.Socket>{ return <I.Socket>{
id: socket.id, id: socket.id,
bind: (name: string, listener: T.PioBindListener) => socket.on(name, (...args: any) => listener.apply(null, args)), bind: (name: string, listener: T.PioBindListener) => {
socket.on(name, (...args: any) => listener.apply(null, args))
},
hook: (name: string, listener: T.PioHookListener) => { hook: (name: string, listener: T.PioHookListener) => {
const args = extractArgs(listener) const args = extractArgs(listener)
@@ -244,7 +246,7 @@ export const makePioSocket = (socket: any): I.Socket => {
fire: (name: string, ...args: any) => new Promise((res, rej) => { fire: (name: string, ...args: any) => new Promise((res, rej) => {
const params: any = [name, ...args] const params: any = [name, ...args]
socket.emit.apply(socket, params) socket.emit.apply(socket, params)
res() res(undefined)
}), }),
unhook: (name: string, listener?: T.AnyFunction) => { unhook: (name: string, listener?: T.AnyFunction) => {
+215 -79
View File
@@ -12,7 +12,7 @@ import { PromiseIOClient } from "../src/PromiseIO/Client";
const noop = (...args) => { } const noop = (...args) => { }
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(onCallback = noop, connectionHandler = noop, hookCloseHandler = noop, closeHandler = noop, errorHandler = (socket, err) => { throw err }) { function makeServer(onCallback = noop, connectionHandler = noop, hookCloseHandler = noop, closeHandler = noop, errorHandler = noop) {
let subcallback let subcallback
const serv = new RPCServer([{ const serv = new RPCServer([{
name: 'test', name: 'test',
@@ -38,7 +38,7 @@ function makeServer(onCallback = noop, connectionHandler = noop, hookCloseHandle
}, },
add, add,
function triggerCallback(...messages: any[]): number { return subcallback.apply({}, messages) }, function triggerCallback(...messages: any[]): number { return subcallback.apply({}, messages) },
function brokenRPC(){ throw new Error("Intended error") } function brokenRPC() { throw new Error("Intended error") }
] ]
}], }],
{ {
@@ -57,15 +57,15 @@ describe('PromiseIO', () => {
const server = new PromiseIO() const server = new PromiseIO()
server.attach(new http.Server()) server.attach(new http.Server())
server.on("socket", clientSocket => { server.on("socket", clientSocket => {
clientSocket.bind("test123", (p1,p2) => { clientSocket.bind("test123", (p1, p2) => {
server.close() server.close()
if(p1 === "p1" && p2 === "p2") if (p1 === "p1" && p2 === "p2")
done() done()
}) })
}); });
server.listen(21003) server.listen(21003)
PromiseIOClient.connect(21003, "localhost", "http").then(cli => { PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => {
cli.fire("test123", "p1", "p2") cli.fire("test123", "p1", "p2")
cli.close() cli.close()
}) })
@@ -75,19 +75,19 @@ describe('PromiseIO', () => {
const server = new PromiseIO() const server = new PromiseIO()
server.attach(new http.Server()) server.attach(new http.Server())
server.on("socket", clientSocket => { server.on("socket", clientSocket => {
clientSocket.hook("test123", (p1,p2) => { clientSocket.hook("test123", (p1, p2) => {
if(p1 === "p1" && p2 === "p2") if (p1 === "p1" && p2 === "p2")
return "OK" return "OK"
}) })
}); });
server.listen(21003) server.listen(21003)
PromiseIOClient.connect(21003, "localhost", "http").then(cli => { PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => {
cli.call("test123", "p1", "p2").then(resp => { cli.call("test123", "p1", "p2").then(resp => {
cli.close() cli.close()
server.close() server.close()
if(resp === "OK") if (resp === "OK")
done() done()
}) })
}) })
@@ -97,15 +97,15 @@ describe('PromiseIO', () => {
const server = new PromiseIO() const server = new PromiseIO()
server.attach(new http.Server()) server.attach(new http.Server())
server.on("socket", clientSocket => { server.on("socket", clientSocket => {
clientSocket.on("test123", (p1,p2) => { clientSocket.on("test123", (p1, p2) => {
server.close() server.close()
if(p1 === "p1" && p2 === "p2") if (p1 === "p1" && p2 === "p2")
done() done()
}) })
}); });
server.listen(21003) server.listen(21003)
PromiseIOClient.connect(21003, "localhost", "http").then(cli => { PromiseIOClient.connect(21003, "localhost", { protocol: 'http' }).then(cli => {
cli.emit("test123", "p1", "p2") cli.emit("test123", "p1", "p2")
cli.close() cli.close()
}) })
@@ -256,6 +256,194 @@ describe('RPCServer with premade http server', () => {
}) })
}) })
describe('should be able to attach to non-standard path', () => {
let client: RPCSocket, server: RPCServer
const echo = (x) => x
before(done => {
server = new RPCServer([{
name: 'HelloWorldRPCGroup',
RPCs: () => [
echo, //named function variable
function echof(x) { return x }, //named function
{
name: 'echoExplicit', //describing object
call: async (x, y, z) => [x, y, z]
}
]
}])
server.listen(21003, {path: '/test'})
client = new RPCSocket(21003, 'localhost', {path: '/test'})
done()
})
after(done => {
client.close()
server.close()
done()
})
it('should be able to use all kinds of RPC definitions', (done) => {
client.connect().then(async () => {
const r0 = await client['HelloWorldRPCGroup'].echo('Hello')
const r1 = await client['HelloWorldRPCGroup'].echof('World')
const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!')
if (r0 === 'Hello' && r1 === 'World' && r2.join('') === 'RPC!') {
done()
} else {
done(new Error("Bad response"))
}
})
})
})
describe('can attach multiple RPCServers to same http server', () => {
const echo = (x) => x
const RPCs = [
echo, //named function variable
function echof(x) { return x }, //named function
{
name: 'echoExplicit', //describing object
call: async (x, y, z) => [x, y, z]
}
]
const RPCExporters = [
{
name: 'HelloWorldRPCGroup',
RPCs: RPCs,
}
]
const RPCExporters2 = [
{
name: 'Grp2',
RPCs: [
function test() { return "/test" }
],
}
]
let client: RPCSocket, client2: RPCSocket, server: RPCServer, server2: RPCServer
before(done => {
const expressServer = express()
const httpServer = new http.Server(expressServer)
server = new RPCServer(
RPCExporters,
)
server2 = new RPCServer(
RPCExporters2
)
server.attach(httpServer)
server2.attach(httpServer, {
path: "test"
})
httpServer.listen(8080)
new RPCSocket(8080, 'localhost').connect().then(sock => {
client = sock
new RPCSocket(8080, 'localhost', { path: "test" }).connect().then(sock2 => {
client2 = sock2
done()
})
})
})
after(done => {
client.close()
client2.close()
server.close()
server2.close()
done()
})
it('both servers should answer', (done) => {
client['HelloWorldRPCGroup'].echo("test").then(res => {
if(res != "test"){
done(new Error("response was "+res))
}else{
client2['Grp2'].test().then(res => {
if(res != "/test"){
done(new Error("response2 was "+res))
}else{
done()
}
})
}
})
})
})
describe("can attach second RPCServer if first is already running", () => {
const RPCExporters = [
{
name: 'HelloWorldRPCGroup',
RPCs: [
function echo (x) { return x}, //named function variable
function echof(x) { return x }, //named function
{
name: 'echoExplicit', //describing object
call: async (x, y, z) => [x, y, z]
}
],
}
]
const RPCExporters2 = [
{
name: 'Grp2',
RPCs: [
function test() { return "/test" }
],
}
]
it("attaches correctly", done => {
const expressServer = express()
const httpServer = new http.Server(expressServer)
const server = new RPCServer(
RPCExporters,
)
const server2 = new RPCServer(
RPCExporters2
)
server.attach(httpServer)
httpServer.listen(8080)
server2.attach(httpServer, {
path: "test"
})
new RPCSocket(8080, 'localhost').connect().then(sock => {
new RPCSocket(8080, 'localhost', { path: "test" }).connect().then(sock2 => {
sock2.Grp2.test().then(resp => {
if(resp === "/test")
done()
else
done(new Error("response did not match"))
server.close()
server2.close()
sock.close()
sock2.close()
})
})
})
})
})
describe('Serverside Triggers', () => { describe('Serverside Triggers', () => {
let server, client let server, client
@@ -273,23 +461,24 @@ describe('Serverside Triggers', () => {
}) })
}) })
/* testing framework has trouble terminating on this one
it('trigger connectionHandler', (done) => { it('trigger connectionHandler', (done) => {
server = makeServer(undefined, closerFunction(done)) server = makeServer(undefined, closerFunction(done))
client = new RPCSocket(21010, "localhost") client = new RPCSocket(21010, "localhost")
client.connect() client.connect()
}) })
*/
it('trigger hook closeHandler', (done) => { it('trigger hook closeHandler', (done) => {
server = makeServer(undefined, undefined, closerFunction(done)) server = makeServer(undefined, undefined, closerFunction(done))
client = new RPCSocket(21010, "localhost") client = new RPCSocket(21010, "localhost")
client.connect().then(_ => { client.connect().then(_ => {
client['test'].subscribe(function cb(){ client['test'].subscribe(function cb() {
cb['destroy']() cb['destroy']()
}).then(_ => client['test'].triggerCallback()) }).then(_ => client['test'].triggerCallback())
}) })
}) })
it('trigger global closeHandler', (done) => { it('trigger global closeHandler', (done) => {
server = makeServer(undefined, undefined, undefined, () => { server = makeServer(undefined, undefined, undefined, () => {
@@ -301,8 +490,6 @@ describe('Serverside Triggers', () => {
client['test'].subscribe(noop).then(_ => client.close()) client['test'].subscribe(noop).then(_ => client.close())
}) })
}) })
}) })
describe('RPCSocket', () => { describe('RPCSocket', () => {
@@ -512,7 +699,7 @@ describe('Sesame should unlock the socket', () => {
client.close() client.close()
server.close() server.close()
}) })
it('should work with sesame', (done) => { it('should work with sesame', (done) => {
client.test.checkCandy().then(c => done()) client.test.checkCandy().then(c => done())
}) })
@@ -526,7 +713,7 @@ describe('Sesame should unlock the socket', () => {
it('should not work without sesame', (done) => { it('should not work without sesame', (done) => {
const sock = new RPCSocket(21004, "localhost") const sock = new RPCSocket(21004, "localhost")
sock.connect( /* no sesame */).then(async (cli) => { sock.connect().then(async (cli) => {
if (!cli.test) if (!cli.test)
done() done()
else { else {
@@ -733,7 +920,6 @@ type myExporterIfc = {
} }
} }
describe("Class binding", () => { describe("Class binding", () => {
let exporter1: MyExporter let exporter1: MyExporter
@@ -802,32 +988,6 @@ describe("Class binding", () => {
serv.close() serv.close()
}) })
/* The server-side socket will enter a 30s timeout if destroyed by a RPC.
to mitigate the impact on testing time these are not run.
it("binds correctly", function(done){
this.timeout(1000)
sock['MyExporter'].myRPC().then((res) => {
done(new Error(res))
}).catch(e => {
//job will time out because of setExporters
allowed = true
done()
})
})
it("changes exporters", (done) => {
sock['MyExporter'].myRPC().then((res) => {
if (res === "Hello Borld")
done()
else
done(new Error(res))
})
})
*/
it("use sesameFilter for available", (done) => { it("use sesameFilter for available", (done) => {
if (sock['MyExporter']) { if (sock['MyExporter']) {
allowed = false allowed = false
@@ -842,6 +1002,14 @@ describe("Class binding", () => {
}) })
}) })
/*
describe('finally', () => {
it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => {
//log(console)
})
})
*/
describe("attaching handlers before connecting", () => { describe("attaching handlers before connecting", () => {
it("fires error if server is unreachable", (done) => { it("fires error if server is unreachable", (done) => {
@@ -870,17 +1038,8 @@ describe("attaching handlers before connecting", () => {
}) })
}) })
/*
* ## 1.11.0 breaking ##
*
* API change: Move from bsock to socketio changes underlying API for when errors are thrown.
* socketio does not throw on unknown listener. This behaviour is considered more consistent with the design
* goals of RPClibrary and was thus adopted
*
it("fires error if call is unknown", (done) => { it("fires error if call is unknown", (done) => {
const serv = new RPCServer(21004) const serv = new RPCServer().listen(21004)
const sock = new RPCSocket(21004, 'localhost') const sock = new RPCSocket(21004, 'localhost')
sock.on('error', (err) => { sock.on('error', (err) => {
@@ -900,27 +1059,4 @@ describe("attaching handlers before connecting", () => {
}) })
}) })
it("demands catch on method invocation if call is unknown", (done) => {
const serv = new RPCServer(21004)
const sock = new RPCSocket(21004, 'localhost')
sock.connect().then(_ => {
sock.call("unknownRPC123", "AAAAA").catch(e => {
sock.close()
serv.close()
done()
})
}).catch(e => {
console.log("unexpected connect catch clause");
done(e)
})
})
*/
}) })
describe('finally', () => {
it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => {
//log(console)
})
})