chore: add full source code

This commit is contained in:
tone
2025-10-14 23:53:27 +08:00
parent e7b9228d70
commit ebb6407221
30 changed files with 6133 additions and 0 deletions

106
src/core/RPCClient.ts Normal file
View File

@@ -0,0 +1,106 @@
import { isHandshakeAccepted, makeHandshakePacket } from "./RPCCommon";
import { RPCConnection } from "./RPCConnection";
import { RPCHandler } from "./RPCHandler";
import { RPCPacket } from "./RPCPacket";
import { RPCSession } from "./RPCSession";
import { createSocketClient } from "./SocketClient";
import { SocketConnection } from "./SocketConnection";
export class RPCClient {
constructor(private rpcHandler: RPCHandler) { }
public async connect(options: {
url: string;
accessKey?: string;
timeout: number;
}): Promise<RPCSession> {
// make socket connection
const socket = createSocketClient();
// handshake by socket
const thisAccessKey = this.rpcHandler.getAccessKey();
/** set 'null' is to make sure all property will not be remove in network transmission */
const handshakePacket = makeHandshakePacket({
state: 0,
thisAccessKey: thisAccessKey || null,
accessKey: options.accessKey || null,
});
/** send handshake request */
const finalClearFns: (() => any | Promise<any>)[] = [];
async function finalClear() {
for (const fn of finalClearFns) {
try { await fn() } catch { }
}
}
let connection: SocketConnection | undefined;
// task1: timeout
let isTimeouted = false;
const timeoutPromise = new Promise<never>((_, reject) => {
let t = setTimeout(async () => {
if (isRequestFinished) {
return;
}
reject(new Error('Connect timeout'));
}, options.timeout);
finalClearFns.push(() => {
clearTimeout(t);
})
})
// task2: send and wait for response
let isRequestFinished = false;
let requestPromise = new Promise<RPCSession>(async (resolve, reject) => {
/** timeout, but connection is still keep */
finalClearFns.push(() => {
if (isTimeouted && connection) {
connection.close().catch(e => { })
}
})
/** clear listener of waiting for response */
finalClearFns.push(() => {
if (connection) {
connection.off('msg', handleListenHandshakeReply);
}
})
connection = await socket.connect(options.url);
if (isTimeouted) {
return;
}
const handleListenHandshakeReply = (msg: unknown) => {
const packet = RPCPacket.Parse(msg, true);
if (packet === null) {
reject(new Error('Connect occured an unknown error'));
return;
}
if (isHandshakeAccepted(packet)) {
resolve(new RPCSession(
new RPCConnection(connection!),
this.rpcHandler,
));
} else {
reject(new Error('Server rejected handshake request'));
}
}
/** listen msg from server, and make sure handshake status */
connection.on('msg', handleListenHandshakeReply);
await connection.send(handshakePacket);
if (isTimeouted) {
return;
}
})
return Promise
.race([timeoutPromise, requestPromise])
.finally(() => finalClear());
}
}

229
src/core/RPCCommon.ts Normal file
View File

@@ -0,0 +1,229 @@
import { isObject } from "@/utils/utils";
import { RPC_ERROR_MESSAGES, RPCErrorCode } from "./RPCError";
import { RPCPacket } from "./RPCPacket";
function makeHandshakePacket(data: {
state: 0;
thisAccessKey: string | null;
accessKey: string | null;
}): RPCPacket;
function makeHandshakePacket(data: {
state: 1;
accept: boolean;
reason?: string;
}): RPCPacket;
function makeHandshakePacket(data: {
state: 0 | 1,
thisAccessKey?: string | null;
accessKey?: string | null;
accept?: boolean;
reason?: string;
}): RPCPacket {
return new RPCPacket({
type: 'handshake',
data,
});
}
export {
makeHandshakePacket,
};
export function verifyHandshakeRequest(options: {
/** handshake request packet */
packet: RPCPacket;
/** If empty, all connections are allowed */
thisAccessKey?: string;
/** If empty, all connections are allowed */
thatAccessKeys?: string[];
}) {
const { data } = options.packet;
if (!isObject(data)) {
return false;
}
if ('state' in data && 'thisAccessKey' in data && 'accessKey' in data) {
const { state, thisAccessKey: thatAccessKey, accessKey } = data;
if (state !== 0) {
return false;
}
if (options.thisAccessKey && options.thisAccessKey !== accessKey) {
return false;
}
if (options.thatAccessKeys && !options.thatAccessKeys.includes(thatAccessKey)) {
return false;
}
return true;
}
return false;
}
export function isHandshakeAccepted(packet: RPCPacket) {
const { data } = packet;
if (!isObject(data)) {
return false;
}
if ('state' in data && data.state === 1) {
if ('accept' in data && typeof data.accept === 'boolean') {
return data.accept;
}
}
return false;
}
function isValidMethodPathExtended(str: unknown): str is string {
if (typeof str !== 'string' || str.length === 0) {
return false;
}
const regex = /^[a-zA-Z0-9_$]+(?::[a-zA-Z0-9_$]+)*$/;
return regex.test(str);
}
export function makeCallPacket(options: {
fnPath: string;
args: any[];
// timeout: number;
}) {
const data = {
...options,
}
return new RPCPacket({
type: 'call',
data,
})
}
export function parseCallPacket(packet: RPCPacket) {
if (!RPCPacket.isCallPacket(packet)) {
return null;
}
if (!isObject(packet.data)) {
return null;
}
const { data } = packet;
if ('fnPath' in data && 'args' in data) {
const { fnPath, args } = data;
if (!isValidMethodPathExtended(fnPath)) {
console.log('66', fnPath)
return null;
}
if (!Array.isArray(args)) {
return null;
}
return {
fnPath,
args,
}
}
return null;
}
type BaseCallResponseOptions = (
| { requestPacket: RPCPacket; requestPacketId?: never }
| { requestPacket?: never; requestPacketId: string }
);
type SuccessResponseOptions = BaseCallResponseOptions & {
status: 'success';
data: any;
requestPacket?: RPCPacket;
requestPacketId?: string;
};
type ErrorResponseOptions = BaseCallResponseOptions & {
status: 'error';
errorCode?: number;
reason?: string;
data?: never;
requestPacket?: RPCPacket;
requestPacketId?: string;
};
export function makeCallResponsePacket(options: SuccessResponseOptions | ErrorResponseOptions): RPCPacket {
let { requestPacket, requestPacketId, ...o } = options;
requestPacketId = requestPacketId ?? requestPacket?.id;
if (!requestPacketId) {
throw new Error('Request Packet Id is required');
}
const data = {
...o,
}
if (data.status === 'error') {
if (!data.errorCode) {
const errorCode = RPCErrorCode.GENERAL_ERROR;
data.errorCode = errorCode;
data.reason = RPC_ERROR_MESSAGES[errorCode];
}
if (data.errorCode && !data.reason) {
data.reason = RPC_ERROR_MESSAGES[data.errorCode]
?? RPC_ERROR_MESSAGES[RPCErrorCode.GENERAL_ERROR];
}
}
return new RPCPacket({
id: requestPacketId,
type: 'response',
data,
})
}
export function parseCallResponsePacket(packet: RPCPacket) {
if (!RPCPacket.isCallResponsePacket(packet)) {
return null;
}
if (!isObject(packet.data)) {
return null;
}
const { data } = packet;
if (!('status' in data)) {
return null;
}
const { status } = data;
if (typeof status !== 'string') {
return null;
}
if (status === 'success') {
return {
success: {
data: data.data,
},
error: null,
}
} else if (status === 'error') {
if ('errorCode' in data && 'reason' in data) {
const { errorCode, reason } = data;
if (typeof errorCode !== 'number' || typeof reason !== 'string') {
return null;
}
return {
success: null,
error: {
errorCode,
reason,
}
}
}
}
return null;
}

204
src/core/RPCConnection.ts Normal file
View File

@@ -0,0 +1,204 @@
import { EventEmitter } from "@/utils/EventEmitter";
import { SocketConnection } from "./SocketConnection";
import { RPCPacket } from "./RPCPacket";
import { makeCallPacket, makeCallResponsePacket, parseCallPacket, parseCallResponsePacket } from "./RPCCommon";
import { RPCProvider } from "./RPCProvider";
import { RPCError, RPCErrorCode } from "./RPCError";
interface RPCConnectionEvents {
call: RPCPacket;
callResponse: RPCPacket;
handshake: RPCPacket;
unknownMsg: unknown;
unknownPacket: RPCPacket;
closed: void;
}
export class RPCConnection extends EventEmitter<RPCConnectionEvents> {
closed: boolean = false;
private callResponseEmitter = new EventEmitter<{
[id: string]: RPCPacket;
}>();
constructor(public socket: SocketConnection) {
super();
socket.on('closed', () => {
this.emit('closed');
this.callResponseEmitter.removeAllListeners();
this.closed = true;
});
socket.on('msg', (msg) => {
const packet = RPCPacket.Parse(msg, true);
if (packet === null) {
this.emit('unknownMsg', msg);
return;
}
if (RPCPacket.isCallPacket(packet)) {
this.emit('call', packet);
return;
}
if (RPCPacket.isCallResponsePacket(packet)) {
this.emit('callResponse', packet);
return;
}
/** In fact, it will never be triggered */
if (RPCPacket.isHandshakePacket(packet)) {
this.emit('handshake', packet);
return;
}
this.emit('unknownPacket', packet);
});
/** route by packet.id */
this.on('callResponse', (packet) => {
this.callResponseEmitter.emit(packet.id, packet);
})
}
/** @throws */
public async callRequest(options: {
fnPath: string;
args: any[];
timeout: number;
}): Promise<any> {
const { fnPath, args } = options;
const packet = makeCallPacket({
fnPath,
args
});
let resolve: (data: any) => void;
let reject: (data: any) => void;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const cancelTimeoutTimer = (() => {
const t = setTimeout(() => {
reject(new RPCError({
errorCode: RPCErrorCode.TIMEOUT_ERROR,
}))
}, options.timeout);
return () => clearTimeout(t);
})();
promise.finally(() => {
this.callResponseEmitter.removeAllListeners(packet.id);
cancelTimeoutTimer();
})
const handleCallResponsePacket = (packet: RPCPacket) => {
const result = parseCallResponsePacket(packet);
if (result === null) {
return reject(new RPCError({
errorCode: RPCErrorCode.UNKNOWN_ERROR,
}));;
}
const { success, error } = result;
if (success) {
return resolve(success.data);
}
if (error) {
return reject(new RPCError({
errorCode: error.errorCode,
reason: error.reason
}));
}
return reject(new RPCError({
errorCode: RPCErrorCode.UNKNOWN_ERROR,
}));;
}
this.callResponseEmitter.on(packet.id, handleCallResponsePacket);
/** send call request */
this.socket.send(packet);
return promise;
}
public onCallRequest(getProvider: () => RPCProvider | undefined) {
this.on('call', async (packet) => {
const request = parseCallPacket(packet);
if (request === null) {
return this.socket.send(makeCallResponsePacket({
status: 'error',
requestPacket: packet,
errorCode: RPCErrorCode.CALL_PROTOCOL_ERROR,
})).catch(() => { })
}
// call the function
const provider = getProvider();
if (!provider) {
return this.socket.send(makeCallResponsePacket({
status: 'error',
requestPacket: packet,
errorCode: RPCErrorCode.PROVIDER_NOT_AVAILABLE,
}))
}
const { fnPath, args } = request;
const fn = this.getProviderFunction(provider, fnPath);
if (!fn) {
return this.socket.send(makeCallResponsePacket({
status: 'error',
requestPacket: packet,
errorCode: RPCErrorCode.METHOD_NOT_FOUND,
}))
}
try {
const result = await fn(...args);
this.socket.send(makeCallResponsePacket({
status: 'success',
requestPacket: packet,
data: result,
}))
} catch (error) {
this.socket.send(makeCallResponsePacket({
status: 'error',
requestPacket: packet,
errorCode: RPCErrorCode.SERVER_ERROR,
...(error instanceof RPCError ? {
errorCode: error.errorCode,
reason: error.reason,
} : {})
}))
}
})
}
private getProviderFunction(provider: RPCProvider, fnPath: string) {
const paths = fnPath.split(':');
let fnThis: any = provider;
let fn: any = provider;
try {
while (paths.length) {
const path = paths.shift()!;
fn = fn[path];
if (paths.length !== 0) {
fnThis = fn;
}
}
if (typeof fn === 'function') {
return fn.bind(fnThis);
}
throw new Error();
} catch (error) {
return null;
}
}
}

48
src/core/RPCError.ts Normal file
View File

@@ -0,0 +1,48 @@
export enum RPCErrorCode {
GENERAL_ERROR = -1,
UNKNOWN_ERROR = -2,
SERVER_ERROR = -200,
METHOD_NOT_FOUND = -201,
METHOD_PROTECTED = -202,
PROVIDER_NOT_AVAILABLE = -203,
AUTH_REJECTED = -300,
HANDSHAKE_INCOMPLETE = -400,
TIMEOUT_ERROR = -500,
CALL_PROTOCOL_ERROR = -600,
}
export const RPC_ERROR_MESSAGES: Record<RPCErrorCode | number, string> = {
[RPCErrorCode.GENERAL_ERROR]: 'General error',
[RPCErrorCode.UNKNOWN_ERROR]: 'Unknown error',
[RPCErrorCode.SERVER_ERROR]: 'Server error',
[RPCErrorCode.METHOD_NOT_FOUND]: 'Method not found',
[RPCErrorCode.METHOD_PROTECTED]: 'Method is protected',
[RPCErrorCode.PROVIDER_NOT_AVAILABLE]: 'Provider not available',
[RPCErrorCode.AUTH_REJECTED]: 'Authentication rejected',
[RPCErrorCode.HANDSHAKE_INCOMPLETE]: 'Handshake not completed',
[RPCErrorCode.TIMEOUT_ERROR]: 'Request timeout',
[RPCErrorCode.CALL_PROTOCOL_ERROR]: 'Call protocol error',
} as const;
export class RPCError extends Error {
public errorCode: number;
public reason: string;
constructor(
args: {
errorCode?: number;
reason?: string;
} = {}
) {
let { errorCode, reason } = args;
errorCode = errorCode ?? RPCErrorCode.GENERAL_ERROR;
reason = reason
?? RPC_ERROR_MESSAGES[errorCode]
?? RPC_ERROR_MESSAGES[RPCErrorCode.UNKNOWN_ERROR];
super(`[${errorCode}] ${reason}`);
this.errorCode = errorCode;
this.reason = reason;
}
}

142
src/core/RPCHandler.ts Normal file
View File

@@ -0,0 +1,142 @@
import { EventEmitter } from "@/utils/EventEmitter";
import { RPCClient } from "./RPCClient";
import { RPCServer } from "./RPCServer";
import { RPCProvider } from "./RPCProvider";
import { RPCSession } from "./RPCSession";
const DefaultListenOptions = {
port: 5201,
path: '/'
} as const;
const DefaultConnectOptions = {
url: new URL(DefaultListenOptions.path, `http://localhost:${DefaultListenOptions.port}`).href,
/** default is 30 * 1000 in server side */
timeout: 10 * 3000,
} as const;
interface RPCHandlerEvents {
connnect: RPCSession;
}
export class RPCHandler extends EventEmitter<RPCHandlerEvents> {
private rpcClient?: RPCClient;
private rpcServer?: RPCServer;
private provider?: RPCProvider;
private accessKey?: string;
constructor(
args?: {
rpcClient?: RPCClient;
rpcServer?: RPCServer;
}
) {
super();
if (args?.rpcClient) {
this.setRPCProvider(args.rpcClient);
}
if (args?.rpcServer) {
this.setRPCProvider(args.rpcServer);
}
}
setProvider<T extends RPCProvider>(provider: T) {
this.provider = provider;
}
getProvider() {
return this.provider;
}
setAccessKey(accessKey: string | undefined) {
this.accessKey = accessKey;
}
getAccessKey() {
return this.accessKey;
}
setRPCProvider(provider?: RPCClient | RPCServer) {
if (provider instanceof RPCServer) {
this.rpcServer = provider;
} else if (provider instanceof RPCClient) {
this.rpcClient = provider;
} else {
throw new Error();
}
}
async connect(options: {
url?: string;
accessKey?: string;
timeout?: number;
} = {}) {
const rpcClient = this.getRPCProvider('client', true);
return rpcClient.connect({
...DefaultConnectOptions,
...options,
});
}
async listen(options: {
port?: number;
} = {}) {
const rpcServer = this.getRPCProvider('server', true);
return rpcServer.listen({
...DefaultListenOptions,
...options,
}).finally(() => {
rpcServer.on('connect', rpcSession => {
this.emit('connnect', rpcSession);
})
});
}
public getRPCProvider(type: 'client', init: true): RPCClient;
public getRPCProvider(type: 'client', init?: boolean): RPCClient | undefined;
public getRPCProvider(type: 'server', init: true): RPCServer;
public getRPCProvider(type: 'server', init?: boolean): RPCServer | undefined;
public getRPCProvider(type: 'client' | 'server', init?: boolean) {
if (type === 'client') {
if (!this.rpcClient && init) {
this.rpcClient = new RPCClient(this);
}
return this.rpcClient;
} else if (type === 'server') {
if (!this.rpcServer && init) {
this.rpcServer = new RPCServer(this);
}
return this.rpcServer;
} else {
throw new Error();
}
}
}
// const h = new RPCHandler();
// h.setProvider<{
// plus: (a: number, b: number) => number;
// math: {
// minus: (a: number, b: number) => number;
// multiply: (a: number, b: number) => number;
// }
// }>({
// plus(a, b) {
// return a + b
// },
// math: {
// minus(a, b) {
// return a - b;
// },
// multiply(a, b) {
// return a * b;
// },
// }
// })

100
src/core/RPCPacket.ts Normal file
View File

@@ -0,0 +1,100 @@
import { isObject, isString, makeId, ObjectType } from "@/utils/utils";
export type RPCPacketType = 'handshake' | 'call' | 'response';
export class RPCPacket {
id: string;
type: RPCPacketType;
data: ObjectType;
constructor(
args: {
id?: string,
type: RPCPacketType,
data: ObjectType;
}
) {
this.id = args.id ?? makeId();
this.type = args.type;
this.data = args.data;
}
static Parse(value: unknown, safe: true): RPCPacket | null;
static Parse(value: unknown, safe?: boolean): RPCPacket;
static Parse(value: unknown, safe: boolean = false) {
try {
if (isObject(value) && this.isRPCPacket(value)) {
return this.ObjToRPCPacket(value);
} else if (isString(value)) {
const obj = JSON.parse(value);
return this.ObjToRPCPacket(obj);
}
throw new Error(`${value} is not a RPCPacket`);
} catch (error) {
if (safe) {
return null;
}
throw error;
}
}
private static ObjToRPCPacket(obj: RPCPacket) {
const { id, type, data } = obj;
return new RPCPacket({
id, type, data,
})
}
static isRPCPacket(value: unknown): value is RPCPacket {
if (!isObject(value)) {
return false;
}
if (!('id' in value) || !('type' in value) || !('data' in value)) {
return false;
}
return (this.isRPCPacketID(value.id)
&& this.isRPCPacketType(value.type)
&& this.isRPCPacketData(value.data))
}
static isRPCPacketID(value: unknown) {
if (typeof value !== 'string' || value.trim().length === 0) {
return false;
}
return true;
}
static isRPCPacketType(type: unknown) {
if (typeof type !== 'string') {
return false;
}
if (!['handshake', 'call', 'response'].includes(type)) {
return false;
}
return true;
}
static isRPCPacketData(data: unknown) {
return isObject(data);
}
static isHandshakePacket(packet: RPCPacket) {
return packet.type === 'handshake';
}
static isCallPacket(packet: RPCPacket) {
return packet.type === 'call';
}
static isCallResponsePacket(packet: RPCPacket) {
return packet.type === 'response';
}
}

1
src/core/RPCProvider.ts Normal file
View File

@@ -0,0 +1 @@
export type RPCProvider = { [key: string]: RPCProvider | Function | any };

101
src/core/RPCServer.ts Normal file
View File

@@ -0,0 +1,101 @@
import { EventEmitter } from "@/utils/EventEmitter";
import { RPCHandler } from "./RPCHandler";
import { createSocketServer, SocketServer } from "./SocketServer";
import { RPCSession } from "./RPCSession";
import { RPCPacket } from "./RPCPacket";
import { makeCallResponsePacket, makeHandshakePacket, verifyHandshakeRequest } from "./RPCCommon";
import { RPCErrorCode } from "./RPCError";
import { RPCConnection } from "./RPCConnection";
interface RPCServerEvents {
connect: RPCSession;
}
const DefaultRPCServerConfig = {
handshakeTimeout: 30 * 1000,
}
export class RPCServer extends EventEmitter<RPCServerEvents> {
private socketServer?: SocketServer;
constructor(private rpcHandler: RPCHandler) {
super();
}
public async listen(options: {
port: number;
}) {
// call the listen method of socket server
if (!this.socketServer) {
this.socketServer = createSocketServer();
}
const socketServer = this.socketServer;
this.registerSocketServerListener(socketServer);
return socketServer.listen(options);
}
private registerSocketServerListener(socketServer: SocketServer) {
socketServer.on('connect', (socketConnection) => {
let cancelTimeoutTimer = (() => {
let t = setTimeout(() => {
socketConnection.send(makeHandshakePacket({
state: 1,
accept: false,
reason: 'Timeout',
})).catch(() => { });
socketConnection.close();
}, DefaultRPCServerConfig.handshakeTimeout);
return () => clearTimeout(t);
})();
const handleHandshakeRequest = (msg: unknown) => {
// before handshake successfully, it should reject any packet expect handshake
const packet = RPCPacket.Parse(msg, true);
if (!packet) {
return;
}
if (RPCPacket.isCallPacket(packet) || RPCPacket.isCallResponsePacket(packet)) {
socketConnection.send(makeCallResponsePacket({
requestPacket: packet,
status: 'error',
errorCode: RPCErrorCode.HANDSHAKE_INCOMPLETE,
})).catch(() => { });
return;
}
const acceptHandshake = verifyHandshakeRequest({
packet,
/** @todo */
thatAccessKeys: undefined,
thisAccessKey: this.rpcHandler.getAccessKey(),
});
if (acceptHandshake) {
this.emit('connect', new RPCSession(
new RPCConnection(socketConnection),
this.rpcHandler,
));
}
socketConnection.send(makeHandshakePacket({
state: 1,
accept: acceptHandshake,
}));
cancelTimeoutTimer();
removeListener();
}
socketConnection.on('msg', handleHandshakeRequest);
let removeListener = () => {
socketConnection.off('msg', handleHandshakeRequest);
removeListener = () => { };
}
socketConnection.on('closed', removeListener);
});
}
}

40
src/core/RPCSession.ts Normal file
View File

@@ -0,0 +1,40 @@
import { ToDeepPromise } from "@/utils/utils";
import { RPCConnection } from "./RPCConnection";
import { RPCHandler } from "./RPCHandler";
import { RPCProvider } from "./RPCProvider";
export class RPCSession {
constructor(
public readonly connection: RPCConnection,
public readonly rpcHandler: RPCHandler,
) {
connection.onCallRequest(rpcHandler.getProvider.bind(rpcHandler));
}
getAPI<T extends RPCProvider>(): ToDeepPromise<T> {
const createProxy = (path: string[] = []) => {
const func = function () { };
const handler: ProxyHandler<any> = {
get(target, prop) {
const newPath = [...path, prop.toString()];
return createProxy(newPath);
},
apply: (target, thisArg, args) => {
return this.connection.callRequest({
fnPath: path.join(':'),
args: args,
/** @todo accept from caller */
timeout: 10 * 1000,
})
}
};
return new Proxy(func, handler);
}
return createProxy() as unknown as ToDeepPromise<T>;
}
}

27
src/core/SocketClient.ts Normal file
View File

@@ -0,0 +1,27 @@
import { SocketConnection } from "./SocketConnection";
export abstract class SocketClient {
public abstract connect(url: string): Promise<SocketConnection>;
}
interface SocketClientConstructor {
new(...args: any[]): SocketClient;
};
let socketClient: SocketClientConstructor | null = null;
export function injectSocketClient(constructor: SocketClientConstructor) {
socketClient = constructor;
}
export function getSocketClient() {
if (!socketClient) {
throw new Error('No SocketClient constructor has been injected')
}
return socketClient;
}
export function createSocketClient(...args: any[]): SocketClient {
const Constructor = getSocketClient();
return new Constructor(...args);
}

View File

@@ -0,0 +1,12 @@
import { EventEmitter } from "@/utils/EventEmitter";
export interface SocketConnectionBaseEvents {
msg: any;
/** reason */
closed: string | undefined;
}
export abstract class SocketConnection extends EventEmitter<SocketConnectionBaseEvents> {
public abstract send(data: any): Promise<void>;
public abstract close(): Promise<void>;
}

35
src/core/SocketServer.ts Normal file
View File

@@ -0,0 +1,35 @@
import { EventEmitter } from "@/utils/EventEmitter";
import { SocketConnection } from "./SocketConnection";
export interface SocketServerBaseEvents {
connect: SocketConnection;
}
export abstract class SocketServer extends EventEmitter<SocketServerBaseEvents> {
/** @throws Error */
public abstract listen(options: {
port: number;
}): Promise<void>;
}
interface SocketServerConstructor {
new(...args: any[]): SocketServer;
};
let socketServer: SocketServerConstructor | null = null;
export function injectSocketServer(constructor: SocketServerConstructor) {
socketServer = constructor;
}
export function getSocketServer() {
if (!socketServer) {
throw new Error('No SocketServer constructor has been injected')
}
return socketServer;
}
export function createSocketServer(...args: any[]): SocketServer {
const Constructor = getSocketServer();
return new Constructor(...args);
}

View File

@@ -0,0 +1,40 @@
import { SocketClient as SocketClientBase } from "@/core/SocketClient";
import { io } from "socket.io-client";
import { SocketConnection } from "./SocketConnection";
export class SocketClient implements SocketClientBase {
public async connect(url: string): Promise<SocketConnection> {
return new Promise((resolve, reject) => {
const socket = io(url, {
autoConnect: false,
reconnection: false,
});
const conn = new SocketConnection({
sendMethod: (data) => {
socket.emit('c', data);
},
closeMethod: () => {
socket.close();
}
});
socket.on('connect', () => {
resolve(conn);
});
socket.on('disconnect', (reason, description) => {
conn.emit('closed', reason);
})
/** subscribe messages from server */
socket.on('s', (data) => {
conn.emit('msg', data);
})
socket.connect();
})
}
}

View File

@@ -0,0 +1,25 @@
import { SocketConnection as SocketConnectionBase, SocketConnectionBaseEvents } from "@/core/SocketConnection";
import { EventEmitter } from "@/utils/EventEmitter";
interface SocketConnectionEvents extends SocketConnectionBaseEvents {
};
export class SocketConnection
extends EventEmitter<SocketConnectionBaseEvents> implements SocketConnectionBase {
constructor(private args: {
sendMethod: (data: any) => void;
closeMethod: () => void;
}) {
super();
}
public async send(data: any): Promise<void> {
this.args.sendMethod(data);
}
public async close(): Promise<void> {
this.args.closeMethod();
}
}

View File

@@ -0,0 +1,42 @@
import { SocketServer as SocketServerBase, SocketServerBaseEvents } from "@/core/SocketServer";
import { EventEmitter } from "@/utils/EventEmitter";
import { SocketConnection } from "./SocketConnection";
interface SocketServerEvents extends SocketServerBaseEvents {
}
export class SocketServer extends EventEmitter<SocketServerEvents> implements SocketServerBase {
public async listen(options: { port: number; }): Promise<void> {
const { port } = options;
/** only run it */
const { Server } = await import("socket.io");
const io = new Server();
io.on('connection', socket => {
const conn = new SocketConnection({
sendMethod: (data) => {
socket.emit('s', data);
},
closeMethod: () => {
socket.conn.close();
}
});
socket.on('disconnect', (reason, description) => {
conn.emit('closed', reason);
})
/** subscribe messages from client */
socket.on('c', (data) => {
conn.emit('msg', data);
})
this.emit('connect', conn);
})
io.listen(port);
}
}

View File

@@ -0,0 +1,9 @@
import { injectSocketClient } from "@/core/SocketClient";
import { SocketClient } from "./SocketClient";
import { injectSocketServer } from "@/core/SocketServer";
import { SocketServer } from "./SocketServer";
export function injectSocketIOImplements() {
injectSocketClient(SocketClient);
injectSocketServer(SocketServer);
}

22
src/index.ts Normal file
View File

@@ -0,0 +1,22 @@
export { RPCHandler } from "./core/RPCHandler";
export { RPCClient } from "./core/RPCClient";
export { RPCServer } from "./core/RPCServer";
export { RPCConnection } from "./core/RPCConnection";
export { RPC_ERROR_MESSAGES, RPCErrorCode } from "./core/RPCError";
export type { RPCProvider } from './core/RPCProvider';
export type { RPCPacketType } from './core/RPCPacket';
export { RPCPacket } from './core/RPCPacket';
export { RPCSession } from "./core/RPCSession";
export { SocketClient } from "./core/SocketClient";
export { SocketConnection } from "./core/SocketConnection";
export { SocketServer } from "./core/SocketServer";
export { injectSocketClient } from "./core/SocketClient";
export { injectSocketServer } from "./core/SocketServer";
import { injectSocketIOImplements } from "./implements/socket.io";
injectSocketIOImplements();
export {
injectSocketIOImplements,
}

0
src/libs/core.errors.ts Normal file
View File

66
src/utils/EventEmitter.ts Normal file
View File

@@ -0,0 +1,66 @@
type EventType = Record<string, any>;
export class BaseEventEmitter<T extends EventType> {
private events: Map<keyof T, Set<(args: any) => void>> = new Map();
public on<K extends keyof T>(event: K, listener: (args: T[K]) => void) {
this.addListener({ event, listener });
return this;
}
public once<K extends keyof T>(event: K, listener: (args: T[K]) => void) {
this.addListener({ event, listener, once: true });
return this;
}
private addListener<K extends keyof T>({ event, listener, ...args }: {
event: K,
listener: (args: T[K]) => void,
once?: boolean
}) {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
const fn = args.once ? (data: T[K]) => {
listener(data);
this.off(event, fn);
} : listener;
this.events.get(event)!.add(fn);
}
public off<K extends keyof T>(event: K, listener: (args: T[K]) => void) {
if (!this.events.has(event)) {
return;
}
this.events.get(event)!.delete(listener);
return this;
}
protected emit<K extends keyof T>(event: K, data?: T[K]) {
if (!this.events.has(event)) {
return;
}
const listeners = new Set(this.events.get(event)!);
listeners.forEach(fn => fn(data));
}
public removeAllListeners<K extends keyof T>(event?: K) {
if (event !== undefined) {
this.events.delete(event);
} else {
this.events.clear();
}
return this;
}
}
export class EventEmitter<T extends EventType> extends BaseEventEmitter<T> {
public emit<K extends keyof T>(event: K, data?: T[K] | undefined): void {
return super.emit(event, data);
}
}

17
src/utils/utils.ts Normal file
View File

@@ -0,0 +1,17 @@
import md5 from 'md5';
export const makeId = () => md5(`${Date.now()}${Math.random()}`);
export const isObject = (v: unknown): v is Record<string, any> => typeof v === 'object' && v !== null;
export const isString = (v: unknown): v is string => typeof v === 'string';
export type ObjectType = Record<string, any>;
export type ToDeepPromise<T> = {
[K in keyof T]: T[K] extends (...args: infer P) => infer R
? (...args: P) => Promise<R>
: T[K] extends object
? ToDeepPromise<T[K]>
: T[K]
};