before rewrite to have callbacks as FIRST parameter

This commit is contained in:
nitowa
2022-04-07 00:01:41 +02:00
parent a1f4055194
commit b2ffbbfa48
6 changed files with 191 additions and 135 deletions
+16 -10
View File
@@ -4,7 +4,7 @@ import { PromiseIOClient, defaultClientConfig } from './PromiseIO/Client'
import * as T from './Types'; import * as T from './Types';
import * as I from './Interfaces'; import * as I from './Interfaces';
import { stripAfterEquals, appendComma } from './Utils'; 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'; import { DeserializerFactory } from './Decorator';
DeserializerFactory DeserializerFactory
@@ -21,12 +21,12 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
private socket: I.Socket private socket: I.Socket
private handlers: { private handlers: {
[name in string]: T.AnyFunction[] [name in string]: T.GenericFunction[]
} = { } = {
error: [], error: [],
close: [] close: []
} }
private hooks: { [name in string]: T.AnyFunction } = {} private hooks: { [name in string]: T.GenericFunction } = {}
/** /**
* *
@@ -82,7 +82,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @param type 'error' or 'close' * @param type 'error' or 'close'
* @param f The listener to attach * @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.socket) {
if (!this.handlers[type]) if (!this.handlers[type])
this.handlers[type] = [] this.handlers[type] = []
@@ -166,7 +166,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
v.forEach(h => this.socket.on(k, h)) 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]) this.socket.hook(kv[0], kv[1])
}) })
const info: T.ExtendedRpcInfo[] = await this.info(sesame) const info: T.ExtendedRpcInfo[] = await this.info(sesame)
@@ -205,7 +205,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @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: string, fnArgs: string[], sesame?: string): T.AnyFunction { private callGenerator(fnName: string, fnArgs: string[], sesame?: string): T.GenericFunction {
const headerArgs = fnArgs.join(",") const headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",") const argParams = fnArgs.map(stripAfterEquals).join(",")
sesame = appendComma(sesame) sesame = appendComma(sesame)
@@ -221,7 +221,7 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
* @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 frontEndHookGenerator(fnName: string, fnArgs: string[], sesame?: string): T.HookFunction { private frontEndHookGenerator(fnName: string, fnArgs: string[], sesame?: string): T.GenericFunction {
if (sesame) if (sesame)
fnArgs.shift() fnArgs.shift()
@@ -234,16 +234,22 @@ export class RPCSocket<Ifc extends T.RPCInterface = T.RPCInterface> implements I
const destroy_prefix = DESTROY_PREFIX const destroy_prefix = DESTROY_PREFIX
const frontendHookStr = ` const frontendHookStr = `
async (${headerArgs} $__callback__$) => { async (${headerArgs} ${CALLBACK_NAME}) => {
const r = await this.call("${fnName}", ${sesame} ${argParams}) const r = await this.call("${fnName}", ${sesame} ${argParams})
try{ try{
if(r){ if(r){
if(r.uuid){ if(r.uuid){
$__callback__$['destroy'] = () => { ${CALLBACK_NAME}['destroy'] = () => {
this.socket.fire(destroy_prefix+r.uuid) this.socket.fire(destroy_prefix+r.uuid)
this.socket.unhook(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) return deserializer.from(r.return)
}else{ }else{
+2 -2
View File
@@ -15,10 +15,10 @@ export interface Socket {
id?: string id?: string
bind: (name: string, listener: T.PioBindListener) => void bind: (name: string, listener: T.PioBindListener) => void
hook: (rpcname: string, handler: T.PioHookListener) => 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<any> call: (rpcname: string, ...args: any[]) => Promise<any>
fire: (rpcname: string, ...args: any[]) => Promise<any> fire: (rpcname: string, ...args: any[]) => Promise<any>
on: (type: string, f: T.AnyFunction)=>any on: (type: string, f: T.GenericFunction)=>any
emit: (eventName: string, ...args: any[]) => void emit: (eventName: string, ...args: any[]) => void
close(): void close(): void
} }
+1
View File
@@ -14,3 +14,4 @@ RPC did not provide a name.
\n>------------OFFENDING RPC` \n>------------OFFENDING RPC`
export const CLASSNAME_ATTRIBUTE = "$__CLASSNAME__$" export const CLASSNAME_ATTRIBUTE = "$__CLASSNAME__$"
export const DESTROY_PREFIX = "$__DESTROY__$_" export const DESTROY_PREFIX = "$__DESTROY__$_"
export const CALLBACK_NAME = "$__CALLBACK__$"
+48 -42
View File
@@ -1,12 +1,28 @@
import * as I from "./Interfaces"; import * as I from "./Interfaces";
import { RPCSocket } from "./Frontend"; import { RPCSocket } from "./Frontend";
import { PromiseIO } from "./PromiseIO/Server";
export type PioBindListener = (...args: any) => void export type PioBindListener = (...args: any) => void
export type PioHookListener = AnyFunction export type PioHookListener = GenericFunction
export type GenericFunction<Parameters extends any[] = any[], Result = any> = {(...args: Parameters): Result}
export type BackendHook<Func extends GenericFunction> =
GenericFunction<
[
...Head<Parameters<Func>>,
GenericFunction<
Parameters<
AsFunction<
Last<Parameters<Func>>
>
>,
void
>
],
ReturnType<Func>
>
export type AnyFunction = (...args: any[]) => any
export type HookFunction = AnyFunction
export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame: string | undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean> | boolean export type AccessFilter<InterfaceT extends RPCInterface = RPCInterface> = (sesame: string | undefined, exporter: I.RPCExporter<InterfaceT, keyof InterfaceT>) => Promise<boolean> | boolean
export type Visibility = "127.0.0.1" | "0.0.0.0" export type Visibility = "127.0.0.1" | "0.0.0.0"
export type ConnectionHandler = (socket: I.Socket) => void export type ConnectionHandler = (socket: I.Socket) => void
@@ -45,24 +61,23 @@ export type ErrorResponse<T = {}> = Respose<T> & { result: "Error", message?: st
export type RPCType = 'Hook' | 'Unhook' | 'Call' export type RPCType = 'Hook' | 'Unhook' | 'Call'
export type CallRPC<Name, Func extends AnyFunction> = { export type CallRPC<Name, Func extends GenericFunction> = {
name: Name name: Name
call: Func call: Func
} }
export type HookRPC<Name, Func extends GenericFunction> = {
export type HookRPC<Name, Func extends AnyFunction> = {
name: Name name: Name
hook: AnyFunction hook: BackendHook<Func>
onCallback?: AnyFunction onCallback?: GenericFunction
onDestroy?: HookCloseFunction<ReturnType<Func> extends Promise<infer T> ? T : ReturnType<Func>> onDestroy?: HookCloseFunction<ReturnType<Func> extends Promise<infer T> ? T : ReturnType<Func>>
} }
export type RPC<Name, Func extends AnyFunction> = HookRPC<Name, Func> | CallRPC<Name, Func> | Func export type RPC<Name, Func extends GenericFunction> = HookRPC<Name, Func> | CallRPC<Name, Func> | Func
export type RPCInterface<Impl extends RPCInterface = {}> = { export type RPCInterface<Impl extends RPCInterface = {}> = {
[grp in string]: { [grp in string]: {
[rpc in string]: AnyFunction [rpc in string]: GenericFunction
} }
} & Impl } & Impl
@@ -89,7 +104,7 @@ export type HookInfo<SubresT = {}> = BaseInfo & {
export type CallInfo = BaseInfo & { export type CallInfo = BaseInfo & {
type: 'Call', type: 'Call',
call: AnyFunction call: GenericFunction
} }
export type RpcInfo = HookInfo | CallInfo export type RpcInfo = HookInfo | CallInfo
@@ -99,39 +114,30 @@ export type OnFunction = <T extends "error" | "close">(type: T, f: FrontEndHandl
export type HookCloseFunction<T> = (res: T, rpc: HookRPC<any, any>) => any export type HookCloseFunction<T> = (res: T, rpc: HookRPC<any, any>) => any
export type AsyncIfc<Ifc extends RPCInterface> = { [grp in keyof Ifc]: { [rpcname in keyof Ifc[grp]]: AsyncAnyFunction<Ifc[grp][rpcname]> } } export type AsyncIfc<Ifc extends RPCInterface> = { [grp in keyof Ifc]: { [rpcname in keyof Ifc[grp]]: AsyncGenericFunction<Ifc[grp][rpcname]> } }
export type AsyncAnyFunction<F extends AnyFunction = AnyFunction> = F extends (...args: Parameters<F>) => infer R export type AsyncGenericFunction<F extends GenericFunction = GenericFunction> = F extends (...args: Parameters<F>) => infer R
? ((...args: Parameters<F>) => R extends Promise<any> ? R : Promise<R>) ? ((...args: Parameters<F>) => R extends Promise<any> ? R : Promise<R>)
: Promise<any> : Promise<any>
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 } type Destroyable = { destroy: () => void }
export type Callback<A0 = void, A1 = void, A2 = void, A3 = void, A4 = void, A5 = void, A6 = void, A7 = void> = export type Callback<Params extends any[] = []> =
(this: Destroyable, ...args: DYN_PARAM<A0, A1, A2, A3, A4, A5, A6, A7>) => void (this: Destroyable, ...args: Params) => void
type AsFunction<F> = F extends GenericFunction ? F : GenericFunction
type Last<Tuple extends any[]> = Tuple[ Subtract<Length<Tuple>, 1> ]
type Head<T extends any[]> = T extends [ ...infer Head, any ] ? Head : any[]
type Tail<T extends any[]> = T extends [any, ...infer Tail] ? Tail: []
type Length<T extends any[]> =
T extends { length: infer L } ? L : never;
type BuildTuple<L extends number, T extends any[] = []> =
T extends { length: L } ? T : BuildTuple<L, [...T, any]>;
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...(infer U), ...BuildTuple<B>]
? Length<U>
: never;
+4 -2
View File
@@ -76,7 +76,7 @@ export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter<any, any>, e
* Decorate an RPC with the error handler * Decorate an RPC with the error handler
* @param rpcFunction the function to decorate * @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 argsArr = extractArgs(rpcFunction)
const args = argsArr.join(',') const args = argsArr.join(',')
const argsStr = argsArr.map(stripAfterEquals).join(',') const argsStr = argsArr.map(stripAfterEquals).join(',')
@@ -108,6 +108,7 @@ export function stripAfterEquals(str: string): string {
* @returns A {@link HookFunction} * @returns A {@link HookFunction}
*/ */
const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction, injectSocket?: boolean): T.HookInfo['generator'] => { const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction, injectSocket?: boolean): T.HookInfo['generator'] => {
const deserializer = DeserializerFactory
let argsArr = extractArgs(rpc.hook) let argsArr = extractArgs(rpc.hook)
argsArr.pop() //remove callback param argsArr.pop() //remove callback param
@@ -124,6 +125,7 @@ const hookGenerator = (rpc: T.HookRPC<any, any>, errorHandler: T.ErrorHandler, s
const uuid = uuidv4() const uuid = uuidv4()
const res = await rpc.hook(${callArgs} (...cbargs) => { const res = await rpc.hook(${callArgs} (...cbargs) => {
${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``} ${rpc.onCallback ? `rpc.onCallback.apply({}, cbargs)` : ``}
cbargs = cbargs.map(deserializer.makeDeserializable)
$__socket__$.call.apply($__socket__$, [uuid, ...cbargs]) $__socket__$.call.apply($__socket__$, [uuid, ...cbargs])
}) })
${rpc.onDestroy ? `$__socket__$.bind(destroy_prefix+uuid, () => { ${rpc.onDestroy ? `$__socket__$.bind(destroy_prefix+uuid, () => {
@@ -247,7 +249,7 @@ export const makePioSocket = (socket: any): I.Socket => {
res(undefined) res(undefined)
}), }),
unhook: (name: string, listener?: T.AnyFunction) => { unhook: (name: string, listener?: T.GenericFunction) => {
if (listener) { if (listener) {
socket.removeListener(name, listener) socket.removeListener(name, listener)
} else { } else {
+82 -41
View File
@@ -1,7 +1,7 @@
import { describe, it } from "mocha"; import { describe, it } from "mocha";
import { RPCServer, RPCSocket, Serializable } from '../Index' import { RPCServer, RPCSocket, Serializable } from '../Index'
import { RPCExporter, Socket } from "../src/Interfaces"; 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 log from 'why-is-node-running';
import * as http from 'http'; import * as http from 'http';
import * as express from 'express'; import * as express from 'express';
@@ -9,7 +9,7 @@ import * as fetch from 'node-fetch';
import { PromiseIO } from "../src/PromiseIO/Server"; import { PromiseIO } from "../src/PromiseIO/Server";
import { PromiseIOClient } from "../src/PromiseIO/Client"; import { PromiseIOClient } from "../src/PromiseIO/Client";
import { assert, expect } from 'chai'; 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 should = require('chai').should();
var chai = require("chai"); var chai = require("chai");
var chaiAsPromised = require("chai-as-promised"); var chaiAsPromised = require("chai-as-promised");
@@ -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") const x = await client['test'].echo("x")
expect(x).to.be.equal('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) const sum = await client['test'].add(1, 2, 3)
expect(sum).to.be.equal(6) expect(sum).to.be.equal(6)
}) })
@@ -590,7 +590,7 @@ describe('It should do unhook', () => {
let run = 0 let run = 0
const expected = [yesCandy, noCandy, noCandy, noCandy] 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) { await client['test'].subscribe(function myCallback(c) {
if (run == 1) if (run == 1)
(myCallback as any).destroy() (myCallback as any).destroy()
@@ -614,7 +614,7 @@ type topicDTO = { topic: string; }
type SesameTestIfc = { type SesameTestIfc = {
test: { test: {
checkCandy: () => Promise<string> checkCandy: () => Promise<string>
subscribe: (callback: Callback<string>) => Promise<topicDTO> subscribe: (callback: Callback<[string]>) => Promise<topicDTO>
manyParams: <A = string, B = number, C = boolean, D = Object>(a: A, b: B, c: C, d: D) => Promise<[A, B, C, D]> manyParams: <A = string, B = number, C = boolean, D = Object>(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) => { it('callback should work with sesame', (done) => {
client.test.subscribe(function(c){ client.test.subscribe(function (c) {
if (c === candy) { if (c === candy) {
done() done()
} }
@@ -1014,12 +1014,12 @@ describe("attaching handlers before connecting", () => {
describe("class (de-)serialization", () => { describe("class (de-)serialization", () => {
@Serializable() @Serializable()
class SubClass{ class SubClass {
fString = "F" fString = "F"
} }
@Serializable() @Serializable()
class TestClass{ class TestClass {
aString = "A" aString = "A"
aNumber = 46 aNumber = 46
aObject = { aObject = {
@@ -1029,63 +1029,104 @@ describe("class (de-)serialization", () => {
} }
aClassObject = new SubClass() aClassObject = new SubClass()
public returnOK(){ public returnOK() {
return "OK" return "OK"
} }
} }
let myServer: RPCServer; const verifyObject = (obj: any) => {
let mySocket: RPCSocket; 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){ describe("Responses", () => {
myServer = new RPCServer([{ type TestIfc = {
Test: {
returnClass: () => Promise<TestClass>
classCallback: (callback: Callback<[TestClass]>) => Promise<TestClass>
}
}
let myServer: RPCServer<TestIfc>;
let mySocket: ConnectedSocket<TestIfc>;
before(function (done) {
myServer = new RPCServer<TestIfc>([{
name: "Test", name: "Test",
RPCs: [ RPCs: [
function returnClass(){ async function returnClass() {
return new TestClass() return new TestClass()
}, {
name: "classCallback",
hook: async function (callback) {
setTimeout(_ => callback(new TestClass()), 250)
return new TestClass()
}
} }
] ]
}]) }])
myServer.listen(8084) myServer.listen(8084)
mySocket = new RPCSocket(8084, 'localhost') new RPCSocket<TestIfc>(8084, 'localhost').connect().then(connsock => {
mySocket.connect().then(() => done()) mySocket = connsock
done()
})
}) })
after(function(done){ after(function (done) {
mySocket.close() mySocket.close()
myServer.close() myServer.close()
done() done()
}) })
it("receives class in call response", async () => { it("receives class object in call response", async () => {
const obj: TestClass = await mySocket['Test'].returnClass() const obj: TestClass = await mySocket['Test'].returnClass()
verifyObject(obj)
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 () => { it("receives class object in hook response", async function () {
const obj: TestClass = await mySocket['Test'].returnClass() const obj: TestClass = await mySocket.Test.classCallback(noop)
verifyObject(obj)
})
expect(obj).to.be.an.instanceOf(TestClass) it("receives class object in callback", function (done) {
expect(obj.aString).to.be.a('string') mySocket.Test.classCallback(function (cbValue) {
expect(obj.aNumber).to.be.a('number') verifyObject(cbValue)
expect(obj.aObject).to.be.a('object') done()
expect(obj.aObject.x).to.be.a('string') }).then(verifyObject)
expect(obj.aObject.y).to.be.undefined })
expect(obj.aObject.sub).to.be.an.instanceOf(SubClass) })
expect(obj.aClassObject).to.be.an.instanceOf(SubClass) describe("Parameters", () => {
it("Class object in call", function(done){
const server = new RPCServer([
{
name: "Test",
RPCs: [
function callWithClass(testObj: TestClass){
verifyObject(testObj)
done()
}
]
}
]).listen(8086)
expect(obj.returnOK()).to.be.equal('OK') new RPCSocket(8086, 'localhost').connect().then(sock => {
sock['Test'].callWithClass(new TestClass()).then(_ => {
sock.close()
server.close()
})
})
})
}) })
}) })