diff --git a/src/Backend.ts b/src/Backend.ts index bd8a453..bf7a7ed 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -11,9 +11,9 @@ import * as I from './Interfaces'; * A Websocket-server-on-steroids with built-in RPC capabilities */ export class RPCServer< - SubResType = {} + SubResType = {}, + InterfaceT extends T.RPCInterface = T.RPCInterface, > implements I.Destroyable{ - private ws = http.createServer() private io = bsock.createServer() private visibility:T.Visibility @@ -29,7 +29,7 @@ export class RPCServer< */ constructor( private port:number, - private exporters: I.RPCExporter[] = [], + private exporters: I.RPCExporter, keyof T.RPCInterface, SubResType>[] = [], conf: T.SocketConf = {} ){ diff --git a/src/Frontend.ts b/src/Frontend.ts index 617d3cf..612d5c6 100644 --- a/src/Frontend.ts +++ b/src/Frontend.ts @@ -17,6 +17,11 @@ function stripAfterEquals(str:string):string{ * A websocket-on-steroids with built-in RPC capabilities */ export class RPCSocket implements I.Socket{ + static async makeSocket(port:number, server: string, tls: boolean = false): Promise> { + const socket = new RPCSocket(port, server, tls) + return await socket.connect() + } + private socket: I.Socket /** @@ -34,7 +39,7 @@ export class RPCSocket implements I.Socket{ * @param name The function name to listen on * @param handler The handler to attach */ - public hook(name: T.Name, handler: (...args:any[]) => any | Promise){ + public hook(name: string, handler: (...args:any[]) => any | Promise){ return this.socket.hook(name, handler) } @@ -42,7 +47,7 @@ export class RPCSocket implements I.Socket{ * Removes a {@link hook} listener by name. * @param name The function name */ - public unhook(name: T.Name){ + public unhook(name: string){ return this.socket.unhook(name) } @@ -74,7 +79,7 @@ export class RPCSocket implements I.Socket{ * @param rpcname The function to call * @param args other arguments */ - public async call (rpcname: T.Name, ...args: T.Any[]) : Promise{ + public async call (rpcname: string, ...args: any[]) : Promise{ 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 args other arguments */ - public async fire(rpcname: T.Name, ...args: T.Any[]) : Promise{ + public async fire(rpcname: string, ...args: any[]) : Promise{ await this.socket.fire.apply(this.socket, [rpcname, ...args]) } /** * Connects to the server and attaches available RPCs to this object */ - public async connect(){ + public async connect() : Promise>{ this.socket = await bsock.connect(this.port, this.server, this.tls) 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].bind(this) }) + return > this } /** @@ -123,7 +129,7 @@ export class RPCSocket implements I.Socket{ * @param fnName The function name * @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 argParams = fnArgs.map(stripAfterEquals).join(",") 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 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 argParams = fnArgs.map(stripAfterEquals).join(",") return eval( `( () => async (`+headerArgs+(headerArgs.length!==0?",":"")+` callback) => { diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 0000349..3950345 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -4,9 +4,13 @@ import * as I from "./Interfaces" /** * Interface for all classes that may export RPCs */ -export interface RPCExporter{ - name: T.Name - exportRPCs() : T.RPC[] +export interface RPCExporter< + Ifc extends T.RPCInterface, + K extends keyof Ifc, + SubresT = {} +>{ + name: K + exportRPCs() : T.RPC[] } /** @@ -14,10 +18,10 @@ export interface RPCExporter{ */ export interface Socket extends Destroyable { port: number - hook: (rpcname: T.Name, handler: (...args:any[]) => any | Promise) => I.Socket - unhook: (rpcname:T.Name) => I.Socket - call: (rpcname:T.Name, ...args: T.Any[]) => Promise - fire: (rpcname:T.Name, ...args: T.Any[]) => Promise + hook: (rpcname: string, handler: T.AnyFunction) => I.Socket + unhook: (rpcname:string) => I.Socket + call: (rpcname:string, ...args: any[]) => Promise + fire: (rpcname:string, ...args: any[]) => Promise on: T.OnFunction close() : void } diff --git a/src/Types.ts b/src/Types.ts index 8b60ee6..77b8d3c 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -1,10 +1,11 @@ import * as I from "./Interfaces"; +export type AnyFunction = (...any:any)=>any +export type AsyncFunction = ReturnType extends Promise ? Fn : (...any:Parameters) => Promise> +export type RPCGroup = { [name in string] : AsyncFunction } +export type RPCInterface = { [groupName in string]: RPCGroup } & Impl + 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 ErrorHandler = (socket:I.Socket, error:any) => void export type CloseHandler = (socket:I.Socket) => void @@ -26,29 +27,32 @@ export type SubscriptionResponse = Respose & { result: "Success"; uui export type RPCType = 'Hook' | 'Unhook' | 'Call' -export type HookRPC = { - name: Name - hook: HookFunction +export type HookT = AsyncFunction> +export type CallT = AsyncFunction + +export type HookRPC = { + name: K + hook: HookT onCallback?: CallbackFunction, - onClose?: HookCloseFunction + onClose?: HookCloseFunction } -export type CallRPC = { - name: Name - call: AsyncFunction -} | Function +export type CallRPC = { + name: K + call: CallT +} -export type RPC = CallRPC | HookRPC +export type RPC = CallRPC | HookRPC export type BaseInfo = { - name: Name, - owner: Name, - argNames: Name[], + name: string, + owner: string, + argNames: string[], } export type HookInfo = BaseInfo & { type: 'Hook', - generator: (socket?:I.Socket) => HookFunction + generator: (socket?:I.Socket) => HookFunction } export type CallInfo = BaseInfo & { @@ -60,7 +64,6 @@ export type RpcInfo = HookInfo | CallInfo export type ExtendedRpcInfo = RpcInfo & { uniqueName: string } export type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => I.Socket -export type HookCloseFunction = (res:SubscriptionResponse, rpc:HookRPC) => any -export type HookFunction = (...args:any) => Promise | ErrorResponse> -export type AsyncFunction = (...args:any) => Promise -export type CallbackFunction = (...args:any) => void \ No newline at end of file +export type HookCloseFunction = (res:SubscriptionResponse, rpc:HookRPC) => any +export type HookFunction = AsyncFunction<(...args:Parameters) => SubscriptionResponse | ErrorResponse> +export type CallbackFunction = (callback:AnyFunction, ...args:any) => any \ No newline at end of file diff --git a/src/Utils.ts b/src/Utils.ts index 17beed0..b9442a8 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -10,7 +10,7 @@ import { SubscriptionResponse } from "./Types"; * @param owner The owning RPC group's name * @throws Error on RPC without name property */ -export const rpcToRpcinfo = (rpc : T.RPC, owner: T.Owner):T.RpcInfo => { +export const rpcToRpcinfo = (rpc : T.RPC, owner: string):T.RpcInfo => { switch (typeof rpc){ case "object": if(rpc['call']){ @@ -22,7 +22,7 @@ export const rpcToRpcinfo = (rpc : T.RPC, owner: T.Owner) call: rpc['call'], } }else{ - const generator = hookGenerator(>rpc) + const generator = hookGenerator(>rpc) return { owner: owner, argNames: extractArgs(generator(undefined)), @@ -44,7 +44,7 @@ RPC did not provide a name. argNames: extractArgs(rpc), type: "Call", name: rpc.name, - call: async(...args) => rpc.apply({}, args), + call: async(...args) => (rpc).apply({}, args), } } throw new Error("Bad socketIORPC type "+ typeof rpc) @@ -56,7 +56,7 @@ RPC did not provide a name. * @param exporter The exporter * @param makeUnique @default true Attach a suffix to RPC names */ -export function rpcHooker(socket: I.Socket, exporter:I.RPCExporter, makeUnique = true):T.ExtendedRpcInfo[]{ +export function rpcHooker(socket: I.Socket, exporter:I.RPCExporter, makeUnique = true):T.ExtendedRpcInfo[]{ const owner = exporter.name const RPCs = [...exporter.exportRPCs()] const suffix = makeUnique?"-"+uuidv4().substr(0,4):"" @@ -82,7 +82,7 @@ export function rpcHooker(socket: I.Socket, exporter:I.RPCExporter * @param rpc The RPC to transform * @returns A {@link HookFunction} */ -const hookGenerator = (rpc:T.HookRPC): T.HookInfo['generator'] => { +const hookGenerator = (rpc:T.HookRPC): T.HookInfo['generator'] => { const argsArr = extractArgs(rpc.hook) argsArr.pop() const args = argsArr.join(',') @@ -107,8 +107,8 @@ const hookGenerator = (rpc:T.HookRPC): T.HookInfo['generator'] => { * Extract a string list of parameters from a function * @param f The source function */ -const extractArgs = (f:Function):T.Arg[] => { - let fn +const extractArgs = (f:Function):string[] => { + let fn:string return (fn = String(f)).substr(0, fn.indexOf(")")).substr(fn.indexOf("(")+1).split(",") } diff --git a/test/Test.ts b/test/Test.ts index bd39dce..1f5d29f 100644 --- a/test/Test.ts +++ b/test/Test.ts @@ -54,7 +54,7 @@ function makeServer(){ describe('RPCServer', () => { - let server: RPCServer<{ topic: string}> + let server: RPCServer<{ topic: string }, any> before((done) => { server = makeServer() @@ -160,8 +160,8 @@ describe('It should do unhook', () => { name: "test", exportRPCs: () => [{ name: 'subscribe', - hook: async(callback) => { - cb = callback + hook: async(callback):Promise> => { + cb = callback return { result: "Success", uuid: uuidv4(), diff --git a/test/devtest.ts b/test/devtest.ts new file mode 100644 index 0000000..c3972fb --- /dev/null +++ b/test/devtest.ts @@ -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, + subscribe: (param:string, callback:Function) => Promise>, + unsubscribe: (uuid:string) => Promise + }, + Group2: { + echo: (x:string) => Promise + } +}> + +const callbacks:Map = new Map() + +new RPCServer<{a:string}, MyInterface>(20000, + [{ + name: "Group1", + exportRPCs: () => [ + >{ + name: 'subscribe', + hook: async (param, callback) => { const _uuid = ""+Math.random(); console.log(param); callbacks.set(_uuid, callback); return { result: 'Success', a: '3', uuid: _uuid} } + }, + >{ + name: 'unsubscribe', + call: async (uuid) => { callbacks.delete(uuid) } + }, + >{ + name: 'triggerCallbacks', + call: async (...args) => { callbacks.forEach(cb => cb.apply({}, args)) } + }] + },{ + name: 'Group2', + exportRPCs: () => [ + >{ + 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(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(20001, 'localhost').then((async (client) => { + console.log(client) + const r = await client.Group2.echo("hee") + console.log(r) +})) \ No newline at end of file