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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 tonecn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

220
README.md Normal file
View File

@@ -0,0 +1,220 @@
# TypeSRPC
**TypeSRPC** is a lightweight, type-safe RPC (Remote Procedure Call) framework for TypeScript that enables seamless communication between client and server with full type inference and deep nested method support.
## ✨ Features
- **Full TypeScript Support**: Automatic type inference.
- **Deep Nested API**: Supports arbitrarily nested method structures (e.g., `api.math.utils.absolute()`).
- **Promise-Based**: All remote calls return promises—no callback hell.
- **Pluggable Transport Layer**: Easily swap underlying socket implementations (default: Socket.IO).
- **Access Control**: Built-in access key authentication for secure connections.
- **Lightweight & Zero Boilerplate**: Define your provider once—no need to manually declare RPC interfaces.
- **Bidirectional Communication**: Both client and server can expose and consume APIs.
## 📦 Installation
```bash
npm install typesrpc
# or
yarn add typesrpc
# or
pnpm add typesrpc
```
## 🚀 Quick Start
### 1. Define Your Providers
```ts
// Server-side provider
type ServerProvider = {
add: (a: number, b: number) => number;
math: {
multiply: (a: number, b: number) => number;
utils: {
absolute: (num: number) => number;
};
};
};
const serverProvider = {
add(a: number, b: number) { return a + b; },
math: {
multiply(a: number, b: number) { return a * b; },
utils: {
absolute(num: number) { return Math.abs(num); }
}
}
};
```
```ts
// Client-side provider (optional, for bidirectional RPC)
type ClientProvider = {
getName: () => string;
sub: {
getName: () => string;
};
};
const clientProvider = {
name: 'Client1',
getName() { return this.name; },
sub: {
name: 'SubClient',
getName() { return this.name; }
}
};
```
### 2. Set Up Server & Client
```ts
import { RPCHandler } from 'typesrpc';
// Server
const server = new RPCHandler();
server.setProvider(serverProvider);
await server.listen({ port: 3000 });
// Client
const client = new RPCHandler();
client.setProvider(clientProvider);
const session = await client.connect({ url: 'http://localhost:3000' });
// Get typed API proxies
const serverAPI = session.getAPI<ServerProvider>();
const clientAPI = session.getAPI<ClientProvider>(); // if server also consumes client API
```
### 3. Make Remote Calls
```ts
// All methods return Promises
const sum = await serverAPI.add(2, 3); // 5
const product = await serverAPI.math.multiply(4, 5); // 20
const abs = await serverAPI.math.utils.absolute(-10); // 10
const name = await clientAPI.getName(); // 'Client1'
```
> 💡 **Note**: The `ToDeepPromise<T>` utility (exported internally) automatically converts all synchronous methods in your provider to async (`Promise<T>`), so you can `await` them on the client side.
---
## 🔐 Access Key Authentication
Secure your RPC endpoints with access keys:
### Server (require access key)
```ts
const server = new RPCHandler();
server.setAccessKey('my-secret-key');
await server.listen({ port: 3001 });
```
### Client (provide access key)
```ts
const client = new RPCHandler();
await client.connect({
url: 'http://localhost:3001',
accessKey: 'my-secret-key' // required!
});
```
Connections without a valid access key will be rejected.
---
## ⚙️ Custom Socket Implementation
TypeSRPC supports pluggable transport layers. By default, it uses **Socket.IO**, but you can replace it with any real-time communication library (e.g., WebSocket, SignalR, etc.).
### How to Inject a Custom Implementation
1. Implement the required interfaces:
- `SocketClient`
- `SocketServer`
- `SocketConnection`
2. Inject your implementations:
```ts
// my-socket-impl/index.ts
import { injectSocketClient, injectSocketServer } from 'typesrpc';
import { MySocketClient } from './MySocketClient';
import { MySocketServer } from './MySocketServer';
export function injectMySocketImpl() {
injectSocketClient(MySocketClient);
injectSocketServer(MySocketServer);
}
```
3. Call the injector **before** creating any `RPCHandler` instances:
```ts
import { injectMySocketImpl } from './my-socket-impl';
injectMySocketImpl(); // Must be called once at app startup
const handler = new RPCHandler(); // Now uses your custom socket layer
```
> ✅ The built-in Socket.IO implementation is injected automatically when you import typesrpc. To override it, call your custom injector before creating any RPCHandler instances (but after importing typesrpc).
---
## 📁 Project Structure
```
src/
├── core/ # Core RPC logic (transport-agnostic)
├── implements/ # Transport implementations
│ └── socket.io/ # Default: Socket.IO adapter
├── utils/ # Utilities (e.g., ToDeepPromise, EventEmitter)
└── index.ts # Public API exports + default Socket.IO injection
```
---
## 🧪 Testing
The project includes comprehensive tests:
- **Unit**: `npm run test:unit`
- **Integration**: `npm run test:integration`
- **E2E**: `npm run test:e2e`
- **Coverage**: `npm run test:coverage`
Run all tests with:
```bash
npm test
```
---
## 📄 API Reference
### `RPCHandler`
Main entry point for both client and server.
- `.setProvider(provider: T)` Register local methods.
- `.setAccessKey(key: string)` Set access key (server-side).
- `.listen(options?)` Start server.
- `.connect(options?)` Connect to server; returns `Promise<RPCSession>`.
### `RPCSession`
Represents an active RPC connection.
- `.getAPI<T>()` Get a typed proxy to the remote provider.
---
## 📜 License
MIT License © [tonecn](https://github.com/tonecn)
---
> **Note**: This library is designed for development and internal tooling. For production use in public-facing services, ensure proper authentication, rate limiting, and input validation are implemented on top of the access key mechanism.

220
README_zh.md Normal file
View File

@@ -0,0 +1,220 @@
# TypeSRPC
**TypeSRPC** 是一个轻量级、类型安全的 TypeScript RPC远程过程调用框架支持客户端与服务器之间的无缝通信并提供完整的类型推断和深度嵌套方法支持。
## ✨ 特性
- **完整的 TypeScript 支持**:自动类型推断。
- **深度嵌套 API**:支持任意层级嵌套的方法结构(例如 `api.math.utils.absolute()`)。
- **基于 Promise**:所有远程调用均返回 Promise告别回调地狱。
- **可插拔传输层**:轻松更换底层 socket 实现(默认使用 Socket.IO
- **访问控制**:内置访问密钥认证,保障连接安全。
- **轻量且零样板代码**:只需定义一次服务提供者,无需手动声明 RPC 接口。
- **双向通信**:客户端和服务器均可暴露和调用对方的 API。
## 📦 安装
```bash
npm install typesrpc
# 或
yarn add typesrpc
# 或
pnpm add typesrpc
```
## 🚀 快速开始
### 1. 定义你的服务提供者
```ts
// 服务端提供者
type ServerProvider = {
add: (a: number, b: number) => number;
math: {
multiply: (a: number, b: number) => number;
utils: {
absolute: (num: number) => number;
};
};
};
const serverProvider = {
add(a: number, b: number) { return a + b; },
math: {
multiply(a: number, b: number) { return a * b; },
utils: {
absolute(num: number) { return Math.abs(num); }
}
}
};
```
```ts
// 客户端提供者(可选,用于双向 RPC
type ClientProvider = {
getName: () => string;
sub: {
getName: () => string;
};
};
const clientProvider = {
name: 'Client1',
getName() { return this.name; },
sub: {
name: 'SubClient',
getName() { return this.name; }
}
};
```
### 2. 设置服务端与客户端
```ts
import { RPCHandler } from 'typesrpc';
// 服务端
const server = new RPCHandler();
server.setProvider(serverProvider);
await server.listen({ port: 3000 });
// 客户端
const client = new RPCHandler();
client.setProvider(clientProvider);
const session = await client.connect({ url: 'http://localhost:3000' });
// 获取带类型的 API 代理
const serverAPI = session.getAPI<ServerProvider>();
const clientAPI = session.getAPI<ClientProvider>(); // 如果服务端也需要调用客户端 API
```
### 3. 发起远程调用
```ts
// 所有方法均返回 Promise
const sum = await serverAPI.add(2, 3); // 5
const product = await serverAPI.math.multiply(4, 5); // 20
const abs = await serverAPI.math.utils.absolute(-10); // 10
const name = await clientAPI.getName(); // 'Client1'
```
> 💡 **提示**:内部导出的工具类型 `ToDeepPromise<T>` 会自动将你提供者中的所有同步方法转换为异步方法(返回 `Promise<T>`),因此你可以在客户端直接使用 `await`。
---
## 🔐 访问密钥认证
通过访问密钥保护你的 RPC 端点:
### 服务端(要求访问密钥)
```ts
const server = new RPCHandler();
server.setAccessKey('my-secret-key');
await server.listen({ port: 3001 });
```
### 客户端(提供访问密钥)
```ts
const client = new RPCHandler();
await client.connect({
url: 'http://localhost:3001',
accessKey: 'my-secret-key' // 必填!
});
```
未提供有效访问密钥的连接将被拒绝。
---
## ⚙️ 自定义 Socket 实现
TypeSRPC 支持可插拔的传输层。默认使用 **Socket.IO**,但你可以替换为任意实时通信库(如原生 WebSocket、SignalR 等)。
### 如何注入自定义实现
1. 实现以下接口:
- `SocketClient`
- `SocketServer`
- `SocketConnection`
2. 注入你的实现:
```ts
// my-socket-impl/index.ts
import { injectSocketClient, injectSocketServer } from 'typesrpc';
import { MySocketClient } from './MySocketClient';
import { MySocketServer } from './MySocketServer';
export function injectMySocketImpl() {
injectSocketClient(MySocketClient);
injectSocketServer(MySocketServer);
}
```
3. 在创建任何 `RPCHandler` 实例**之前**调用注入器:
```ts
import { injectMySocketImpl } from './my-socket-impl';
injectMySocketImpl(); // 必须在应用启动时调用一次
const handler = new RPCHandler(); // 现在使用你的自定义 socket 层
```
> ✅ 内置的 Socket.IO 实现在你导入 typesrpc 时会自动注入。若要覆盖它,请在创建任何 `RPCHandler` 实例之前(但在导入 typesrpc 之后)调用你的自定义注入器。
---
## 📁 项目结构
```
src/
├── core/ # 核心 RPC 逻辑(与传输层无关)
├── implements/ # 传输层实现
│ └── socket.io/ # 默认Socket.IO 适配器
├── utils/ # 工具函数(如 ToDeepPromise、EventEmitter
└── index.ts # 公共 API 导出 + 默认 Socket.IO 注入
```
---
## 🧪 测试
项目包含全面的测试:
- **单元测试**`npm run test:unit`
- **集成测试**`npm run test:integration`
- **端到端测试**`npm run test:e2e`
- **覆盖率**`npm run test:coverage`
运行全部测试:
```bash
npm test
```
---
## 📄 API 参考
### `RPCHandler`
客户端和服务端的主入口。
- `.setProvider(provider: T)` 注册本地方法。
- `.setAccessKey(key: string)` 设置访问密钥(服务端)。
- `.listen(options?)` 启动服务端。
- `.connect(options?)` 连接服务端;返回 `Promise<RPCSession>`
### `RPCSession`
表示一个活跃的 RPC 连接。
- `.getAPI<T>()` 获取远程提供者的类型化代理。
---
## 📜 许可证
MIT 许可证 © [tonecn](https://github.com/tonecn)
---
> **注意**:本库适用于开发和内部工具场景。若用于面向公众的生产服务,请在访问密钥机制之上额外实现完善的认证、限流和输入验证措施。

View File

@@ -0,0 +1,35 @@
import { RPCHandler } from "@/index"
describe('Rpc accessKey test', () => {
test('none accesskey', async () => {
const server = new RPCHandler();
await server.listen({
port: 5202
});
const client = new RPCHandler();
expect(client.connect({
url: 'http://localhost:5202'
})).resolves.toBeDefined()
})
test('server required', async () => {
const serverAccesskey = 'abc123';
const server = new RPCHandler();
server.setAccessKey(serverAccesskey)
await server.listen({
port: 5203
});
const client = new RPCHandler();
expect(client.connect({
url: 'http://localhost:5203'
})).rejects.toThrow()
const client2 = new RPCHandler();
expect(client2.connect({
url: 'http://localhost:5203',
accessKey: serverAccesskey
})).resolves.toBeDefined()
})
})

View File

@@ -0,0 +1,85 @@
import { RPCHandler } from "@/index"
import { ToDeepPromise } from "@/utils/utils";
type ServerProvider = {
add: (a: number, b: number) => number;
math: {
multiply: (a: number, b: number) => number;
utils: {
absolute: (num: number) => number;
};
}
}
const serverProvider = {
add(a: number, b: number) {
return a + b;
},
math: {
multiply(a: number, b: number) {
return a * b
},
utils: {
absolute(num: number) {
return Math.abs(num);
},
}
}
}
type ClientProvider = {
getName: () => string;
sub: {
getName: () => string;
}
}
const clientProvider = {
name: '1',
getName() {
return this.name;
},
sub: {
name: '2',
getName() {
return this.name;
}
}
}
let clientAPI: ToDeepPromise<ClientProvider>
let serverAPI: ToDeepPromise<ServerProvider>
describe('Rpc full flow test', () => {
beforeAll(async () => {
const server = new RPCHandler();
server.setProvider(serverProvider)
await server.listen();
server.on('connnect', (rpcSession) => {
clientAPI = rpcSession.getAPI<ClientProvider>();
})
const client = new RPCHandler();
client.setProvider(clientProvider);
await client.connect().then((rpcSession) => {
serverAPI = rpcSession.getAPI<ServerProvider>();
});
})
test('server', async () => {
const addResult = await serverAPI.add(1, 1);
expect(addResult).toBe(2);
const multiplyResult = await serverAPI.math.multiply(2, 3);
expect(multiplyResult).toBe(6);
const absoluteResult = await serverAPI.math.utils.absolute(-1);
expect(absoluteResult).toBe(1);
})
test('client', async () => {
const name1 = await clientAPI.getName();
expect(name1).toBe(clientProvider.name);
const name2 = await clientAPI.sub.getName();
expect(name2).toBe(clientProvider.sub.name);
})
})

View File

@@ -0,0 +1,23 @@
import { isObject, isString, makeId } from "@/utils/utils"
test('makeId', () => {
const id = makeId();
expect(id.length).toBe(32);
expect(typeof id === 'string').toBeTruthy();
})
test('isObject', () => {
const nullObj = null;
expect(isObject(nullObj)).toBeFalsy();
const normalObj = {};
expect(isObject(normalObj)).toBeTruthy();
})
test('isString', () => {
const emptyStr = '';
expect(isString(emptyStr)).toBeTruthy();
const str = 'str';
expect(isString(str)).toBeTruthy();
const aNumber = 1;
expect(isString(aNumber)).toBeFalsy();
})

17
jest.config.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: [
'**/__tests__/(unit|e2e|integration)/**/*.test.ts'
],
collectCoverageFrom: [
'src/**/*.{ts,js}',
'!src/**/*.d.ts'
],
coverageDirectory: './coverage',
coverageReporters: ['html', 'lcov', 'text-summary'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
testTimeout: 15000
};

50
package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "typesrpc",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest",
"test:unit": "jest __tests__/unit",
"test:integration": "jest __tests__/integration",
"test:e2e": "jest __tests__/e2e",
"test:coverage": "jest --coverage",
"build": "tsc",
"start": "tsc && tsc-alias && node dist/index.js"
},
"keywords": [
"rpc",
"typescript",
"remote-procedure-call",
"socket",
"realtime",
"type-safe",
"nested-api",
"bidirectional",
"pluggable",
"socket.io",
"websocket",
"microservices",
"client-server",
"lightweight"
],
"author": "tonecn",
"license": "MIT",
"dependencies": {
"@types/express": "^5.0.3",
"@types/md5": "^2.3.5",
"express": "^5.1.0",
"md5": "^2.3.0",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^24.0.15",
"jest": "^30.0.5",
"ts-jest": "^29.4.1",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.2"
}
}

4081
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

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]
};

115
tsconfig.json Normal file
View File

@@ -0,0 +1,115 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"@/*": [
"src/*",
],
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist/", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": [
"src/**/*",
"__tests__/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}