clean up type system, remove subres
This commit is contained in:
+36
-40
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'){
|
||||
|
||||
Reference in New Issue
Block a user