stuff
This commit is contained in:
Vendored
-20
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
Vendored
-17
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
Vendored
-14
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
Vendored
-54
@@ -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>;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
Vendored
-4
@@ -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[];
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
Vendored
-97
@@ -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 {};
|
|
||||||
@@ -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;
|
|
||||||
Vendored
-6
@@ -1,6 +0,0 @@
|
|||||||
import { SocketioRPC, Name } from "../Types";
|
|
||||||
export interface Exporter {
|
|
||||||
name: Name;
|
|
||||||
exportRPCs(): SocketioRPC[];
|
|
||||||
exportPublicRPCs(): SocketioRPC[];
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
Vendored
-11
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
@@ -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))
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { SocketioRPC, Name } from "../Types";
|
|
||||||
|
|
||||||
export interface Exporter{
|
|
||||||
name: Name
|
|
||||||
exportRPCs() : SocketioRPC[]
|
|
||||||
exportPublicRPCs() : SocketioRPC[]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
+41
-9
@@ -1,27 +1,28 @@
|
|||||||
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',
|
mode: 'production',
|
||||||
target: "web",
|
target: "web",
|
||||||
entry: path.resolve(__dirname, 'Client.ts'),
|
entry: path.resolve(__dirname, 'Client.ts'),
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, '../../lib'),
|
path: path.resolve(__dirname, '../lib'),
|
||||||
filename: 'Frontend.min.js',
|
filename: 'Frontend.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 +30,37 @@ module.exports = {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
externals: {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backendConf = {
|
||||||
|
mode: 'production',
|
||||||
|
target: "node",
|
||||||
|
entry: path.resolve(__dirname, 'Server.ts'),
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, '../lib'),
|
||||||
|
filename: 'Backend.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/,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = [frontendConf, backendConf]
|
||||||
|
|||||||
+5
-6
@@ -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')
|
||||||
|
|||||||
+3
-3
@@ -5,10 +5,10 @@
|
|||||||
"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"],
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user