From b2ffbbfa48354af98cff2d3a85b8485453215c4a Mon Sep 17 00:00:00 2001 From: nitowa Date: Thu, 7 Apr 2022 00:01:41 +0200 Subject: [PATCH] before rewrite to have callbacks as FIRST parameter --- src/Frontend.ts | 26 ++++--- src/Interfaces.ts | 4 +- src/Strings.ts | 3 +- src/Types.ts | 94 +++++++++++----------- src/Utils.ts | 6 +- test/Test.ts | 193 ++++++++++++++++++++++++++++------------------ 6 files changed, 191 insertions(+), 135 deletions(-) diff --git a/src/Frontend.ts b/src/Frontend.ts index 15cba90..3b067c1 100644 --- a/src/Frontend.ts +++ b/src/Frontend.ts @@ -4,7 +4,7 @@ import { PromiseIOClient, defaultClientConfig } from './PromiseIO/Client' import * as T from './Types'; import * as I from './Interfaces'; import { stripAfterEquals, appendComma } from './Utils'; -import { DESTROY_PREFIX, SOCKET_NOT_CONNECTED, UNKNOWN_RPC_IDENTIFIER, USER_DEFINED_TIMEOUT } from './Strings'; +import { CALLBACK_NAME, DESTROY_PREFIX, SOCKET_NOT_CONNECTED, UNKNOWN_RPC_IDENTIFIER, USER_DEFINED_TIMEOUT } from './Strings'; import { DeserializerFactory } from './Decorator'; DeserializerFactory @@ -21,12 +21,12 @@ export class RPCSocket implements I private socket: I.Socket private handlers: { - [name in string]: T.AnyFunction[] + [name in string]: T.GenericFunction[] } = { error: [], close: [] } - private hooks: { [name in string]: T.AnyFunction } = {} + private hooks: { [name in string]: T.GenericFunction } = {} /** * @@ -82,7 +82,7 @@ export class RPCSocket implements I * @param type 'error' or 'close' * @param f The listener to attach */ - public on(type: string, f: T.AnyFunction) { + public on(type: string, f: T.GenericFunction) { if (!this.socket) { if (!this.handlers[type]) this.handlers[type] = [] @@ -166,7 +166,7 @@ export class RPCSocket implements I v.forEach(h => this.socket.on(k, h)) }) - Object.entries(this.hooks).forEach((kv: [string, T.AnyFunction]) => { + Object.entries(this.hooks).forEach((kv: [string, T.GenericFunction]) => { this.socket.hook(kv[0], kv[1]) }) const info: T.ExtendedRpcInfo[] = await this.info(sesame) @@ -205,7 +205,7 @@ export class RPCSocket implements I * @param fnName The function name * @param fnArgs A string-list of parameters */ - private callGenerator(fnName: string, fnArgs: string[], sesame?: string): T.AnyFunction { + private callGenerator(fnName: string, fnArgs: string[], sesame?: string): T.GenericFunction { const headerArgs = fnArgs.join(",") const argParams = fnArgs.map(stripAfterEquals).join(",") sesame = appendComma(sesame) @@ -221,7 +221,7 @@ export class RPCSocket implements I * @param fnName The function name * @param fnArgs A string-list of parameters */ - private frontEndHookGenerator(fnName: string, fnArgs: string[], sesame?: string): T.HookFunction { + private frontEndHookGenerator(fnName: string, fnArgs: string[], sesame?: string): T.GenericFunction { if (sesame) fnArgs.shift() @@ -234,16 +234,22 @@ export class RPCSocket implements I const destroy_prefix = DESTROY_PREFIX const frontendHookStr = ` - async (${headerArgs} $__callback__$) => { + async (${headerArgs} ${CALLBACK_NAME}) => { const r = await this.call("${fnName}", ${sesame} ${argParams}) try{ if(r){ if(r.uuid){ - $__callback__$['destroy'] = () => { + ${CALLBACK_NAME}['destroy'] = () => { this.socket.fire(destroy_prefix+r.uuid) this.socket.unhook(r.uuid) } - this.socket.hook(r.uuid, $__callback__$) + ${CALLBACK_NAME} = ${CALLBACK_NAME}.bind({ + destroy: ${CALLBACK_NAME}['destroy'] + }) + + this.socket.hook(r.uuid, (...args) => { + ${CALLBACK_NAME}.apply(${CALLBACK_NAME}, args.map(deserializer.from)) + }) } return deserializer.from(r.return) }else{ diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 351df96..dd6d047 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -15,10 +15,10 @@ export interface Socket { id?: string bind: (name: string, listener: T.PioBindListener) => void hook: (rpcname: string, handler: T.PioHookListener) => void - unhook: (rpcname: string, listener?:T.AnyFunction) => void + unhook: (rpcname: string, listener?:T.GenericFunction) => void call: (rpcname: string, ...args: any[]) => Promise fire: (rpcname: string, ...args: any[]) => Promise - on: (type: string, f: T.AnyFunction)=>any + on: (type: string, f: T.GenericFunction)=>any emit: (eventName: string, ...args: any[]) => void close(): void } \ No newline at end of file diff --git a/src/Strings.ts b/src/Strings.ts index a5693c4..8b154fb 100644 --- a/src/Strings.ts +++ b/src/Strings.ts @@ -13,4 +13,5 @@ RPC did not provide a name. \n${name} \n>------------OFFENDING RPC` export const CLASSNAME_ATTRIBUTE = "$__CLASSNAME__$" -export const DESTROY_PREFIX = "$__DESTROY__$_" \ No newline at end of file +export const DESTROY_PREFIX = "$__DESTROY__$_" +export const CALLBACK_NAME = "$__CALLBACK__$" \ No newline at end of file diff --git a/src/Types.ts b/src/Types.ts index 3157f17..c2428d2 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -1,12 +1,28 @@ import * as I from "./Interfaces"; import { RPCSocket } from "./Frontend"; -import { PromiseIO } from "./PromiseIO/Server"; export type PioBindListener = (...args: any) => void -export type PioHookListener = AnyFunction +export type PioHookListener = GenericFunction + +export type GenericFunction = {(...args: Parameters): Result} + +export type BackendHook = + GenericFunction< + [ + ...Head>, + GenericFunction< + Parameters< + AsFunction< + Last> + > + >, + void + > + ], + ReturnType + > + -export type AnyFunction = (...args: any[]) => any -export type HookFunction = AnyFunction export type AccessFilter = (sesame: string | undefined, exporter: I.RPCExporter) => Promise | boolean export type Visibility = "127.0.0.1" | "0.0.0.0" export type ConnectionHandler = (socket: I.Socket) => void @@ -45,24 +61,23 @@ export type ErrorResponse = Respose & { result: "Error", message?: st export type RPCType = 'Hook' | 'Unhook' | 'Call' -export type CallRPC = { +export type CallRPC = { name: Name call: Func } - -export type HookRPC = { +export type HookRPC = { name: Name - hook: AnyFunction - onCallback?: AnyFunction + hook: BackendHook + onCallback?: GenericFunction onDestroy?: HookCloseFunction extends Promise ? T : ReturnType> } -export type RPC = HookRPC | CallRPC | Func +export type RPC = HookRPC | CallRPC | Func export type RPCInterface = { [grp in string]: { - [rpc in string]: AnyFunction + [rpc in string]: GenericFunction } } & Impl @@ -71,8 +86,8 @@ export type exportT = { } export type RPCDefinitions = { - [grp in keyof Ifc]: ({ - [rpc in keyof Ifc[grp]]: RPC + [grp in keyof Ifc]: ({ + [rpc in keyof Ifc[grp]]: RPC }[keyof Ifc[grp]])[] } @@ -89,7 +104,7 @@ export type HookInfo = BaseInfo & { export type CallInfo = BaseInfo & { type: 'Call', - call: AnyFunction + call: GenericFunction } export type RpcInfo = HookInfo | CallInfo @@ -99,39 +114,30 @@ export type OnFunction = (type: T, f: FrontEndHandl export type HookCloseFunction = (res: T, rpc: HookRPC) => any -export type AsyncIfc = { [grp in keyof Ifc]: { [rpcname in keyof Ifc[grp]]: AsyncAnyFunction } } +export type AsyncIfc = { [grp in keyof Ifc]: { [rpcname in keyof Ifc[grp]]: AsyncGenericFunction } } -export type AsyncAnyFunction = F extends (...args: Parameters) => infer R +export type AsyncGenericFunction = F extends (...args: Parameters) => infer R ? ((...args: Parameters) => R extends Promise ? R : Promise) : Promise -type DYN_PARAM< - A = void, - B = void, - C = void, - D = void, - E = void, - F = void, - G = void, - H = void, -> = H extends void ? - G extends void ? - F extends void ? - E extends void ? - D extends void ? - C extends void ? - B extends void ? - A extends void ? - [] - : [A] - : [A,B] - :[A,B,C] - :[A,B,C,D] - :[A,B,C,D,E] - :[A,B,C,D,E,F] - :[A,B,C,D,E,F,G] - :[A,B,C,D,E,F,G,H] type Destroyable = { destroy: () => void } -export type Callback = - (this: Destroyable, ...args: DYN_PARAM) => void +export type Callback = + (this: Destroyable, ...args: Params) => void + +type AsFunction = F extends GenericFunction ? F : GenericFunction + +type Last = Tuple[ Subtract, 1> ] +type Head = T extends [ ...infer Head, any ] ? Head : any[] +type Tail = T extends [any, ...infer Tail] ? Tail: [] + +type Length = + T extends { length: infer L } ? L : never; + +type BuildTuple = + T extends { length: L } ? T : BuildTuple; + +type Subtract = + BuildTuple extends [...(infer U), ...BuildTuple] + ? Length + : never; \ No newline at end of file diff --git a/src/Utils.ts b/src/Utils.ts index d0ebd41..07b2d9b 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -76,7 +76,7 @@ export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter, 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.GenericFunction, errorHandler: T.ErrorHandler): T.GenericFunction => { const argsArr = extractArgs(rpcFunction) const args = argsArr.join(',') const argsStr = argsArr.map(stripAfterEquals).join(',') @@ -108,6 +108,7 @@ export function stripAfterEquals(str: string): string { * @returns A {@link HookFunction} */ const hookGenerator = (rpc: T.HookRPC, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction, injectSocket?: boolean): T.HookInfo['generator'] => { + const deserializer = DeserializerFactory let argsArr = extractArgs(rpc.hook) argsArr.pop() //remove callback param @@ -124,6 +125,7 @@ const hookGenerator = (rpc: T.HookRPC, errorHandler: T.ErrorHandler, s const uuid = uuidv4() const res = await rpc.hook(${callArgs} (...cbargs) => { ${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``} + cbargs = cbargs.map(deserializer.makeDeserializable) $__socket__$.call.apply($__socket__$, [uuid, ...cbargs]) }) ${rpc.onDestroy ? `$__socket__$.bind(destroy_prefix+uuid, () => { @@ -247,7 +249,7 @@ export const makePioSocket = (socket: any): I.Socket => { res(undefined) }), - unhook: (name: string, listener?: T.AnyFunction) => { + unhook: (name: string, listener?: T.GenericFunction) => { if (listener) { socket.removeListener(name, listener) } else { diff --git a/test/Test.ts b/test/Test.ts index ecf2daf..59c1ce3 100644 --- a/test/Test.ts +++ b/test/Test.ts @@ -1,7 +1,7 @@ import { describe, it } from "mocha"; import { RPCServer, RPCSocket, Serializable } from '../Index' import { RPCExporter, Socket } from "../src/Interfaces"; -import { ConnectedSocket, Callback } from "../src/Types"; +import { ConnectedSocket, Callback, GenericFunction } from "../src/Types"; import * as log from 'why-is-node-running'; import * as http from 'http'; import * as express from 'express'; @@ -9,11 +9,11 @@ import * as fetch from 'node-fetch'; import { PromiseIO } from "../src/PromiseIO/Server"; import { PromiseIOClient } from "../src/PromiseIO/Client"; import { assert, expect } from 'chai'; -import { USER_DEFINED_TIMEOUT } from "../src/Strings"; +import { CLASSNAME_ATTRIBUTE, USER_DEFINED_TIMEOUT } from "../src/Strings"; var should = require('chai').should(); var chai = require("chai"); var chaiAsPromised = require("chai-as-promised"); - + chai.use(chaiAsPromised); const noop = (...args) => { } @@ -489,12 +489,12 @@ describe('RPCSocket', () => { }) - it('should have rpc echo', async() => { + it('should have rpc echo', async () => { const x = await client['test'].echo("x") expect(x).to.be.equal('x') }) - it('should add up to 6', async() => { + it('should add up to 6', async () => { const sum = await client['test'].add(1, 2, 3) expect(sum).to.be.equal(6) }) @@ -590,7 +590,7 @@ describe('It should do unhook', () => { let run = 0 const expected = [yesCandy, noCandy, noCandy, noCandy] - it('Unhook+unsubscribe should stop callbacks', async() => { + it('Unhook+unsubscribe should stop callbacks', async () => { await client['test'].subscribe(function myCallback(c) { if (run == 1) (myCallback as any).destroy() @@ -614,7 +614,7 @@ type topicDTO = { topic: string; } type SesameTestIfc = { test: { checkCandy: () => Promise - subscribe: (callback: Callback) => Promise + subscribe: (callback: Callback<[string]>) => Promise manyParams: (a: A, b: B, c: C, d: D) => Promise<[A, B, C, D]> } } @@ -688,7 +688,7 @@ describe('Sesame should unlock the socket', () => { }) it('callback should work with sesame', (done) => { - client.test.subscribe(function(c){ + client.test.subscribe(function (c) { if (c === candy) { done() } @@ -725,21 +725,21 @@ describe('Error handling', () => { a: 'a', b: 'b' }) - .then(r => { - if (r != null) - done(new Error("UNEXPECTED RESULT " + r)) - }) - .catch((e) => { - if (e.message === errtxt) - done() - else - done(e) - }) - .finally(() => { - cli.close() - sock.close() - server.close() - }) + .then(r => { + if (r != null) + done(new Error("UNEXPECTED RESULT " + r)) + }) + .catch((e) => { + if (e.message === errtxt) + done() + else + done(e) + }) + .finally(() => { + cli.close() + sock.close() + server.close() + }) }) }) @@ -1014,12 +1014,12 @@ describe("attaching handlers before connecting", () => { describe("class (de-)serialization", () => { @Serializable() - class SubClass{ + class SubClass { fString = "F" } @Serializable() - class TestClass{ + class TestClass { aString = "A" aNumber = 46 aObject = { @@ -1029,63 +1029,104 @@ describe("class (de-)serialization", () => { } aClassObject = new SubClass() - public returnOK(){ + public returnOK() { return "OK" } } - let myServer: RPCServer; - let mySocket: RPCSocket; + const verifyObject = (obj: any) => { + expect(obj).to.be.an.instanceOf(TestClass) + expect(obj.aString).to.be.a('string') + expect(obj.aNumber).to.be.a('number') + expect(obj.aObject).to.be.a('object') + expect(obj.aObject.x).to.be.a('string') + expect(obj.aObject.y).to.be.undefined + expect(obj.aObject.sub).to.be.an.instanceOf(SubClass) + expect(obj.aClassObject).to.be.an.instanceOf(SubClass) + expect(obj).to.not.have.key(CLASSNAME_ATTRIBUTE) + expect(obj.aObject.sub).to.not.have.key(CLASSNAME_ATTRIBUTE) + expect(obj.aClassObject).to.not.have.key(CLASSNAME_ATTRIBUTE) + expect(obj.returnOK()).to.be.equal('OK') + } - before(function(done){ - myServer = new RPCServer([{ - name: "Test", - RPCs: [ - function returnClass(){ - return new TestClass() + describe("Responses", () => { + type TestIfc = { + Test: { + returnClass: () => Promise + classCallback: (callback: Callback<[TestClass]>) => Promise + } + } + + let myServer: RPCServer; + let mySocket: ConnectedSocket; + + before(function (done) { + myServer = new RPCServer([{ + name: "Test", + RPCs: [ + async function returnClass() { + return new TestClass() + }, { + name: "classCallback", + hook: async function (callback) { + setTimeout(_ => callback(new TestClass()), 250) + return new TestClass() + } + } + ] + }]) + myServer.listen(8084) + + new RPCSocket(8084, 'localhost').connect().then(connsock => { + mySocket = connsock + done() + }) + }) + + after(function (done) { + mySocket.close() + myServer.close() + done() + }) + + + it("receives class object in call response", async () => { + const obj: TestClass = await mySocket['Test'].returnClass() + verifyObject(obj) + }) + + it("receives class object in hook response", async function () { + const obj: TestClass = await mySocket.Test.classCallback(noop) + verifyObject(obj) + }) + + it("receives class object in callback", function (done) { + mySocket.Test.classCallback(function (cbValue) { + verifyObject(cbValue) + done() + }).then(verifyObject) + }) + }) + describe("Parameters", () => { + it("Class object in call", function(done){ + const server = new RPCServer([ + { + name: "Test", + RPCs: [ + function callWithClass(testObj: TestClass){ + verifyObject(testObj) + done() + } + ] } - ] - }]) - myServer.listen(8084) + ]).listen(8086) - mySocket = new RPCSocket(8084, 'localhost') - mySocket.connect().then(() => done()) - }) - - after(function(done){ - mySocket.close() - myServer.close() - done() - }) - - - it("receives class in call response", async () => { - const obj: TestClass = await mySocket['Test'].returnClass() - - expect(obj).to.be.an.instanceOf(TestClass) - expect(obj.aString).to.be.a('string') - expect(obj.aNumber).to.be.a('number') - expect(obj.aObject).to.be.a('object') - expect(obj.aObject.x).to.be.a('string') - expect(obj.aObject.y).to.be.undefined - expect(obj.aObject.sub).to.be.an.instanceOf(SubClass) - expect(obj.aClassObject).to.be.an.instanceOf(SubClass) - - expect(obj.returnOK()).to.be.equal('OK') - }) - - it("receives class in hook response", async () => { - const obj: TestClass = await mySocket['Test'].returnClass() - - expect(obj).to.be.an.instanceOf(TestClass) - expect(obj.aString).to.be.a('string') - expect(obj.aNumber).to.be.a('number') - expect(obj.aObject).to.be.a('object') - expect(obj.aObject.x).to.be.a('string') - expect(obj.aObject.y).to.be.undefined - expect(obj.aObject.sub).to.be.an.instanceOf(SubClass) - expect(obj.aClassObject).to.be.an.instanceOf(SubClass) - - expect(obj.returnOK()).to.be.equal('OK') + new RPCSocket(8086, 'localhost').connect().then(sock => { + sock['Test'].callWithClass(new TestClass()).then(_ => { + sock.close() + server.close() + }) + }) + }) }) }) \ No newline at end of file