fix listener bugs

This commit is contained in:
2020-11-29 05:50:53 +01:00
parent 2b722822cf
commit 8352ac1bf7
9 changed files with 278 additions and 144 deletions
+12 -10
View File
@@ -10,7 +10,7 @@ export class RPCServer<
InterfaceT extends T.RPCInterface = T.RPCInterface,
> {
private pio = PromiseIO.createServer()
private pio = new PromiseIO()
private closeHandler: T.CloseHandler
private errorHandler: T.ErrorHandler
private connectionHandler: T.ConnectionHandler
@@ -37,7 +37,6 @@ export class RPCServer<
return this.sesame!(sesame!)
})
this.errorHandler = (socket: I.Socket | PromiseIO) => (error: any, rpcName: string, args: any[]) => {
if (conf.errorHandler) conf.errorHandler(socket, error, rpcName, args)
else throw error
@@ -65,11 +64,13 @@ export class RPCServer<
}
try {
this.pio.on('socket', (socket: I.Socket) => {
socket.on('error', (err) => this.errorHandler(socket, err, "system", []))
socket.on('close', () => this.closeHandler(socket))
this.connectionHandler(socket)
this.initRPCs(socket)
this.pio.on('socket', (clientSocket: I.Socket) => {
const sock:any = clientSocket;
clientSocket.on('disconnect', () => this.closeHandler(clientSocket))
this.connectionHandler(clientSocket)
this.initRPCs(clientSocket)
})
} catch (e) {
this.errorHandler(this.pio, e, 'system', [])
@@ -88,12 +89,13 @@ export class RPCServer<
return this
}
protected initRPCs(socket: I.Socket) {
socket.hook('info', async (sesame?: string) => {
protected initRPCs(clientSocket: I.Socket) {
clientSocket.hook('info', async (sesame?: string) => {
const rpcs = await Promise.all(this.exporters.map(async exp => {
const allowed = await this.accessFilter(sesame, exp)
if (!allowed) return []
return U.rpcHooker(socket, exp, this.errorHandler, this.sesame)
const infos = U.rpcHooker(clientSocket, exp, this.errorHandler, this.sesame)
return infos
}))
return rpcs.flat()
})
+13 -9
View File
@@ -16,7 +16,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
return await socket.connect(sesame)
}
private protocol: 'http:' | 'https:'
private protocol: 'http' | 'https'
private socket: I.Socket
private handlers : {
[name in string]: T.AnyFunction[]
@@ -32,9 +32,9 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param server Server address
* @param tls @default false use TLS
*/
constructor(public port:number, private server: string, conf:T.SocketConf = { tls: false }){
constructor(public port:number, public address: string, conf:T.SocketConf = { tls: false }){
Object.defineProperty(this, 'socket', {value: undefined, writable: true})
this.protocol = conf.tls ? "https:" : "http:"
this.protocol = conf.tls ? "https" : "http"
}
/**
@@ -141,7 +141,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
public async connect( sesame?: string ) : Promise<T.ConnectedSocket<Ifc>> {
try{
this.socket = await PromiseIOClient.connect(this.port, this.server, this.protocol)
this.socket = await PromiseIOClient.connect(this.port, this.address, this.protocol)
}catch(e){
this.handlers['error'].forEach(h => h(e))
throw e
@@ -215,14 +215,17 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
sesame = appendComma(sesame, true)
headerArgs = fnArgs.length>0?headerArgs+",":headerArgs
return eval( `
async (${headerArgs} callback) => {
const frontendHookStr = `
async (${headerArgs} $__callback__$) => {
const r = await this.call("${fnName}", ${sesame} ${argParams})
try{
if(r){
if(r.uuid){
callback['destroy'] = () => { this.socket.unhook(r.uuid) }
this.socket.hook(r.uuid, callback)
$__callback__$['destroy'] = () => {
this.socket.fire(r.uuid)
this.socket.unhook(r.uuid)
}
this.socket.hook(r.uuid, $__callback__$)
}
return r.return
}else{
@@ -231,6 +234,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
}catch(e){
throw e
}
}`)
}`
return eval(frontendHookStr)
}
}
+1 -1
View File
@@ -20,6 +20,6 @@ export interface Socket {
call: (rpcname: string, ...args: any[]) => Promise<any>
fire: (rpcname: string, ...args: any[]) => Promise<any>
on: (type: string, f: T.AnyFunction)=>any
emit: (eventName: string, data: any) => void
emit: (eventName: string, ...args: any[]) => void
close(): void
}
+8 -3
View File
@@ -5,21 +5,26 @@ import * as socketio from 'socket.io-client'
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", protocol : 'http' | 'https' = "http"): Promise<I.Socket> => new Promise((res, rej) => {
try {
const socket = socketio(`${protocol}//${host}:${port}`, {
const address = `${host}:${port}`
const socket = socketio(`${protocol}://${address}`, {
reconnectionAttempts: 2,
reconnectionDelay: 200,
timeout: 450,
reconnection: false,
})
socket.on('connect_error', e => {
sock.emit('error', e)
rej(e)
})
socket['address'] = address
const sock = U.makePioSocket(socket)
socket.on('connect', ()=>{ res(sock) })
socket.on('connect', ()=>{
res(sock)
})
/*
+15 -6
View File
@@ -4,6 +4,7 @@ import * as U from '../Utils'
import * as T from '../Types'
import socketio = require('socket.io')
export class PromiseIO {
io?: Server
httpServer: httpServer
@@ -12,15 +13,23 @@ export class PromiseIO {
connect: []
}
static createServer(): PromiseIO {
return new PromiseIO();
}
attach(httpServer: httpServer) {
this.httpServer = httpServer
this.io = socketio(httpServer, { cookie:false })
this.io!.on('connection', (sock: Socket) => {
const pioSock = U.makePioSocket(sock)
this.io!.on('connection', (clientSocket: Socket) => {
console.log(this.listeners);
clientSocket.use((packet, next) => {
console.log(this.listeners[packet[0]]);
console.log(packet[0]);
next()
})
clientSocket['address'] = clientSocket.handshake.headers["x-real-ip"] || clientSocket.handshake.address
const pioSock = U.makePioSocket(clientSocket)
this.listeners['socket'].forEach(listener => listener(pioSock))
this.listeners['connect'].forEach(listener => listener(pioSock))
/*
+4 -4
View File
@@ -5,8 +5,6 @@ import { PromiseIO } from "./PromiseIO/Server";
export type PioBindListener = (...args: any) => void
export type PioHookListener = AnyFunction
export type AnyFunction = (...args:any) => any
export type HookFunction = AnyFunction
export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame:string|undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean> | boolean
@@ -57,7 +55,7 @@ export type HookRPC<Name, Func extends AnyFunction> = {
name: Name
hook: Func
onCallback?: AnyFunction
onClose?: HookCloseFunction<ReturnType<Func> extends Promise<infer T> ? T : ReturnType<Func>>
onDestroy?: HookCloseFunction<ReturnType<Func> extends Promise<infer T> ? T : ReturnType<Func>>
}
export type RPC<Name, Func extends AnyFunction> = HookRPC<Name, Func> | CallRPC<Name,Func> | Func
@@ -101,4 +99,6 @@ export type HookCloseFunction<T> = (res: T, rpc:HookRPC<any, any>) => any
export type AsyncIfc<Ifc extends RPCInterface> = { [grp in keyof Ifc]: {[rpcname in keyof Ifc[grp]] : AsyncAnyFunction<Ifc[grp][rpcname]> } }
export type AsyncAnyFunction<F extends AnyFunction = AnyFunction> = F extends (...args: Parameters<F>) => infer R ? ((...args: Parameters<F>) => R extends Promise<any> ? R : Promise<R> ) : Promise<any>
export type AsyncAnyFunction<F extends AnyFunction = AnyFunction> = F extends (...args: Parameters<F>) => infer R
? ((...args: Parameters<F>) => R extends Promise<any> ? R : Promise<R> )
: Promise<any>
+54 -57
View File
@@ -2,8 +2,7 @@ import * as uuidv4 from "uuid/v4"
import * as T from "./Types";
import * as I from "./Interfaces";
import { Server as ioServer, Socket as ioSocket, Socket } from "socket.io"
import { Socket as ioClientSocket } from "socket.io-client"
import { Socket } from "socket.io"
/**
* Translate an RPC to RPCInfo for serialization.
@@ -17,20 +16,22 @@ export const rpcToRpcinfo = (socket: I.Socket, rpc: T.RPC<any, any>, owner: stri
switch (typeof rpc) {
case "object":
if (rpc['call']) {
const _rpc: T.CallRPC<any, any> = rpc
return {
owner: owner,
argNames: extractArgs(rpc['call']),
type: "Call",
name: rpc.name,
call: sesame ? async (_sesame, ...args) => { if (sesame(_sesame)) return await rpc['call'].apply({}, args); socket.close() } : rpc['call'], // check & remove sesame
call: sesame ? async ($__sesame__$, ...args) => { if (sesame($__sesame__$)) return await rpc['call'].apply({}, args); socket.close() } : rpc['call'], // check & remove sesame
}
} else {
const generator = hookGenerator(<T.HookRPC<any, any>>rpc, errorHandler, sesame)
const _rpc: T.HookRPC<any, any> = rpc
const generator = hookGenerator(_rpc, errorHandler, sesame)
return {
owner: owner,
argNames: extractArgs(generator(undefined)),
type: "Hook",
name: rpc.name,
name: _rpc.name,
generator: generator,
}
}
@@ -47,7 +48,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); throw makeError(rpc.name) } : 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)
@@ -55,23 +56,22 @@ RPC did not provide a name.
/**
* Utility function to apply the RPCs of an {@link RPCExporter}.
* @param socket The websocket (implementation: bsock) to hook on
* @param serverSocket The websocket (implementation: socket.io) to hook on
* @param exporter The exporter
* @param makeUnique @default true Attach a suffix to RPC names
*/
export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] {
export function rpcHooker(serverSocket: I.Socket, exporter: I.RPCExporter<any, any>, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] {
const owner = exporter.name
const RPCs = typeof exporter.RPCs === "function" ? exporter.RPCs() : exporter.RPCs
return RPCs.map(rpc => rpcToRpcinfo(socket, rpc, owner, errorHandler, sesame))
return RPCs
.map(rpc => rpcToRpcinfo(serverSocket, rpc, owner, errorHandler, sesame))
.map(info => {
const suffix = makeUnique ? "-" + uuidv4().substr(0, 4) : ""
const ret: any = info
ret.uniqueName = info.name + suffix
let rpcFunction = info.type === 'Hook' ? info.generator(socket)
: info.call
socket.hook(ret.uniqueName, callGenerator(info.name, socket, rpcFunction, errorHandler))
let rpcFunction = info.type === 'Hook' ? info.generator(serverSocket) : info.call
serverSocket.hook(ret.uniqueName, callGenerator(info.name, serverSocket, rpcFunction, errorHandler))
return ret
})
}
@@ -80,18 +80,20 @@ export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter<any, any>, e
* Decorate an RPC with the error handler
* @param rpcFunction the function to decorate
*/
const callGenerator = (rpcName: string, socket: I.Socket, rpcFunction: T.AnyFunction, errorHandler: T.ErrorHandler): T.AnyFunction => {
const callGenerator = (rpcName: string, $__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 + `) => {
const callStr = `async (${args}) => {
try{
return await rpcFunction(`+ argsStr + `)
return await rpcFunction(${argsStr})
}catch(e){
errorHandler(socket)(e, rpcName, [`+ args + `])
errorHandler($__socket__$)(e, rpcName, [${args}])
}
}`)
}`
return eval(callStr);
}
/**
@@ -107,43 +109,39 @@ export function stripAfterEquals(str: string): string {
* @param rpc The RPC to transform
* @returns A {@link HookFunction}
*/
const hookGenerator = (rpc: T.HookRPC<any, any>, /*not unused!*/ errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => {
const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction, injectSocket?: boolean): T.HookInfo['generator'] => {
let argsArr = extractArgs(rpc.hook)
argsArr.pop() //remove 'callback' from the end
let callArgs = argsArr.join(',')
argsArr.pop() //remove callback param
let callArgs = argsArr.join(',')
const args = sesameFn ? (['sesame', ...argsArr].join(','))
: callArgs
: callArgs
callArgs = appendComma(callArgs, false)
//note rpc.hook is the associated RPC, not a socket.hook
return eval(`
(clientSocket) => async (${args}) => {
const hookStr = `
($__socket__$) => async (${args}) => {
try{
if(sesameFn && !sesameFn(sesame)) return
const uuid = uuidv4()
const res = await rpc.hook(${callArgs} (...cbargs) => {
if(rpc.onCallback){
rpc.onCallback.apply({}, cbargs)
}
clientSocket.call.apply(clientSocket, [uuid, ...cbargs])
${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``}
$__socket__$.call.apply($__socket__$, [uuid, ...cbargs])
})
if(rpc.onClose){
clientSocket.on('close', () => rpc.onClose(res, rpc))
}
${rpc.onDestroy ? `$__socket__$.bind(uuid, () => {
rpc.onDestroy(res, rpc)
})` : ``}
return {'uuid': uuid, 'return': res}
}catch(e){
//can throw to pass exception to client or swallow to keep it local
errorHandler(clientSocket)(e, ${rpc.name}, [${args}])
errorHandler($__socket__$)(e, ${rpc.name}, [${args}])
}
}`)
}`
return eval(hookStr)
}
const makeError = (callName: string) => {
return 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
@@ -189,24 +187,25 @@ export function fixNames(o: Object): void {
})
}
/**
* Transforms a socket.io instance into one conforming to I.Socket
* @param socket A socket.io socket
*/
export const makePioSocket = (socket: any): I.Socket => {
return {
bind: (name: string, listener: T.PioBindListener) => socket.on(name, (...args: any) => {
const ack = args.pop()
listener.apply(null, args)
ack()
}),
return <I.Socket>{
id: socket.id,
bind: (name: string, listener: T.PioBindListener) => socket.on(name, (...args: any) => listener.apply(null, args)),
hook: (name: string, listener: T.PioHookListener) => {
const args = extractArgs(listener)
let argNames
let restParam = args.find(e => e.includes('...'))
if(!restParam){
argNames = [...args, '...__args__'].join(',')
restParam = '__args__'
}else{
if (!restParam) {
argNames = [...args, '...$__args__$'].join(',')
restParam = '$__args__$'
} else {
argNames = [...args].join(',')
restParam = restParam.replace('...','')
restParam = restParam.replace('...', '')
}
const decoratedListener = eval(`(() => async (${argNames}) => {
@@ -229,7 +228,7 @@ export const makePioSocket = (socket: any): I.Socket => {
call: (name: string, ...args: any) => {
return new Promise((res, rej) => {
const params: any = [name, ...args, (resp) => {
if(isError(resp)){
if (isError(resp)) {
const err = new Error()
err.stack = resp.stack
err.name = resp.name
@@ -238,7 +237,7 @@ export const makePioSocket = (socket: any): I.Socket => {
}
res(resp)
}]
socket.emit.apply(socket, params)
socket.emit.apply(socket, params)
})
},
@@ -256,17 +255,15 @@ export const makePioSocket = (socket: any): I.Socket => {
}
},
id: socket.id,
on: (...args) => socket.on.apply(socket, args),
emit: (...args) => socket.emit.apply(socket, args),
close: () => {
socket
socket.disconnect(true)
}
}
}
export const isError = function(e){
return e && e.stack && e.message && typeof e.stack === 'string'
&& typeof e.message === 'string';
}
export const isError = function (e) {
return e && e.stack && e.message && typeof e.stack === 'string'
&& typeof e.message === 'string';
}