This commit is contained in:
2019-09-23 16:47:14 +02:00
parent ef8e42c97f
commit 257c1eeced
7 changed files with 135 additions and 48 deletions
+3 -3
View File
@@ -11,9 +11,9 @@ import * as I from './Interfaces';
* A Websocket-server-on-steroids with built-in RPC capabilities * A Websocket-server-on-steroids with built-in RPC capabilities
*/ */
export class RPCServer< export class RPCServer<
SubResType = {} SubResType = {},
InterfaceT extends T.RPCInterface = T.RPCInterface,
> implements I.Destroyable{ > implements I.Destroyable{
private ws = http.createServer() private ws = http.createServer()
private io = bsock.createServer() private io = bsock.createServer()
private visibility:T.Visibility private visibility:T.Visibility
@@ -29,7 +29,7 @@ export class RPCServer<
*/ */
constructor( constructor(
private port:number, private port:number,
private exporters: I.RPCExporter<SubResType>[] = [], private exporters: I.RPCExporter<T.RPCInterface<InterfaceT>, keyof T.RPCInterface<InterfaceT>, SubResType>[] = [],
conf: T.SocketConf = {} conf: T.SocketConf = {}
){ ){
+13 -7
View File
@@ -17,6 +17,11 @@ function stripAfterEquals(str:string):string{
* A websocket-on-steroids with built-in RPC capabilities * A websocket-on-steroids with built-in RPC capabilities
*/ */
export class RPCSocket implements I.Socket{ export class RPCSocket implements I.Socket{
static async makeSocket<T extends T.RPCInterface= T.RPCInterface>(port:number, server: string, tls: boolean = false): Promise<RPCSocket & T.RPCInterface<T>> {
const socket = <RPCSocket & T> new RPCSocket(port, server, tls)
return await socket.connect<T>()
}
private socket: I.Socket private socket: I.Socket
/** /**
@@ -34,7 +39,7 @@ export class RPCSocket implements I.Socket{
* @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: T.Name, handler: (...args:any[]) => any | Promise<any>){ public hook(name: string, handler: (...args:any[]) => any | Promise<any>){
return this.socket.hook(name, handler) return this.socket.hook(name, handler)
} }
@@ -42,7 +47,7 @@ export class RPCSocket implements I.Socket{
* 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: T.Name){ public unhook(name: string){
return this.socket.unhook(name) return this.socket.unhook(name)
} }
@@ -74,7 +79,7 @@ export class RPCSocket implements I.Socket{
* @param rpcname The function to call * @param rpcname The function to call
* @param args other arguments * @param args other arguments
*/ */
public async call (rpcname: T.Name, ...args: T.Any[]) : Promise<T.Any>{ public async call (rpcname: string, ...args: any[]) : Promise<any>{
return await this.socket.call.apply(this.socket, [rpcname, ...args]) return await this.socket.call.apply(this.socket, [rpcname, ...args])
} }
@@ -83,14 +88,14 @@ export class RPCSocket implements I.Socket{
* @param rpcname The function to call * @param rpcname The function to call
* @param args other arguments * @param args other arguments
*/ */
public async fire(rpcname: T.Name, ...args: T.Any[]) : Promise<void>{ public async fire(rpcname: string, ...args: any[]) : Promise<void>{
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(){ public async connect<T extends T.RPCInterface= T.RPCInterface>() : Promise<RPCSocket & T.RPCInterface<T>>{
this.socket = await bsock.connect(this.port, this.server, this.tls) this.socket = await bsock.connect(this.port, this.server, this.tls)
const info:T.ExtendedRpcInfo[] = await this.info() const info:T.ExtendedRpcInfo[] = await this.info()
@@ -109,6 +114,7 @@ export class RPCSocket implements I.Socket{
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 <RPCSocket & T.RPCInterface<T>> <any> this
} }
/** /**
@@ -123,7 +129,7 @@ export class RPCSocket implements I.Socket{
* @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: T.Name, fnArgs:T.Arg[]): T.AsyncFunction{ private callGenerator(fnName: string, fnArgs:string[]): T.AsyncFunction{
const headerArgs = fnArgs.join(",") const headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",") const argParams = fnArgs.map(stripAfterEquals).join(",")
return eval( '( () => async ('+headerArgs+') => { return await this.socket.call("'+fnName+'", '+argParams+')} )()' ) return eval( '( () => async ('+headerArgs+') => { return await this.socket.call("'+fnName+'", '+argParams+')} )()' )
@@ -134,7 +140,7 @@ export class RPCSocket implements I.Socket{
* @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 hookGenerator(fnName: T.Name, fnArgs:T.Arg[]): T.HookFunction{ private hookGenerator(fnName: string, fnArgs:string[]): T.HookFunction{
const headerArgs = fnArgs.join(",") const headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",") const argParams = fnArgs.map(stripAfterEquals).join(",")
return eval( `( () => async (`+headerArgs+(headerArgs.length!==0?",":"")+` callback) => { return eval( `( () => async (`+headerArgs+(headerArgs.length!==0?",":"")+` callback) => {
+11 -7
View File
@@ -4,9 +4,13 @@ import * as I from "./Interfaces"
/** /**
* Interface for all classes that may export RPCs * Interface for all classes that may export RPCs
*/ */
export interface RPCExporter<T = {}>{ export interface RPCExporter<
name: T.Name Ifc extends T.RPCInterface,
exportRPCs() : T.RPC<T>[] K extends keyof Ifc,
SubresT = {}
>{
name: K
exportRPCs() : T.RPC<Ifc[K], keyof Ifc[K], SubresT>[]
} }
/** /**
@@ -14,10 +18,10 @@ export interface RPCExporter<T = {}>{
*/ */
export interface Socket extends Destroyable { export interface Socket extends Destroyable {
port: number port: number
hook: (rpcname: T.Name, handler: (...args:any[]) => any | Promise<any>) => I.Socket hook: (rpcname: string, handler: T.AnyFunction) => I.Socket
unhook: (rpcname:T.Name) => I.Socket unhook: (rpcname:string) => I.Socket
call: (rpcname:T.Name, ...args: T.Any[]) => Promise<T.Any> call: (rpcname:string, ...args: any[]) => Promise<any>
fire: (rpcname:T.Name, ...args: T.Any[]) => Promise<T.Any> fire: (rpcname:string, ...args: any[]) => Promise<any>
on: T.OnFunction on: T.OnFunction
close() : void close() : void
} }
+24 -21
View File
@@ -1,10 +1,11 @@
import * as I from "./Interfaces"; import * as I from "./Interfaces";
export type AnyFunction = (...any:any)=>any
export type AsyncFunction<Fn extends AnyFunction = AnyFunction> = ReturnType<Fn> extends Promise<any> ? Fn : (...any:Parameters<Fn>) => Promise<ReturnType<Fn>>
export type RPCGroup = { [name in string] : AsyncFunction }
export type RPCInterface<Impl extends RPCInterface = {}> = { [groupName in string]: RPCGroup } & Impl
export type Visibility = "127.0.0.1" | "0.0.0.0" export type Visibility = "127.0.0.1" | "0.0.0.0"
export type Any = any
export type Arg = string
export type Name = Arg
export type Owner = Name
export type ConnectionHandler = (socket:I.Socket) => void 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
@@ -26,29 +27,32 @@ export type SubscriptionResponse<T = {}> = Respose<T> & { result: "Success"; uui
export type RPCType = 'Hook' | 'Unhook' | 'Call' export type RPCType = 'Hook' | 'Unhook' | 'Call'
export type HookRPC<T = {}> = { export type HookT<G extends RPCGroup, K extends keyof G, SubresT> = AsyncFunction<HookFunction<G[K], SubresT>>
name: Name export type CallT<G extends RPCGroup, K extends keyof G> = AsyncFunction<G[K]>
hook: HookFunction<T>
export type HookRPC<G extends RPCGroup, K extends keyof G, SubresT = {}> = {
name: K
hook: HookT<G, K, SubresT>
onCallback?: CallbackFunction, onCallback?: CallbackFunction,
onClose?: HookCloseFunction<T> onClose?: HookCloseFunction<SubresT>
} }
export type CallRPC = { export type CallRPC<G extends RPCGroup, K extends keyof G> = {
name: Name name: K
call: AsyncFunction call: CallT<G,K>
} | Function }
export type RPC<T = {}> = CallRPC | HookRPC<T> export type RPC<G extends RPCGroup, Kg extends keyof G, SubresT> = CallRPC<G, Kg> | HookRPC<G, Kg, SubresT>
export type BaseInfo = { export type BaseInfo = {
name: Name, name: string,
owner: Name, owner: string,
argNames: Name[], argNames: string[],
} }
export type HookInfo<T = {}> = BaseInfo & { export type HookInfo<T = {}> = BaseInfo & {
type: 'Hook', type: 'Hook',
generator: (socket?:I.Socket) => HookFunction<T> generator: (socket?:I.Socket) => HookFunction<AnyFunction, T>
} }
export type CallInfo = BaseInfo & { export type CallInfo = BaseInfo & {
@@ -60,7 +64,6 @@ export type RpcInfo = HookInfo | CallInfo
export type ExtendedRpcInfo = RpcInfo & { uniqueName: string } export type ExtendedRpcInfo = RpcInfo & { uniqueName: string }
export type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => I.Socket export type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => I.Socket
export type HookCloseFunction<T = {}> = (res:SubscriptionResponse<T>, rpc:HookRPC<T>) => any export type HookCloseFunction<T = {}> = (res:SubscriptionResponse<T>, rpc:HookRPC<any, any, T>) => any
export type HookFunction<T = {}> = (...args:any) => Promise<SubscriptionResponse<T> | ErrorResponse> export type HookFunction<F extends AnyFunction = AnyFunction, SubResT = {}> = AsyncFunction<(...args:Parameters<F>) => SubscriptionResponse<SubResT> | ErrorResponse>
export type AsyncFunction = (...args:any) => Promise<any> export type CallbackFunction = (callback:AnyFunction, ...args:any) => any
export type CallbackFunction = (...args:any) => void
+7 -7
View File
@@ -10,7 +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<SubResT>, owner: T.Owner):T.RpcInfo => { export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<any, any, SubResT>, owner: string):T.RpcInfo => {
switch (typeof rpc){ switch (typeof rpc){
case "object": case "object":
if(rpc['call']){ if(rpc['call']){
@@ -22,7 +22,7 @@ export const rpcToRpcinfo = <SubResT = {}>(rpc : T.RPC<SubResT>, owner: T.Owner)
call: rpc['call'], call: rpc['call'],
} }
}else{ }else{
const generator = hookGenerator(<T.HookRPC<any>>rpc) const generator = hookGenerator(<T.HookRPC<any, any, any>>rpc)
return { return {
owner: owner, owner: owner,
argNames: extractArgs(generator(undefined)), argNames: extractArgs(generator(undefined)),
@@ -44,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: async(...args) => rpc.apply({}, args), call: async(...args) => (<Function>rpc).apply({}, args),
} }
} }
throw new Error("Bad socketIORPC type "+ typeof rpc) throw new Error("Bad socketIORPC type "+ typeof rpc)
@@ -56,7 +56,7 @@ 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<SubResT>, makeUnique = true):T.ExtendedRpcInfo[]{ export function rpcHooker<SubResT = {}>(socket: I.Socket, exporter:I.RPCExporter<any, any, SubResT>, makeUnique = true):T.ExtendedRpcInfo[]{
const owner = exporter.name const owner = exporter.name
const RPCs = [...exporter.exportRPCs()] const RPCs = [...exporter.exportRPCs()]
const suffix = makeUnique?"-"+uuidv4().substr(0,4):"" const suffix = makeUnique?"-"+uuidv4().substr(0,4):""
@@ -82,7 +82,7 @@ export function rpcHooker<SubResT = {}>(socket: I.Socket, exporter:I.RPCExporter
* @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>): T.HookInfo['generator'] => { const hookGenerator = (rpc:T.HookRPC<any, any, any>): T.HookInfo['generator'] => {
const argsArr = extractArgs(rpc.hook) const argsArr = extractArgs(rpc.hook)
argsArr.pop() argsArr.pop()
const args = argsArr.join(',') const args = argsArr.join(',')
@@ -107,8 +107,8 @@ const hookGenerator = (rpc:T.HookRPC<any>): T.HookInfo['generator'] => {
* Extract a string list of parameters from a function * Extract a string list of parameters from a function
* @param f The source function * @param f The source function
*/ */
const extractArgs = (f:Function):T.Arg[] => { const extractArgs = (f:Function):string[] => {
let fn let fn:string
return (fn = String(f)).substr(0, fn.indexOf(")")).substr(fn.indexOf("(")+1).split(",") return (fn = String(f)).substr(0, fn.indexOf(")")).substr(fn.indexOf("(")+1).split(",")
} }
+3 -3
View File
@@ -54,7 +54,7 @@ function makeServer(){
describe('RPCServer', () => { describe('RPCServer', () => {
let server: RPCServer<{ topic: string}> let server: RPCServer<{ topic: string }, any>
before((done) => { before((done) => {
server = makeServer() server = makeServer()
@@ -160,8 +160,8 @@ describe('It should do unhook', () => {
name: "test", name: "test",
exportRPCs: () => [{ exportRPCs: () => [{
name: 'subscribe', name: 'subscribe',
hook: async(callback) => { hook: async(callback):Promise<SubscriptionResponse<{topic:string}>> => {
cb = callback cb = <Function> callback
return { return {
result: "Success", result: "Success",
uuid: uuidv4(), uuid: uuidv4(),
+74
View File
@@ -0,0 +1,74 @@
import { RPCServer } from "../src/Backend";
import { SubscriptionResponse, CallRPC, HookRPC, RPCInterface } from "../src/Types";
import { RPCSocket } from "../src/Frontend";
type MyInterface = RPCInterface<{
Group1: {
triggerCallbacks: (...args:any[]) => Promise<void>,
subscribe: (param:string, callback:Function) => Promise<SubscriptionResponse<{a: string}>>,
unsubscribe: (uuid:string) => Promise<void>
},
Group2: {
echo: (x:string) => Promise<string>
}
}>
const callbacks:Map<string, Function> = new Map()
new RPCServer<{a:string}, MyInterface>(20000,
[{
name: "Group1",
exportRPCs: () => [
<HookRPC<MyInterface['Group1'], 'subscribe', {a:string}>>{
name: 'subscribe',
hook: async (param, callback) => { const _uuid = ""+Math.random(); console.log(param); callbacks.set(_uuid, callback); return { result: 'Success', a: '3', uuid: _uuid} }
},
<CallRPC<MyInterface['Group1'], 'unsubscribe'>>{
name: 'unsubscribe',
call: async (uuid) => { callbacks.delete(uuid) }
},
<CallRPC<MyInterface['Group1'], 'triggerCallbacks'>>{
name: 'triggerCallbacks',
call: async (...args) => { callbacks.forEach(cb => cb.apply({}, args)) }
}]
},{
name: 'Group2',
exportRPCs: () => [
<CallRPC<MyInterface['Group2'], 'echo'>>{
name: 'echo',
call: async (x) => x
}]
}]
)
new RPCServer<{a:string}, MyInterface>(20001,
[{
name: "Group1",
exportRPCs: () => []
},{
name: 'Group2',
exportRPCs: () => [{
name: 'echo',
call: async (x) => x+" lol"
}]
}]
)
RPCSocket.makeSocket<MyInterface>(20000, 'localhost').then((async (client) => {
console.log(client)
const res = await client.Group1.subscribe('test', async (...args:any) => {
console.log.apply(console, args)
/* close the callbacks once you're done */
await client.Group1.unsubscribe(res.uuid)
client.unhook(res.uuid)
})
await client.Group1.triggerCallbacks("Hello", "World", "Callbacks")
}))
RPCSocket.makeSocket<MyInterface>(20001, 'localhost').then((async (client) => {
console.log(client)
const r = await client.Group2.echo("hee")
console.log(r)
}))