This commit is contained in:
Daniel Hübleitner
2019-09-18 05:19:42 +02:00
parent 3cfbb3558f
commit ea18b22463
31 changed files with 174 additions and 1041 deletions
-20
View File
@@ -1,20 +0,0 @@
import { Socket } from './interfaces/Socket';
export declare class Client implements Socket {
port: number;
private server;
private tls;
private socket;
constructor(port: number, server: string, tls?: boolean);
hook(name: any, args: any): Socket;
unhook(name: any): Socket;
on(type: "error" | "close", f: (e?: any) => void): Socket;
destroy(): void;
close(): void;
call(rpcname: string, ...args: any[]): Promise<any>;
fire(rpcname: string, ...args: any[]): Promise<any>;
connect(): Promise<void>;
info(): Promise<any>;
private callGenerator;
private hookGenerator;
private unhookGenerator;
}
-88
View File
@@ -1,88 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var bsock = require('bsock');
//fix args with defaults like "force = true" -> "force"
function stripAfterEquals(str) {
return str.split("=")[0];
}
class Client {
constructor(port, server, tls = false) {
this.port = port;
this.server = server;
this.tls = tls;
}
hook(name, args) {
return this.socket.hook(name, args);
}
unhook(name) {
return this.socket.unhook(name);
}
on(type, f) {
return this.socket.on(type, name);
}
destroy() {
return this.socket.destroy();
}
close() {
return this.socket.close();
}
async call(rpcname, ...args) {
return await this.socket.call.apply(this.socket, [rpcname, ...args]);
}
async fire(rpcname, ...args) {
return await this.socket.fire.apply(this.socket, [rpcname, ...args]);
}
async connect() {
this.socket = await bsock.connect(this.port, this.server, this.tls);
const info = await this.info();
info.forEach(i => {
let f;
switch (i.type) {
case 'Call':
f = this.callGenerator(i.uniqueName, i.argNames);
break;
case 'Hook':
f = this.hookGenerator(i.uniqueName, i.argNames);
break;
case 'Unhook':
f = this.unhookGenerator(i.uniqueName, i.argNames);
break;
}
if (this[i.owner] == null)
this[i.owner] = {};
this[i.owner][i.name] = f;
this[i.owner][i.name].bind(this);
});
}
async info() {
return await this.socket.call('info');
}
callGenerator(fnName, fnArgs) {
const headerArgs = fnArgs.join(",");
const argParams = fnArgs.map(stripAfterEquals).join(",");
return eval('( () => async (' + headerArgs + ') => { return await this.socket.call("' + fnName + '", ' + argParams + ')} )()');
}
hookGenerator(fnName, fnArgs) {
const headerArgs = fnArgs.join(",");
const argParams = fnArgs.map(stripAfterEquals).join(",");
return eval(`( () => async (` + headerArgs + (headerArgs.length !== 0 ? "," : "") + ` callback) => {
const r = await this.socket.call("` + fnName + `", ` + argParams + `)
if(r.uid != null){
this.socket.hook(res.uid, callback)
}
return res
} )()`);
}
unhookGenerator(fnName, fnArgs) {
const headerArgs = fnArgs.join(",");
const argParams = fnArgs.map(stripAfterEquals).join(",");
if (fnArgs.length != 1)
console.error("UnhookFunction", fnName, "specified more than one argument: (" + headerArgs + ")");
return eval(`( () => async (` + headerArgs + `) => {
const r = await this.socket.call("` + fnName + `", ` + argParams + `)
this.socket.unhook(` + argParams + `)
return res
} )()`);
}
}
exports.Client = Client;
-17
View File
@@ -1,17 +0,0 @@
export declare type Outcome = "Success" | "Error";
export declare class Response {
message?: string | undefined;
constructor(message?: string | undefined);
}
export declare class SuccessResponse extends Response {
result: Outcome;
constructor(message?: string);
}
export declare class ErrorResponse extends Response {
result: Outcome;
constructor(message?: string);
}
export declare class SubscriptionResponse extends SuccessResponse {
uid: string;
constructor(uid: string, message?: string);
}
-30
View File
@@ -1,30 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/* Responses */
class Response {
constructor(message) {
this.message = message;
}
}
exports.Response = Response;
class SuccessResponse extends Response {
constructor(message) {
super(message);
this.result = "Success";
}
}
exports.SuccessResponse = SuccessResponse;
class ErrorResponse extends Response {
constructor(message = "Unknown error") {
super(message);
this.result = "Error";
}
}
exports.ErrorResponse = ErrorResponse;
class SubscriptionResponse extends SuccessResponse {
constructor(uid, message) {
super(message);
this.uid = uid;
}
}
exports.SubscriptionResponse = SubscriptionResponse;
-14
View File
@@ -1,14 +0,0 @@
import { SocketConf } from './Types';
import { Exporter } from './interfaces/Exporter';
import { Socket } from "./interfaces/Socket";
export declare class Server {
private port;
private rpcExporters;
private conf;
private io;
private wsServer;
constructor(port: number, rpcExporters?: Exporter[], conf?: SocketConf);
private startWebsocket;
protected initRPCs(socket: Socket): void;
protected initPublicRPCs(socket: Socket): void;
}
-58
View File
@@ -1,58 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const http = require("http");
const bsock = require("bsock");
const Util_1 = require("./Util");
class Server {
constructor(port, rpcExporters = [], conf = {
errorHandler: (socket) => (error) => { socket.destroy(); console.error(error); },
closeHandler: (socket) => () => { console.log("Socket closing"); },
connectionHandler: (socket) => { console.log("New websocket connection in port " + socket.port); },
visibility: "127.0.0.1"
}) {
this.port = port;
this.rpcExporters = rpcExporters;
this.conf = conf;
this.io = bsock.createServer();
this.wsServer = http.createServer();
this.startWebsocket();
}
startWebsocket() {
try {
this.io.attach(this.wsServer);
this.io.on('socket', (socket) => {
socket.on('error', this.conf.errorHandler(socket));
socket.on('close', this.conf.closeHandler(socket));
if (this.conf.visibility === "127.0.0.1")
this.initRPCs(socket);
else
this.initPublicRPCs(socket);
});
this.wsServer.listen(this.port, this.conf.visibility);
}
catch (e) {
//@ts-ignore
this.errorHandler(undefined)("Unable to connect to socket");
}
}
initRPCs(socket) {
const infoRPC = [
{
name: 'info',
type: 'Call',
func: async () => rpcInfos
}
];
const rpcInfos = [
...Util_1.rpcHooker(socket, "RPC", infoRPC, false),
...this.rpcExporters.flatMap(exporter => Util_1.rpcHooker(socket, exporter.name, [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()]))
];
}
initPublicRPCs(socket) {
const rpcInfos = [
...Util_1.rpcHooker(socket, "Admin", adminRPCs, false),
...this.rpcExporters.flatMap(exporter => Util_1.rpcHooker(socket, exporter.name, exporter.exportPublicRPCs()))
];
}
}
exports.Server = Server;
-54
View File
@@ -1,54 +0,0 @@
import { SuccessResponse, ErrorResponse, SubscriptionResponse } from "./Response";
import { Socket } from "./interfaces/Socket";
export declare type Visibility = "127.0.0.1" | "0.0.0.0";
export declare type Name = string;
export declare type SocketConf = {
connectionHandler: (socket: Socket) => void;
errorHandler: (socket: Socket) => (error: any) => void;
closeHandler: (socket: Socket) => () => void;
visibility: Visibility;
};
export declare type rpcType = 'Hook' | 'Unhook' | 'Call';
export declare type BaseRPC = {
type: rpcType;
name: string;
};
export declare type HookRPC = BaseRPC & {
type: 'Hook';
func: CallbackFunction;
unhook: UnhookFunction;
};
export declare type UnhookRPC = BaseRPC & {
type: 'Unhook';
func: UnhookFunction;
};
export declare type CallRPC = BaseRPC & {
type: 'Call';
func: (...args: any[]) => Promise<any>;
};
export declare type SocketioRPC = CallRPC | UnhookRPC | HookRPC;
export declare type BaseInfo = {
owner: string;
argNames: string[];
};
export declare type HookInfo = BaseRPC & BaseInfo & {
type: 'Hook';
generator: (socket: any) => CallbackFunction;
unhook: UnhookFunction;
};
export declare type UnhookInfo = BaseRPC & BaseInfo & {
type: 'Unhook';
func: UnhookFunction;
};
export declare type CallInfo = BaseRPC & BaseInfo & {
type: 'Call';
func: AsyncFunction;
};
export declare type RpcInfo = HookInfo | UnhookInfo | CallInfo;
export declare type ExtendedRpcInfo = RpcInfo & {
uniqueName: string;
};
export declare type OnFunction = (type: 'error' | 'close', f: (e?: any) => void) => Socket;
export declare type UnhookFunction = (uid: string) => Promise<SuccessResponse | ErrorResponse>;
export declare type CallbackFunction = (...args: any[]) => Promise<SubscriptionResponse | ErrorResponse>;
export declare type AsyncFunction = (...args: any[]) => Promise<any>;
-2
View File
@@ -1,2 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-4
View File
@@ -1,4 +0,0 @@
import { SocketioRPC, RpcInfo, ExtendedRpcInfo } from "./Types";
import { Socket } from "./interfaces/Socket";
export declare const rpcToRpcinfo: (rpc: SocketioRPC, owner: string) => RpcInfo;
export declare const rpcHooker: (socket: Socket, owner: string, RPCs: SocketioRPC[], makeUnique?: boolean) => ExtendedRpcInfo[];
-75
View File
@@ -1,75 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const uuid = require("uuid/v4");
exports.rpcToRpcinfo = (rpc, owner) => {
switch (rpc.type) {
case "Call":
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
};
case "Unhook":
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
};
case "Hook":
const generator = hookGenerator(rpc);
return {
owner: owner,
argNames: extractArgs(generator(undefined)),
type: rpc.type,
name: rpc.name,
unhook: rpc.unhook,
generator: generator,
};
}
};
exports.rpcHooker = (socket, owner, RPCs, makeUnique = true) => {
const suffix = makeUnique ? "-" + uuid().substr(0, 4) : "";
return RPCs.map(rpc => exports.rpcToRpcinfo(rpc, owner))
.map(info => {
const ret = info;
ret.uniqueName = info.name + suffix;
switch (info.type) {
case "Hook":
socket.hook(ret.uniqueName, info.generator(socket));
break;
default:
socket.hook(ret.uniqueName, info.func);
}
socket.on('close', () => socket.unhook(info.name));
return ret;
});
};
const hookGenerator = (rpc) => {
const argsArr = extractArgs(rpc.func);
argsArr.pop();
const args = argsArr.join(',');
return eval(`(socket) => async (` + args + `) => {
const res = await rpc.func(` + args + (args.length !== 0 ? ',' : '') + ` (x) => {
socket.call(res.uid, x)
})
if(res.result == 'Success'){
socket.on('close', async () => {
const unhookRes = await rpc.unhook(res.uid)
console.log("Specific close handler for", rpc.name, res.uid, unhookRes)
})
}
return res
}`);
};
const extractArgs = (f) => {
let fn = String(f);
let args = fn.substr(0, fn.indexOf(")"));
args = args.substr(fn.indexOf("(") + 1);
let ret = args.split(",");
return ret;
};
-97
View File
@@ -1,97 +0,0 @@
import { Socket } from "./RPCSocketServer";
declare type rpcType = 'hook' | 'unhook' | 'call';
export declare type Outcome = "Success" | "Error";
export declare type Visibility = "127.0.0.1" | "0.0.0.0";
export declare class Response {
message?: string | undefined;
constructor(message?: string | undefined);
}
export declare class SuccessResponse extends Response {
result: Outcome;
constructor(message?: string);
}
export declare class ErrorResponse extends Response {
result: Outcome;
constructor(message?: string);
}
export declare class SubscriptionResponse extends SuccessResponse {
uid: string;
constructor(uid: string, message?: string);
}
export declare type UnhookFunction = (uid: string) => Promise<SuccessResponse | ErrorResponse>;
export declare type callbackFunction = (...args: any[]) => Promise<SubscriptionResponse | ErrorResponse>;
export declare type AsyncFunction = (...args: any[]) => Promise<any>;
export interface RPCExporter {
name: string;
exportRPCs(): socketioRPC[];
exportPublicRPCs(): socketioRPC[];
}
declare type baseRPC = {
type: rpcType;
name: string;
};
declare type hookRPC = baseRPC & {
type: 'hook';
func: callbackFunction;
unhook: UnhookFunction;
};
declare type unhookRPC = baseRPC & {
type: 'unhook';
func: UnhookFunction;
};
declare type callRPC = baseRPC & {
type: 'call';
func: (...args: any[]) => Promise<any>;
};
export declare type socketioRPC = callRPC | unhookRPC | hookRPC;
export declare type baseInfo = {
owner: string;
argNames: string[];
};
declare type HookInfo = baseRPC & baseInfo & {
type: 'hook';
generator: (socket: any) => callbackFunction;
unhook: UnhookFunction;
};
declare type UnhookInfo = baseRPC & baseInfo & {
type: 'unhook';
func: UnhookFunction;
};
declare type CallInfo = baseRPC & baseInfo & {
type: 'call';
func: AsyncFunction;
};
declare type RpcInfo = HookInfo | UnhookInfo | CallInfo;
export declare type ExtendedRpcInfo = RpcInfo & {
uniqueName: string;
};
export declare const rpcToRpcinfo: (rpc: socketioRPC, owner: string) => RpcInfo;
declare type OnFunction = (type: 'error' | 'close', f: (e?: any) => void) => Socket;
export interface Socket {
port: number;
hook: (rpcname: string, ...args: any[]) => Socket;
unhook: (rpcname: string) => Socket;
call: (rpcname: string, ...args: any[]) => Promise<any>;
fire: (rpcname: string, ...args: any[]) => Promise<any>;
on: OnFunction;
destroy: () => void;
close: () => void;
}
export declare type RPCSocketConf = {
connectionHandler: (socket: Socket) => void;
errorHandler: (socket: Socket) => (error: any) => void;
closeHandler: (socket: Socket) => () => void;
};
export declare class RPCSocketServer {
private port;
private rpcExporters;
private visibility;
private conf;
private io;
private wsServer;
constructor(port: number, rpcExporters?: RPCExporter[], visibility?: Visibility, conf?: RPCSocketConf);
private startWebsocket;
protected initRPCs(socket: Socket): void;
protected initPublicRPCs(socket: Socket): void;
}
export {};
-153
View File
@@ -1,153 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const http = require("http");
const bsock = require("bsock");
const uuid = require("uuid/v4");
/* Responses */
class Response {
constructor(message) {
this.message = message;
}
}
exports.Response = Response;
class SuccessResponse extends Response {
constructor(message) {
super(message);
this.result = "Success";
}
}
exports.SuccessResponse = SuccessResponse;
class ErrorResponse extends Response {
constructor(message = "Unknown error") {
super(message);
this.result = "Error";
}
}
exports.ErrorResponse = ErrorResponse;
class SubscriptionResponse extends SuccessResponse {
constructor(uid, message) {
super(message);
this.uid = uid;
}
}
exports.SubscriptionResponse = SubscriptionResponse;
exports.rpcToRpcinfo = (rpc, owner) => {
switch (rpc.type) {
case "call":
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
};
case "unhook":
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
};
case "hook":
const generator = hookGenerator(rpc);
return {
owner: owner,
argNames: extractArgs(generator(undefined)),
type: rpc.type,
name: rpc.name,
unhook: rpc.unhook,
generator: generator,
};
}
};
function rpcHooker(socket, exporter, makeUnique = true) {
const owner = exporter.name;
const RPCs = [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()];
const suffix = makeUnique ? "-" + uuid().substr(0, 4) : "";
return RPCs.map(rpc => exports.rpcToRpcinfo(rpc, owner))
.map(info => {
const ret = info;
ret.uniqueName = info.name + suffix;
switch (info.type) {
case "hook":
socket.hook(ret.uniqueName, info.generator(socket));
break;
default:
socket.hook(ret.uniqueName, info.func);
}
socket.on('close', () => socket.unhook(info.name));
return ret;
});
}
const hookGenerator = (rpc) => {
const argsArr = extractArgs(rpc.func);
argsArr.pop();
const args = argsArr.join(',');
return eval(`(socket) => async (` + args + `) => {
const res = await rpc.func(` + args + (args.length !== 0 ? ',' : '') + ` (x) => {
socket.call(res.uid, x)
})
if(res.result == 'Success'){
socket.on('close', async () => {
const unhookRes = await rpc.unhook(res.uid)
console.log("Specific close handler for", rpc.name, res.uid, unhookRes)
})
}
return res
}`);
};
const extractArgs = (f) => {
let fn = String(f);
let args = fn.substr(0, fn.indexOf(")"));
args = args.substr(fn.indexOf("(") + 1);
let ret = args.split(",");
return ret;
};
class RPCSocketServer {
constructor(port, rpcExporters = [], visibility = "127.0.0.1", conf = {
errorHandler: (socket) => (error) => { socket.destroy(); console.error(error); },
closeHandler: (socket) => () => { console.log("Socket closing"); },
connectionHandler: (socket) => { console.log("New websocket connection in port " + socket.port); }
}) {
this.port = port;
this.rpcExporters = rpcExporters;
this.visibility = visibility;
this.conf = conf;
this.io = bsock.createServer();
this.wsServer = http.createServer();
this.startWebsocket();
}
startWebsocket() {
try {
this.io.attach(this.wsServer);
this.io.on('socket', (socket) => {
socket.on('error', this.conf.errorHandler(socket));
socket.on('close', this.conf.closeHandler(socket));
if (this.visibility === "127.0.0.1")
this.initRPCs(socket);
else
this.initPublicRPCs(socket);
});
this.wsServer.listen(this.port, this.visibility);
}
catch (e) {
//@ts-ignore
this.errorHandler(undefined)("Unable to connect to socket");
}
}
initRPCs(socket) {
socket.hook('info', () => rpcInfos);
const rpcInfos = [
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter))
];
}
initPublicRPCs(socket) {
socket.hook('info', () => rpcInfos);
const rpcInfos = [
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter))
];
}
}
exports.RPCSocketServer = RPCSocketServer;
-6
View File
@@ -1,6 +0,0 @@
import { SocketioRPC, Name } from "../Types";
export interface Exporter {
name: Name;
exportRPCs(): SocketioRPC[];
exportPublicRPCs(): SocketioRPC[];
}
-2
View File
@@ -1,2 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-11
View File
@@ -1,11 +0,0 @@
import { OnFunction } from "../Types";
export interface Socket {
port: number;
hook: (rpcname: string, ...args: any[]) => Socket;
unhook: (rpcname: string) => Socket;
call: (rpcname: string, ...args: any[]) => Promise<any>;
fire: (rpcname: string, ...args: any[]) => Promise<any>;
on: OnFunction;
destroy: () => void;
close: () => void;
}
-2
View File
@@ -1,2 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-1
View File
@@ -1 +0,0 @@
export {};
-23
View File
@@ -1,23 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Server_1 = require("../src/Server");
//@ts-ignore
const Client_1 = require("../src/Client");
new Server_1.Server(20000, [{
name: "HelloWorldRPCGroup",
exportPublicRPCs: () => [],
exportRPCs: () => [{
type: 'call',
name: 'echo',
func: async (s) => s,
}],
}]);
<<<<<<< HEAD
const caller = new Client_1.Client(20000, 'localhost');
=======
const caller = new RPCSocket_1.RPCSocket(20000, 'localhost');
>>>>>>> 17dc58c5b3fd3c76113d592d895400498578affa
caller.connect().then(_ => {
caller.info().then(console.log);
caller["HelloWorldRPCGroup"].echo("x").then(console.log);
});
+2 -1
View File
@@ -23,5 +23,6 @@
"dependencies": { "dependencies": {
"bsock": "^0.1.9", "bsock": "^0.1.9",
"uuid": "^3.3.3" "uuid": "^3.3.3"
} },
"files": []
} }
+24 -18
View File
@@ -1,50 +1,56 @@
'use strict'
var bsock = require('bsock') var bsock = require('bsock')
import { ExtendedRpcInfo, UnhookFunction, CallbackFunction, AsyncFunction } from "./Types"; import * as I from './Interfaces';
import { Socket } from './interfaces/Socket' import * as T from './Types';
//fix args with defaults like "force = true" -> "force" //fix args with defaults like "force = true" -> "force"
function stripAfterEquals(str:string){ function stripAfterEquals(str:string){
return str.split("=")[0] return str.split("=")[0]
} }
export class Client implements Socket{ export class Client implements I.Socket{
private socket: Socket
private socket: I.Socket
constructor(public port:number, private server: string, private tls: boolean = false){ constructor(public port:number, private server: string, private tls: boolean = false){
} }
hook(name, args){
public hook(name: T.Name, args: T.Arg){
return this.socket.hook(name, args) return this.socket.hook(name, args)
} }
unhook(name){ public unhook(name: T.Name){
return this.socket.unhook(name) return this.socket.unhook(name)
} }
on(type: "error" | "close", f: (e?: any) => void){ public on(type: "error" | "close", f: (e?: any) => void){
return this.socket.on(type, name) return this.socket.on(type, f)
} }
destroy(){ public destroy(){
return this.socket.destroy() return this.socket.destroy()
} }
close(){ public close(){
return this.socket.close() return this.socket.close()
} }
async call (rpcname: string, ...args: any[]) : Promise<any>{ public async call (rpcname: T.Name, ...args: T.Any[]) : Promise<T.Any>{
return await this.socket.call.apply(this.socket, [rpcname, ...args]) return await this.socket.call.apply(this.socket, [rpcname, ...args])
} }
async fire(rpcname: string, ...args: any[]) : Promise<any>{ public async fire(rpcname: T.Name, ...args: T.Any[]) : Promise<T.Any>{
return await this.socket.fire.apply(this.socket, [rpcname, ...args]) return await this.socket.fire.apply(this.socket, [rpcname, ...args])
} }
async connect(){ public async connect(){
this.socket = await bsock.connect(this.port, this.server, this.tls) this.socket = await bsock.connect(this.port, this.server, this.tls)
const info:ExtendedRpcInfo[] = await this.info() const info:T.ExtendedRpcInfo[] = await this.info()
info.forEach(i => { info.forEach(i => {
let f: any let f: any
switch (i.type) { switch (i.type) {
@@ -65,17 +71,17 @@ export class Client implements Socket{
}) })
} }
async info(){ public async info(){
return await this.socket.call('info') return await this.socket.call('info')
} }
private callGenerator(fnName, fnArgs:string[]): AsyncFunction{ private callGenerator(fnName: T.Name, fnArgs:T.Arg[]): 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+')} )()' )
} }
private hookGenerator(fnName, fnArgs:string[]): CallbackFunction{ private hookGenerator(fnName: T.Name, fnArgs:T.Arg[]): T.CallbackFunction{
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) => {
@@ -87,7 +93,7 @@ export class Client implements Socket{
} )()` ) } )()` )
} }
private unhookGenerator(fnName, fnArgs:string[]): UnhookFunction{ private unhookGenerator(fnName: T.Name, fnArgs:T.Arg[]): T.UnhookFunction{
const headerArgs = fnArgs.join(",") const headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",") const argParams = fnArgs.map(stripAfterEquals).join(",")
if(fnArgs.length != 1) if(fnArgs.length != 1)
+19
View File
@@ -0,0 +1,19 @@
import * as T from "./Types";
import * as I from "./Interfaces"
export interface Exporter{
name: T.Name
localRPCs() : T.RPC[]
publicRPCs() : T.RPC[]
}
export interface Socket {
port: number
hook: (rpcname: T.Name, ...args: T.Any[]) => I.Socket
unhook: (rpcname:T.Name) => I.Socket
call: (rpcname:T.Name, ...args: T.Any[]) => Promise<T.Any>
fire: (rpcname:T.Name, ...args: T.Any[]) => Promise<T.Any>
on: T.OnFunction
destroy: ()=>void
close: ()=>void
}
+26 -26
View File
@@ -1,23 +1,23 @@
'use strict'
import http = require('http'); import http = require('http');
import bsock = require('bsock'); import bsock = require('bsock');
import { ExtendedRpcInfo, SocketConf, SocketioRPC } from './Types'; import * as T from './Types';
import { rpcHooker } from './Util'; import * as U from './Utils';
import { Exporter } from './interfaces/Exporter'; import * as I from './Interfaces';
import { Socket } from "./interfaces/Socket";
export class Server{ export class Server{
private ws = http.createServer()
private io = bsock.createServer() private io = bsock.createServer()
private wsServer = http.createServer()
constructor( constructor(
private port:number, private port:number,
private rpcExporters: Exporter[] = [], private exporters: I.Exporter[] = [],
private conf: SocketConf = { private conf: T.SocketConf = {
errorHandler: (socket:Socket) => (error:any) => { socket.destroy(); console.error(error) }, errorHandler: (socket:I.Socket) => (error:any) => { socket.destroy(); console.error(error) },
closeHandler: (socket:Socket) => () => { console.log("Socket closing") }, closeHandler: (socket:I.Socket) => () => { console.log("Socket closing") },
connectionHandler: (socket:Socket) => { console.log("New websocket connection in port "+socket.port)}, connectionHandler: (socket:I.Socket) => { console.log("New websocket connection in port "+socket.port)},
visibility: "127.0.0.1" visibility: "127.0.0.1"
} }
){ ){
@@ -26,8 +26,8 @@ export class Server{
private startWebsocket(){ private startWebsocket(){
try{ try{
this.io.attach(this.wsServer) this.io.attach(this.ws)
this.io.on('socket', (socket:Socket) => { this.io.on('socket', (socket:I.Socket) => {
socket.on('error', this.conf.errorHandler(socket)) socket.on('error', this.conf.errorHandler(socket))
socket.on('close', this.conf.closeHandler(socket)) socket.on('close', this.conf.closeHandler(socket))
if(this.conf.visibility === "127.0.0.1") if(this.conf.visibility === "127.0.0.1")
@@ -35,26 +35,26 @@ export class Server{
else else
this.initPublicRPCs(socket) this.initPublicRPCs(socket)
}) })
this.wsServer.listen(this.port, this.conf.visibility) this.ws.listen(this.port, this.conf.visibility)
}catch(e){ }catch(e){
//@ts-ignore //@ts-ignore
this.errorHandler(undefined)("Unable to connect to socket") this.errorHandler(undefined)("Unable to connect to socket")
} }
} }
protected initRPCs(socket:Socket){ protected initRPCs(socket:I.Socket){
const infoRPC:SocketioRPC[] = [ socket.hook('info', () => rpcInfos)
{
name: 'info', const rpcInfos:T.ExtendedRpcInfo[] = [
type: 'Call', ...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter))
func: async () => rpcInfos
}
]
const rpcInfos:ExtendedRpcInfo[] = [
...rpcHooker(socket, "RPC", infoRPC, false),
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter.name, [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()]))
] ]
} }
protected initPublicRPCs(socket:Socket){} protected initPublicRPCs(socket:I.Socket){
socket.hook('info', () => rpcInfos)
const rpcInfos:T.ExtendedRpcInfo[] = [
...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter))
]
}
} }
+23 -20
View File
@@ -1,44 +1,47 @@
import { SuccessResponse, ErrorResponse, SubscriptionResponse } from "./Response"; import * as R from "./Responses";
import { Socket } from "./interfaces/Socket"; import * as I from "./Interfaces";
export type Visibility = "127.0.0.1" | "0.0.0.0" export type Visibility = "127.0.0.1" | "0.0.0.0"
export type Name = string export type Any = any
export type Arg = string
export type Name = Arg
export type Owner = Name
export type SocketConf = { export type SocketConf = {
connectionHandler: (socket:Socket) => void connectionHandler: (socket:I.Socket) => void
errorHandler: (socket:Socket) => (error:any) => void errorHandler: (socket:I.Socket) => (error:any) => void
closeHandler: (socket:Socket) => () => void closeHandler: (socket:I.Socket) => () => void
visibility: Visibility visibility: Visibility
} }
export type rpcType = 'Hook' | 'Unhook' | 'Call' export type RPCType = 'Hook' | 'Unhook' | 'Call'
export type BaseRPC = { export type BaseRPC = {
type: rpcType type: RPCType
name: string name: Name
} }
export type HookRPC = BaseRPC & { export type HookRPC = BaseRPC & {
type: 'Hook' type: 'Hook'
func: CallbackFunction clbk: CallbackFunction
unhook: UnhookFunction unhook: UnhookFunction
} }
export type UnhookRPC = BaseRPC & { export type UnhookRPC = BaseRPC & {
type: 'Unhook' type: 'Unhook'
func: UnhookFunction unhook: UnhookFunction
} }
export type CallRPC = BaseRPC & { export type CallRPC = BaseRPC & {
type: 'Call' type: 'Call'
func: (...args) => Promise<any> call: AsyncFunction
} }
export type SocketioRPC = CallRPC | UnhookRPC | HookRPC export type RPC = CallRPC | UnhookRPC | HookRPC
export type BaseInfo = { export type BaseInfo = {
owner: string, owner: Name,
argNames: string[], argNames: Name[],
} }
export type HookInfo = BaseRPC & BaseInfo & { export type HookInfo = BaseRPC & BaseInfo & {
@@ -49,18 +52,18 @@ export type HookInfo = BaseRPC & BaseInfo & {
export type UnhookInfo = BaseRPC & BaseInfo & { export type UnhookInfo = BaseRPC & BaseInfo & {
type: 'Unhook', type: 'Unhook',
func: UnhookFunction unhook: UnhookFunction
} }
export type CallInfo = BaseRPC & BaseInfo & { export type CallInfo = BaseRPC & BaseInfo & {
type: 'Call', type: 'Call',
func: AsyncFunction call: AsyncFunction
} }
export type RpcInfo = HookInfo | UnhookInfo | CallInfo export type RpcInfo = HookInfo | UnhookInfo | CallInfo
export type ExtendedRpcInfo = RpcInfo & { uniqueName: string } export type ExtendedRpcInfo = RpcInfo & { uniqueName: string }
export type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => Socket export type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => I.Socket
export type UnhookFunction = (uid:string) => Promise<SuccessResponse | ErrorResponse> export type UnhookFunction = (uid:string) => Promise<R.SuccessResponse | R.ErrorResponse>
export type CallbackFunction = (...args) => Promise<SubscriptionResponse | ErrorResponse> export type CallbackFunction = (...args) => Promise<R.SubscriptionResponse | R.ErrorResponse>
export type AsyncFunction = (...args) => Promise<any> export type AsyncFunction = (...args) => Promise<any>
+20 -15
View File
@@ -1,25 +1,25 @@
import * as uuid from "uuid/v4" import * as uuid from "uuid/v4"
import { HookRPC, HookInfo, SocketioRPC, RpcInfo, ExtendedRpcInfo } from "./Types"; import * as T from "./Types";
import { Socket } from "./interfaces/Socket"; import * as I from "./Interfaces";
export const rpcToRpcinfo = (rpc : SocketioRPC, owner: string):RpcInfo => { export const rpcToRpcinfo = (rpc : T.RPC, owner: T.Owner):T.RpcInfo => {
switch(rpc.type){ switch(rpc.type){
case "Call" : case "Call" :
return { return {
owner: owner, owner: owner,
argNames: extractArgs(rpc.func), argNames: extractArgs(rpc.call),
type: rpc.type, type: rpc.type,
name: rpc.name, name: rpc.name,
func: rpc.func, call: rpc.call,
} }
case "Unhook" : case "Unhook" :
return { return {
owner: owner, owner: owner,
argNames: extractArgs(rpc.func), argNames: extractArgs(rpc.unhook),
type: rpc.type, type: rpc.type,
name: rpc.name, name: rpc.name,
func: rpc.func, unhook: rpc.unhook,
} }
case "Hook" : case "Hook" :
const generator = hookGenerator(rpc) const generator = hookGenerator(rpc)
@@ -33,8 +33,9 @@ export const rpcToRpcinfo = (rpc : SocketioRPC, owner: string):RpcInfo => {
} }
} }
} }
export function rpcHooker(socket: I.Socket, exporter:I.Exporter, makeUnique = true):T.ExtendedRpcInfo[]{
export const rpcHooker = (socket: Socket, owner:string, RPCs: SocketioRPC[], makeUnique = true):ExtendedRpcInfo[] => { const owner = exporter.name
const RPCs = [...exporter.publicRPCs(), ...exporter.localRPCs()]
const suffix = makeUnique?"-"+uuid().substr(0,4):"" const suffix = makeUnique?"-"+uuid().substr(0,4):""
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner)) return RPCs.map(rpc => rpcToRpcinfo(rpc, owner))
.map(info => { .map(info => {
@@ -45,21 +46,25 @@ export const rpcHooker = (socket: Socket, owner:string, RPCs: SocketioRPC[], mak
case "Hook": case "Hook":
socket.hook(ret.uniqueName, info.generator(socket)) socket.hook(ret.uniqueName, info.generator(socket))
break; break;
default: case "Unhook":
socket.hook(ret.uniqueName, info.func) socket.hook(ret.uniqueName, info.unhook)
break;
case "Call":
socket.hook(ret.uniqueName, info.call)
break;
} }
socket.on('close', () => socket.unhook(info.name)) socket.on('close', () => socket.unhook(info.name))
return ret return ret
}) })
} }
const hookGenerator = (rpc:HookRPC): HookInfo['generator'] => { const hookGenerator = (rpc:T.HookRPC): T.HookInfo['generator'] => {
const argsArr = extractArgs(rpc.func) const argsArr = extractArgs(rpc.clbk)
argsArr.pop() argsArr.pop()
const args = argsArr.join(',') const args = argsArr.join(',')
return eval(`(socket) => async (`+args+`) => { return eval(`(socket) => async (`+args+`) => {
const res = await rpc.func(`+args+(args.length!==0?',':'')+` (x) => { const res = await rpc.clbk(`+args+(args.length!==0?',':'')+` (x) => {
socket.call(res.uid, x) socket.call(res.uid, x)
}) })
if(res.result == 'Success'){ if(res.result == 'Success'){
@@ -73,7 +78,7 @@ const hookGenerator = (rpc:HookRPC): HookInfo['generator'] => {
}`) }`)
} }
const extractArgs = (f:Function):string[] => { const extractArgs = (f:Function):T.Arg[] => {
let fn = String(f) let fn = String(f)
let args = fn.substr(0, fn.indexOf(")")) let args = fn.substr(0, fn.indexOf(")"))
args = args.substr(fn.indexOf("(")+1) args = args.substr(fn.indexOf("(")+1)
-256
View File
@@ -1,256 +0,0 @@
import http = require('http');
import bsock = require('bsock');
import * as uuid from "uuid/v4"
import { Socket } from "./RPCSocketServer"
type rpcType = 'hook' | 'unhook' | 'call'
export type Outcome = "Success" | "Error"
export type Visibility = "127.0.0.1" | "0.0.0.0"
/* Responses */
export class Response{
constructor(
public message?:string
){}
}
export class SuccessResponse extends Response{
result:Outcome = "Success"
constructor(
message?:string
){
super(message)
}
}
export class ErrorResponse extends Response{
result:Outcome = "Error"
constructor(
message: string = "Unknown error"
){
super(message)
}
}
export class SubscriptionResponse extends SuccessResponse{
constructor(
public uid: string,
message?:string
){
super(message)
}
}
export type UnhookFunction = (uid:string) => Promise<SuccessResponse | ErrorResponse>
export type callbackFunction = (...args) => Promise<SubscriptionResponse | ErrorResponse>
export type AsyncFunction = (...args) => Promise<any>
export interface RPCExporter{
name: string
exportRPCs() : socketioRPC[]
exportPublicRPCs() : socketioRPC[]
}
type baseRPC = {
type: rpcType
name: string
}
type hookRPC = baseRPC & {
type: 'hook'
func: callbackFunction
unhook: UnhookFunction
}
type unhookRPC = baseRPC & {
type: 'unhook'
func: UnhookFunction
}
type callRPC = baseRPC & {
type: 'call'
func: (...args) => Promise<any>
}
export type socketioRPC = callRPC | unhookRPC | hookRPC
export type baseInfo = {
owner: string,
argNames: string[],
}
type HookInfo = baseRPC & baseInfo & {
type: 'hook',
generator: (socket) => callbackFunction
unhook: UnhookFunction
}
type UnhookInfo = baseRPC & baseInfo & {
type: 'unhook',
func: UnhookFunction
}
type CallInfo = baseRPC & baseInfo & {
type: 'call',
func: AsyncFunction
}
type RpcInfo = HookInfo | UnhookInfo | CallInfo
export type ExtendedRpcInfo = RpcInfo & { uniqueName: string }
export const rpcToRpcinfo = (rpc : socketioRPC, owner: string):RpcInfo => {
switch(rpc.type){
case "call" :
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
}
case "unhook" :
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
}
case "hook" :
const generator = hookGenerator(rpc)
return {
owner: owner,
argNames: extractArgs(generator(undefined)),
type: rpc.type,
name: rpc.name,
unhook: rpc.unhook,
generator: generator,
}
}
}
function rpcHooker(socket: Socket, exporter:RPCExporter, makeUnique = true):ExtendedRpcInfo[]{
const owner = exporter.name
const RPCs = [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()]
const suffix = makeUnique?"-"+uuid().substr(0,4):""
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner))
.map(info => {
const ret:any = info
ret.uniqueName = info.name+suffix
switch(info.type){
case "hook":
socket.hook(ret.uniqueName, info.generator(socket))
break;
default:
socket.hook(ret.uniqueName, info.func)
}
socket.on('close', () => socket.unhook(info.name))
return ret
})
}
const hookGenerator = (rpc:hookRPC): HookInfo['generator'] => {
const argsArr = extractArgs(rpc.func)
argsArr.pop()
const args = argsArr.join(',')
return eval(`(socket) => async (`+args+`) => {
const res = await rpc.func(`+args+(args.length!==0?',':'')+` (x) => {
socket.call(res.uid, x)
})
if(res.result == 'Success'){
socket.on('close', async () => {
const unhookRes = await rpc.unhook(res.uid)
console.log("Specific close handler for", rpc.name, res.uid, unhookRes)
})
}
return res
}`)
}
const extractArgs = (f:Function):string[] => {
let fn = String(f)
let args = fn.substr(0, fn.indexOf(")"))
args = args.substr(fn.indexOf("(")+1)
let ret = args.split(",")
return ret
}
type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => Socket
export interface Socket {
port: number
hook: (rpcname: string, ...args: any[]) => Socket
unhook: (rpcname:string) => Socket
call: (rpcname:string, ...args: any[]) => Promise<any>
fire: (rpcname:string, ...args: any[]) => Promise<any>
on: OnFunction
destroy: ()=>void
close: ()=>void
}
export type RPCSocketConf = {
connectionHandler: (socket:Socket) => void
errorHandler: (socket:Socket) => (error:any) => void
closeHandler: (socket:Socket) => () => void
}
export class RPCSocketServer{
private io = bsock.createServer()
private wsServer = http.createServer()
constructor(
private port:number,
private rpcExporters: RPCExporter[] = [],
private visibility: Visibility = "127.0.0.1",
private conf: RPCSocketConf = {
errorHandler: (socket:Socket) => (error:any) => { socket.destroy(); console.error(error) },
closeHandler: (socket:Socket) => () => { console.log("Socket closing") },
connectionHandler: (socket:Socket) => { console.log("New websocket connection in port "+socket.port) }
}
){
this.startWebsocket()
}
private startWebsocket(){
try{
this.io.attach(this.wsServer)
this.io.on('socket', (socket:Socket) => {
socket.on('error', this.conf.errorHandler(socket))
socket.on('close', this.conf.closeHandler(socket))
if(this.visibility === "127.0.0.1")
this.initRPCs(socket)
else
this.initPublicRPCs(socket)
})
this.wsServer.listen(this.port, this.visibility)
}catch(e){
//@ts-ignore
this.errorHandler(undefined)("Unable to connect to socket")
}
}
protected initRPCs(socket:Socket){
socket.hook('info', () => rpcInfos)
const rpcInfos:ExtendedRpcInfo[] = [
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter))
]
}
protected initPublicRPCs(socket:Socket){
socket.hook('info', () => rpcInfos)
const rpcInfos:ExtendedRpcInfo[] = [
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter))
]
}
}
-7
View File
@@ -1,7 +0,0 @@
import { SocketioRPC, Name } from "../Types";
export interface Exporter{
name: Name
exportRPCs() : SocketioRPC[]
exportPublicRPCs() : SocketioRPC[]
}
-12
View File
@@ -1,12 +0,0 @@
import { OnFunction } from "../Types";
export interface Socket {
port: number
hook: (rpcname: string, ...args: any[]) => Socket
unhook: (rpcname:string) => Socket
call: (rpcname:string, ...args: any[]) => Promise<any>
fire: (rpcname:string, ...args: any[]) => Promise<any>
on: OnFunction
destroy: ()=>void
close: ()=>void
}
+43 -11
View File
@@ -1,27 +1,59 @@
const path = require('path'); const path = require('path');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
module.exports = { const frontendConf = {
mode: 'production',
target: "web",
entry: path.resolve(__dirname, 'Client.ts'),
output: {
path: path.resolve(__dirname, '../lib'),
filename: 'Frontend.js',
libraryTarget: 'commonjs',
},
module: {
rules: [
{ test: /\.ts?$/, loader: "ts-loader" }
]
},
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
exclude: [
/\.\/(.*)\/.ts/,
/\.\/(.*).ts/,
],
}),
],
},
}
const backendConf = {
mode: 'production', mode: 'production',
target: "web", target: "node",
entry: path.resolve(__dirname, 'Client.ts'), entry: path.resolve(__dirname, 'Server.ts'),
output: { output: {
path: path.resolve(__dirname, '../../lib'), path: path.resolve(__dirname, '../lib'),
filename: 'Frontend.min.js', filename: 'Backend.js',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
}, },
resolve: {
// Add `.ts` and `.tsx` as a resolvable extension.
extensions: [".ts", ".tsx", ".js"]
},
module: { module: {
rules: [ rules: [
{ test: /\.ts?$/, loader: "ts-loader" } { test: /\.ts?$/, loader: "ts-loader" }
] ]
}, },
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
optimization: { optimization: {
minimize: true,
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({
parallel: true,
exclude: [ exclude: [
/\.\/(.*)\/.ts/, /\.\/(.*)\/.ts/,
/\.\/(.*).ts/, /\.\/(.*).ts/,
@@ -29,6 +61,6 @@ module.exports = {
}), }),
], ],
}, },
externals: {
}
} }
module.exports = [frontendConf, backendConf]
+5 -6
View File
@@ -1,15 +1,14 @@
import { Server } from '../src/Server' import { Server } from '../src/Server'
//@ts-ignore
import {Client} from '../src/Client' import {Client} from '../src/Client'
new Server(20000, [{ new Server(20000, [{
name: "HelloWorldRPCGroup", name: "HelloWorldRPCGroup",
exportPublicRPCs: () => [], publicRPCs: () => [],
exportRPCs: () => [{ localRPCs: () => [{
type: 'call', type: 'Call',
name: 'echo', name: 'echo',
func: async (s:string) => s, call: async (s:string) => s,
}], }]
}]) }])
const caller = new Client(20000, 'localhost') const caller = new Client(20000, 'localhost')
+12 -12
View File
@@ -1,14 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"noImplicitAny": false, "noImplicitAny": false,
"target": "ESnext", "target": "ESnext",
"module": "commonjs", "module": "commonjs",
"declaration": true, "declaration": true,
"outDir": "./lib", "outDir": "./js",
"strict": true, "strict": true,
"experimentalDecorators": true "experimentalDecorators": true
}, },
"include": ["src/backend/**/*", "test/**/*"], "include": ["src/**/*.ts", "test/**/*.ts"],
"exclude": ["node_modules", "**/__tests__/*", "src/frontend/**/*"] "exclude": ["node_modules"],
} }