clean up type system, remove subres

This commit is contained in:
2020-03-17 23:35:47 +01:00
parent bb94b1c405
commit 7d580c4c23
9 changed files with 459 additions and 462 deletions
+36 -40
View File
@@ -2,30 +2,26 @@
import http = require('http');
import bsock = require('bsock');
import * as T from './Types';
import * as U from './Utils';
import * as T from './Types';
import * as U from './Utils';
import * as I from './Interfaces';
import { Socket } from 'dgram';
type Exporters<InterfaceT extends T.RPCInterface = T.RPCInterface, SubResType = {}> = I.RPCExporter<T.RPCInterface<InterfaceT>, keyof InterfaceT, SubResType>[]
/**
* A Websocket-server-on-steroids with built-in RPC capabilities
*/
export class RPCServer<
SubResType = {},
InterfaceT extends T.RPCInterface = T.RPCInterface,
> implements I.Destroyable{
> implements I.Destroyable {
private ws = http.createServer()
private io = bsock.createServer()
private visibility:T.Visibility
private closeHandler:T.CloseHandler
private visibility: T.Visibility
private closeHandler: T.CloseHandler
private errorHandler: T.ErrorHandler
private connectionHandler: T.ConnectionHandler
private sesame? : T.SesameFunction
private accessFilter: T.AccessFilter<InterfaceT, SubResType>
private sesame?: T.SesameFunction
private accessFilter: T.AccessFilter<InterfaceT>
/**
* @throws On RPC with no name
@@ -34,71 +30,70 @@ export class RPCServer<
* @param conf A {@link SocketConf} object with optional settings
*/
constructor(
private port:number,
private exporters: Exporters<InterfaceT, SubResType> = [],
conf: T.ServerConf<InterfaceT, SubResType> = {}
){
if(!conf.visibility) this.visibility = "0.0.0.0"
private port: number,
private exporters: T.ExporterArray<InterfaceT> = [],
conf: T.ServerConf<InterfaceT> = {}
) {
if (!conf.visibility) this.visibility = "0.0.0.0"
if(conf.sesame){
if (conf.sesame) {
this.sesame = U.makeSesameFunction(conf.sesame)
}
this.accessFilter = conf.accessFilter || (async (sesame) => {
if(!this.sesame) return true
if (!this.sesame) return true
return this.sesame!(sesame!)
})
this.errorHandler = (socket:I.Socket) => (error:any, rpcName:string, args: any[]) => {
if(conf.errorHandler) conf.errorHandler(socket, error, rpcName, args)
this.errorHandler = (socket: I.Socket) => (error: any, rpcName: string, args: any[]) => {
if (conf.errorHandler) conf.errorHandler(socket, error, rpcName, args)
else throw error
}
this.closeHandler = (socket:I.Socket) => {
if(conf.closeHandler) conf.closeHandler(socket)
this.closeHandler = (socket: I.Socket) => {
if (conf.closeHandler) conf.closeHandler(socket)
}
this.connectionHandler = (socket:I.Socket) => {
if(conf.connectionHandler) conf.connectionHandler(socket)
this.connectionHandler = (socket: I.Socket) => {
if (conf.connectionHandler) conf.connectionHandler(socket)
}
exporters.forEach(U.fixNames) //TSC for some reason doesn't preserve name properties of methods
let badRPC = exporters.flatMap(ex => ex.exportRPCs()).find(rpc => !rpc.name)
if(badRPC){
if (badRPC) {
throw new Error(`
RPC did not provide a name.
\nUse 'funtion name(..){ .. }' syntax instead.
\n
\n<------------OFFENDING RPC:
\n`+badRPC.toString()+`
\n`+ badRPC.toString() + `
\n>------------OFFENDING RPC`)
}
this.startWebsocket()
}
private startWebsocket(){
try{
private startWebsocket() {
try {
this.io.attach(this.ws)
this.io.on('socket', (socket:I.Socket) => {
this.io.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.ws.listen(this.port, this.visibility)
}catch(e){
this.ws = this.ws.listen(this.port, this.visibility)
} catch (e) {
this.errorHandler(this.io, e, 'system', [])
}
}
protected initRPCs(socket:I.Socket){
socket.hook('info', async (sesame? : string) => {
protected initRPCs(socket: I.Socket) {
socket.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 []
if (!allowed) return []
return U.rpcHooker(socket, exp, this.errorHandler, this.sesame)
}))
return rpcs.flat()
@@ -108,8 +103,7 @@ export class RPCServer<
/**
* Publishes a new list of Exporters. This destroys and restarts the socket
* @param exporters the exporters to publish
*/
public setExporters(exporters: Exporters<InterfaceT, SubResType>):any{
public setExporters(exporters: T.ExporterArray<InterfaceT>): any {
exporters.forEach(U.fixNames)
this.destroy()
this.ws = http.createServer()
@@ -117,6 +111,8 @@ export class RPCServer<
this.exporters = exporters
this.startWebsocket()
}
*/
destroy(): void {
this.io.close()
+16 -7
View File
@@ -7,12 +7,12 @@ import * as I from './Interfaces';
import { stripAfterEquals, appendComma } from './Utils';
/**
* A websocket-on-steroids with built-in RPC capabilities
*/
export class RPCSocket implements I.Socket{
static async makeSocket<T extends T.RPCInterface = T.RPCInterface>(port:number, server: string, sesame?:string, conf?:T.SocketConf): Promise<RPCSocket & T> {
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>> {
const socket = new RPCSocket(port, server, conf)
return await socket.connect<T>(sesame)
}
@@ -128,7 +128,7 @@ export class RPCSocket implements I.Socket{
/**
* Connects to the server and attaches available RPCs to this object
*/
public async connect<T extends T.RPCInterface= T.RPCInterface>( sesame?: string ) : Promise<RPCSocket & T>{
public async connect<T extends T.RPCInterface= Ifc>( sesame?: string ) : Promise<T.ConnectedSocket<T>> {
this.socket = await bsock.connect(this.port, this.server, this.conf.tls?this.conf.tls:false)
this.errorHandlers.forEach(h => this.socket.on('error', h))
this.closeHandlers.forEach(h => this.socket.on('close', h))
@@ -197,10 +197,19 @@ export class RPCSocket implements I.Socket{
return eval( `
async (${headerArgs} callback) => {
const r = await this.call("${fnName}", ${sesame} ${argParams})
if(r && r.result === 'Success'){
this.socket.hook(r.uuid, callback)
try{
if(r){
if(r.uuid){
callback['destroy'] = () => { this.socket.unhook(r.uuid) }
this.socket.hook(r.uuid, callback)
}
return r.return
}else{
throw new Error("Empty response")
}
}catch(e){
throw e
}
return r
}`)
}
}
+4 -5
View File
@@ -4,13 +4,12 @@ import * as I from "./Interfaces"
/**
* Interface for all classes that export RPCs
*/
export interface RPCExporter<
export type RPCExporter<
Ifc extends T.RPCInterface = T.RPCInterface,
Name extends keyof Ifc = keyof Ifc,
SubresT = {}
>{
Name extends keyof Ifc = string,
> = {
name: Name
exportRPCs() : T.RPCInterfaceArray<Ifc, SubresT>[Name]
exportRPCs() : T.RPCDefinitions<Ifc>[Name]
}
/**
+26 -18
View File
@@ -1,8 +1,9 @@
import * as I from "./Interfaces";
import { RPCSocket } from "./Frontend";
export type AnyFunction = (...args:any) => any
export type HookFunction<F extends AnyFunction = AnyFunction, SubresT = {}> = (...args: Parameters<F>) => Promise<SubscriptionResponse<SubresT> | ErrorResponse>
export type AccessFilter<InterfaceT extends RPCInterface, SubresT> = (sesame:string|undefined, exporter: I.RPCExporter<RPCInterface<InterfaceT>, keyof InterfaceT, SubresT>) => Promise<boolean>
export type HookFunction = AnyFunction
export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame:string|undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean>
export type Visibility = "127.0.0.1" | "0.0.0.0"
export type ConnectionHandler = (socket:I.Socket) => void
export type ErrorHandler = (socket:I.Socket, error:any, rpcName: string, args: any[]) => void
@@ -17,8 +18,12 @@ export type FrontEndHandlerType = {
'close' : () => void
}
export type ServerConf<InterfaceT extends RPCInterface, SubresT> = {
accessFilter?: AccessFilter<InterfaceT, SubresT>
export type ExporterArray<InterfaceT extends RPCInterface = RPCInterface> = I.RPCExporter<RPCInterface<InterfaceT>, keyof InterfaceT>[]
export type ConnectedSocket<T extends RPCInterface = RPCInterface> = RPCSocket & T
export type ServerConf<InterfaceT extends RPCInterface> = {
accessFilter?: AccessFilter<InterfaceT>
connectionHandler?: ConnectionHandler
errorHandler?: ErrorHandler
closeHandler?: CloseHandler
@@ -36,23 +41,23 @@ export type Outcome = "Success" | "Error"
export type Respose<T> = T & { result: Outcome }
export type SuccessResponse<T = {}> = Respose<T> & { result: "Success" }
export type ErrorResponse<T = {}> = Respose<T> & { result: "Error", message?:string }
export type SubscriptionResponse<T = {}> = Respose<T> & { result: "Success"; uuid: string }
export type RPCType = 'Hook' | 'Unhook' | 'Call'
export type CallRPC<N, F> = {
name: N
call: F
export type CallRPC<Name, Func extends AnyFunction> = {
name: Name
call: Func
}
export type HookRPC<N, F extends AnyFunction, SubresT = {}> = {
name: N
hook: HookFunction<F, SubresT>
export type HookRPC<Name, Func extends AnyFunction> = {
name: Name
hook: Func
onCallback?: AnyFunction
onClose?: HookCloseFunction<SubresT>
onClose?: HookCloseFunction<ReturnType<Func> extends Promise<infer T> ? T : ReturnType<Func>>
}
export type RPC<N, F extends AnyFunction, SubresT = {}> = HookRPC<N, F, SubresT> | CallRPC<N,F> | F
export type RPC<Name, Func extends AnyFunction> = HookRPC<Name, Func> | CallRPC<Name,Func> | Func
export type RPCInterface<Impl extends RPCInterface = {}> = {
[grp in string] : {
@@ -60,10 +65,13 @@ export type RPCInterface<Impl extends RPCInterface = {}> = {
}
} & Impl
export type RPCInterfaceArray<Itfc extends RPCInterface, SubresT = {}> = {
[grp in keyof Itfc]: Array<
{ [rpc in keyof Itfc[grp]]: RPC<rpc, Itfc[grp][rpc], SubresT> }[keyof Itfc[grp]]
>
export type exportT = {
[group in string]: {}
}
//This probably has lots of issues
export type RPCDefinitions<Ifc extends RPCInterface> = {
[grp in keyof Ifc]:( { [rpc in keyof Ifc[grp]]: RPC<rpc, Ifc[grp][rpc]> }[keyof Ifc[grp]] )[]
}
export type BaseInfo = {
@@ -86,4 +94,4 @@ export type RpcInfo = HookInfo | CallInfo
export type ExtendedRpcInfo = RpcInfo & { uniqueName: string }
export type OnFunction = <T extends "error" | "close">(type: T, f: FrontEndHandlerType[T]) => void
export type HookCloseFunction<T = {}> = (res:SubscriptionResponse<T>, rpc:HookRPC<any, any, T>) => any
export type HookCloseFunction<T> = (res: T, rpc:HookRPC<any, any>) => any
+17 -24
View File
@@ -2,7 +2,6 @@ import * as uuidv4 from "uuid/v4"
import * as T from "./Types";
import * as I from "./Interfaces";
import { SubscriptionResponse } from "./Types";
/**
* Translate an RPC to RPCInfo for serialization.
@@ -12,7 +11,7 @@ import { SubscriptionResponse } from "./Types";
* @param sesame optional sesame phrase to prepend before all RPC arguments
* @throws Error on RPC without name property
*/
export const rpcToRpcinfo = <SubResT = {}>(socket: I.Socket, rpc : T.RPC<any, any, SubResT>, owner: string, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction):T.RpcInfo => {
export const rpcToRpcinfo = (socket: I.Socket, rpc : T.RPC<any, any>, owner: string, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction):T.RpcInfo => {
switch (typeof rpc){
case "object":
if(rpc['call']){
@@ -24,7 +23,7 @@ export const rpcToRpcinfo = <SubResT = {}>(socket: I.Socket, rpc : T.RPC<any, an
call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); socket.destroy()}:rpc['call'], // check & remove sesame
}
}else{
const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc, errorHandler, sesame)
const generator = hookGenerator(<T.HookRPC<any, any>>rpc, errorHandler, sesame)
return {
owner: owner,
argNames: extractArgs(generator(undefined)),
@@ -58,10 +57,9 @@ 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>, 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 RPCs = [...exporter.exportRPCs()]
const RPCs = exporter.exportRPCs()
return RPCs.map(rpc => rpcToRpcinfo(socket, rpc, owner, errorHandler, sesame))
.map(info => {
@@ -107,7 +105,7 @@ export function stripAfterEquals(str:string):string{
* @param rpc The RPC to transform
* @returns A {@link HookFunction}
*/
const hookGenerator = (rpc:T.HookRPC<any, any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => {
const hookGenerator = (rpc:T.HookRPC<any, any>, /*not unused!*/ errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => {
let argsArr = extractArgs(rpc.hook)
argsArr.pop() //remove 'callback' from the end
let callArgs = argsArr.join(',')
@@ -119,16 +117,23 @@ const hookGenerator = (rpc:T.HookRPC<any, any, any>, errorHandler: T.ErrorHandle
//note rpc.hook is the associated RPC, not a socket.hook
return eval(`
(socket) => async (${args}) => {
(clientSocket) => 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)
socket.call.apply(socket, [res.uuid, ...cbargs])
if(rpc.onCallback){
rpc.onCallback.apply({}, cbargs)
}
clientSocket.call.apply(clientSocket, [uuid, ...cbargs])
})
return res
if(rpc.onClose){
clientSocket.on('close', () => rpc.onClose(res, rpc))
}
return {'uuid': uuid, 'return': res}
}catch(e){
errorHandler(socket)(e, ${rpc.name}, [${args}])
//can throw to pass exception to client or swallow to keep it local
errorHandler(clientSocket)(e, ${rpc.name}, [${args}])
}
}`)
}
@@ -147,18 +152,6 @@ const extractArgs = (f:Function):string[] => {
return fn!==""?fn.split(',') : []
}
/**
* Simple utility function to create basic {@link SubscriptionResponse}
* @param uuid optional uuid to use, otherwise defaults to uuid/v4
*/
export function makeSubResponse<T extends {} = {}>(extension:T):SubscriptionResponse & T{
return {
result: "Success",
uuid: uuidv4(),
...extension
}
}
export function makeSesameFunction (sesame : T.SesameFunction | string) : T.SesameFunction {
if(typeof sesame === 'function'){