├── .gitignore ├── .mocharc.js ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── api-extractor.json ├── benchmark ├── config │ └── BenchmarkConfig.ts ├── http.ts ├── models │ ├── HTTPRunner.ts │ └── WsRunner.ts ├── protocols │ ├── PtlTest.ts │ └── proto.ts ├── server │ ├── http.ts │ └── ws.ts ├── tsconfig.json └── ws.ts ├── package-lock.json ├── package.json ├── res └── mongodb-polyfill.d.ts ├── rollup.config.js ├── scripts ├── copyright.js └── postBuild.js ├── src ├── client │ ├── http │ │ ├── HttpClient.ts │ │ └── HttpProxy.ts │ └── ws │ │ ├── WebSocketProxy.ts │ │ └── WsClient.ts ├── index.ts ├── models │ ├── HttpUtil.ts │ ├── Pool.ts │ ├── getClassObjectId.ts │ └── version.ts └── server │ ├── base │ ├── ApiCall.ts │ ├── BaseCall.ts │ ├── BaseConnection.ts │ ├── BaseServer.ts │ └── MsgCall.ts │ ├── http │ ├── ApiCallHttp.ts │ ├── HttpConnection.ts │ ├── HttpServer.ts │ └── MsgCallHttp.ts │ ├── inner │ ├── ApiCallInner.ts │ └── InnerConnection.ts │ ├── models │ ├── PrefixLogger.ts │ └── TerminalColorLogger.ts │ └── ws │ ├── ApiCallWs.ts │ ├── MsgCallWs.ts │ ├── WsConnection.ts │ └── WsServer.ts ├── test.ts ├── test ├── Base.ts ├── api │ ├── ApiObjId.ts │ ├── ApiTest.ts │ └── a │ │ └── b │ │ └── c │ │ └── ApiTest.ts ├── cases │ ├── http.test.ts │ ├── httpJSON.test.ts │ ├── https.test.ts │ ├── httpsJSON.test.ts │ ├── inner.test.ts │ ├── inputBuffer.test.ts │ ├── inputJSON.test.ts │ ├── ws.test.ts │ ├── wsJSON.test.ts │ ├── wss.test.ts │ └── wssJSON.test.ts ├── proto │ ├── MsgChat.ts │ ├── MsgTest.ts │ ├── PtlObjId.ts │ ├── PtlTest.ts │ ├── a │ │ └── b │ │ │ └── c │ │ │ └── PtlTest.ts │ └── serviceProto.ts ├── server.crt ├── server.key ├── test.ts ├── try │ ├── client │ │ ├── http.ts │ │ └── ws.ts │ ├── massive.ts │ ├── no-res-issue │ │ ├── client │ │ │ └── index.ts │ │ └── server │ │ │ ├── index.ts │ │ │ ├── protocols │ │ │ ├── PtlTest.ts │ │ │ └── proto.ts │ │ │ └── src │ │ │ └── api │ │ │ └── ApiTest.ts │ ├── package-lock.json │ ├── package.json │ ├── proto │ │ ├── MsgChat.ts │ │ ├── PtlTest.ts │ │ ├── a │ │ │ └── b │ │ │ │ └── c │ │ │ │ └── PtlTest.ts │ │ ├── serviceProto.ts │ │ └── typeProto.json │ ├── server │ │ ├── api │ │ │ ├── ApiTest.ts │ │ │ └── a │ │ │ │ └── b │ │ │ │ └── c │ │ │ │ └── ApiTest.ts │ │ ├── http.ts │ │ └── ws.ts │ └── tsconfig.json └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | yarn.lock 4 | logs 5 | .rpt2_cache 6 | .nyc_output 7 | coverage 8 | docs 9 | temp 10 | lib 11 | .ds_store -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | require: [ 3 | 'ts-node/register', 4 | './test/Base.ts' 5 | ], 6 | exit: true, 7 | bail: true, 8 | timeout: 999999, 9 | 'preserve-symlinks': true, 10 | spec: [ 11 | './test/cases/http.test.ts', 12 | './test/cases/httpJSON.test.ts', 13 | './test/cases/ws.test.ts', 14 | './test/cases/wsJSON.test.ts', 15 | './test/cases/inner.test.ts', 16 | './test/cases/inputJSON.test.ts', 17 | './test/cases/inputBuffer.test.ts', 18 | './test/cases/https.test.ts', 19 | './test/cases/httpsJSON.test.ts', 20 | './test/cases/wss.test.ts', 21 | './test/cases/wssJSON.test.ts', 22 | 23 | ], 24 | // parallel: false, 25 | 26 | // 'expose-gc': true, 27 | // fgrep: 'Same-name' 28 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # 构建产物 2 | dist 3 | build 4 | 5 | # 依赖目录 6 | node_modules 7 | 8 | # 其他不需要格式化的文件 9 | .cache 10 | .vscode 11 | .idea 12 | *.min.js 13 | *.min.css 14 | *.svg 15 | *.ico 16 | package-lock.json 17 | yarn.lock 18 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 每行代码长度 3 | printWidth: 100, 4 | // 使用单引号 5 | singleQuote: false, 6 | // 在对象或数组最后一个元素后面是否加逗号 7 | trailingComma: 'es5', 8 | // 在括号和对象的文字之间加上一个空格 9 | bracketSpacing: true, 10 | // 缩进 11 | tabWidth: 2, 12 | // 使用空格缩进 13 | useTabs: false, 14 | // 分号 15 | semi: false, 16 | // jsx 标签的反尖括号需要换行 17 | jsxBracketSameLine: false, 18 | // 箭头函数参数括号 19 | arrowParens: 'avoid', 20 | // 换行符使用 lf 21 | endOfLine: 'lf', 22 | // 对象的 key 仅在必要时用引号 23 | quoteProps: 'as-needed', 24 | // jsx 使用双引号 25 | jsxSingleQuote: false, 26 | // 每个文件格式化的范围是文件的全部内容 27 | rangeStart: 0, 28 | rangeEnd: Infinity, 29 | // 不需要写文件开头的 @prettier 30 | requirePragma: false, 31 | // 不需要自动在文件开头插入 @prettier 32 | insertPragma: false, 33 | // 使用默认的折行标准 34 | proseWrap: 'preserve', 35 | // 根据显示样式决定 html 要不要折行 36 | htmlWhitespaceSensitivity: 'css', 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "mocha current file", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "${file}" 14 | ], 15 | "internalConsoleOptions": "openOnSessionStart", 16 | "cwd": "${workspaceFolder}" 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "ts-node current file", 22 | "protocol": "inspector", 23 | "args": [ 24 | "${relativeFile}" 25 | ], 26 | "cwd": "${workspaceRoot}", 27 | "runtimeArgs": [ 28 | "-r", 29 | "ts-node/register" 30 | ], 31 | "internalConsoleOptions": "openOnSessionStart" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## [3.4.18] - 2025-05-28 4 | ### Added 5 | - `wsOptions` for `WsServer` 6 | 7 | ## [3.4.17] - 2024-11-11 8 | ### Added 9 | - `server.inputJSON` support `logger` param 10 | 11 | ## [3.4.16] - 2024-05-05 12 | ### Fixed 13 | - Update dependencies `tsbuffer` 14 | 15 | ## [3.4.14] - 2023-12-28 16 | ### Fixed 17 | - Update dependencies 18 | 19 | ## [3.4.13] - 2023-04-12 20 | ### Fixed 21 | - Change log level of `[SendReturnErr]` to `debug` when ws connection is closed 22 | 23 | ## [3.4.12] - 2023-02-07 24 | ### Added 25 | - Add `closeTimeout` for `WsConnection.close` 26 | ### Fixed 27 | - Heartbeat timeout should terminate ws connection instantly 28 | 29 | ## [3.4.11] - 2022-11-26 30 | ### Changed 31 | - Update deps 32 | 33 | ## [3.4.10] - 2022-11-19 34 | ### Changed 35 | - Add `httpReq.rawBody` for `HttpConnection` 36 | - `postConnectFlow` now would execute when http connection is established instead of request end 37 | 38 | ## [3.4.9] - 2022-11-18 39 | ### Fixed 40 | - Fixed that `preRecvDataFlow` is not enter if URL is not standard when using JSON mode 41 | 42 | ## [3.4.8] - 2022-10-19 43 | ### Fixed 44 | - Update to `tsrpc-base-client@2.1.10` 45 | 46 | ## [3.4.7] - 2022-10-15 47 | ### Fixed 48 | - Issue that `ws.onClose` is not called when `wsClient.disconnect()` manually at CocosCreator Android platform. 49 | - Issue that the name of Api cannot be the same with it of Msg when using `WsClient` in JSON mode. 50 | 51 | ## [3.4.6] - 2022-09-28 52 | ### Fixed 53 | - Fixed issue that `logLevel` not works 54 | 55 | ## [3.4.5] - 2022-08-10 56 | ### Fixed 57 | - Ignore incoming data after heartbeat timeout 58 | 59 | ## [3.4.2] - 2022-06-28 60 | ### Added 61 | - New options `logConnect` for `WsServer` 62 | - Protect `WebSocketProxy.onClose` executed duplicately. 63 | ### Fixed 64 | - Bug: `WebSocketProxy.onClose` is not executed when heartbeat timeout when WIFI is broken 65 | 66 | ## [3.4.1] - 2022-06-25 67 | ### Added 68 | - New client flow: `preRecvMsgFlow` and `postRecvMsgFlow` 69 | - Support `server.listenMsg` by regexp 70 | 71 | ## [3.4.0] - 2022-06-14 72 | ### Added 73 | - `https` options for `HttpServer` 74 | - `wss` options for `WsServer` 75 | - Support using the same name with API and message 76 | ### Changed 77 | - Deprecate `serviceName` in `preRecvDataFlow`, use `serviceId` instead 78 | - Optimized log color 79 | 80 | ## [3.3.3] - 2022-06-07 81 | ### Fixed 82 | - Remove `bson` dependency, import `ObjectId` dynamically. 83 | 84 | ## [3.3.2] - 2022-06-01 85 | ### Fixed 86 | - Update dependencies 87 | 88 | ## [3.3.1] - 2022-05-07 89 | ### Fixed 90 | - `HttpConnection.status` not correct when request aborted by client 91 | 92 | ## [3.3.0] - 2022-04-15 93 | ### Added 94 | - Builtin heartbeat support 95 | - New options `logLevel` 96 | ### Fixed 97 | - Add response header `Content-Type: application/json; charset=utf-8` for JSON mode under HttpServer, to fix the decoding issue in Chrome dev tools. 98 | 99 | ## [3.2.5] - 2022-04-12 100 | ### Added 101 | - New server options `corsMaxAge` to optimized preflight requests, default value is 3600. 102 | ### Fixed 103 | - `NonNullable` cannot be encoded and decoded when as a property in interface 104 | 105 | ## [3.2.3] - 2022-03-25 106 | ### Added 107 | - Print debug-level log when "pre flow" is canceled 108 | ### Changed 109 | - Log `[ResErr]` renamed to `[ApiErr]` to consist with client's. 110 | - Log `ApiRes` and `ApiErr` once they are ready to send, instead of after send them. 111 | ### Fixed 112 | - When `preSendDataFlow` return undefined, do not send "Internal Server Error". 113 | - Remove some unused code. 114 | 115 | ## [3.2.2] - 2022-03-22 116 | ### Fixed 117 | - `postDisconnectFlow` not executed when `disconnect()` manually 118 | 119 | 120 | ## [3.2.1] - 2022-03-21 121 | ### Added 122 | - `preRecvDataFlow` add param `serviceName` 123 | - Support change `dataType` in `postConnectFlow` 124 | ### Fixed 125 | - Remark text error 126 | 127 | ## [3.2.0] - 2022-02-26 128 | ### Added 129 | - Support using `keyof` 130 | - Support type alias and `keyof` in `Pick` and `Omit` 131 | - Support `Pick` and `Omit` 132 | - Support `interface` extends Mapped Type, like `Pick` `Omit` 133 | - Support `Pick` 134 | - Support `Pick` 135 | - Support `Pick` and `Pick`, the same to `Omit` 136 | - Support reference enum value as literal type,like: 137 | ```ts 138 | export enum Types { 139 | Type1, 140 | Type2 141 | } 142 | export interface Obj { 143 | type: Types.Type1, 144 | value: string 145 | } 146 | ``` 147 | ### Changed 148 | - `SchemaType` switched to class 149 | 150 | ## [3.1.9] - 2022-01-12 151 | ### Added 152 | - `mongodb-polyfill.d.ts` to fixed mongodb type bug. 153 | 154 | ## [3.1.6] - 2021-12-29 155 | ### Changed 156 | - Return request type error detail when using JSON 157 | 158 | ## [3.1.5] - 2021-12-23 159 | ### Fixed 160 | - Optimize aliyun FC support of `server.inputJSON` 161 | 162 | ## [3.1.4] - 2021-12-18 163 | ### Added 164 | - `WsServer` now support client use `buffer` as transfering format when server set `json: true` 165 | ### Fixed 166 | - Type error when disable `skipLibChecks` 167 | - Cannot resolve JSON when `headers` is `application/json; charset=utf-8` 168 | - Cannot resolve serviceName when there is query string in the URL 169 | 170 | ## [3.1.3] - 2021-12-04 171 | ### Added 172 | - `conn.listenMsg` 173 | ### Fixed 174 | - Do not `broadcastMsg` when `conns.length` is `0` 175 | 176 | ## [3.1.2] - 2021-11-17 177 | ### Added 178 | - `server.inputJSON` and `server.inputBuffer` 179 | - Add new dataType `json` 180 | 181 | ## [3.1.1] - 2021-11-09 182 | ### Added 183 | - HTTP Text 传输模式下,区分 HTTP 状态码返回,不再统一返回 200 184 | 185 | ## [3.1.0] - 2021-11-08 186 | ### Added 187 | - WebSocket 支持 JSON 格式传输 188 | - JSON 格式传输支持 `ArrayBuffer`、`Date`、`ObjectId`,自动根据协议编解码为 `string` 189 | ### Changed 190 | - `jsonEnabled` -> `json` 191 | 192 | ## [3.0.14] - 2021-10-25 193 | ### Added 194 | - 增加 `server.autoImplementApi` 第二个参数 `delay`,用于延迟自动协议注册,加快冷启动速度。 195 | 196 | ## [3.0.13] - 2021-10-22 197 | ### Added 198 | - 增加 `server.callApi` 的支持,以更方便的适配 Serverless 云函数等自定义传输场景。 199 | 200 | ## [3.0.12] - 2021-10-22 201 | ### Fixed 202 | - 修复 `WsServer` 客户端断开连接后,日志显示的 `ActiveConn` 总是比实际多 1 的 BUG 203 | 204 | ## [3.0.11] - 2021-10-18 205 | ### Added 206 | - 增加对 `mongodb/ObjectId` 的支持 207 | 208 | ## [3.0.10] - 2021-10-13 209 | ### Changed 210 | - `BaseConnection` 泛型参数默认为 `any`,便于扩展类型 211 | - `HttpClient` and `WsClient` no longer have default type param 212 | 213 | ## [3.0.9] - 2021-10-06 214 | ### Changed 215 | - `strictNullChecks` 默认改为 `false` 216 | 217 | ## [3.0.8] - 2021-10-06 218 | ### Added 219 | - Optimize log level 220 | 221 | ## [3.0.7] - 2021-10-06 222 | ### Added 223 | - Optimize log color 224 | ## [3.0.6] - 2021-09-30 225 | ### Added 226 | - "Server started at ..." 前增加 "ERROR:X API registered failed." 227 | ### Changed 228 | - `HttpServer.onInputBufferError` 改为 `call.error('InputBufferError')` 229 | - 替换 `colors` 为 `chalk` 230 | 231 | ## [3.0.5] - 2021-08-14 232 | ### Added 233 | - Optimize log for `sendMsg` and `broadcastMsg` 234 | - Return `Internal Server Error` when `SendReturnErr` occured 235 | 236 | ### Changed 237 | - Remove error `API not return anything` 238 | - handler of `client.listenMsg` changed to `(msg, msgName, client)=>void` 239 | 240 | ### Fixed 241 | - NodeJS 12 compability issue (`Uint8Array` and `Buffer` is not treated samely) 242 | 243 | ## [3.0.3] - 2021-06-27 244 | 245 | ### Added 246 | - `server.listenMsg` would return `handler` that passed in -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) King Wang. https://github.com/k8w 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TSRPC 2 | 3 | EN / [中文](https://tsrpc.cn/docs/introduction.html) 4 | 5 | A TypeScript RPC framework with runtime type checking and binary serialization. 6 | 7 | Official site: https://tsrpc.cn (English version is on the way) 8 | 9 | ## Features 10 | - Runtime type checking 11 | - Binary serialization 12 | - Pure TypeScript, without any decorater or other language 13 | - HTTP / WebSocket / and more protocols... 14 | - Optional backward-compatibility to JSON 15 | - High performance and reliable, verified by services over 100,000,000 users 16 | 17 | ## Create Full-stack Project 18 | ``` 19 | npx create-tsrpc-app@latest 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Define Protocol (Shared) 25 | ```ts 26 | export interface ReqHello { 27 | name: string; 28 | } 29 | 30 | export interface ResHello { 31 | reply: string; 32 | } 33 | ``` 34 | 35 | ### Implement API (Server) 36 | ```ts 37 | import { ApiCall } from "tsrpc"; 38 | 39 | export async function ApiHello(call: ApiCall) { 40 | call.succ({ 41 | reply: 'Hello, ' + call.req.name 42 | }); 43 | } 44 | ``` 45 | 46 | ### Call API (Client) 47 | ```ts 48 | let ret = await client.callApi('Hello', { 49 | name: 'World' 50 | }); 51 | ``` 52 | 53 | ## Examples 54 | 55 | https://github.com/k8w/tsrpc-examples 56 | 57 | ## Serialization Algorithm 58 | The best TypeScript serialization algorithm ever. 59 | Without any 3rd-party IDL language (like protobuf), it is fully based on TypeScript source file. Define the protocols directly by your code. 60 | 61 | This is powered by [TSBuffer](https://github.com/tsbuffer), which is going to be open-source. 62 | 63 | TypeScript has the best type system, with some unique advanced features like union type, intersection type, mapped type, etc. 64 | 65 | TSBuffer may be the only serialization algorithm that support them all. 66 | 67 | 68 | 69 | ## API Reference 70 | See [API Reference](./docs/api/tsrpc.md). -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | /** 2 | * Config file for API Extractor. For more info, please visit: https://api-extractor.com 3 | */ 4 | { 5 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 6 | /** 7 | * Optionally specifies another JSON config file that this file extends from. This provides a way for 8 | * standard settings to be shared across multiple projects. 9 | * 10 | * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains 11 | * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be 12 | * resolved using NodeJS require(). 13 | * 14 | * SUPPORTED TOKENS: none 15 | * DEFAULT VALUE: "" 16 | */ 17 | // "extends": "./shared/api-extractor-base.json" 18 | // "extends": "my-package/include/api-extractor-base.json" 19 | /** 20 | * Determines the "" token that can be used with other config file settings. The project folder 21 | * typically contains the tsconfig.json and package.json config files, but the path is user-defined. 22 | * 23 | * The path is resolved relative to the folder of the config file that contains the setting. 24 | * 25 | * The default value for "projectFolder" is the token "", which means the folder is determined by traversing 26 | * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder 27 | * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error 28 | * will be reported. 29 | * 30 | * SUPPORTED TOKENS: 31 | * DEFAULT VALUE: "" 32 | */ 33 | // "projectFolder": "..", 34 | /** 35 | * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor 36 | * analyzes the symbols exported by this module. 37 | * 38 | * The file extension must be ".d.ts" and not ".ts". 39 | * 40 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 41 | * prepend a folder token such as "". 42 | * 43 | * SUPPORTED TOKENS: , , 44 | */ 45 | "mainEntryPointFilePath": "/lib/index.d.ts", 46 | /** 47 | * A list of NPM package names whose exports should be treated as part of this package. 48 | * 49 | * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", 50 | * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part 51 | * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly 52 | * imports library2. To avoid this, we can specify: 53 | * 54 | * "bundledPackages": [ "library2" ], 55 | * 56 | * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been 57 | * local files for library1. 58 | */ 59 | "bundledPackages": [], 60 | /** 61 | * Determines how the TypeScript compiler engine will be invoked by API Extractor. 62 | */ 63 | "compiler": { 64 | /** 65 | * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. 66 | * 67 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 68 | * prepend a folder token such as "". 69 | * 70 | * Note: This setting will be ignored if "overrideTsconfig" is used. 71 | * 72 | * SUPPORTED TOKENS: , , 73 | * DEFAULT VALUE: "/tsconfig.json" 74 | */ 75 | // "tsconfigFilePath": "/tsconfig.json", 76 | /** 77 | * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. 78 | * The object must conform to the TypeScript tsconfig schema: 79 | * 80 | * http://json.schemastore.org/tsconfig 81 | * 82 | * If omitted, then the tsconfig.json file will be read from the "projectFolder". 83 | * 84 | * DEFAULT VALUE: no overrideTsconfig section 85 | */ 86 | // "overrideTsconfig": { 87 | // . . . 88 | // } 89 | /** 90 | * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended 91 | * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when 92 | * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses 93 | * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. 94 | * 95 | * DEFAULT VALUE: false 96 | */ 97 | // "skipLibCheck": true, 98 | }, 99 | /** 100 | * Configures how the API report file (*.api.md) will be generated. 101 | */ 102 | "apiReport": { 103 | /** 104 | * (REQUIRED) Whether to generate an API report. 105 | */ 106 | "enabled": false 107 | /** 108 | * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce 109 | * a full file path. 110 | * 111 | * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". 112 | * 113 | * SUPPORTED TOKENS: , 114 | * DEFAULT VALUE: ".api.md" 115 | */ 116 | // "reportFileName": ".api.md", 117 | /** 118 | * Specifies the folder where the API report file is written. The file name portion is determined by 119 | * the "reportFileName" setting. 120 | * 121 | * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, 122 | * e.g. for an API review. 123 | * 124 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 125 | * prepend a folder token such as "". 126 | * 127 | * SUPPORTED TOKENS: , , 128 | * DEFAULT VALUE: "/etc/" 129 | */ 130 | // "reportFolder": "/etc/", 131 | /** 132 | * Specifies the folder where the temporary report file is written. The file name portion is determined by 133 | * the "reportFileName" setting. 134 | * 135 | * After the temporary file is written to disk, it is compared with the file in the "reportFolder". 136 | * If they are different, a production build will fail. 137 | * 138 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 139 | * prepend a folder token such as "". 140 | * 141 | * SUPPORTED TOKENS: , , 142 | * DEFAULT VALUE: "/temp/" 143 | */ 144 | // "reportTempFolder": "/temp/" 145 | }, 146 | /** 147 | * Configures how the doc model file (*.api.json) will be generated. 148 | */ 149 | "docModel": { 150 | /** 151 | * (REQUIRED) Whether to generate a doc model file. 152 | */ 153 | "enabled": true 154 | /** 155 | * The output path for the doc model file. The file extension should be ".api.json". 156 | * 157 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 158 | * prepend a folder token such as "". 159 | * 160 | * SUPPORTED TOKENS: , , 161 | * DEFAULT VALUE: "/temp/.api.json" 162 | */ 163 | // "apiJsonFilePath": "/temp/.api.json" 164 | }, 165 | /** 166 | * Configures how the .d.ts rollup file will be generated. 167 | */ 168 | "dtsRollup": { 169 | /** 170 | * (REQUIRED) Whether to generate the .d.ts rollup file. 171 | */ 172 | "enabled": true, 173 | /** 174 | * Specifies the output path for a .d.ts rollup file to be generated without any trimming. 175 | * This file will include all declarations that are exported by the main entry point. 176 | * 177 | * If the path is an empty string, then this file will not be written. 178 | * 179 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 180 | * prepend a folder token such as "". 181 | * 182 | * SUPPORTED TOKENS: , , 183 | * DEFAULT VALUE: "/dist/.d.ts" 184 | */ 185 | "untrimmedFilePath": "", 186 | /** 187 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. 188 | * This file will include only declarations that are marked as "@public" or "@beta". 189 | * 190 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 191 | * prepend a folder token such as "". 192 | * 193 | * SUPPORTED TOKENS: , , 194 | * DEFAULT VALUE: "" 195 | */ 196 | // "betaTrimmedFilePath": "/dist/-beta.d.ts", 197 | /** 198 | * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. 199 | * This file will include only declarations that are marked as "@public". 200 | * 201 | * If the path is an empty string, then this file will not be written. 202 | * 203 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 204 | * prepend a folder token such as "". 205 | * 206 | * SUPPORTED TOKENS: , , 207 | * DEFAULT VALUE: "" 208 | */ 209 | "publicTrimmedFilePath": "/dist/index.d.ts", 210 | /** 211 | * When a declaration is trimmed, by default it will be replaced by a code comment such as 212 | * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the 213 | * declaration completely. 214 | * 215 | * DEFAULT VALUE: false 216 | */ 217 | "omitTrimmingComments": true 218 | }, 219 | /** 220 | * Configures how the tsdoc-metadata.json file will be generated. 221 | */ 222 | "tsdocMetadata": { 223 | /** 224 | * Whether to generate the tsdoc-metadata.json file. 225 | * 226 | * DEFAULT VALUE: true 227 | */ 228 | "enabled": false 229 | /** 230 | * Specifies where the TSDoc metadata file should be written. 231 | * 232 | * The path is resolved relative to the folder of the config file that contains the setting; to change this, 233 | * prepend a folder token such as "". 234 | * 235 | * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", 236 | * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup 237 | * falls back to "tsdoc-metadata.json" in the package folder. 238 | * 239 | * SUPPORTED TOKENS: , , 240 | * DEFAULT VALUE: "" 241 | */ 242 | // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" 243 | }, 244 | /** 245 | * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files 246 | * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. 247 | * To use the OS's default newline kind, specify "os". 248 | * 249 | * DEFAULT VALUE: "crlf" 250 | */ 251 | // "newlineKind": "crlf", 252 | /** 253 | * Configures how API Extractor reports error and warning messages produced during analysis. 254 | * 255 | * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. 256 | */ 257 | "messages": { 258 | /** 259 | * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing 260 | * the input .d.ts files. 261 | * 262 | * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" 263 | * 264 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 265 | */ 266 | "compilerMessageReporting": { 267 | /** 268 | * Configures the default routing for messages that don't match an explicit rule in this table. 269 | */ 270 | "default": { 271 | /** 272 | * Specifies whether the message should be written to the the tool's output log. Note that 273 | * the "addToApiReportFile" property may supersede this option. 274 | * 275 | * Possible values: "error", "warning", "none" 276 | * 277 | * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail 278 | * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes 279 | * the "--local" option), the warning is displayed but the build will not fail. 280 | * 281 | * DEFAULT VALUE: "warning" 282 | */ 283 | "logLevel": "warning" 284 | /** 285 | * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), 286 | * then the message will be written inside that file; otherwise, the message is instead logged according to 287 | * the "logLevel" option. 288 | * 289 | * DEFAULT VALUE: false 290 | */ 291 | // "addToApiReportFile": false 292 | } 293 | // "TS2551": { 294 | // "logLevel": "warning", 295 | // "addToApiReportFile": true 296 | // }, 297 | // 298 | // . . . 299 | }, 300 | /** 301 | * Configures handling of messages reported by API Extractor during its analysis. 302 | * 303 | * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" 304 | * 305 | * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings 306 | */ 307 | "extractorMessageReporting": { 308 | "default": { 309 | "logLevel": "warning" 310 | // "addToApiReportFile": false 311 | }, 312 | "ae-missing-release-tag": { 313 | "logLevel": "none" 314 | } 315 | // "ae-extra-release-tag": { 316 | // "logLevel": "warning", 317 | // "addToApiReportFile": true 318 | // }, 319 | // 320 | // . . . 321 | }, 322 | /** 323 | * Configures handling of messages reported by the TSDoc parser when analyzing code comments. 324 | * 325 | * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" 326 | * 327 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 328 | */ 329 | "tsdocMessageReporting": { 330 | "default": { 331 | "logLevel": "warning" 332 | // "addToApiReportFile": false 333 | }, 334 | "tsdoc-param-tag-missing-hyphen": { 335 | "logLevel": "none" 336 | } 337 | // "tsdoc-link-tag-unescaped-text": { 338 | // "logLevel": "warning", 339 | // "addToApiReportFile": true 340 | // }, 341 | // 342 | // . . . 343 | } 344 | } 345 | } -------------------------------------------------------------------------------- /benchmark/config/BenchmarkConfig.ts: -------------------------------------------------------------------------------- 1 | export const benchmarkConfig = { 2 | /** 压测使用的APIServer */ 3 | server: 'http://127.0.0.1:3000', 4 | 5 | /** 一共运行几次压测事务 */ 6 | total: 200000, 7 | /** 同时并发的请求数量 */ 8 | concurrency: 100, 9 | /** API请求的超时时间(超时将断开HTTP连接,释放资源,前端默认为10) */ 10 | timeout: 10000, 11 | /** 是否将错误的详情日志打印到Log */ 12 | showError: false 13 | } -------------------------------------------------------------------------------- /benchmark/http.ts: -------------------------------------------------------------------------------- 1 | import { benchmarkConfig } from './config/BenchmarkConfig'; 2 | import { HttpRunner } from './models/HTTPRunner'; 3 | 4 | const req = { 5 | a: 123456, 6 | b: 'Hello, World!', 7 | c: true, 8 | d: new Uint8Array(100000) 9 | } 10 | 11 | new HttpRunner(async function () { 12 | await this.callApi('Test', req); 13 | }, benchmarkConfig).start(); -------------------------------------------------------------------------------- /benchmark/models/HTTPRunner.ts: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import * as http from "http"; 3 | import * as https from "https"; 4 | import 'k8w-extend-native'; 5 | import { TsrpcError, TsrpcErrorType } from "tsrpc-proto"; 6 | import { HttpClient } from '../../src/client/http/HttpClient'; 7 | import { benchmarkConfig } from "../config/BenchmarkConfig"; 8 | import { serviceProto } from '../protocols/proto'; 9 | 10 | export interface HttpRunnerConfig { 11 | total: number; 12 | concurrency: number; 13 | showError?: boolean; 14 | } 15 | 16 | export class HttpRunner { 17 | 18 | private _config: HttpRunnerConfig; 19 | 20 | // 执行单个事务的方法 21 | private _single: (this: HttpRunner) => Promise; 22 | 23 | // 执行进度信息 24 | private _progress?: { 25 | startTime: number, 26 | lastSuccTime?: number, 27 | started: number, 28 | finished: number, 29 | succ: number, 30 | fail: number 31 | }; 32 | 33 | constructor(single: HttpRunner['_single'], config: HttpRunnerConfig) { 34 | this._single = single.bind(this); 35 | this._config = config; 36 | } 37 | 38 | start() { 39 | this._progress = { 40 | startTime: Date.now(), 41 | started: 0, 42 | finished: 0, 43 | succ: 0, 44 | fail: 0 45 | } 46 | 47 | // 启动并发 48 | for (let i = 0; i < this._config.concurrency; ++i) { 49 | this._doTrans(); 50 | } 51 | 52 | console.log('Benchmark start!'); 53 | this._startReport(); 54 | } 55 | 56 | private _doTrans() { 57 | if (this._isStoped || !this._progress) { 58 | return; 59 | } 60 | 61 | if (this._progress.started < this._config.total) { 62 | ++this._progress.started; 63 | let startTime = Date.now(); 64 | this._single().then(v => { 65 | ++this._progress!.succ; 66 | this._progress!.lastSuccTime = Date.now(); 67 | }).catch(e => { 68 | ++this._progress!.fail; 69 | if (this._config.showError) { 70 | console.error('[Error]', e.message); 71 | } 72 | }).then(() => { 73 | ++this._progress!.finished; 74 | if (this._progress!.finished === this._config.total) { 75 | this._finish(); 76 | } 77 | else { 78 | this._doTrans(); 79 | } 80 | }) 81 | } 82 | } 83 | 84 | private _reportInterval?: NodeJS.Timeout; 85 | private _startReport() { 86 | this._reportInterval = setInterval(() => { 87 | this._report(); 88 | }, 1000) 89 | } 90 | 91 | private _isStoped = false; 92 | stop() { 93 | this._isStoped = true; 94 | } 95 | 96 | private _finish() { 97 | if (!this._progress) { 98 | return; 99 | } 100 | 101 | this._reportInterval && clearInterval(this._reportInterval); 102 | 103 | console.log('\n\n-------------------------------\n Benchmark finished! \n-------------------------------'); 104 | 105 | let usedTime = Date.now() - this._progress.startTime; 106 | console.log(` Transaction Execution Result `.bgBlue.white); 107 | console.log(`Started=${this._progress.started}, Finished=${this._progress.finished}, UsedTime=${usedTime}ms`.green); 108 | console.log(`Succ=${this._progress.succ}, Fail=${this._progress.fail}, TPS=${this._progress.succ / (this._progress.lastSuccTime! - this._progress.startTime) * 1000 | 0}\n`.green) 109 | 110 | // TIME TPS(完成的) 111 | console.log(` API Execution Result `.bgBlue.white); 112 | 113 | // [KEY] RPS(完成的) AVG P95 P99 114 | for (let key in this._apiStat) { 115 | let stat = this._apiStat[key]; 116 | stat.resTime = stat.resTime.orderBy(v => v); 117 | 118 | let send = stat.sendReq; 119 | let succ = stat.resTime.length; 120 | let netErr = stat.networkError; 121 | let apiErr = stat.otherError; 122 | let avg = stat.resTime[stat.resTime.length >> 1] | 0; 123 | let p95 = stat.resTime[stat.resTime.length * 0.95 | 0] | 0; 124 | let p99 = stat.resTime[stat.resTime.length * 0.99 | 0] | 0; 125 | 126 | this._logTable([ 127 | [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr', 'AVG ', 'P95 ', 'P99 '], 128 | ['', '' + send, 129 | { text: '' + succ, color: 'green' }, 130 | { text: '' + (succ / (stat.last.succTime - stat.startTime) * 1000 | 0), color: 'green' }, 131 | netErr ? { text: '' + netErr, color: 'red' } : '0', 132 | apiErr ? { text: '' + apiErr, color: 'red' } : '0', 133 | { text: avg ? avg + 'ms' : '-', color: 'yellow' }, 134 | { text: p95 ? p95 + 'ms' : '-', color: 'yellow' }, 135 | { text: p99 ? p99 + 'ms' : '-', color: 'yellow' } 136 | ] 137 | ]) 138 | } 139 | } 140 | 141 | private _apiStat: { 142 | [key: string]: { 143 | sendReq: number, 144 | resTime: number[], 145 | succ: number, 146 | networkError: number, 147 | otherError: number, 148 | startTime: number, 149 | last: { 150 | sendReq: number, 151 | resTime: number[], 152 | succ: number, 153 | networkError: number, 154 | otherError: number, 155 | startTime: number, 156 | succTime: number 157 | } 158 | } 159 | } = {}; 160 | 161 | private _maxApiNameLength = 0; 162 | /** 163 | * callApi 并且计入统计 164 | */ 165 | callApi: typeof benchmarkClient.callApi = async (apiName, req) => { 166 | this._maxApiNameLength = Math.max(apiName.length, this._maxApiNameLength); 167 | 168 | if (!this._apiStat[apiName]) { 169 | this._apiStat[apiName] = { 170 | sendReq: 0, 171 | resTime: [], 172 | succ: 0, 173 | networkError: 0, 174 | otherError: 0, 175 | startTime: Date.now(), 176 | last: { 177 | sendReq: 0, 178 | resTime: [], 179 | succ: 0, 180 | networkError: 0, 181 | otherError: 0, 182 | startTime: Date.now(), 183 | succTime: 0 184 | } 185 | }; 186 | } 187 | 188 | ++this._apiStat[apiName].sendReq; 189 | ++this._apiStat[apiName].last.sendReq; 190 | 191 | let startTime = Date.now(); 192 | let ret = await benchmarkClient.callApi(apiName, req); 193 | 194 | if (ret.isSucc) { 195 | this._apiStat[apiName].last.succTime = Date.now(); 196 | this._apiStat[apiName].resTime.push(Date.now() - startTime); 197 | this._apiStat[apiName].last.resTime.push(Date.now() - startTime); 198 | ++this._apiStat[apiName].succ; 199 | ++this._apiStat[apiName].last.succ; 200 | } 201 | else { 202 | if (ret.err.type === TsrpcErrorType.NetworkError) { 203 | ++this._apiStat[apiName].networkError; 204 | ++this._apiStat[apiName].last.networkError; 205 | } 206 | else { 207 | ++this._apiStat[apiName].otherError; 208 | ++this._apiStat[apiName].last.otherError; 209 | } 210 | } 211 | 212 | return ret; 213 | } 214 | 215 | private _report() { 216 | console.log(new Date().format('hh:mm:ss').gray, `Started=${this._progress!.started}/${this._config.total}, Finished=${this._progress!.finished}, Succ=${this._progress!.succ.toString().green}, Fail=${this._progress!.fail.toString()[this._progress!.fail > 0 ? 'red' : 'white']}`, 217 | this._progress!.lastSuccTime ? `TPS=${this._progress!.succ / (this._progress!.lastSuccTime - this._progress!.startTime) * 1000 | 0}` : '') 218 | 219 | for (let key in this._apiStat) { 220 | let stat = this._apiStat[key]; 221 | 222 | let send = stat.last.sendReq; 223 | let succ = stat.last.resTime.length; 224 | let netErr = stat.last.networkError; 225 | let apiErr = stat.last.otherError; 226 | 227 | this._logTable([ 228 | [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr'], 229 | ['', '' + send, 230 | { text: '' + succ, color: 'green' }, 231 | { text: '' + (succ / (stat.last.succTime - stat.last.startTime) * 1000 | 0), color: 'green' }, 232 | netErr ? { text: '' + netErr, color: 'red' } : '0', 233 | apiErr ? { text: '' + apiErr, color: 'red' } : '0' 234 | ] 235 | ]) 236 | 237 | Object.assign(stat.last, { 238 | sendReq: 0, 239 | resTime: [], 240 | succ: 0, 241 | networkError: 0, 242 | otherError: 0, 243 | startTime: Date.now(), 244 | }) 245 | } 246 | } 247 | 248 | private _logTable(rows: [TableCellItem[], TableCellItem[]]) { 249 | let cellWidths: number[] = []; 250 | for (let cell of rows[0]) { 251 | cellWidths.push(typeof cell === 'string' ? cell.length + 4 : cell.text.length + 4); 252 | } 253 | 254 | for (let row of rows) { 255 | let line = ''; 256 | for (let i = 0; i < row.length; ++i) { 257 | let cell = row[i]; 258 | let cellWidth = cellWidths[i]; 259 | if (typeof cell === 'string') { 260 | line += cell + ' '.repeat(cellWidth - cell.length); 261 | } 262 | else { 263 | line += cell.text[cell.color] + ' '.repeat(cellWidth - cell.text.length); 264 | } 265 | } 266 | console.log(line); 267 | } 268 | } 269 | } 270 | 271 | export const benchmarkClient = new HttpClient(serviceProto, { 272 | server: benchmarkConfig.server, 273 | logger: { 274 | debug: function () { }, 275 | log: function () { }, 276 | warn: function () { }, 277 | error: function () { }, 278 | }, 279 | timeout: benchmarkConfig.timeout, 280 | agent: new (benchmarkConfig.server.startsWith('https') ? https : http).Agent({ 281 | keepAlive: true 282 | }) 283 | }) 284 | 285 | type TableCellItem = (string | { text: string, color: 'green' | 'red' | 'yellow' }); -------------------------------------------------------------------------------- /benchmark/models/WsRunner.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import 'colors'; 3 | import 'k8w-extend-native'; 4 | import { TsrpcErrorType } from "tsrpc-proto"; 5 | import { WsClient } from '../../src/client/ws/WsClient'; 6 | import { benchmarkConfig } from "../config/BenchmarkConfig"; 7 | import { serviceProto } from '../protocols/proto'; 8 | 9 | export interface WsRunnerConfig { 10 | total: number; 11 | concurrency: number; 12 | showError?: boolean; 13 | } 14 | 15 | export class WsRunner { 16 | 17 | private _config: WsRunnerConfig; 18 | 19 | // 执行单个事务的方法 20 | private _single: (this: WsRunner) => Promise; 21 | 22 | // 执行进度信息 23 | private _progress?: { 24 | startTime: number, 25 | lastSuccTime?: number, 26 | started: number, 27 | finished: number, 28 | succ: number, 29 | fail: number 30 | }; 31 | 32 | constructor(single: WsRunner['_single'], config: WsRunnerConfig) { 33 | this._single = single.bind(this); 34 | this._config = config; 35 | } 36 | 37 | async start() { 38 | this._progress = { 39 | startTime: Date.now(), 40 | started: 0, 41 | finished: 0, 42 | succ: 0, 43 | fail: 0 44 | } 45 | 46 | assert.ok(await benchmarkClient.connect(), 'Connect failed'); 47 | 48 | // 启动并发 49 | for (let i = 0; i < this._config.concurrency; ++i) { 50 | this._doTrans(); 51 | } 52 | 53 | console.log('Benchmark start!'); 54 | this._startReport(); 55 | } 56 | 57 | private _doTrans() { 58 | if (this._isStoped || !this._progress) { 59 | return; 60 | } 61 | 62 | if (this._progress.started < this._config.total) { 63 | ++this._progress.started; 64 | let startTime = Date.now(); 65 | this._single().then(v => { 66 | ++this._progress!.succ; 67 | this._progress!.lastSuccTime = Date.now(); 68 | }).catch(e => { 69 | ++this._progress!.fail; 70 | if (this._config.showError) { 71 | console.error('[Error]', e.message); 72 | } 73 | }).then(() => { 74 | ++this._progress!.finished; 75 | if (this._progress!.finished === this._config.total) { 76 | this._finish(); 77 | } 78 | else { 79 | this._doTrans(); 80 | } 81 | }) 82 | } 83 | } 84 | 85 | private _reportInterval?: NodeJS.Timeout; 86 | private _startReport() { 87 | this._reportInterval = setInterval(() => { 88 | this._report(); 89 | }, 1000) 90 | } 91 | 92 | private _isStoped = false; 93 | stop() { 94 | this._isStoped = true; 95 | } 96 | 97 | private _finish() { 98 | if (!this._progress) { 99 | return; 100 | } 101 | 102 | this._reportInterval && clearInterval(this._reportInterval); 103 | 104 | console.log('\n\n-------------------------------\n Benchmark finished! \n-------------------------------'); 105 | 106 | let usedTime = Date.now() - this._progress.startTime; 107 | console.log(` Transaction Execution Result `.bgBlue.white); 108 | console.log(`Started=${this._progress.started}, Finished=${this._progress.finished}, UsedTime=${usedTime}ms`.green); 109 | console.log(`Succ=${this._progress.succ}, Fail=${this._progress.fail}, TPS=${this._progress.succ / (this._progress.lastSuccTime! - this._progress.startTime) * 1000 | 0}\n`.green) 110 | 111 | // TIME TPS(完成的) 112 | console.log(` API Execution Result `.bgBlue.white); 113 | 114 | // [KEY] RPS(完成的) AVG P95 P99 115 | for (let key in this._apiStat) { 116 | let stat = this._apiStat[key]; 117 | stat.resTime = stat.resTime.orderBy(v => v); 118 | 119 | let send = stat.sendReq; 120 | let succ = stat.resTime.length; 121 | let netErr = stat.networkError; 122 | let apiErr = stat.otherError; 123 | let avg = stat.resTime[stat.resTime.length >> 1] | 0; 124 | let p95 = stat.resTime[stat.resTime.length * 0.95 | 0] | 0; 125 | let p99 = stat.resTime[stat.resTime.length * 0.99 | 0] | 0; 126 | 127 | this._logTable([ 128 | [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr', 'AVG ', 'P95 ', 'P99 '], 129 | ['', '' + send, 130 | { text: '' + succ, color: 'green' }, 131 | { text: '' + (succ / (stat.last.succTime - stat.startTime) * 1000 | 0), color: 'green' }, 132 | netErr ? { text: '' + netErr, color: 'red' } : '0', 133 | apiErr ? { text: '' + apiErr, color: 'red' } : '0', 134 | { text: avg ? avg + 'ms' : '-', color: 'yellow' }, 135 | { text: p95 ? p95 + 'ms' : '-', color: 'yellow' }, 136 | { text: p99 ? p99 + 'ms' : '-', color: 'yellow' } 137 | ] 138 | ]) 139 | } 140 | } 141 | 142 | private _apiStat: { 143 | [key: string]: { 144 | sendReq: number, 145 | resTime: number[], 146 | succ: number, 147 | networkError: number, 148 | otherError: number, 149 | startTime: number, 150 | last: { 151 | sendReq: number, 152 | resTime: number[], 153 | succ: number, 154 | networkError: number, 155 | otherError: number, 156 | startTime: number, 157 | succTime: number 158 | } 159 | } 160 | } = {}; 161 | 162 | private _maxApiNameLength = 0; 163 | /** 164 | * callApi 并且计入统计 165 | */ 166 | callApi: typeof benchmarkClient.callApi = async (apiName, req) => { 167 | this._maxApiNameLength = Math.max(apiName.length, this._maxApiNameLength); 168 | 169 | if (!this._apiStat[apiName]) { 170 | this._apiStat[apiName] = { 171 | sendReq: 0, 172 | resTime: [], 173 | succ: 0, 174 | networkError: 0, 175 | otherError: 0, 176 | startTime: Date.now(), 177 | last: { 178 | sendReq: 0, 179 | resTime: [], 180 | succ: 0, 181 | networkError: 0, 182 | otherError: 0, 183 | startTime: Date.now(), 184 | succTime: 0 185 | } 186 | }; 187 | } 188 | 189 | ++this._apiStat[apiName].sendReq; 190 | ++this._apiStat[apiName].last.sendReq; 191 | 192 | let startTime = Date.now(); 193 | let ret = await benchmarkClient.callApi(apiName, req); 194 | 195 | if (ret.isSucc) { 196 | this._apiStat[apiName].last.succTime = Date.now(); 197 | this._apiStat[apiName].resTime.push(Date.now() - startTime); 198 | this._apiStat[apiName].last.resTime.push(Date.now() - startTime); 199 | ++this._apiStat[apiName].succ; 200 | ++this._apiStat[apiName].last.succ; 201 | } 202 | else { 203 | if (ret.err.type === TsrpcErrorType.NetworkError) { 204 | ++this._apiStat[apiName].networkError; 205 | ++this._apiStat[apiName].last.networkError; 206 | } 207 | else { 208 | ++this._apiStat[apiName].otherError; 209 | ++this._apiStat[apiName].last.otherError; 210 | } 211 | } 212 | 213 | return ret; 214 | } 215 | 216 | private _report() { 217 | console.log(new Date().format('hh:mm:ss').gray, `Started=${this._progress!.started}/${this._config.total}, Finished=${this._progress!.finished}, Succ=${this._progress!.succ.toString().green}, Fail=${this._progress!.fail.toString()[this._progress!.fail > 0 ? 'red' : 'white']}`, 218 | this._progress!.lastSuccTime ? `TPS=${this._progress!.succ / (this._progress!.lastSuccTime - this._progress!.startTime) * 1000 | 0}` : '') 219 | 220 | for (let key in this._apiStat) { 221 | let stat = this._apiStat[key]; 222 | 223 | let send = stat.last.sendReq; 224 | let succ = stat.last.resTime.length; 225 | let netErr = stat.last.networkError; 226 | let apiErr = stat.last.otherError; 227 | 228 | this._logTable([ 229 | [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr'], 230 | ['', '' + send, 231 | { text: '' + succ, color: 'green' }, 232 | { text: '' + (succ / (stat.last.succTime - stat.last.startTime) * 1000 | 0), color: 'green' }, 233 | netErr ? { text: '' + netErr, color: 'red' } : '0', 234 | apiErr ? { text: '' + apiErr, color: 'red' } : '0' 235 | ] 236 | ]) 237 | 238 | Object.assign(stat.last, { 239 | sendReq: 0, 240 | resTime: [], 241 | succ: 0, 242 | networkError: 0, 243 | otherError: 0, 244 | startTime: Date.now(), 245 | }) 246 | } 247 | } 248 | 249 | private _logTable(rows: [TableCellItem[], TableCellItem[]]) { 250 | let cellWidths: number[] = []; 251 | for (let cell of rows[0]) { 252 | cellWidths.push(typeof cell === 'string' ? cell.length + 4 : cell.text.length + 4); 253 | } 254 | 255 | for (let row of rows) { 256 | let line = ''; 257 | for (let i = 0; i < row.length; ++i) { 258 | let cell = row[i]; 259 | let cellWidth = cellWidths[i]; 260 | if (typeof cell === 'string') { 261 | line += cell + ' '.repeat(cellWidth - cell.length); 262 | } 263 | else { 264 | line += cell.text[cell.color] + ' '.repeat(cellWidth - cell.text.length); 265 | } 266 | } 267 | console.log(line); 268 | } 269 | } 270 | } 271 | 272 | export const benchmarkClient = new WsClient(serviceProto, { 273 | server: benchmarkConfig.server, 274 | logger: { 275 | debug: function () { }, 276 | log: function () { }, 277 | warn: function () { }, 278 | error: function () { }, 279 | }, 280 | timeout: benchmarkConfig.timeout 281 | }) 282 | 283 | type TableCellItem = (string | { text: string, color: 'green' | 'red' | 'yellow' }); -------------------------------------------------------------------------------- /benchmark/protocols/PtlTest.ts: -------------------------------------------------------------------------------- 1 | import { uint } from 'tsbuffer-schema'; 2 | export interface ReqTest { 3 | a?: uint; 4 | b?: string; 5 | c?: boolean; 6 | d?: Uint8Array; 7 | } 8 | 9 | export interface ResTest { 10 | a?: uint; 11 | b?: string; 12 | c?: boolean; 13 | d?: Uint8Array; 14 | } -------------------------------------------------------------------------------- /benchmark/protocols/proto.ts: -------------------------------------------------------------------------------- 1 | import { ServiceProto } from 'tsrpc-proto'; 2 | import { ReqTest, ResTest } from './PtlTest' 3 | 4 | export interface ServiceType { 5 | api: { 6 | "Test": { 7 | req: ReqTest, 8 | res: ResTest 9 | } 10 | }, 11 | msg: { 12 | 13 | } 14 | } 15 | 16 | export const serviceProto: ServiceProto = { 17 | "services": [ 18 | { 19 | "id": 0, 20 | "name": "Test", 21 | "type": "api" 22 | } 23 | ], 24 | "types": { 25 | "PtlTest/ReqTest": { 26 | "type": "Interface", 27 | "properties": [ 28 | { 29 | "id": 0, 30 | "name": "a", 31 | "type": { 32 | "type": "Number", 33 | "scalarType": "uint" 34 | }, 35 | "optional": true 36 | }, 37 | { 38 | "id": 1, 39 | "name": "b", 40 | "type": { 41 | "type": "String" 42 | }, 43 | "optional": true 44 | }, 45 | { 46 | "id": 2, 47 | "name": "c", 48 | "type": { 49 | "type": "Boolean" 50 | }, 51 | "optional": true 52 | }, 53 | { 54 | "id": 3, 55 | "name": "d", 56 | "type": { 57 | "type": "Buffer", 58 | "arrayType": "Uint8Array" 59 | }, 60 | "optional": true 61 | } 62 | ] 63 | }, 64 | "PtlTest/ResTest": { 65 | "type": "Interface", 66 | "properties": [ 67 | { 68 | "id": 0, 69 | "name": "a", 70 | "type": { 71 | "type": "Number", 72 | "scalarType": "uint" 73 | }, 74 | "optional": true 75 | }, 76 | { 77 | "id": 1, 78 | "name": "b", 79 | "type": { 80 | "type": "String" 81 | }, 82 | "optional": true 83 | }, 84 | { 85 | "id": 2, 86 | "name": "c", 87 | "type": { 88 | "type": "Boolean" 89 | }, 90 | "optional": true 91 | }, 92 | { 93 | "id": 3, 94 | "name": "d", 95 | "type": { 96 | "type": "Buffer", 97 | "arrayType": "Uint8Array" 98 | }, 99 | "optional": true 100 | } 101 | ] 102 | } 103 | } 104 | }; -------------------------------------------------------------------------------- /benchmark/server/http.ts: -------------------------------------------------------------------------------- 1 | import { HttpServer } from '../../src/index'; 2 | import { serviceProto } from "../protocols/proto"; 3 | 4 | async function main() { 5 | let server = new HttpServer(serviceProto, { 6 | logger: { 7 | debug: () => { }, 8 | log: () => { }, 9 | error: console.error.bind(console), 10 | warn: console.warn.bind(console) 11 | } 12 | }); 13 | 14 | server.implementApi('Test', call => { 15 | call.succ(call.req); 16 | }); 17 | 18 | await server.start(); 19 | 20 | setInterval(() => { 21 | let used = process.memoryUsage().heapUsed / 1024 / 1024; 22 | console.log(`内存: ${Math.round(used * 100) / 100} MB`); 23 | }, 2000) 24 | } 25 | 26 | main(); -------------------------------------------------------------------------------- /benchmark/server/ws.ts: -------------------------------------------------------------------------------- 1 | import { WsServer } from '../../src/index'; 2 | import { serviceProto } from "../protocols/proto"; 3 | 4 | async function main() { 5 | let server = new WsServer(serviceProto, { 6 | logger: { 7 | debug: () => { }, 8 | log: () => { }, 9 | error: console.error.bind(console), 10 | warn: console.warn.bind(console) 11 | } 12 | }); 13 | 14 | server.implementApi('Test', call => { 15 | call.succ(call.req); 16 | }); 17 | 18 | await server.start(); 19 | 20 | setInterval(() => { 21 | let used = process.memoryUsage().heapUsed / 1024 / 1024; 22 | console.log(`内存: ${Math.round(used * 100) / 100} MB`); 23 | }, 2000) 24 | } 25 | 26 | main(); -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /benchmark/ws.ts: -------------------------------------------------------------------------------- 1 | import { benchmarkConfig } from './config/BenchmarkConfig'; 2 | import { WsRunner } from './models/WsRunner'; 3 | 4 | const req = { 5 | a: 123456, 6 | b: 'Hello, World!', 7 | c: true, 8 | d: new Uint8Array(100000) 9 | } 10 | 11 | new WsRunner(async function () { 12 | await this.callApi('Test', req); 13 | }, benchmarkConfig).start(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsrpc", 3 | "version": "3.4.18", 4 | "description": "A TypeScript RPC Framework, with runtime type checking and built-in serialization, support both HTTP and WebSocket.", 5 | "main": "index.js", 6 | "exports": { 7 | "require": "./index.js", 8 | "import": "./index.mjs" 9 | }, 10 | "typings": "index.d.ts", 11 | "directories": { 12 | "doc": "docs" 13 | }, 14 | "scripts": { 15 | "test": "npx mocha", 16 | "genTestProto": "npx tsrpc-cli@latest proto --input test/proto --output test/proto/serviceProto.ts", 17 | "coverage": "nyc mocha test/**/*.test.ts && start coverage\\index.html", 18 | "build": "npm run format && npm run build:js && npm run build:dts && npm run build:doc && node scripts/postBuild && cp package.json LICENSE README.md dist/", 19 | "build:js": "rm -rf dist && npx rollup -c", 20 | "build:dts": "rm -rf lib && npx tsc && npx api-extractor run --local --verbose && rm -rf lib", 21 | "build:doc": "rm -rf docs/api && npx api-documenter markdown --input temp --output docs/api", 22 | "format": "prettier --write \"{src,test}/**/*.{js,jsx,ts,tsx,css,less,scss,json,md}\"", 23 | "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,less,scss,json,md}\"" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/k8w/tsrpc.git" 28 | }, 29 | "keywords": [ 30 | "k8w", 31 | "ts", 32 | "rpc", 33 | "grpc", 34 | "tsbuffer", 35 | "fullstack", 36 | "websocket", 37 | "protobuf", 38 | "socket.io" 39 | ], 40 | "author": "k8w", 41 | "license": "MIT", 42 | "devDependencies": { 43 | "@microsoft/api-documenter": "^7.24.2", 44 | "@microsoft/api-extractor": "^7.43.1", 45 | "@types/chai": "^4.3.16", 46 | "@types/mocha": "^8.2.3", 47 | "@types/node": "^15.14.9", 48 | "@types/uuid": "^8.3.4", 49 | "chai": "^4.4.1", 50 | "mocha": "^9.2.2", 51 | "mongodb": "^4.17.2", 52 | "nyc": "^15.1.0", 53 | "prettier": "^3.5.3", 54 | "rollup": "^2.79.1", 55 | "rollup-plugin-typescript2": "^0.36.0", 56 | "ts-node": "^10.9.2", 57 | "typescript": "^4.9.5" 58 | }, 59 | "dependencies": { 60 | "@types/ws": "^7.4.7", 61 | "chalk": "^4.1.2", 62 | "tsbuffer": "^2.2.10", 63 | "tsrpc-base-client": "^2.1.15", 64 | "tsrpc-proto": "^1.4.3", 65 | "uuid": "^8.3.2", 66 | "ws": "^7.5.9" 67 | }, 68 | "nyc": { 69 | "extension": [ 70 | ".ts" 71 | ], 72 | "include": [ 73 | "src/**/*.ts" 74 | ], 75 | "reporter": [ 76 | "html" 77 | ], 78 | "all": true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /res/mongodb-polyfill.d.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import { OmitUnion } from 'k8w-extend-native'; 3 | 4 | type InsertOneResult = any; 5 | type OptionalId = any; 6 | type Document = any; 7 | 8 | declare module 'mongodb' { 9 | export interface Collection { 10 | insertOne(doc: OptionalUnlessRequiredId_1): Promise>; 11 | } 12 | export type OptionalUnlessRequiredId_1 = TSchema extends { 13 | _id: ObjectId; 14 | } ? (OmitUnion & { _id?: ObjectId }) : TSchema extends { 15 | _id: any; 16 | } ? TSchema : OptionalId; 17 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | 3 | export default [ 4 | { 5 | input: './src/index.ts', 6 | output: [{ 7 | format: 'cjs', 8 | file: './dist/index.js', 9 | banner: require('./scripts/copyright') 10 | }], 11 | plugins: [ 12 | typescript({ 13 | tsconfigOverride: { 14 | compilerOptions: { 15 | declaration: false, 16 | declarationMap: false, 17 | module: "esnext" 18 | } 19 | } 20 | }) 21 | ] 22 | }, 23 | { 24 | input: './src/index.ts', 25 | output: [{ 26 | format: 'es', 27 | file: './dist/index.mjs', 28 | banner: require('./scripts/copyright') 29 | }], 30 | plugins: [ 31 | typescript({ 32 | tsconfigOverride: { 33 | compilerOptions: { 34 | declaration: false, 35 | declarationMap: false, 36 | module: "esnext" 37 | } 38 | } 39 | }) 40 | ] 41 | } 42 | ] -------------------------------------------------------------------------------- /scripts/copyright.js: -------------------------------------------------------------------------------- 1 | module.exports = `/*! 2 | * TSRPC v${require('../package.json').version} 3 | * ----------------------------------------- 4 | * Copyright (c) King Wang. 5 | * MIT License 6 | * https://github.com/k8w/tsrpc 7 | */` -------------------------------------------------------------------------------- /scripts/postBuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // remove private / protected index.d.ts 5 | (() => { 6 | let content = fs.readFileSync(path.resolve(__dirname, '../dist/index.d.ts'), 'utf-8'); 7 | content = content.replace(/^\s*(private|protected)\s+\_.+;/g, ''); 8 | content = require('./copyright') + '\n' + content; 9 | fs.writeFileSync(path.resolve(__dirname, '../dist/index.d.ts'), content, 'utf-8'); 10 | })(); 11 | 12 | // replace __TSRPC_VERSION__from index.js/mjs 13 | [ 14 | path.resolve(__dirname, '../dist/index.js'), 15 | path.resolve(__dirname, '../dist/index.mjs') 16 | ].forEach(filepath => { 17 | let content = fs.readFileSync(filepath, 'utf-8'); 18 | content = content.replace('__TSRPC_VERSION__', require('../package.json').version);; 19 | fs.writeFileSync(filepath, content, 'utf-8'); 20 | }); 21 | 22 | // mongodb-polyfill 23 | fs.copyFileSync(path.resolve(__dirname, '../res/mongodb-polyfill.d.ts'), path.resolve(__dirname, '../dist/mongodb-polyfill.d.ts')); 24 | let content = fs.readFileSync(path.resolve(__dirname, '../dist/index.d.ts'), 'utf-8'); 25 | content = content.replace(`/// `, `/// \n/// `) 26 | fs.writeFileSync(path.resolve(__dirname, '../dist/index.d.ts'), content, 'utf-8'); -------------------------------------------------------------------------------- /src/client/http/HttpClient.ts: -------------------------------------------------------------------------------- 1 | import http from "http" 2 | import https from "https" 3 | import { 4 | BaseHttpClient, 5 | BaseHttpClientOptions, 6 | defaultBaseHttpClientOptions, 7 | } from "tsrpc-base-client" 8 | import { BaseServiceType, ServiceProto } from "tsrpc-proto" 9 | import { getClassObjectId } from "../../models/getClassObjectId" 10 | import { HttpProxy } from "./HttpProxy" 11 | 12 | /** 13 | * Client for TSRPC HTTP Server. 14 | * It uses native http module of NodeJS. 15 | * @typeParam ServiceType - `ServiceType` from generated `proto.ts` 16 | */ 17 | export class HttpClient extends BaseHttpClient { 18 | readonly options!: Readonly 19 | 20 | constructor(proto: ServiceProto, options?: Partial) { 21 | let httpProxy = new HttpProxy() 22 | super(proto, httpProxy, { 23 | customObjectIdClass: getClassObjectId(), 24 | ...defaultHttpClientOptions, 25 | ...options, 26 | }) 27 | 28 | httpProxy.agent = this.options.agent 29 | } 30 | } 31 | 32 | const defaultHttpClientOptions: HttpClientOptions = { 33 | ...defaultBaseHttpClientOptions, 34 | } 35 | 36 | export interface HttpClientOptions extends BaseHttpClientOptions { 37 | /** NodeJS HTTP Agent */ 38 | agent?: http.Agent | https.Agent 39 | } 40 | -------------------------------------------------------------------------------- /src/client/http/HttpProxy.ts: -------------------------------------------------------------------------------- 1 | import http from "http" 2 | import https from "https" 3 | import { IHttpProxy } from "tsrpc-base-client" 4 | import { TsrpcError } from "tsrpc-proto" 5 | 6 | /** @internal */ 7 | export class HttpProxy implements IHttpProxy { 8 | /** NodeJS HTTP Agent */ 9 | agent?: http.Agent | https.Agent 10 | 11 | fetch(options: Parameters[0]): ReturnType { 12 | let nodeHttp = options.url.startsWith("https://") ? https : http 13 | 14 | let rs!: ( 15 | v: { isSucc: true; res: string | Uint8Array } | { isSucc: false; err: TsrpcError } 16 | ) => void 17 | let promise: ReturnType["promise"] = new Promise(_rs => { 18 | rs = _rs 19 | }) 20 | 21 | let httpReq: http.ClientRequest 22 | httpReq = nodeHttp.request( 23 | options.url, 24 | { 25 | method: options.method, 26 | agent: this.agent, 27 | timeout: options.timeout, 28 | headers: options.headers, 29 | }, 30 | httpRes => { 31 | let data: Buffer[] = [] 32 | httpRes.on("data", (v: Buffer) => { 33 | data.push(v) 34 | }) 35 | httpRes.on("end", () => { 36 | let buf: Uint8Array = Buffer.concat(data) 37 | if (options.responseType === "text") { 38 | rs({ 39 | isSucc: true, 40 | res: buf.toString(), 41 | }) 42 | } else { 43 | rs({ 44 | isSucc: true, 45 | res: buf, 46 | }) 47 | } 48 | }) 49 | } 50 | ) 51 | 52 | httpReq.on("error", e => { 53 | rs({ 54 | isSucc: false, 55 | err: new TsrpcError(e.message, { 56 | type: TsrpcError.Type.NetworkError, 57 | code: (e as any).code, 58 | }), 59 | }) 60 | }) 61 | 62 | // Timeout 63 | httpReq.on("timeout", () => { 64 | rs({ 65 | isSucc: false, 66 | err: new TsrpcError("Request timeout", { 67 | type: TsrpcError.Type.NetworkError, 68 | code: "ECONNABORTED", 69 | }), 70 | }) 71 | }) 72 | 73 | let buf = options.data 74 | httpReq.end( 75 | typeof buf === "string" ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) 76 | ) 77 | 78 | let abort = httpReq.abort.bind(httpReq) 79 | 80 | return { 81 | promise: promise, 82 | abort: abort, 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/client/ws/WebSocketProxy.ts: -------------------------------------------------------------------------------- 1 | import { IWebSocketProxy } from "tsrpc-base-client" 2 | import { TsrpcError } from "tsrpc-proto" 3 | import WebSocket from "ws" 4 | 5 | /** 6 | * @internal 7 | */ 8 | export class WebSocketProxy implements IWebSocketProxy { 9 | options!: IWebSocketProxy["options"] 10 | 11 | private _ws?: WebSocket 12 | connect(server: string, protocols?: string[]): void { 13 | this._ws = new WebSocket(server, protocols) 14 | this._ws.onopen = this.options.onOpen 15 | this._ws.onclose = e => { 16 | this.options.onClose(e.code, e.reason) 17 | this._ws = undefined 18 | } 19 | this._ws.onerror = e => { 20 | this.options.onError(e.error) 21 | } 22 | this._ws.onmessage = e => { 23 | if (e.data instanceof ArrayBuffer) { 24 | this.options.onMessage(new Uint8Array(e.data)) 25 | } else if (Array.isArray(e.data)) { 26 | this.options.onMessage(Buffer.concat(e.data)) 27 | } else { 28 | this.options.onMessage(e.data) 29 | } 30 | } 31 | } 32 | close(code?: number, reason?: string): void { 33 | this._ws?.close(code, reason) 34 | this._ws = undefined 35 | } 36 | send(data: string | Uint8Array): Promise<{ err?: TsrpcError | undefined }> { 37 | return new Promise(rs => { 38 | this._ws?.send(data, err => { 39 | if (err) { 40 | this.options.logger?.error("WebSocket Send Error:", err) 41 | rs({ 42 | err: new TsrpcError("Network Error", { 43 | code: "SEND_BUF_ERR", 44 | type: TsrpcError.Type.NetworkError, 45 | innerErr: err, 46 | }), 47 | }) 48 | return 49 | } 50 | rs({}) 51 | }) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/client/ws/WsClient.ts: -------------------------------------------------------------------------------- 1 | import { BaseWsClient, BaseWsClientOptions, defaultBaseWsClientOptions } from "tsrpc-base-client" 2 | import { BaseServiceType, ServiceProto } from "tsrpc-proto" 3 | import { getClassObjectId } from "../../models/getClassObjectId" 4 | import { WebSocketProxy } from "./WebSocketProxy" 5 | 6 | /** 7 | * Client for TSRPC WebSocket Server. 8 | * @typeParam ServiceType - `ServiceType` from generated `proto.ts` 9 | */ 10 | export class WsClient extends BaseWsClient { 11 | readonly options!: Readonly 12 | 13 | constructor(proto: ServiceProto, options?: Partial) { 14 | let wsp = new WebSocketProxy() 15 | super(proto, wsp, { 16 | customObjectIdClass: getClassObjectId(), 17 | ...defaultWsClientOptions, 18 | ...options, 19 | }) 20 | } 21 | } 22 | 23 | const defaultWsClientOptions: WsClientOptions = { 24 | ...defaultBaseWsClientOptions, 25 | } 26 | 27 | export interface WsClientOptions extends BaseWsClientOptions {} 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "k8w-extend-native" 2 | 3 | // Common 4 | export * from "tsrpc-base-client" 5 | export * from "tsrpc-proto" 6 | export * from "./client/http/HttpClient" 7 | export * from "./client/ws/WsClient" 8 | export * from "./models/version" 9 | export * from "./server/base/ApiCall" 10 | // Base 11 | export * from "./server/base/BaseCall" 12 | export * from "./server/base/BaseConnection" 13 | export * from "./server/base/BaseServer" 14 | export * from "./server/base/MsgCall" 15 | export * from "./server/http/ApiCallHttp" 16 | // Http 17 | export * from "./server/http/HttpConnection" 18 | export * from "./server/http/HttpServer" 19 | export * from "./server/http/MsgCallHttp" 20 | export * from "./server/models/PrefixLogger" 21 | export * from "./server/models/TerminalColorLogger" 22 | // WebSocket 23 | export * from "./server/ws/ApiCallWs" 24 | export * from "./server/ws/MsgCallWs" 25 | export * from "./server/ws/WsConnection" 26 | export * from "./server/ws/WsServer" 27 | -------------------------------------------------------------------------------- /src/models/HttpUtil.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http" 2 | 3 | export class HttpUtil { 4 | static getClientIp(req: http.IncomingMessage) { 5 | var ipAddress 6 | // The request may be forwarded from local web server. 7 | var forwardedIpsStr = req.headers["x-forwarded-for"] as string | undefined 8 | if (forwardedIpsStr) { 9 | // 'x-forwarded-for' header may return multiple IP addresses in 10 | // the format: "client IP, proxy 1 IP, proxy 2 IP" so take the 11 | // the first one 12 | var forwardedIps = forwardedIpsStr.split(",") 13 | ipAddress = forwardedIps[0] 14 | } 15 | if (!ipAddress) { 16 | // If request was not forwarded 17 | ipAddress = req.connection.remoteAddress 18 | } 19 | // Remove prefix ::ffff: 20 | return ipAddress ? ipAddress.replace(/^::ffff:/, "") : "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/models/Pool.ts: -------------------------------------------------------------------------------- 1 | export class Pool { 2 | private _pools: ItemClass[] = [] 3 | private _itemClass: { new (): ItemClass } 4 | enabled: boolean 5 | 6 | constructor(itemClass: { new (): ItemClass }, enabled: boolean) { 7 | this._itemClass = itemClass 8 | this.enabled = enabled 9 | } 10 | 11 | get() { 12 | let item = this.enabled && this._pools.pop() 13 | if (!item) { 14 | item = new this._itemClass() 15 | } 16 | item.reuse?.() 17 | return item 18 | } 19 | 20 | put(item: ItemClass) { 21 | if (!this.enabled || this._pools.indexOf(item) > -1) { 22 | return 23 | } 24 | 25 | item.unuse?.() 26 | this._pools.push(item) 27 | } 28 | } 29 | 30 | export interface PoolItem { 31 | reuse: () => void 32 | unuse: () => void 33 | } 34 | -------------------------------------------------------------------------------- /src/models/getClassObjectId.ts: -------------------------------------------------------------------------------- 1 | export function getClassObjectId(): { new (id?: any): any } { 2 | let classObjId: any 3 | try { 4 | classObjId = require("mongodb").ObjectId 5 | } catch {} 6 | 7 | if (!classObjId) { 8 | try { 9 | classObjId = require("bson").ObjectId 10 | } catch {} 11 | } 12 | 13 | if (!classObjId) { 14 | classObjId = String 15 | } 16 | 17 | return classObjId 18 | } 19 | -------------------------------------------------------------------------------- /src/models/version.ts: -------------------------------------------------------------------------------- 1 | /** Version of TSRPC */ 2 | export const TSRPC_VERSION = "__TSRPC_VERSION__" 3 | -------------------------------------------------------------------------------- /src/server/base/ApiCall.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { TSBuffer } from "tsbuffer" 3 | import { ApiService, TransportDataUtil } from "tsrpc-base-client" 4 | import { 5 | ApiReturn, 6 | BaseServiceType, 7 | ServerOutputData, 8 | TsrpcError, 9 | TsrpcErrorData, 10 | TsrpcErrorType, 11 | } from "tsrpc-proto" 12 | import { PrefixLogger } from "../models/PrefixLogger" 13 | import { BaseCall, BaseCallOptions } from "./BaseCall" 14 | import { BaseConnection } from "./BaseConnection" 15 | 16 | export interface ApiCallOptions 17 | extends BaseCallOptions { 18 | /** Which service the Call is belong to */ 19 | service: ApiService 20 | /** Only exists in long connection, it is used to associate request and response. 21 | * It is created by the client, and the server would return the same value in `ApiReturn`. 22 | */ 23 | sn?: number 24 | /** Request Data */ 25 | req: Req 26 | } 27 | 28 | /** 29 | * A call request by `client.callApi()` 30 | * @typeParam Req - Type of request 31 | * @typeParam Res - Type of response 32 | * @typeParam ServiceType - The same `ServiceType` to server, it is used for code auto hint. 33 | */ 34 | export abstract class ApiCall< 35 | Req = any, 36 | Res = any, 37 | ServiceType extends BaseServiceType = any, 38 | > extends BaseCall { 39 | readonly type = "api" as const 40 | 41 | /** 42 | * Which `ApiService` the request is calling for 43 | */ 44 | readonly service!: ApiService 45 | /** Only exists in long connection, it is used to associate request and response. 46 | * It is created by the client, and the server would return the same value in `ApiReturn`. 47 | */ 48 | readonly sn?: number 49 | /** 50 | * Request data from the client, type of it is checked by the framework already. 51 | */ 52 | readonly req: Req 53 | 54 | constructor(options: ApiCallOptions, logger?: PrefixLogger) { 55 | super( 56 | options, 57 | logger ?? 58 | new PrefixLogger({ 59 | logger: options.conn.logger, 60 | prefixs: [ 61 | `${chalk.cyan.underline(`[Api:${options.service.name}]`)}${options.sn !== undefined ? chalk.gray(` SN=${options.sn}`) : ""}`, 62 | ], 63 | }) 64 | ) 65 | 66 | this.sn = options.sn 67 | this.req = options.req 68 | } 69 | 70 | protected _return?: ApiReturn 71 | /** 72 | * Response Data that sent already. 73 | * `undefined` means no return data is sent yet. (Never `call.succ()` and `call.error()`) 74 | */ 75 | public get return(): ApiReturn | undefined { 76 | return this._return 77 | } 78 | 79 | protected _usedTime: number | undefined 80 | /** Time from received req to send return data */ 81 | public get usedTime(): number | undefined { 82 | return this._usedTime 83 | } 84 | 85 | /** 86 | * Send a successful `ApiReturn` with response data 87 | * @param res - Response data 88 | * @returns Promise resolved means the buffer is sent to kernel 89 | */ 90 | succ(res: Res): Promise { 91 | return this._prepareReturn({ 92 | isSucc: true, 93 | res: res, 94 | }) 95 | } 96 | 97 | /** 98 | * Send a error `ApiReturn` with a `TsrpcError` 99 | * @returns Promise resolved means the buffer is sent to kernel 100 | */ 101 | error(message: string, info?: Partial): Promise 102 | error(err: TsrpcError): Promise 103 | error(errOrMsg: string | TsrpcError, data?: Partial): Promise { 104 | let error: TsrpcError = typeof errOrMsg === "string" ? new TsrpcError(errOrMsg, data) : errOrMsg 105 | return this._prepareReturn({ 106 | isSucc: false, 107 | err: error, 108 | }) 109 | } 110 | 111 | protected async _prepareReturn(ret: ApiReturn): Promise { 112 | if (this._return) { 113 | return 114 | } 115 | this._return = ret 116 | 117 | // Pre Flow 118 | let preFlow = await this.server.flows.preApiReturnFlow.exec( 119 | { call: this, return: ret }, 120 | this.logger 121 | ) 122 | // Stopped! 123 | if (!preFlow) { 124 | this.logger.debug("[preApiReturnFlow]", "Canceled") 125 | return 126 | } 127 | ret = preFlow.return 128 | 129 | // record & log ret 130 | this._usedTime = Date.now() - this.startTime 131 | if (ret.isSucc) { 132 | this.logger.log( 133 | chalk.green("[ApiRes]"), 134 | `${this.usedTime}ms`, 135 | this.server.options.logResBody ? ret.res : "" 136 | ) 137 | } else { 138 | if (ret.err.type === TsrpcErrorType.ApiError) { 139 | this.logger.log(chalk.red("[ApiErr]"), `${this.usedTime}ms`, ret.err, "req=", this.req) 140 | } else { 141 | this.logger.error(chalk.red("[ApiErr]"), `${this.usedTime}ms`, ret.err, "req=", this.req) 142 | } 143 | } 144 | 145 | // Do send! 146 | this._return = ret 147 | let opSend = await this._sendReturn(ret) 148 | if (!opSend.isSucc) { 149 | if (opSend.canceledByFlow) { 150 | this.logger.debug(`[${opSend.canceledByFlow}]`, "Canceled") 151 | } else { 152 | this.logger.error("[SendDataErr]", opSend.errMsg) 153 | if (ret.isSucc || ret.err.type === TsrpcErrorType.ApiError) { 154 | this._return = undefined 155 | this.server.onInternalServerError({ message: opSend.errMsg, name: "SendReturnErr" }, this) 156 | } 157 | } 158 | 159 | return 160 | } 161 | 162 | // Post Flow 163 | await this.server.flows.postApiReturnFlow.exec(preFlow, this.logger) 164 | } 165 | 166 | protected async _sendReturn(ret: ApiReturn): ReturnType> { 167 | // Encode 168 | let opServerOutput = ApiCall.encodeApiReturn( 169 | this.server.tsbuffer, 170 | this.service, 171 | ret, 172 | this.conn.dataType, 173 | this.sn 174 | ) 175 | if (!opServerOutput.isSucc) { 176 | this.server.onInternalServerError( 177 | { 178 | message: opServerOutput.errMsg, 179 | stack: " |- TransportDataUtil.encodeApiReturn\n |- ApiCall._sendReturn", 180 | }, 181 | this 182 | ) 183 | return opServerOutput 184 | } 185 | 186 | let opSend = await this.conn.sendData(opServerOutput.output) 187 | if (!opSend.isSucc) { 188 | return opSend 189 | } 190 | return opSend 191 | } 192 | 193 | static encodeApiReturn( 194 | tsbuffer: TSBuffer, 195 | service: ApiService, 196 | apiReturn: ApiReturn, 197 | type: "text", 198 | sn?: number 199 | ): EncodeApiReturnOutput 200 | static encodeApiReturn( 201 | tsbuffer: TSBuffer, 202 | service: ApiService, 203 | apiReturn: ApiReturn, 204 | type: "buffer", 205 | sn?: number 206 | ): EncodeApiReturnOutput 207 | static encodeApiReturn( 208 | tsbuffer: TSBuffer, 209 | service: ApiService, 210 | apiReturn: ApiReturn, 211 | type: "json", 212 | sn?: number 213 | ): EncodeApiReturnOutput 214 | static encodeApiReturn( 215 | tsbuffer: TSBuffer, 216 | service: ApiService, 217 | apiReturn: ApiReturn, 218 | type: "text" | "buffer" | "json", 219 | sn?: number 220 | ): 221 | | EncodeApiReturnOutput 222 | | EncodeApiReturnOutput 223 | | EncodeApiReturnOutput 224 | static encodeApiReturn( 225 | tsbuffer: TSBuffer, 226 | service: ApiService, 227 | apiReturn: ApiReturn, 228 | type: "text" | "buffer" | "json", 229 | sn?: number 230 | ): 231 | | EncodeApiReturnOutput 232 | | EncodeApiReturnOutput 233 | | EncodeApiReturnOutput { 234 | if (type === "buffer") { 235 | let serverOutputData: ServerOutputData = { 236 | sn: sn, 237 | serviceId: sn !== undefined ? service.id : undefined, 238 | } 239 | if (apiReturn.isSucc) { 240 | let op = tsbuffer.encode(apiReturn.res, service.resSchemaId) 241 | if (!op.isSucc) { 242 | return op 243 | } 244 | serverOutputData.buffer = op.buf 245 | } else { 246 | serverOutputData.error = apiReturn.err 247 | } 248 | 249 | let op = TransportDataUtil.tsbuffer.encode(serverOutputData, "ServerOutputData") 250 | return op.isSucc ? { isSucc: true, output: op.buf } : { isSucc: false, errMsg: op.errMsg } 251 | } else { 252 | apiReturn = { ...apiReturn } 253 | if (apiReturn.isSucc) { 254 | let op = tsbuffer.encodeJSON(apiReturn.res, service.resSchemaId) 255 | if (!op.isSucc) { 256 | return op 257 | } 258 | apiReturn.res = op.json 259 | } else { 260 | apiReturn.err = { 261 | ...apiReturn.err, 262 | } 263 | } 264 | let json = sn == undefined ? apiReturn : [service.name, apiReturn, sn] 265 | return { isSucc: true, output: type === "json" ? json : JSON.stringify(json) } 266 | } 267 | } 268 | } 269 | 270 | export type SendReturnMethod = (ret: ApiReturn) => ReturnType 271 | 272 | export declare type EncodeApiReturnOutput = 273 | | { 274 | isSucc: true 275 | /** Encoded binary buffer */ 276 | output: T 277 | errMsg?: undefined 278 | } 279 | | { 280 | isSucc: false 281 | /** Error message */ 282 | errMsg: string 283 | output?: undefined 284 | } 285 | -------------------------------------------------------------------------------- /src/server/base/BaseCall.ts: -------------------------------------------------------------------------------- 1 | import { ApiService, MsgService } from "tsrpc-base-client" 2 | import { BaseServiceType } from "tsrpc-proto" 3 | import { PrefixLogger } from "../models/PrefixLogger" 4 | import { BaseConnection } from "./BaseConnection" 5 | 6 | export interface BaseCallOptions { 7 | /** Connection */ 8 | conn: BaseConnection 9 | /** Which service the call is belong to */ 10 | service: ApiService | MsgService 11 | } 12 | 13 | export abstract class BaseCall { 14 | readonly conn: BaseConnection 15 | readonly service: ApiService | MsgService 16 | /** Time that server created the call */ 17 | readonly startTime: number 18 | readonly logger: PrefixLogger 19 | 20 | constructor(options: BaseCallOptions, logger: PrefixLogger) { 21 | this.conn = options.conn 22 | this.service = options.service 23 | this.startTime = Date.now() 24 | this.logger = logger 25 | } 26 | 27 | get server(): this["conn"]["server"] { 28 | return this.conn.server 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/server/base/BaseConnection.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { MsgHandlerManager, ParsedServerInput, TransportDataUtil } from "tsrpc-base-client" 3 | import { BaseServiceType } from "tsrpc-proto" 4 | import { PrefixLogger } from "../models/PrefixLogger" 5 | import { ApiCall } from "./ApiCall" 6 | import { BaseServer, MsgHandler } from "./BaseServer" 7 | import { MsgCall } from "./MsgCall" 8 | 9 | export interface BaseConnectionOptions { 10 | /** Created by server, each Call has a unique id. */ 11 | id: string 12 | /** Client IP address */ 13 | ip: string 14 | server: BaseServer 15 | dataType: "text" | "buffer" | "json" 16 | } 17 | 18 | export abstract class BaseConnection { 19 | /** It is long connection or short connection */ 20 | abstract readonly type: "LONG" | "SHORT" 21 | 22 | protected abstract readonly ApiCallClass: { new (options: any): ApiCall } 23 | protected abstract readonly MsgCallClass: { new (options: any): MsgCall } 24 | 25 | /** Connection unique ID */ 26 | readonly id: string 27 | /** Client IP address */ 28 | readonly ip: string 29 | readonly server: BaseServer 30 | readonly logger: PrefixLogger 31 | dataType: BaseConnectionOptions["dataType"] 32 | 33 | constructor(options: BaseConnectionOptions, logger: PrefixLogger) { 34 | this.id = options.id 35 | this.ip = options.ip 36 | this.server = options.server 37 | this.logger = logger 38 | this.dataType = options.dataType 39 | } 40 | 41 | abstract get status(): ConnectionStatus 42 | /** Close the connection */ 43 | abstract close(reason?: string): void 44 | 45 | /** Send buffer (with pre-flow and post-flow) */ 46 | async sendData( 47 | data: string | Uint8Array | object, 48 | call?: ApiCall 49 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string; canceledByFlow?: string }> { 50 | // Pre Flow 51 | let pre = await this.server.flows.preSendDataFlow.exec( 52 | { conn: this, data: data, call: call }, 53 | call?.logger || this.logger 54 | ) 55 | if (!pre) { 56 | return { 57 | isSucc: false, 58 | errMsg: "Canceled by preSendDataFlow", 59 | canceledByFlow: "preSendDataFlow", 60 | } 61 | } 62 | data = pre.data 63 | 64 | // @deprecated Pre Buffer Flow 65 | if (data instanceof Uint8Array) { 66 | let preBuf = await this.server.flows.preSendBufferFlow.exec( 67 | { conn: this, buf: data, call: call }, 68 | call?.logger || this.logger 69 | ) 70 | if (!preBuf) { 71 | return { 72 | isSucc: false, 73 | errMsg: "Canceled by preSendBufferFlow", 74 | canceledByFlow: "preSendBufferFlow", 75 | } 76 | } 77 | data = preBuf.buf 78 | } 79 | 80 | // debugBuf log 81 | if (this.server.options.debugBuf) { 82 | if (typeof data === "string") { 83 | ;(call?.logger ?? this.logger)?.debug(`[SendText] length=${data.length}`, data) 84 | } else if (data instanceof Uint8Array) { 85 | ;(call?.logger ?? this.logger)?.debug(`[SendBuf] length=${data.length}`, data) 86 | } else { 87 | ;(call?.logger ?? this.logger)?.debug("[SendJSON]", data) 88 | } 89 | } 90 | 91 | return this.doSendData(data, call) 92 | } 93 | protected abstract doSendData( 94 | data: string | Uint8Array | object, 95 | call?: ApiCall 96 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string }> 97 | 98 | makeCall(input: ParsedServerInput): ApiCall | MsgCall { 99 | if (input.type === "api") { 100 | return new this.ApiCallClass({ 101 | conn: this, 102 | service: input.service, 103 | req: input.req, 104 | sn: input.sn, 105 | }) 106 | } else { 107 | return new this.MsgCallClass({ 108 | conn: this, 109 | service: input.service, 110 | msg: input.msg, 111 | }) 112 | } 113 | } 114 | 115 | /** 116 | * Send message to the client, only be available when it is long connection. 117 | * @param msgName 118 | * @param msg - Message body 119 | * @returns Promise resolved when the buffer is sent to kernel, it not represents the server received it. 120 | */ 121 | async sendMsg( 122 | msgName: T, 123 | msg: ServiceType["msg"][T] 124 | ): ReturnType { 125 | if (this.type === "SHORT") { 126 | this.logger.warn("[SendMsgErr]", `[${msgName}]`, "Short connection cannot sendMsg") 127 | return { isSucc: false, errMsg: "Short connection cannot sendMsg" } 128 | } 129 | 130 | let service = this.server.serviceMap.msgName2Service[msgName as string] 131 | if (!service) { 132 | this.logger.warn("[SendMsgErr]", `[${msgName}]`, `Invalid msg name: ${msgName}`) 133 | return { isSucc: false, errMsg: `Invalid msg name: ${msgName}` } 134 | } 135 | 136 | // Pre Flow 137 | let pre = await this.server.flows.preSendMsgFlow.exec( 138 | { conn: this, service: service, msg: msg }, 139 | this.logger 140 | ) 141 | if (!pre) { 142 | this.logger.debug("[preSendMsgFlow]", "Canceled") 143 | return { 144 | isSucc: false, 145 | errMsg: "Canceled by preSendMsgFlow", 146 | canceledByFlow: "preSendMsgFlow", 147 | } 148 | } 149 | msg = pre.msg 150 | 151 | // Encode 152 | let opServerOutput = TransportDataUtil.encodeServerMsg( 153 | this.server.tsbuffer, 154 | service, 155 | msg, 156 | this.dataType, 157 | this.type 158 | ) 159 | if (!opServerOutput.isSucc) { 160 | this.logger.warn("[SendMsgErr]", `[${msgName}]`, opServerOutput.errMsg) 161 | return opServerOutput 162 | } 163 | 164 | // Do send! 165 | this.server.options.logMsg && 166 | this.logger.log(chalk.cyan.underline(`[Msg:${msgName}]`), chalk.green("[SendMsg]"), msg) 167 | let opSend = await this.sendData(opServerOutput.output) 168 | if (!opSend.isSucc) { 169 | return opSend 170 | } 171 | 172 | // Post Flow 173 | await this.server.flows.postSendMsgFlow.exec(pre, this.logger) 174 | 175 | return { isSucc: true } 176 | } 177 | 178 | // 多个Handler将异步并行执行 179 | private _msgHandlers?: MsgHandlerManager 180 | /** 181 | * Add a message handler, 182 | * duplicate handlers to the same `msgName` would be ignored. 183 | * @param msgName 184 | * @param handler 185 | */ 186 | listenMsg< 187 | Msg extends string & keyof ServiceType["msg"], 188 | Call extends MsgCall, 189 | >(msgName: Msg, handler: MsgHandler): MsgHandler { 190 | if (!this._msgHandlers) { 191 | this._msgHandlers = new MsgHandlerManager() 192 | } 193 | this._msgHandlers.addHandler(msgName as string, handler) 194 | return handler 195 | } 196 | /** 197 | * Remove a message handler 198 | */ 199 | unlistenMsg< 200 | Msg extends string & keyof ServiceType["msg"], 201 | Call extends MsgCall, 202 | >(msgName: Msg, handler: Function): void { 203 | if (!this._msgHandlers) { 204 | this._msgHandlers = new MsgHandlerManager() 205 | } 206 | this._msgHandlers.removeHandler(msgName as string, handler) 207 | } 208 | /** 209 | * Remove all handlers from a message 210 | */ 211 | unlistenMsgAll< 212 | Msg extends string & keyof ServiceType["msg"], 213 | Call extends MsgCall, 214 | >(msgName: Msg): void { 215 | if (!this._msgHandlers) { 216 | this._msgHandlers = new MsgHandlerManager() 217 | } 218 | this._msgHandlers.removeAllHandlers(msgName as string) 219 | } 220 | } 221 | 222 | export enum ConnectionStatus { 223 | Opened = "OPENED", 224 | Closing = "CLOSING", 225 | Closed = "CLOSED", 226 | } 227 | -------------------------------------------------------------------------------- /src/server/base/MsgCall.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { MsgService } from "tsrpc-base-client" 3 | import { BaseServiceType } from "tsrpc-proto" 4 | import { PrefixLogger } from "../models/PrefixLogger" 5 | import { BaseCall, BaseCallOptions } from "./BaseCall" 6 | 7 | export interface MsgCallOptions 8 | extends BaseCallOptions { 9 | service: MsgService 10 | msg: Msg 11 | } 12 | 13 | /** 14 | * A call request by `client.sendMsg()` 15 | * @typeParam Msg - Type of the message 16 | * @typeParam ServiceType - The same `ServiceType` to server, it is used for code auto hint. 17 | */ 18 | export abstract class MsgCall< 19 | Msg = any, 20 | ServiceType extends BaseServiceType = any, 21 | > extends BaseCall { 22 | readonly type = "msg" as const 23 | 24 | readonly service!: MsgService 25 | readonly msg: Msg 26 | 27 | constructor(options: MsgCallOptions, logger?: PrefixLogger) { 28 | super( 29 | options, 30 | logger ?? 31 | new PrefixLogger({ 32 | logger: options.conn.logger, 33 | prefixs: [chalk.cyan.underline(`[Msg:${options.service.name}]`)], 34 | }) 35 | ) 36 | 37 | this.msg = options.msg 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/server/http/ApiCallHttp.ts: -------------------------------------------------------------------------------- 1 | import { ApiReturn, BaseServiceType, TsrpcErrorType } from "tsrpc-proto" 2 | import { ApiCall, ApiCallOptions } from "../base/ApiCall" 3 | import { HttpConnection } from "./HttpConnection" 4 | 5 | export interface ApiCallHttpOptions 6 | extends ApiCallOptions { 7 | conn: HttpConnection 8 | } 9 | export class ApiCallHttp< 10 | Req = any, 11 | Res = any, 12 | ServiceType extends BaseServiceType = any, 13 | > extends ApiCall { 14 | readonly conn!: HttpConnection 15 | 16 | constructor(options: ApiCallHttpOptions) { 17 | super(options) 18 | } 19 | 20 | protected async _sendReturn( 21 | ret: ApiReturn 22 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string }> { 23 | if (this.conn.dataType === "text") { 24 | if (ret.isSucc) { 25 | this.conn.httpRes.statusCode = 200 26 | } else { 27 | this.conn.httpRes.statusCode = ret.err.type === TsrpcErrorType.ApiError ? 200 : 500 28 | } 29 | } 30 | return super._sendReturn(ret) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/server/http/HttpConnection.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import * as http from "http" 3 | import { ParsedServerInput } from "tsrpc-base-client" 4 | import { BaseServiceType } from "tsrpc-proto" 5 | import { ApiCall } from "../base/ApiCall" 6 | import { BaseConnection, BaseConnectionOptions, ConnectionStatus } from "../base/BaseConnection" 7 | import { PrefixLogger } from "../models/PrefixLogger" 8 | import { ApiCallHttp } from "./ApiCallHttp" 9 | import { HttpServer } from "./HttpServer" 10 | import { MsgCallHttp } from "./MsgCallHttp" 11 | 12 | export interface HttpConnectionOptions 13 | extends BaseConnectionOptions { 14 | server: HttpServer 15 | httpReq: http.IncomingMessage 16 | httpRes: http.ServerResponse 17 | } 18 | 19 | export class HttpConnection< 20 | ServiceType extends BaseServiceType = any, 21 | > extends BaseConnection { 22 | readonly type = "SHORT" 23 | 24 | protected readonly ApiCallClass = ApiCallHttp 25 | protected readonly MsgCallClass = MsgCallHttp 26 | 27 | readonly httpReq: http.IncomingMessage & { rawBody?: Buffer } 28 | readonly httpRes: http.ServerResponse 29 | readonly server!: HttpServer 30 | /** 31 | * Whether the transportation of the connection is JSON encoded instead of binary encoded. 32 | */ 33 | readonly isJSON: boolean | undefined 34 | 35 | /** 36 | * In short connection, one connection correspond one call. 37 | * It may be `undefined` when the request data is not fully received yet. 38 | */ 39 | call?: ApiCallHttp | MsgCallHttp 40 | 41 | constructor(options: HttpConnectionOptions) { 42 | super( 43 | options, 44 | new PrefixLogger({ 45 | logger: options.server.logger, 46 | prefixs: [chalk.gray(`${options.ip} #${options.id}`)], 47 | }) 48 | ) 49 | 50 | this.httpReq = options.httpReq 51 | this.httpRes = options.httpRes 52 | } 53 | 54 | public get status(): ConnectionStatus { 55 | if (this.httpRes.socket?.writableFinished) { 56 | return ConnectionStatus.Closed 57 | } else if (this.httpRes.socket?.writableEnded) { 58 | return ConnectionStatus.Closing 59 | } else { 60 | return ConnectionStatus.Opened 61 | } 62 | } 63 | 64 | protected async doSendData( 65 | data: string | Uint8Array, 66 | call?: ApiCall 67 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string }> { 68 | if (typeof data === "string") { 69 | this.httpRes.setHeader("Content-Type", "application/json; charset=utf-8") 70 | } 71 | this.httpRes.end( 72 | typeof data === "string" ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength) 73 | ) 74 | return { isSucc: true } 75 | } 76 | 77 | /** 78 | * Close the connection, the reason would be attached to response header `X-TSRPC-Close-Reason`. 79 | */ 80 | close(reason?: string) { 81 | if (this.status !== ConnectionStatus.Opened) { 82 | return 83 | } 84 | 85 | // 有Reason代表是异常关闭 86 | if (reason) { 87 | this.logger.warn(this.httpReq.method, this.httpReq.url, reason) 88 | } 89 | reason && this.httpRes.setHeader("X-TSRPC-Close-Reason", reason) 90 | this.httpRes.end() 91 | } 92 | 93 | // HTTP Server 一个conn只有一个call,对应关联之 94 | makeCall(input: ParsedServerInput): ApiCallHttp | MsgCallHttp { 95 | let call = super.makeCall(input) as ApiCallHttp | MsgCallHttp 96 | this.call = call 97 | return call 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/server/http/HttpServer.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http" 2 | import https from "https" 3 | import { BaseServiceType, ServiceProto } from "tsrpc-proto" 4 | import { HttpUtil } from "../../models/HttpUtil" 5 | import { TSRPC_VERSION } from "../../models/version" 6 | import { 7 | BaseServer, 8 | BaseServerOptions, 9 | defaultBaseServerOptions, 10 | ServerStatus, 11 | } from "../base/BaseServer" 12 | import { HttpConnection } from "./HttpConnection" 13 | 14 | /** 15 | * TSRPC Server, based on HTTP connection. 16 | * @typeParam ServiceType - `ServiceType` from generated `proto.ts` 17 | */ 18 | export class HttpServer extends BaseServer { 19 | readonly options!: HttpServerOptions 20 | 21 | constructor(proto: ServiceProto, options?: Partial>) { 22 | super(proto, { 23 | ...defaultHttpServerOptions, 24 | ...options, 25 | }) 26 | 27 | // 确保 jsonHostPath 以 / 开头和结尾 28 | this.options.jsonHostPath = this.options.jsonHostPath 29 | ? (this.options.jsonHostPath.startsWith("/") ? "" : "/") + 30 | this.options.jsonHostPath + 31 | (this.options.jsonHostPath.endsWith("/") ? "" : "/") 32 | : "/" 33 | } 34 | 35 | /** Native `http.Server` of NodeJS */ 36 | httpServer?: http.Server | https.Server 37 | /** 38 | * {@inheritDoc BaseServer.start} 39 | */ 40 | start(): Promise { 41 | if (this.httpServer) { 42 | throw new Error("Server already started") 43 | } 44 | 45 | return new Promise(rs => { 46 | this._status = ServerStatus.Opening 47 | this.logger.log(`Starting ${this.options.https ? "HTTPS" : "HTTP"} server ...`) 48 | this.httpServer = (this.options.https ? https : http).createServer( 49 | { 50 | ...this.options.https, 51 | }, 52 | (httpReq, httpRes) => { 53 | if (this.status !== ServerStatus.Opened) { 54 | httpRes.statusCode = 503 55 | httpRes.end() 56 | return 57 | } 58 | 59 | let ip = HttpUtil.getClientIp(httpReq) 60 | let isJSON = 61 | this.options.jsonEnabled && 62 | httpReq.headers["content-type"]?.toLowerCase().includes("application/json") && 63 | httpReq.method === "POST" && 64 | httpReq.url?.startsWith(this.options.jsonHostPath) 65 | let conn: HttpConnection = new HttpConnection({ 66 | server: this, 67 | id: "" + this._connIdCounter.getNext(), 68 | ip: ip, 69 | httpReq: httpReq, 70 | httpRes: httpRes, 71 | dataType: isJSON ? "text" : "buffer", 72 | }) 73 | this.flows.postConnectFlow.exec(conn, conn.logger) 74 | 75 | httpRes.statusCode = 200 76 | httpRes.setHeader("X-Powered-By", `TSRPC ${TSRPC_VERSION}`) 77 | if (this.options.cors) { 78 | httpRes.setHeader("Access-Control-Allow-Origin", this.options.cors) 79 | httpRes.setHeader("Access-Control-Allow-Headers", "Content-Type,*") 80 | if (this.options.corsMaxAge) { 81 | httpRes.setHeader("Access-Control-Max-Age", "" + this.options.corsMaxAge) 82 | } 83 | if (httpReq.method === "OPTIONS") { 84 | httpRes.writeHead(200) 85 | httpRes.end() 86 | return 87 | } 88 | } 89 | 90 | let chunks: Buffer[] = [] 91 | httpReq.on("data", data => { 92 | chunks.push(data) 93 | }) 94 | 95 | httpReq.on("end", async () => { 96 | let buf = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks) 97 | conn.httpReq.rawBody = buf 98 | 99 | if (conn.dataType === "text") { 100 | let url = conn.httpReq.url! 101 | 102 | let urlEndPos = url.indexOf("?") 103 | let isMsg: boolean = false 104 | if (urlEndPos > -1) { 105 | isMsg = url 106 | .slice(urlEndPos + 1) 107 | .split("&") 108 | .some(v => v === "type=msg") 109 | url = url.slice(0, urlEndPos) 110 | } 111 | 112 | // Parse serviceId 113 | let serviceName = url.slice(this.options.jsonHostPath.length) 114 | let serviceId: number | undefined 115 | if (isMsg) { 116 | serviceId = this.serviceMap.msgName2Service[serviceName]?.id 117 | } else { 118 | serviceId = this.serviceMap.apiName2Service[serviceName]?.id 119 | } 120 | 121 | const data = buf.toString() 122 | this._onRecvData(conn, data, serviceId) 123 | } else { 124 | this._onRecvData(conn, buf) 125 | } 126 | }) 127 | 128 | // 处理连接异常关闭的情况 129 | httpRes.on("close", async () => { 130 | // 客户端Abort 131 | if (httpReq.aborted) { 132 | ;(conn.call?.logger ?? conn.logger).log("[ReqAborted]") 133 | } 134 | // 非Abort,异常中断:直到连接关闭,Client也未end 135 | else if (!conn.httpReq.rawBody) { 136 | conn.logger.warn("Socket closed before request end", { 137 | url: httpReq.url, 138 | method: httpReq.method, 139 | ip: ip, 140 | chunksLength: chunks.length, 141 | chunksSize: chunks.sum(v => v.byteLength), 142 | reqComplete: httpReq.complete, 143 | headers: httpReq.rawHeaders, 144 | }) 145 | } 146 | // 有Conn,但连接非正常end:直到连接关闭,也未调用过 httpRes.end 方法 147 | else if (!httpRes.writableEnded) { 148 | ;(conn.call?.logger || conn.logger).warn("Socket closed without response") 149 | } 150 | 151 | // Post Flow 152 | await this.flows.postDisconnectFlow.exec({ conn: conn }, conn.logger) 153 | }) 154 | } 155 | ) 156 | 157 | if (this.options.socketTimeout) { 158 | this.httpServer.timeout = this.options.socketTimeout 159 | } 160 | if (this.options.keepAliveTimeout) { 161 | this.httpServer.keepAliveTimeout = this.options.keepAliveTimeout 162 | } 163 | 164 | this.httpServer.listen(this.options.port, () => { 165 | this._status = ServerStatus.Opened 166 | this.logger.log(`Server started at ${this.options.port}.`) 167 | rs() 168 | }) 169 | }) 170 | } 171 | 172 | /** 173 | * {@inheritDoc BaseServer.stop} 174 | */ 175 | async stop(): Promise { 176 | if (!this.httpServer) { 177 | return 178 | } 179 | this.logger.log("Stopping server...") 180 | 181 | return new Promise(rs => { 182 | this._status = ServerStatus.Closing 183 | 184 | // 立即close,不再接受新请求 185 | // 等所有连接都断开后rs 186 | this.httpServer?.close(err => { 187 | this._status = ServerStatus.Closed 188 | this.httpServer = undefined 189 | 190 | if (err) { 191 | this.logger.error(err) 192 | } 193 | this.logger.log("Server stopped") 194 | rs() 195 | }) 196 | }) 197 | } 198 | } 199 | 200 | export interface HttpServerOptions 201 | extends BaseServerOptions { 202 | /** Which port the HTTP server listen to */ 203 | port: number 204 | 205 | /** 206 | * HTTPS options, the server would use https instead of http if this value is defined. 207 | * NOTICE: Once you enabled https, you CANNOT visit the server via `http://` anymore. 208 | * If you need visit the server via both `http://` and `https://`, you can start 2 HttpServer (one with `https` and another without). 209 | * @defaultValue `undefined` 210 | */ 211 | https?: { 212 | /** 213 | * @example 214 | * fs.readFileSync('xxx-key.pem'); 215 | */ 216 | key: https.ServerOptions["key"] 217 | 218 | /** 219 | * @example 220 | * fs.readFileSync('xxx-cert.pem'); 221 | */ 222 | cert: https.ServerOptions["cert"] 223 | } 224 | 225 | /** 226 | * Passed to the `timeout` property to the native `http.Server` of NodeJS, in milliseconds. 227 | * `0` and `undefined` will disable the socket timeout behavior. 228 | * NOTICE: this `socketTimeout` be `undefined` only means disabling of the socket timeout, the `apiTimeout` is still working. 229 | * `socketTimeout` should always greater than `apiTimeout`. 230 | * @defaultValue `undefined` 231 | * @see {@link https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_server_timeout} 232 | */ 233 | socketTimeout?: number 234 | 235 | /** 236 | * Passed to the `keepAliveTimeout` property to the native `http.Server` of NodeJS, in milliseconds. 237 | * It means keep-alive timeout of HTTP socket connection. 238 | * @defaultValue 5000 (from NodeJS) 239 | * @see {@link https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_server_keepalivetimeout} 240 | */ 241 | keepAliveTimeout?: number 242 | 243 | /** 244 | * Response header value of `Access-Control-Allow-Origin`. 245 | * If this has any value, it would also set `Access-Control-Allow-Headers` as `*`. 246 | * `undefined` means no CORS header. 247 | * @defaultValue `*` 248 | */ 249 | cors?: string 250 | 251 | /** 252 | * Response header value of `Access-Control-Allow-Origin`. 253 | * @defaultValue `3600` 254 | */ 255 | corsMaxAge?: number 256 | 257 | /** 258 | * Actual URL path is `${jsonHostPath}/${apiName}`. 259 | * For example, if `jsonHostPath` is `'/api'`, then you can send `POST /api/a/b/c/Test` to call API `a/b/c/Test`. 260 | * @defaultValue `'/'` 261 | */ 262 | jsonHostPath: string 263 | } 264 | 265 | export const defaultHttpServerOptions: HttpServerOptions = { 266 | ...defaultBaseServerOptions, 267 | port: 3000, 268 | cors: "*", 269 | corsMaxAge: 3600, 270 | jsonHostPath: "/", 271 | 272 | // TODO: keep-alive time (to SLB) 273 | } 274 | -------------------------------------------------------------------------------- /src/server/http/MsgCallHttp.ts: -------------------------------------------------------------------------------- 1 | import { BaseServiceType } from "tsrpc-proto" 2 | import { MsgCall, MsgCallOptions } from "../base/MsgCall" 3 | import { HttpConnection } from "./HttpConnection" 4 | 5 | export interface MsgCallHttpOptions 6 | extends MsgCallOptions { 7 | conn: HttpConnection 8 | } 9 | export class MsgCallHttp extends MsgCall< 10 | Msg, 11 | ServiceType 12 | > { 13 | readonly conn!: HttpConnection 14 | 15 | constructor(options: MsgCallHttpOptions) { 16 | super(options) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/server/inner/ApiCallInner.ts: -------------------------------------------------------------------------------- 1 | import { ApiReturn, BaseServiceType } from "tsrpc-proto" 2 | import { ApiCall, ApiCallOptions } from "../base/ApiCall" 3 | import { InnerConnection } from "./InnerConnection" 4 | 5 | export interface ApiCallInnerOptions 6 | extends ApiCallOptions { 7 | conn: InnerConnection 8 | } 9 | export class ApiCallInner< 10 | Req = any, 11 | Res = any, 12 | ServiceType extends BaseServiceType = any, 13 | > extends ApiCall { 14 | readonly conn!: InnerConnection 15 | 16 | constructor(options: ApiCallInnerOptions) { 17 | super(options) 18 | } 19 | 20 | protected async _sendReturn( 21 | ret: ApiReturn 22 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string }> { 23 | if (this.conn.return.type === "raw") { 24 | // Validate Res 25 | if (ret.isSucc) { 26 | let resValidate = this.server.tsbuffer.validate(ret.res, this.service.resSchemaId) 27 | if (!resValidate.isSucc) { 28 | return resValidate 29 | } 30 | } 31 | return this.conn.sendData(ret) 32 | } 33 | 34 | return super._sendReturn(ret) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/server/inner/InnerConnection.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { ApiReturn, TsrpcError, TsrpcErrorType } from "tsrpc-proto" 3 | import { ApiCall, BaseConnection, BaseServiceType, PrefixLogger, TransportDataUtil } from "../.." 4 | import { BaseConnectionOptions, ConnectionStatus } from "../base/BaseConnection" 5 | import { ApiCallInner } from "./ApiCallInner" 6 | 7 | export interface InnerConnectionOptions 8 | extends BaseConnectionOptions { 9 | return: 10 | | { 11 | type: "raw" | "json" 12 | rs: (ret: ApiReturn) => void 13 | } 14 | | { 15 | type: "buffer" 16 | rs: (ret: Uint8Array) => void 17 | } 18 | } 19 | 20 | /** 21 | * Server can `callApi` it self by using this inner connection 22 | */ 23 | export class InnerConnection< 24 | ServiceType extends BaseServiceType = any, 25 | > extends BaseConnection { 26 | readonly type = "SHORT" 27 | 28 | protected readonly ApiCallClass = ApiCallInner 29 | protected readonly MsgCallClass = null as any 30 | 31 | return!: InnerConnectionOptions["return"] 32 | 33 | constructor(options: InnerConnectionOptions, logger?: PrefixLogger) { 34 | super( 35 | options, 36 | logger ?? 37 | new PrefixLogger({ 38 | logger: options.server.logger, 39 | prefixs: [chalk.gray(`Inner #${options.id}`)], 40 | }) 41 | ) 42 | 43 | this.return = options.return 44 | } 45 | 46 | private _status: ConnectionStatus = ConnectionStatus.Opened 47 | get status(): ConnectionStatus { 48 | return this._status 49 | } 50 | 51 | close(reason?: string): void { 52 | this.doSendData({ 53 | isSucc: false, 54 | err: new TsrpcError(reason ?? "Internal Server Error", { 55 | type: TsrpcErrorType.ServerError, 56 | code: "CONN_CLOSED", 57 | reason: reason, 58 | }), 59 | }) 60 | } 61 | 62 | protected async doSendData( 63 | data: Uint8Array | ApiReturn, 64 | call?: ApiCall 65 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string }> { 66 | this._status = ConnectionStatus.Closed 67 | 68 | if (this.return.type === "buffer") { 69 | if (!(data instanceof Uint8Array)) { 70 | // encode tsrpc error 71 | if (!data.isSucc) { 72 | let op = TransportDataUtil.tsbuffer.encode( 73 | { 74 | error: data.err, 75 | }, 76 | "ServerOutputData" 77 | ) 78 | if (op.isSucc) { 79 | return this.doSendData(op.buf, call) 80 | } 81 | } 82 | return { isSucc: false, errMsg: "Error data type" } 83 | } 84 | this.return.rs(data) 85 | return { isSucc: true } 86 | } else { 87 | if (data instanceof Uint8Array) { 88 | return { isSucc: false, errMsg: "Error data type" } 89 | } 90 | this.return.rs(data) 91 | return { isSucc: true } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/server/models/PrefixLogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "tsrpc-proto" 2 | 3 | export interface PrefixLoggerOptions { 4 | logger: Logger 5 | prefixs: (string | (() => string))[] 6 | } 7 | 8 | /** 9 | * Auto add prefix using existed `Logger` 10 | */ 11 | export class PrefixLogger implements Logger { 12 | readonly logger: PrefixLoggerOptions["logger"] 13 | readonly prefixs: PrefixLoggerOptions["prefixs"] 14 | 15 | constructor(options: PrefixLoggerOptions) { 16 | this.logger = options.logger 17 | this.prefixs = options.prefixs 18 | } 19 | 20 | getPrefix(): string[] { 21 | return this.prefixs.map(v => (typeof v === "string" ? v : v())) 22 | } 23 | 24 | log(...args: any[]) { 25 | this.logger.log(...this.getPrefix().concat(args)) 26 | } 27 | 28 | debug(...args: any[]) { 29 | this.logger.debug(...this.getPrefix().concat(args)) 30 | } 31 | 32 | warn(...args: any[]) { 33 | this.logger.warn(...this.getPrefix().concat(args)) 34 | } 35 | 36 | error(...args: any[]) { 37 | this.logger.error(...this.getPrefix().concat(args)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/server/models/TerminalColorLogger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { Logger } from "tsrpc-proto" 3 | 4 | export interface TerminalColorLoggerOptions { 5 | /** 6 | * Process ID prefix 7 | * @defaultValue `process.pid` 8 | */ 9 | pid: string 10 | 11 | /** 12 | * `undefined` represents not print time 13 | * @defaultValue 'yyyy-MM-dd hh:mm:ss' 14 | */ 15 | timeFormat?: string 16 | } 17 | 18 | /** 19 | * Print log to terminal, with color. 20 | */ 21 | export class TerminalColorLogger implements Logger { 22 | options: TerminalColorLoggerOptions = { 23 | pid: process.pid.toString(), 24 | timeFormat: "yyyy-MM-dd hh:mm:ss", 25 | } 26 | 27 | private _pid: string 28 | constructor(options?: Partial) { 29 | Object.assign(this.options, options) 30 | this._pid = this.options.pid ? `<${this.options.pid}> ` : "" 31 | } 32 | 33 | private _time(): string { 34 | return this.options.timeFormat ? new Date().format(this.options.timeFormat) : "" 35 | } 36 | 37 | debug(...args: any[]) { 38 | console.debug.call( 39 | console, 40 | chalk.gray(`${this._pid}${this._time()}`), 41 | chalk.cyan("[DEBUG]"), 42 | ...args 43 | ) 44 | } 45 | 46 | log(...args: any[]) { 47 | console.log.call( 48 | console, 49 | chalk.gray(`${this._pid}${this._time()}`), 50 | chalk.green("[INFO]"), 51 | ...args 52 | ) 53 | } 54 | 55 | warn(...args: any[]) { 56 | console.warn.call( 57 | console, 58 | chalk.gray(`${this._pid}${this._time()}`), 59 | chalk.yellow("[WARN]"), 60 | ...args 61 | ) 62 | } 63 | 64 | error(...args: any[]) { 65 | console.error.call( 66 | console, 67 | chalk.gray(`${this._pid}${this._time()}`), 68 | chalk.red("[ERROR]"), 69 | ...args 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/server/ws/ApiCallWs.ts: -------------------------------------------------------------------------------- 1 | import { ApiReturn, BaseServiceType } from "tsrpc-proto" 2 | import { ApiCall, ApiCallOptions } from "../base/ApiCall" 3 | import { ConnectionStatus } from "../base/BaseConnection" 4 | import { WsConnection } from "./WsConnection" 5 | 6 | export interface ApiCallWsOptions 7 | extends ApiCallOptions { 8 | conn: WsConnection 9 | } 10 | 11 | export class ApiCallWs< 12 | Req = any, 13 | Res = any, 14 | ServiceType extends BaseServiceType = any, 15 | > extends ApiCall { 16 | readonly conn!: WsConnection 17 | 18 | constructor(options: ApiCallWsOptions) { 19 | super(options) 20 | } 21 | 22 | protected async _prepareReturn(ret: ApiReturn): Promise { 23 | if (this.conn.status !== ConnectionStatus.Opened) { 24 | this.logger.debug("[SendReturnErr]", "WebSocket is not opened", ret) 25 | this._return = ret 26 | return 27 | } 28 | 29 | return super._prepareReturn(ret) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/ws/MsgCallWs.ts: -------------------------------------------------------------------------------- 1 | import { BaseServiceType } from "tsrpc-proto" 2 | import { MsgCall, MsgCallOptions } from "../base/MsgCall" 3 | import { WsConnection } from "./WsConnection" 4 | 5 | export interface MsgCallWsOptions 6 | extends MsgCallOptions { 7 | conn: WsConnection 8 | } 9 | export class MsgCallWs extends MsgCall< 10 | Msg, 11 | ServiceType 12 | > { 13 | readonly conn!: WsConnection 14 | 15 | constructor(options: MsgCallWsOptions) { 16 | super(options) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/server/ws/WsConnection.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import * as http from "http" 3 | import { TransportDataUtil } from "tsrpc-base-client" 4 | import { BaseServiceType } from "tsrpc-proto" 5 | import * as WebSocket from "ws" 6 | import { BaseConnection, BaseConnectionOptions, ConnectionStatus } from "../base/BaseConnection" 7 | import { PrefixLogger } from "../models/PrefixLogger" 8 | import { ApiCallWs } from "./ApiCallWs" 9 | import { MsgCallWs } from "./MsgCallWs" 10 | import { WsServer } from "./WsServer" 11 | 12 | export interface WsConnectionOptions 13 | extends BaseConnectionOptions { 14 | server: WsServer 15 | ws: WebSocket 16 | httpReq: http.IncomingMessage 17 | onClose: (conn: WsConnection, code: number, reason: string) => Promise 18 | dataType: "text" | "buffer" 19 | isDataTypeConfirmed?: boolean 20 | } 21 | 22 | /** 23 | * Connected client 24 | */ 25 | export class WsConnection< 26 | ServiceType extends BaseServiceType = any, 27 | > extends BaseConnection { 28 | readonly type = "LONG" 29 | 30 | protected readonly ApiCallClass = ApiCallWs 31 | protected readonly MsgCallClass = MsgCallWs 32 | 33 | readonly ws: WebSocket 34 | readonly httpReq: http.IncomingMessage 35 | readonly server!: WsServer 36 | dataType!: "text" | "buffer" 37 | // 是否已经收到了客户端的第一条消息,以确认了客户端的 dataType 38 | isDataTypeConfirmed?: boolean 39 | 40 | constructor(options: WsConnectionOptions) { 41 | super( 42 | options, 43 | new PrefixLogger({ 44 | logger: options.server.logger, 45 | prefixs: [chalk.gray(`${options.ip} Conn#${options.id}`)], 46 | }) 47 | ) 48 | this.ws = options.ws 49 | this.httpReq = options.httpReq 50 | this.isDataTypeConfirmed = options.isDataTypeConfirmed 51 | 52 | if (this.server.options.heartbeatWaitTime) { 53 | const timeout = this.server.options.heartbeatWaitTime 54 | this._heartbeatInterval = setInterval(() => { 55 | if (Date.now() - this._lastHeartbeatTime > timeout) { 56 | this.logger.debug("Receive heartbeat timeout") 57 | this.close("Receive heartbeat timeout", 3001, 1000) 58 | } 59 | }, timeout) 60 | } 61 | 62 | // Init WS 63 | this.ws.onclose = async e => { 64 | if (this._heartbeatInterval) { 65 | clearInterval(this._heartbeatInterval) 66 | this._heartbeatInterval = undefined 67 | } 68 | await options.onClose(this, e.code, e.reason) 69 | this._rsClose?.() 70 | } 71 | this.ws.onerror = e => { 72 | this.logger.warn("[ClientErr]", e.error) 73 | } 74 | this.ws.onmessage = e => { 75 | let data: Buffer | string 76 | if (e.data instanceof ArrayBuffer) { 77 | data = Buffer.from(e.data) 78 | } else if (Array.isArray(e.data)) { 79 | data = Buffer.concat(e.data) 80 | } else if (Buffer.isBuffer(e.data)) { 81 | data = e.data 82 | } else { 83 | data = e.data 84 | } 85 | 86 | // 心跳包,直接回复 87 | if (data instanceof Buffer && data.equals(TransportDataUtil.HeartbeatPacket)) { 88 | this.server.options.debugBuf && 89 | this.logger.log("[Heartbeat] Recv ping and send pong", TransportDataUtil.HeartbeatPacket) 90 | this._lastHeartbeatTime = Date.now() 91 | this.ws.send(TransportDataUtil.HeartbeatPacket) 92 | return 93 | } 94 | 95 | // dataType 尚未确认,自动检测 96 | if (!this.isDataTypeConfirmed) { 97 | if (this.server.options.jsonEnabled && typeof data === "string") { 98 | this.dataType = "text" 99 | } else { 100 | this.dataType = "buffer" 101 | } 102 | 103 | this.isDataTypeConfirmed = true 104 | } 105 | 106 | // dataType 已确认 107 | this.server._onRecvData(this, data) 108 | } 109 | } 110 | 111 | private _lastHeartbeatTime = 0 112 | private _heartbeatInterval?: ReturnType 113 | 114 | get status(): ConnectionStatus { 115 | if (this.ws.readyState === WebSocket.CLOSED) { 116 | return ConnectionStatus.Closed 117 | } 118 | if (this.ws.readyState === WebSocket.CLOSING) { 119 | return ConnectionStatus.Closing 120 | } 121 | return ConnectionStatus.Opened 122 | } 123 | 124 | protected async doSendData( 125 | data: string | Uint8Array, 126 | call?: ApiCallWs 127 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string }> { 128 | let opSend = await new Promise<{ isSucc: true } | { isSucc: false; errMsg: string }>(rs => { 129 | this.ws.send(data, e => { 130 | e ? rs({ isSucc: false, errMsg: e.message || "Send buffer error" }) : rs({ isSucc: true }) 131 | }) 132 | }) 133 | if (!opSend.isSucc) { 134 | return opSend 135 | } 136 | 137 | return { isSucc: true } 138 | } 139 | 140 | protected _rsClose?: () => void 141 | /** Close WebSocket connection */ 142 | close(reason?: string, code = 1000, closeTimeout = 3000): Promise { 143 | // 停止心跳 144 | if (this._heartbeatInterval) { 145 | clearInterval(this._heartbeatInterval) 146 | this._heartbeatInterval = undefined 147 | } 148 | 149 | // 已连接 Close之 150 | return new Promise(rs => { 151 | this._rsClose = rs 152 | const ws = this.ws 153 | ws.close(code, reason) 154 | // 超时保护 155 | setTimeout(() => { 156 | if (ws.readyState !== WebSocket.CLOSED) { 157 | ws.terminate() 158 | } 159 | }, closeTimeout) 160 | }).finally(() => { 161 | this._rsClose = undefined 162 | }) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/server/ws/WsServer.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import * as http from "http" 3 | import https from "https" 4 | import { EncodeOutput, TransportDataUtil } from "tsrpc-base-client" 5 | import { BaseServiceType, ServiceProto } from "tsrpc-proto" 6 | import * as WebSocket from "ws" 7 | import { Server as WebSocketServer } from "ws" 8 | import { HttpUtil } from "../../models/HttpUtil" 9 | import { 10 | BaseServer, 11 | BaseServerOptions, 12 | defaultBaseServerOptions, 13 | ServerStatus, 14 | } from "../base/BaseServer" 15 | import { WsConnection } from "./WsConnection" 16 | 17 | /** 18 | * TSRPC Server, based on WebSocket connection. 19 | * It can support realtime cases. 20 | * @typeParam ServiceType - `ServiceType` from generated `proto.ts` 21 | */ 22 | export class WsServer extends BaseServer { 23 | readonly options!: WsServerOptions 24 | 25 | readonly connections: WsConnection[] = [] 26 | private readonly _id2Conn: { [connId: string]: WsConnection | undefined } = {} 27 | 28 | constructor(proto: ServiceProto, options?: Partial>) { 29 | super(proto, { 30 | ...defaultWsServerOptions, 31 | ...options, 32 | }) 33 | } 34 | 35 | private _wsServer?: WebSocketServer 36 | private _httpServer?: http.Server | https.Server 37 | 38 | /** 39 | * {@inheritDoc BaseServer.start} 40 | */ 41 | start(): Promise { 42 | if (this._wsServer) { 43 | throw new Error("Server already started") 44 | } 45 | this._status = ServerStatus.Opening 46 | return new Promise((rs, rj) => { 47 | this.logger.log(`Starting ${this.options.wss ? "WSS" : "WS"} server...`) 48 | 49 | // Create HTTP/S Server 50 | this._httpServer = (this.options.wss ? https : http).createServer({ 51 | ...this.options.wss, 52 | }) 53 | 54 | // Create WebSocket Server 55 | this._wsServer = new WebSocketServer({ 56 | server: this._httpServer, 57 | ...this.options.wsOptions, 58 | }) 59 | this._wsServer.on("connection", this._onClientConnect) 60 | 61 | // Start Server 62 | this._httpServer.listen(this.options.port, () => { 63 | this._status = ServerStatus.Opened 64 | this.logger.log(`Server started at ${this.options.port}.`) 65 | rs() 66 | }) 67 | }) 68 | } 69 | 70 | /** 71 | * {@inheritDoc BaseServer.stop} 72 | */ 73 | async stop(): Promise { 74 | // Closed Already 75 | if (!this._wsServer) { 76 | throw new Error("Server has not been started") 77 | } 78 | if (this._status === ServerStatus.Closed) { 79 | throw new Error("Server is closed already") 80 | } 81 | 82 | this._status = ServerStatus.Closing 83 | 84 | return new Promise(async (rs, rj) => { 85 | await Promise.all(this.connections.map(v => v.close("Server stopped"))) 86 | // Close HTTP Server 87 | await new Promise((rs1, rj1) => { 88 | this._httpServer?.close(err => { 89 | err ? rj1(err) : rs1() 90 | }) 91 | }) 92 | // Close WS Server 93 | this._wsServer!.close(err => { 94 | err ? rj(err) : rs() 95 | }) 96 | }).then(() => { 97 | this.logger.log("Server stopped") 98 | this._status = ServerStatus.Closed 99 | this._wsServer = undefined 100 | }) 101 | } 102 | 103 | private _onClientConnect = (ws: WebSocket, httpReq: http.IncomingMessage) => { 104 | // 停止中 不再接受新的连接 105 | if (this._status !== ServerStatus.Opened) { 106 | ws.close(1012) 107 | return 108 | } 109 | 110 | // 推测 dataType 和 isDataTypeConfirmed 111 | let isDataTypeConfirmed = true 112 | let dataType: "text" | "buffer" 113 | let protocols = httpReq.headers["sec-websocket-protocol"] 114 | ?.split(",") 115 | .map(v => v.trim()) 116 | .filter(v => !!v) 117 | if (protocols?.includes("text")) { 118 | dataType = "text" 119 | } else if (protocols?.includes("buffer")) { 120 | dataType = "buffer" 121 | } else { 122 | dataType = this.options.jsonEnabled ? "text" : "buffer" 123 | isDataTypeConfirmed = false 124 | } 125 | 126 | // Create Active Connection 127 | let conn = new WsConnection({ 128 | id: "" + this._connIdCounter.getNext(), 129 | ip: HttpUtil.getClientIp(httpReq), 130 | server: this, 131 | ws: ws, 132 | httpReq: httpReq, 133 | onClose: this._onClientClose, 134 | dataType: dataType, 135 | isDataTypeConfirmed: isDataTypeConfirmed, 136 | }) 137 | this.connections.push(conn) 138 | this._id2Conn[conn.id] = conn 139 | 140 | this.options.logConnect && 141 | conn.logger.log(chalk.green("[Connected]"), `ActiveConn=${this.connections.length}`) 142 | this.flows.postConnectFlow.exec(conn, conn.logger) 143 | } 144 | 145 | private _onClientClose = async ( 146 | conn: WsConnection, 147 | code: number, 148 | reason: string 149 | ) => { 150 | this.connections.removeOne(v => v.id === conn.id) 151 | delete this._id2Conn[conn.id] 152 | this.options.logConnect && 153 | conn.logger.log( 154 | chalk.green("[Disconnected]"), 155 | `Code=${code} ${reason ? `Reason=${reason} ` : ""}ActiveConn=${this.connections.length}` 156 | ) 157 | 158 | await this.flows.postDisconnectFlow.exec({ conn: conn, reason: reason }, conn.logger) 159 | } 160 | 161 | /** 162 | * Send the same message to many connections. 163 | * No matter how many target connections are, the message would be only encoded once. 164 | * @param msgName 165 | * @param msg - Message body 166 | * @param connIds - `id` of target connections, `undefined` means broadcast to every connections. 167 | * @returns Send result, `isSucc: true` means the message buffer is sent to kernel, not represents the clients received. 168 | */ 169 | async broadcastMsg( 170 | msgName: T, 171 | msg: ServiceType["msg"][T], 172 | conns?: WsConnection[] 173 | ): Promise<{ isSucc: true } | { isSucc: false; errMsg: string }> { 174 | let connAll = false 175 | if (!conns) { 176 | conns = this.connections 177 | connAll = true 178 | } 179 | 180 | const connStr = () => (connAll ? "*" : conns!.map(v => v.id).join(",")) 181 | 182 | if (!conns.length) { 183 | return { isSucc: true } 184 | } 185 | 186 | if (this.status !== ServerStatus.Opened) { 187 | this.logger.warn("[BroadcastMsgErr]", `[${msgName}]`, `[To:${connStr()}]`, "Server not open") 188 | return { isSucc: false, errMsg: "Server not open" } 189 | } 190 | 191 | // GetService 192 | let service = this.serviceMap.msgName2Service[msgName as string] 193 | if (!service) { 194 | this.logger.warn( 195 | "[BroadcastMsgErr]", 196 | `[${msgName}]`, 197 | `[To:${connStr()}]`, 198 | "Invalid msg name: " + msgName 199 | ) 200 | return { isSucc: false, errMsg: "Invalid msg name: " + msgName } 201 | } 202 | 203 | // Encode group by dataType 204 | let _opEncodeBuf: EncodeOutput | undefined 205 | let _opEncodeText: EncodeOutput | undefined 206 | const getOpEncodeBuf = () => { 207 | if (!_opEncodeBuf) { 208 | _opEncodeBuf = TransportDataUtil.encodeServerMsg( 209 | this.tsbuffer, 210 | service!, 211 | msg, 212 | "buffer", 213 | "LONG" 214 | ) 215 | } 216 | return _opEncodeBuf 217 | } 218 | const getOpEncodeText = () => { 219 | if (!_opEncodeText) { 220 | _opEncodeText = TransportDataUtil.encodeServerMsg( 221 | this.tsbuffer, 222 | service!, 223 | msg, 224 | "text", 225 | "LONG" 226 | ) 227 | } 228 | return _opEncodeText 229 | } 230 | 231 | // 测试一下编码可以通过 232 | let op = conns.some(v => v.dataType === "buffer") ? getOpEncodeBuf() : getOpEncodeText() 233 | if (!op.isSucc) { 234 | this.logger.warn("[BroadcastMsgErr]", `[${msgName}]`, `[To:${connStr()}]`, op.errMsg) 235 | return op 236 | } 237 | 238 | this.options.logMsg && 239 | this.logger.log(`[BroadcastMsg]`, `[${msgName}]`, `[To:${connStr()}]`, msg) 240 | 241 | // Batch send 242 | let errMsgs: string[] = [] 243 | return Promise.all( 244 | conns.map(async conn => { 245 | // Pre Flow 246 | let pre = await this.flows.preSendMsgFlow.exec( 247 | { conn: conn, service: service!, msg: msg }, 248 | this.logger 249 | ) 250 | if (!pre) { 251 | conn.logger.debug("[preSendMsgFlow]", "Canceled") 252 | return { isSucc: false, errMsg: "Prevented by preSendMsgFlow" } 253 | } 254 | msg = pre.msg 255 | 256 | // Do send! 257 | let opSend = await conn.sendData( 258 | (conn.dataType === "buffer" ? getOpEncodeBuf() : getOpEncodeText())!.output! 259 | ) 260 | if (!opSend.isSucc) { 261 | return opSend 262 | } 263 | 264 | // Post Flow 265 | this.flows.postSendMsgFlow.exec(pre, this.logger) 266 | 267 | return { isSucc: true } 268 | }) 269 | ).then(results => { 270 | for (let i = 0; i < results.length; ++i) { 271 | let op = results[i] 272 | if (!op.isSucc) { 273 | errMsgs.push(`Conn#conns[i].id: ${op.errMsg}`) 274 | } 275 | } 276 | if (errMsgs.length) { 277 | return { isSucc: false, errMsg: errMsgs.join("\n") } 278 | } else { 279 | return { isSucc: true } 280 | } 281 | }) 282 | } 283 | } 284 | 285 | export interface WsServerOptions 286 | extends BaseServerOptions { 287 | /** Which port the WebSocket server is listen to */ 288 | port: number 289 | 290 | /** Whether to print `[Connected]` and `[Disconnected]` into log */ 291 | logConnect: boolean 292 | 293 | /** 294 | * HTTPS options, the server would use wss instead of http if this value is defined. 295 | * NOTICE: Once you enabled wss, you CANNOT visit the server via `ws://` anymore. 296 | * If you need visit the server via both `ws://` and `wss://`, you can start 2 HttpServer (one with `wss` and another without). 297 | * @defaultValue `undefined` 298 | */ 299 | wss?: { 300 | /** 301 | * @example 302 | * fs.readFileSync('xxx-key.pem'); 303 | */ 304 | key: https.ServerOptions["key"] 305 | 306 | /** 307 | * @example 308 | * fs.readFileSync('xxx-cert.pem'); 309 | */ 310 | cert: https.ServerOptions["cert"] 311 | } 312 | 313 | /** 314 | * Close a connection if not receive heartbeat after the time (ms). 315 | * This value should be greater than `client.heartbeat.interval`, for exmaple 2x of it. 316 | * `undefined` or `0` represent disable this feature. 317 | * @defaultValue `undefined` 318 | */ 319 | heartbeatWaitTime?: number 320 | 321 | /** 322 | * WebSocket server options, transfer to 'ws' package. 323 | */ 324 | wsOptions?: WebSocket.ServerOptions 325 | } 326 | 327 | const defaultWsServerOptions: WsServerOptions = { 328 | ...defaultBaseServerOptions, 329 | port: 3000, 330 | logConnect: false, 331 | } 332 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | console.log(Date.now()) 2 | for (let i = 0; i < 10000000000; ++i) { 3 | let a = {}; 4 | } 5 | console.log(Date.now()) -------------------------------------------------------------------------------- /test/Base.ts: -------------------------------------------------------------------------------- 1 | import "k8w-extend-native" 2 | -------------------------------------------------------------------------------- /test/api/ApiObjId.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from "../../src/server/base/ApiCall" 2 | import { ReqObjId, ResObjId } from "../proto/PtlObjId" 3 | 4 | export async function ApiObjId(call: ApiCall) { 5 | call.succ({ 6 | id2: call.req.id1, 7 | buf: call.req.buf, 8 | date: call.req.date, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /test/api/ApiTest.ts: -------------------------------------------------------------------------------- 1 | import { TsrpcError } from "tsrpc-proto" 2 | import { ApiCall } from "../../src/server/base/ApiCall" 3 | import { ReqTest, ResTest } from "../proto/PtlTest" 4 | 5 | export async function ApiTest(call: ApiCall) { 6 | if (call.req.name === "InnerError") { 7 | await new Promise(rs => { 8 | setTimeout(rs, 50) 9 | }) 10 | throw new Error("Test InnerError") 11 | } else if (call.req.name === "TsrpcError") { 12 | await new Promise(rs => { 13 | setTimeout(rs, 50) 14 | }) 15 | throw new TsrpcError("Test TsrpcError", { 16 | code: "CODE_TEST", 17 | info: "ErrInfo Test", 18 | }) 19 | } else if (call.req.name === "error") { 20 | await new Promise(rs => { 21 | setTimeout(rs, 50) 22 | }) 23 | call.error("Got an error") 24 | } else { 25 | await new Promise(rs => { 26 | setTimeout(rs, 50) 27 | }) 28 | call.succ({ 29 | reply: "Test reply: " + call.req.name, 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/api/a/b/c/ApiTest.ts: -------------------------------------------------------------------------------- 1 | import { TsrpcError } from "tsrpc-proto" 2 | 3 | export async function ApiTest(call: any) { 4 | if (call.req.name === "InnerError") { 5 | throw new Error("a/b/c/Test InnerError") 6 | } else if (call.req.name === "TsrpcError") { 7 | throw new TsrpcError("a/b/c/Test TsrpcError", { 8 | code: "CODE_TEST", 9 | info: "ErrInfo a/b/c/Test", 10 | }) 11 | } else if (call.req.name === "error") { 12 | call.error("Got an error") 13 | } else { 14 | call.succ({ 15 | reply: "a/b/c/Test reply: " + call.req.name, 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/cases/inner.test.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "bson" 2 | import { assert } from "chai" 3 | import chalk from "chalk" 4 | import * as path from "path" 5 | import { ServiceProto, TsrpcError, TsrpcErrorType } from "tsrpc-proto" 6 | import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from "../../src" 7 | import { HttpServer } from "../../src/server/http/HttpServer" 8 | import { PrefixLogger } from "../../src/server/models/PrefixLogger" 9 | import { ApiTest as ApiAbcTest } from "../api/a/b/c/ApiTest" 10 | import { ApiTest } from "../api/ApiTest" 11 | import { MsgChat } from "../proto/MsgChat" 12 | import { ReqTest, ResTest } from "../proto/PtlTest" 13 | import { serviceProto, ServiceType } from "../proto/serviceProto" 14 | 15 | const serverLogger = new PrefixLogger({ 16 | prefixs: [chalk.bgGreen.white(" Server ")], 17 | logger: new TerminalColorLogger({ pid: "Server" }), 18 | }) 19 | 20 | const getProto = () => Object.merge({}, serviceProto) as ServiceProto 21 | 22 | async function testApi(server: HttpServer) { 23 | // Succ 24 | assert.deepStrictEqual( 25 | await server.callApi("Test", { 26 | name: "Req1", 27 | }), 28 | { 29 | isSucc: true, 30 | res: { 31 | reply: "Test reply: Req1", 32 | }, 33 | } 34 | ) 35 | assert.deepStrictEqual( 36 | await server.callApi("a/b/c/Test", { 37 | name: "Req2", 38 | }), 39 | { 40 | isSucc: true, 41 | res: { 42 | reply: "a/b/c/Test reply: Req2", 43 | }, 44 | } 45 | ) 46 | 47 | // Inner error 48 | for (let v of ["Test", "a/b/c/Test"]) { 49 | let ret = await server.callApi(v as any, { 50 | name: "InnerError", 51 | }) 52 | delete ret.err!.innerErr.stack 53 | 54 | assert.deepStrictEqual(ret, { 55 | isSucc: false, 56 | err: new TsrpcError("Internal Server Error", { 57 | code: "INTERNAL_ERR", 58 | type: TsrpcErrorType.ServerError, 59 | innerErr: `${v} InnerError`, 60 | }), 61 | }) 62 | } 63 | 64 | // TsrpcError 65 | for (let v of ["Test", "a/b/c/Test"]) { 66 | let ret = await server.callApi(v as any, { 67 | name: "TsrpcError", 68 | }) 69 | assert.deepStrictEqual(ret, { 70 | isSucc: false, 71 | err: new TsrpcError(`${v} TsrpcError`, { 72 | code: "CODE_TEST", 73 | type: TsrpcErrorType.ApiError, 74 | info: "ErrInfo " + v, 75 | }), 76 | }) 77 | } 78 | 79 | // call.error 80 | for (let v of ["Test", "a/b/c/Test"]) { 81 | let ret = await server.callApi(v as any, { 82 | name: "error", 83 | }) 84 | assert.deepStrictEqual(ret, { 85 | isSucc: false, 86 | err: new TsrpcError("Got an error", { 87 | type: TsrpcErrorType.ApiError, 88 | }), 89 | }) 90 | } 91 | } 92 | 93 | describe("HTTP Server & Client basic", function () { 94 | it("implement API manually", async function () { 95 | let server = new HttpServer(getProto(), { 96 | logger: serverLogger, 97 | debugBuf: true, 98 | }) 99 | 100 | server.implementApi("Test", ApiTest) 101 | server.implementApi("a/b/c/Test", ApiAbcTest) 102 | 103 | await testApi(server) 104 | }) 105 | 106 | it("extend call in handler", function () { 107 | let server = new HttpServer(getProto(), { 108 | logger: serverLogger, 109 | debugBuf: true, 110 | }) 111 | 112 | type MyApiCall = ApiCall & { 113 | value1?: string 114 | value2: string 115 | } 116 | type MyMsgCall = MsgCall & { 117 | value1?: string 118 | value2: string 119 | } 120 | 121 | server.implementApi("Test", (call: MyApiCall) => { 122 | call.value1 = "xxx" 123 | call.value2 = "xxx" 124 | }) 125 | server.listenMsg("Chat", (call: MyMsgCall) => { 126 | call.msg.content 127 | call.value1 = "xxx" 128 | call.value2 = "xxx" 129 | }) 130 | }) 131 | 132 | it("extend call in flow", function () { 133 | let server = new HttpServer(getProto(), { 134 | logger: serverLogger, 135 | debugBuf: true, 136 | }) 137 | 138 | type MyApiCall = ApiCall & { 139 | value1?: string 140 | value2: string 141 | } 142 | type MyMsgCall = MsgCall & { 143 | value1?: string 144 | value2: string 145 | } 146 | type MyConn = HttpConnection & { 147 | currentUser: { 148 | uid: string 149 | nickName: string 150 | } 151 | } 152 | 153 | server.flows.postConnectFlow.push((conn: MyConn) => { 154 | conn.currentUser.nickName = "asdf" 155 | return conn 156 | }) 157 | server.flows.postConnectFlow.exec(null as any as MyConn, console) 158 | server.flows.preApiCallFlow.push((call: MyApiCall) => { 159 | call.value2 = "x" 160 | return call 161 | }) 162 | server.flows.preSendMsgFlow.push((call: MyMsgCall) => { 163 | call.value2 = "f" 164 | return call 165 | }) 166 | }) 167 | 168 | it("autoImplementApi", async function () { 169 | let server = new HttpServer(getProto(), { 170 | logger: serverLogger, 171 | apiTimeout: 5000, 172 | }) 173 | 174 | await server.autoImplementApi(path.resolve(__dirname, "../api")) 175 | 176 | await testApi(server) 177 | }) 178 | 179 | it("autoImplementApi delay", async function () { 180 | let server = new HttpServer(getProto(), { 181 | logger: serverLogger, 182 | apiTimeout: 5000, 183 | }) 184 | 185 | server.autoImplementApi(path.resolve(__dirname, "../api"), true) 186 | 187 | await testApi(server) 188 | }) 189 | 190 | it("error", async function () { 191 | let server = new HttpServer(getProto(), { 192 | logger: serverLogger, 193 | }) 194 | 195 | let ret = await server.callApi("TesASFt" as any, { name: "xx" } as any) 196 | console.log(ret) 197 | assert.strictEqual(ret.isSucc, false) 198 | assert.strictEqual(ret.err?.code, "ERR_API_NAME") 199 | assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError) 200 | }) 201 | 202 | it("server timeout", async function () { 203 | let server = new HttpServer(getProto(), { 204 | logger: serverLogger, 205 | apiTimeout: 100, 206 | }) 207 | server.implementApi("Test", call => { 208 | return new Promise(rs => { 209 | setTimeout(() => { 210 | call.req && 211 | call.succ({ 212 | reply: "Hi, " + call.req.name, 213 | }) 214 | rs() 215 | }, 200) 216 | }) 217 | }) 218 | 219 | let ret = await server.callApi("Test", { name: "Jack" }) 220 | assert.deepStrictEqual(ret, { 221 | isSucc: false, 222 | err: new TsrpcError("Server Timeout", { 223 | code: "SERVER_TIMEOUT", 224 | type: TsrpcErrorType.ServerError, 225 | }), 226 | }) 227 | }) 228 | 229 | it("Graceful stop", async function () { 230 | let server = new HttpServer(getProto(), { 231 | logger: serverLogger, 232 | }) 233 | 234 | let reqNum = 0 235 | server.implementApi("Test", async call => { 236 | if (++reqNum === 10) { 237 | server.gracefulStop() 238 | } 239 | await new Promise(rs => setTimeout(rs, parseInt(call.req.name))) 240 | call.succ({ reply: "OK" }) 241 | }) 242 | 243 | let isStopped = false 244 | 245 | let succNum = 0 246 | await Promise.all( 247 | Array.from({ length: 10 }, (v, i) => 248 | server.callApi("Test", { name: "" + i * 100 }).then(v => { 249 | if (v.res?.reply === "OK") { 250 | ++succNum 251 | } 252 | }) 253 | ) 254 | ) 255 | assert.strictEqual(succNum, 10) 256 | }) 257 | }) 258 | 259 | describe("HTTP Flows", function () { 260 | it("ApiCall flow", async function () { 261 | let server = new HttpServer(getProto(), { 262 | logger: serverLogger, 263 | }) 264 | 265 | server.implementApi("Test", async call => { 266 | call.succ({ reply: "asdgasdgasdgasdg" }) 267 | }) 268 | 269 | server.flows.preApiCallFlow.push(call => { 270 | if (call.req.apiName !== "ObjId") { 271 | call.req.name = "Changed" 272 | } 273 | return call 274 | }) 275 | 276 | server.flows.preApiCallFlow.push(call => { 277 | assert.strictEqual(call.req.name, "Changed") 278 | call.error("You need login") 279 | return call 280 | }) 281 | 282 | let ret = await server.callApi("Test", { name: "xxx" }) 283 | assert.deepStrictEqual(ret, { 284 | isSucc: false, 285 | err: new TsrpcError("You need login"), 286 | }) 287 | }) 288 | 289 | it("ApiCall flow break", async function () { 290 | let server = new HttpServer(getProto(), { 291 | logger: serverLogger, 292 | }) 293 | 294 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 295 | 296 | server.implementApi("Test", async call => { 297 | call.succ({ reply: "asdgasdgasdgasdg" }) 298 | }) 299 | 300 | server.flows.preApiCallFlow.push(call => { 301 | call.error("You need login") 302 | return undefined 303 | }) 304 | 305 | let ret = await server.callApi("Test", { name: "xxx" }) 306 | assert.deepStrictEqual(ret, { 307 | isSucc: false, 308 | err: new TsrpcError("You need login"), 309 | }) 310 | }) 311 | 312 | it("ApiCall flow error", async function () { 313 | let server = new HttpServer(getProto(), { 314 | logger: serverLogger, 315 | }) 316 | 317 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 318 | 319 | server.implementApi("Test", async call => { 320 | call.succ({ reply: "asdgasdgasdgasdg" }) 321 | }) 322 | 323 | server.flows.preApiCallFlow.push(call => { 324 | throw new Error("ASDFASDF") 325 | }) 326 | 327 | let ret = await server.callApi("Test", { name: "xxx" }) 328 | assert.deepStrictEqual(ret, { 329 | isSucc: false, 330 | err: new TsrpcError("Internal Server Error", { 331 | type: TsrpcErrorType.ServerError, 332 | innerErr: "ASDFASDF", 333 | code: "INTERNAL_ERR", 334 | }), 335 | }) 336 | }) 337 | 338 | it("server ApiReturn flow", async function () { 339 | let server = new HttpServer(getProto(), { 340 | logger: serverLogger, 341 | }) 342 | 343 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 344 | 345 | server.implementApi("Test", async call => { 346 | call.succ({ reply: "xxxxxxxxxxxxxxxxxxxx" }) 347 | }) 348 | 349 | server.flows.preApiReturnFlow.push(v => { 350 | flowExecResult.preApiReturnFlow = true 351 | v.return = { 352 | isSucc: false, 353 | err: new TsrpcError("Ret changed"), 354 | } 355 | return v 356 | }) 357 | 358 | let ret = await server.callApi("Test", { name: "xxx" }) 359 | assert.strictEqual(flowExecResult.preApiReturnFlow, true) 360 | assert.deepStrictEqual(ret, { 361 | isSucc: false, 362 | err: new TsrpcError("Ret changed"), 363 | }) 364 | }) 365 | 366 | it("Extended JSON Types", async function () { 367 | let server = new HttpServer(getProto(), { 368 | logger: serverLogger, 369 | }) 370 | await server.autoImplementApi(path.resolve(__dirname, "../api")) 371 | 372 | let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]) 373 | let date = new Date("2021/11/17") 374 | 375 | // ObjectId 376 | let objId1 = new ObjectId() 377 | let ret = await server.callApi("ObjId", { 378 | id1: objId1, 379 | buf: buf, 380 | date: date, 381 | }) 382 | assert.deepStrictEqual(ret, { 383 | isSucc: true, 384 | res: { 385 | id2: objId1, 386 | buf: buf, 387 | date: date, 388 | }, 389 | }) 390 | }) 391 | }) 392 | -------------------------------------------------------------------------------- /test/cases/inputBuffer.test.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "bson" 2 | import { assert } from "chai" 3 | import chalk from "chalk" 4 | import * as path from "path" 5 | import { ServiceProto, TsrpcError, TsrpcErrorType } from "tsrpc-proto" 6 | import { 7 | ApiCall, 8 | ApiService, 9 | BaseServer, 10 | HttpConnection, 11 | MsgCall, 12 | TerminalColorLogger, 13 | TransportDataUtil, 14 | } from "../../src" 15 | import { HttpServer } from "../../src/server/http/HttpServer" 16 | import { PrefixLogger } from "../../src/server/models/PrefixLogger" 17 | import { ApiTest as ApiAbcTest } from "../api/a/b/c/ApiTest" 18 | import { ApiTest } from "../api/ApiTest" 19 | import { MsgChat } from "../proto/MsgChat" 20 | import { ReqTest, ResTest } from "../proto/PtlTest" 21 | import { serviceProto, ServiceType } from "../proto/serviceProto" 22 | 23 | const serverLogger = new PrefixLogger({ 24 | prefixs: [chalk.bgGreen.white(" Server ")], 25 | logger: new TerminalColorLogger({ pid: "Server" }), 26 | }) 27 | 28 | async function inputBuffer(server: BaseServer, apiName: string, req: any) { 29 | let apiSvc: ApiService | undefined = server.serviceMap.apiName2Service[apiName] 30 | let inputBuf = apiSvc 31 | ? (await TransportDataUtil.encodeApiReq(server.tsbuffer, apiSvc, req, "buffer")).output 32 | : new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]) 33 | if (!inputBuf) { 34 | throw new Error("failed to encode inputBuf") 35 | } 36 | 37 | let retBuf = await server.inputBuffer(inputBuf) 38 | assert.ok(retBuf instanceof Uint8Array) 39 | 40 | let opDecode = await TransportDataUtil.parseServerOutout( 41 | server.tsbuffer, 42 | server.serviceMap, 43 | retBuf, 44 | apiSvc?.id ?? 0 45 | ) 46 | if (!opDecode.isSucc) { 47 | throw new Error("decode buf failed") 48 | } 49 | if (opDecode.result.type !== "api") { 50 | throw new Error("decode result is not api") 51 | } 52 | return opDecode.result.ret 53 | } 54 | 55 | const getProto = () => Object.merge({}, serviceProto) as ServiceProto 56 | 57 | async function testApi(server: HttpServer) { 58 | // Succ 59 | assert.deepStrictEqual( 60 | await inputBuffer(server, "Test", { 61 | name: "Req1", 62 | }), 63 | { 64 | isSucc: true, 65 | res: { 66 | reply: "Test reply: Req1", 67 | }, 68 | } 69 | ) 70 | assert.deepStrictEqual( 71 | await inputBuffer(server, "a/b/c/Test", { 72 | name: "Req2", 73 | }), 74 | { 75 | isSucc: true, 76 | res: { 77 | reply: "a/b/c/Test reply: Req2", 78 | }, 79 | } 80 | ) 81 | 82 | // Inner error 83 | for (let v of ["Test", "a/b/c/Test"]) { 84 | let ret = await inputBuffer(server, v as any, { 85 | name: "InnerError", 86 | }) 87 | delete ret.err!.innerErr.stack 88 | 89 | assert.deepStrictEqual(ret, { 90 | isSucc: false, 91 | err: new TsrpcError("Internal Server Error", { 92 | code: "INTERNAL_ERR", 93 | type: TsrpcErrorType.ServerError, 94 | innerErr: `${v} InnerError`, 95 | }), 96 | }) 97 | } 98 | 99 | // TsrpcError 100 | for (let v of ["Test", "a/b/c/Test"]) { 101 | let ret = await inputBuffer(server, v as any, { 102 | name: "TsrpcError", 103 | }) 104 | assert.deepStrictEqual(ret, { 105 | isSucc: false, 106 | err: new TsrpcError(`${v} TsrpcError`, { 107 | code: "CODE_TEST", 108 | type: TsrpcErrorType.ApiError, 109 | info: "ErrInfo " + v, 110 | }), 111 | }) 112 | } 113 | 114 | // call.error 115 | for (let v of ["Test", "a/b/c/Test"]) { 116 | let ret = await inputBuffer(server, v as any, { 117 | name: "error", 118 | }) 119 | assert.deepStrictEqual(ret, { 120 | isSucc: false, 121 | err: new TsrpcError("Got an error", { 122 | type: TsrpcErrorType.ApiError, 123 | }), 124 | }) 125 | } 126 | } 127 | 128 | describe("HTTP Server & Client basic", function () { 129 | it("implement API manually", async function () { 130 | let server = new HttpServer(getProto(), { 131 | logger: serverLogger, 132 | debugBuf: true, 133 | }) 134 | 135 | server.implementApi("Test", ApiTest) 136 | server.implementApi("a/b/c/Test", ApiAbcTest) 137 | 138 | await testApi(server) 139 | }) 140 | 141 | it("extend call in handler", function () { 142 | let server = new HttpServer(getProto(), { 143 | logger: serverLogger, 144 | debugBuf: true, 145 | }) 146 | 147 | type MyApiCall = ApiCall & { 148 | value1?: string 149 | value2: string 150 | } 151 | type MyMsgCall = MsgCall & { 152 | value1?: string 153 | value2: string 154 | } 155 | 156 | server.implementApi("Test", (call: MyApiCall) => { 157 | call.value1 = "xxx" 158 | call.value2 = "xxx" 159 | }) 160 | server.listenMsg("Chat", (call: MyMsgCall) => { 161 | call.msg.content 162 | call.value1 = "xxx" 163 | call.value2 = "xxx" 164 | }) 165 | }) 166 | 167 | it("extend call in flow", function () { 168 | let server = new HttpServer(getProto(), { 169 | logger: serverLogger, 170 | debugBuf: true, 171 | }) 172 | 173 | type MyApiCall = ApiCall & { 174 | value1?: string 175 | value2: string 176 | } 177 | type MyMsgCall = MsgCall & { 178 | value1?: string 179 | value2: string 180 | } 181 | type MyConn = HttpConnection & { 182 | currentUser: { 183 | uid: string 184 | nickName: string 185 | } 186 | } 187 | 188 | server.flows.postConnectFlow.push((conn: MyConn) => { 189 | conn.currentUser.nickName = "asdf" 190 | return conn 191 | }) 192 | server.flows.postConnectFlow.exec(null as any as MyConn, console) 193 | server.flows.preApiCallFlow.push((call: MyApiCall) => { 194 | call.value2 = "x" 195 | return call 196 | }) 197 | server.flows.preSendMsgFlow.push((call: MyMsgCall) => { 198 | call.value2 = "f" 199 | return call 200 | }) 201 | }) 202 | 203 | it("autoImplementApi", async function () { 204 | let server = new HttpServer(getProto(), { 205 | logger: serverLogger, 206 | apiTimeout: 5000, 207 | }) 208 | 209 | await server.autoImplementApi(path.resolve(__dirname, "../api")) 210 | 211 | await testApi(server) 212 | }) 213 | 214 | it("autoImplementApi delay", async function () { 215 | let server = new HttpServer(getProto(), { 216 | logger: serverLogger, 217 | apiTimeout: 5000, 218 | }) 219 | 220 | server.autoImplementApi(path.resolve(__dirname, "../api"), true) 221 | 222 | await testApi(server) 223 | }) 224 | 225 | it("error", async function () { 226 | let server = new HttpServer(getProto(), { 227 | logger: serverLogger, 228 | }) 229 | 230 | let ret = await inputBuffer(server, "TesASFt" as any, { name: "xx" } as any) 231 | console.log(ret) 232 | assert.strictEqual(ret.isSucc, false) 233 | assert.strictEqual(ret.err?.code, "INPUT_DATA_ERR") 234 | assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError) 235 | }) 236 | 237 | it("server timeout", async function () { 238 | let server = new HttpServer(getProto(), { 239 | logger: serverLogger, 240 | apiTimeout: 100, 241 | }) 242 | server.implementApi("Test", call => { 243 | return new Promise(rs => { 244 | setTimeout(() => { 245 | call.req && 246 | call.succ({ 247 | reply: "Hi, " + call.req.name, 248 | }) 249 | rs() 250 | }, 200) 251 | }) 252 | }) 253 | 254 | let ret = await inputBuffer(server, "Test", { name: "Jack" }) 255 | assert.deepStrictEqual(ret, { 256 | isSucc: false, 257 | err: new TsrpcError("Server Timeout", { 258 | code: "SERVER_TIMEOUT", 259 | type: TsrpcErrorType.ServerError, 260 | }), 261 | }) 262 | }) 263 | 264 | it("Graceful stop", async function () { 265 | let server = new HttpServer(getProto(), { 266 | logger: serverLogger, 267 | }) 268 | 269 | let reqNum = 0 270 | server.implementApi("Test", async call => { 271 | if (++reqNum === 10) { 272 | server.gracefulStop() 273 | } 274 | await new Promise(rs => setTimeout(rs, parseInt(call.req.name))) 275 | call.succ({ reply: "OK" }) 276 | }) 277 | 278 | let isStopped = false 279 | 280 | let succNum = 0 281 | await Promise.all( 282 | Array.from({ length: 10 }, (v, i) => 283 | inputBuffer(server, "Test", { name: "" + i * 100 }).then((v: any) => { 284 | if (v.res?.reply === "OK") { 285 | ++succNum 286 | } 287 | }) 288 | ) 289 | ) 290 | assert.strictEqual(succNum, 10) 291 | }) 292 | }) 293 | 294 | describe("HTTP Flows", function () { 295 | it("ApiCall flow", async function () { 296 | let server = new HttpServer(getProto(), { 297 | logger: serverLogger, 298 | }) 299 | 300 | server.implementApi("Test", async call => { 301 | call.succ({ reply: "asdgasdgasdgasdg" }) 302 | }) 303 | 304 | server.flows.preApiCallFlow.push(call => { 305 | if (call.req.apiName !== "ObjId") { 306 | call.req.name = "Changed" 307 | } 308 | return call 309 | }) 310 | 311 | server.flows.preApiCallFlow.push(call => { 312 | assert.strictEqual(call.req.name, "Changed") 313 | call.error("You need login") 314 | return call 315 | }) 316 | 317 | let ret = await inputBuffer(server, "Test", { name: "xxx" }) 318 | assert.deepStrictEqual(ret, { 319 | isSucc: false, 320 | err: new TsrpcError("You need login"), 321 | }) 322 | }) 323 | 324 | it("ApiCall flow break", async function () { 325 | let server = new HttpServer(getProto(), { 326 | logger: serverLogger, 327 | }) 328 | 329 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 330 | 331 | server.implementApi("Test", async call => { 332 | call.succ({ reply: "asdgasdgasdgasdg" }) 333 | }) 334 | 335 | server.flows.preApiCallFlow.push(call => { 336 | call.error("You need login") 337 | return undefined 338 | }) 339 | 340 | let ret = await inputBuffer(server, "Test", { name: "xxx" }) 341 | assert.deepStrictEqual(ret, { 342 | isSucc: false, 343 | err: new TsrpcError("You need login"), 344 | }) 345 | }) 346 | 347 | it("ApiCall flow error", async function () { 348 | let server = new HttpServer(getProto(), { 349 | logger: serverLogger, 350 | }) 351 | 352 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 353 | 354 | server.implementApi("Test", async call => { 355 | call.succ({ reply: "asdgasdgasdgasdg" }) 356 | }) 357 | 358 | server.flows.preApiCallFlow.push(call => { 359 | throw new Error("ASDFASDF") 360 | }) 361 | 362 | let ret = await inputBuffer(server, "Test", { name: "xxx" }) 363 | assert.deepStrictEqual(ret, { 364 | isSucc: false, 365 | err: new TsrpcError("Internal Server Error", { 366 | type: TsrpcErrorType.ServerError, 367 | innerErr: "ASDFASDF", 368 | code: "INTERNAL_ERR", 369 | }), 370 | }) 371 | }) 372 | 373 | it("server ApiReturn flow", async function () { 374 | let server = new HttpServer(getProto(), { 375 | logger: serverLogger, 376 | }) 377 | 378 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 379 | 380 | server.implementApi("Test", async call => { 381 | call.succ({ reply: "xxxxxxxxxxxxxxxxxxxx" }) 382 | }) 383 | 384 | server.flows.preApiReturnFlow.push(v => { 385 | flowExecResult.preApiReturnFlow = true 386 | v.return = { 387 | isSucc: false, 388 | err: new TsrpcError("Ret changed"), 389 | } 390 | return v 391 | }) 392 | 393 | let ret = await inputBuffer(server, "Test", { name: "xxx" }) 394 | assert.strictEqual(flowExecResult.preApiReturnFlow, true) 395 | assert.deepStrictEqual(ret, { 396 | isSucc: false, 397 | err: new TsrpcError("Ret changed"), 398 | }) 399 | }) 400 | 401 | it("Extended JSON Types", async function () { 402 | let server = new HttpServer(getProto(), { 403 | logger: serverLogger, 404 | }) 405 | await server.autoImplementApi(path.resolve(__dirname, "../api")) 406 | 407 | let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]) 408 | let date = new Date("2021/11/17") 409 | 410 | // ObjectId 411 | let objId1 = new ObjectId() 412 | let ret = await inputBuffer(server, "ObjId", { 413 | id1: objId1, 414 | buf: buf, 415 | date: date, 416 | }) 417 | assert.deepStrictEqual(ret, { 418 | isSucc: true, 419 | res: { 420 | id2: objId1, 421 | buf: buf, 422 | date: date, 423 | }, 424 | }) 425 | }) 426 | }) 427 | -------------------------------------------------------------------------------- /test/cases/inputJSON.test.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "bson" 2 | import { assert } from "chai" 3 | import chalk from "chalk" 4 | import * as path from "path" 5 | import { Base64Util } from "tsbuffer" 6 | import { ServiceProto, TsrpcError, TsrpcErrorType } from "tsrpc-proto" 7 | import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from "../../src" 8 | import { HttpServer } from "../../src/server/http/HttpServer" 9 | import { PrefixLogger } from "../../src/server/models/PrefixLogger" 10 | import { ApiTest as ApiAbcTest } from "../api/a/b/c/ApiTest" 11 | import { ApiTest } from "../api/ApiTest" 12 | import { MsgChat } from "../proto/MsgChat" 13 | import { ReqTest, ResTest } from "../proto/PtlTest" 14 | import { serviceProto, ServiceType } from "../proto/serviceProto" 15 | 16 | const serverLogger = new PrefixLogger({ 17 | prefixs: [chalk.bgGreen.white(" Server ")], 18 | logger: new TerminalColorLogger({ pid: "Server" }), 19 | }) 20 | 21 | const getProto = () => Object.merge({}, serviceProto) as ServiceProto 22 | 23 | async function testApi(server: HttpServer) { 24 | // Succ 25 | assert.deepStrictEqual( 26 | await server.inputJSON("Test", { 27 | name: "Req1", 28 | }), 29 | { 30 | isSucc: true, 31 | res: { 32 | reply: "Test reply: Req1", 33 | }, 34 | } 35 | ) 36 | assert.deepStrictEqual( 37 | await server.inputJSON("/a/b/c/Test", { 38 | name: "Req2", 39 | }), 40 | { 41 | isSucc: true, 42 | res: { 43 | reply: "a/b/c/Test reply: Req2", 44 | }, 45 | } 46 | ) 47 | 48 | // Inner error 49 | for (let v of ["Test", "a/b/c/Test"]) { 50 | let ret = await server.inputJSON(v as any, { 51 | name: "InnerError", 52 | }) 53 | delete ret.err!.innerErr.stack 54 | 55 | assert.deepStrictEqual(ret, { 56 | isSucc: false, 57 | err: { 58 | ...new TsrpcError("Internal Server Error", { 59 | code: "INTERNAL_ERR", 60 | type: TsrpcErrorType.ServerError, 61 | innerErr: `${v} InnerError`, 62 | }), 63 | }, 64 | }) 65 | } 66 | 67 | // TsrpcError 68 | for (let v of ["Test", "a/b/c/Test"]) { 69 | let ret = await server.inputJSON(v as any, { 70 | name: "TsrpcError", 71 | }) 72 | assert.deepStrictEqual(ret, { 73 | isSucc: false, 74 | err: { 75 | ...new TsrpcError(`${v} TsrpcError`, { 76 | code: "CODE_TEST", 77 | type: TsrpcErrorType.ApiError, 78 | info: "ErrInfo " + v, 79 | }), 80 | }, 81 | }) 82 | } 83 | 84 | // call.error 85 | for (let v of ["Test", "a/b/c/Test"]) { 86 | let ret = await server.inputJSON(v as any, { 87 | name: "error", 88 | }) 89 | assert.deepStrictEqual(ret, { 90 | isSucc: false, 91 | err: { 92 | ...new TsrpcError("Got an error", { 93 | type: TsrpcErrorType.ApiError, 94 | }), 95 | }, 96 | }) 97 | } 98 | } 99 | 100 | describe("HTTP Server & Client basic", function () { 101 | it("implement API manually", async function () { 102 | let server = new HttpServer(getProto(), { 103 | logger: serverLogger, 104 | debugBuf: true, 105 | }) 106 | 107 | server.implementApi("Test", ApiTest) 108 | server.implementApi("a/b/c/Test", ApiAbcTest) 109 | 110 | await testApi(server) 111 | }) 112 | 113 | it("extend call in handler", function () { 114 | let server = new HttpServer(getProto(), { 115 | logger: serverLogger, 116 | debugBuf: true, 117 | }) 118 | 119 | type MyApiCall = ApiCall & { 120 | value1?: string 121 | value2: string 122 | } 123 | type MyMsgCall = MsgCall & { 124 | value1?: string 125 | value2: string 126 | } 127 | 128 | server.implementApi("Test", (call: MyApiCall) => { 129 | call.value1 = "xxx" 130 | call.value2 = "xxx" 131 | }) 132 | server.listenMsg("Chat", (call: MyMsgCall) => { 133 | call.msg.content 134 | call.value1 = "xxx" 135 | call.value2 = "xxx" 136 | }) 137 | }) 138 | 139 | it("extend call in flow", function () { 140 | let server = new HttpServer(getProto(), { 141 | logger: serverLogger, 142 | debugBuf: true, 143 | }) 144 | 145 | type MyApiCall = ApiCall & { 146 | value1?: string 147 | value2: string 148 | } 149 | type MyMsgCall = MsgCall & { 150 | value1?: string 151 | value2: string 152 | } 153 | type MyConn = HttpConnection & { 154 | currentUser: { 155 | uid: string 156 | nickName: string 157 | } 158 | } 159 | 160 | server.flows.postConnectFlow.push((conn: MyConn) => { 161 | conn.currentUser.nickName = "asdf" 162 | return conn 163 | }) 164 | server.flows.postConnectFlow.exec(null as any as MyConn, console) 165 | server.flows.preApiCallFlow.push((call: MyApiCall) => { 166 | call.value2 = "x" 167 | return call 168 | }) 169 | server.flows.preSendMsgFlow.push((call: MyMsgCall) => { 170 | call.value2 = "f" 171 | return call 172 | }) 173 | }) 174 | 175 | it("autoImplementApi", async function () { 176 | let server = new HttpServer(getProto(), { 177 | logger: serverLogger, 178 | apiTimeout: 5000, 179 | }) 180 | 181 | await server.autoImplementApi(path.resolve(__dirname, "../api")) 182 | 183 | await testApi(server) 184 | }) 185 | 186 | it("autoImplementApi delay", async function () { 187 | let server = new HttpServer(getProto(), { 188 | logger: serverLogger, 189 | apiTimeout: 5000, 190 | }) 191 | 192 | server.autoImplementApi(path.resolve(__dirname, "../api"), true) 193 | 194 | await testApi(server) 195 | }) 196 | 197 | it("error", async function () { 198 | let server = new HttpServer(getProto(), { 199 | logger: serverLogger, 200 | }) 201 | 202 | let ret = await server.inputJSON("TesASFt" as any, { name: "xx" } as any) 203 | console.log(ret) 204 | assert.strictEqual(ret.isSucc, false) 205 | assert.strictEqual(ret.err?.message, "Invalid service name: TesASFt") 206 | assert.strictEqual(ret.err?.code, "INPUT_DATA_ERR") 207 | assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError) 208 | }) 209 | 210 | it("server timeout", async function () { 211 | let server = new HttpServer(getProto(), { 212 | logger: serverLogger, 213 | apiTimeout: 100, 214 | }) 215 | server.implementApi("Test", call => { 216 | return new Promise(rs => { 217 | setTimeout(() => { 218 | call.req && 219 | call.succ({ 220 | reply: "Hi, " + call.req.name, 221 | }) 222 | rs() 223 | }, 200) 224 | }) 225 | }) 226 | 227 | let ret = await server.inputJSON("Test", { name: "Jack" }) 228 | assert.deepStrictEqual(ret, { 229 | isSucc: false, 230 | err: { 231 | ...new TsrpcError("Server Timeout", { 232 | code: "SERVER_TIMEOUT", 233 | type: TsrpcErrorType.ServerError, 234 | }), 235 | }, 236 | }) 237 | }) 238 | 239 | it("Graceful stop", async function () { 240 | let server = new HttpServer(getProto(), { 241 | logger: serverLogger, 242 | }) 243 | 244 | let reqNum = 0 245 | server.implementApi("Test", async call => { 246 | if (++reqNum === 10) { 247 | server.gracefulStop() 248 | } 249 | await new Promise(rs => setTimeout(rs, parseInt(call.req.name))) 250 | call.succ({ reply: "OK" }) 251 | }) 252 | 253 | let isStopped = false 254 | 255 | let succNum = 0 256 | await Promise.all( 257 | Array.from({ length: 10 }, (v, i) => 258 | server.inputJSON("Test", { name: "" + i * 100 }).then((v: any) => { 259 | if (v.res?.reply === "OK") { 260 | ++succNum 261 | } 262 | }) 263 | ) 264 | ) 265 | assert.strictEqual(succNum, 10) 266 | }) 267 | }) 268 | 269 | describe("HTTP Flows", function () { 270 | it("ApiCall flow", async function () { 271 | let server = new HttpServer(getProto(), { 272 | logger: serverLogger, 273 | }) 274 | 275 | server.implementApi("Test", async call => { 276 | call.succ({ reply: "asdgasdgasdgasdg" }) 277 | }) 278 | 279 | server.flows.preApiCallFlow.push(call => { 280 | if (call.req.apiName !== "ObjId") { 281 | call.req.name = "Changed" 282 | } 283 | return call 284 | }) 285 | 286 | server.flows.preApiCallFlow.push(call => { 287 | assert.strictEqual(call.req.name, "Changed") 288 | call.error("You need login") 289 | return call 290 | }) 291 | 292 | let ret = await server.inputJSON("Test", { name: "xxx" }) 293 | assert.deepStrictEqual(ret, { 294 | isSucc: false, 295 | err: { 296 | ...new TsrpcError("You need login"), 297 | }, 298 | }) 299 | }) 300 | 301 | it("ApiCall flow break", async function () { 302 | let server = new HttpServer(getProto(), { 303 | logger: serverLogger, 304 | }) 305 | 306 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 307 | 308 | server.implementApi("Test", async call => { 309 | call.succ({ reply: "asdgasdgasdgasdg" }) 310 | }) 311 | 312 | server.flows.preApiCallFlow.push(call => { 313 | call.error("You need login") 314 | return undefined 315 | }) 316 | 317 | let ret = await server.inputJSON("Test", { name: "xxx" }) 318 | assert.deepStrictEqual(ret, { 319 | isSucc: false, 320 | err: { 321 | ...new TsrpcError("You need login"), 322 | }, 323 | }) 324 | }) 325 | 326 | it("ApiCall flow error", async function () { 327 | let server = new HttpServer(getProto(), { 328 | logger: serverLogger, 329 | }) 330 | 331 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 332 | 333 | server.implementApi("Test", async call => { 334 | call.succ({ reply: "asdgasdgasdgasdg" }) 335 | }) 336 | 337 | server.flows.preApiCallFlow.push(call => { 338 | throw new Error("ASDFASDF") 339 | }) 340 | 341 | let ret = await server.inputJSON("Test", { name: "xxx" }) 342 | assert.deepStrictEqual(ret, { 343 | isSucc: false, 344 | err: { 345 | ...new TsrpcError("Internal Server Error", { 346 | type: TsrpcErrorType.ServerError, 347 | innerErr: "ASDFASDF", 348 | code: "INTERNAL_ERR", 349 | }), 350 | }, 351 | }) 352 | }) 353 | 354 | it("server ApiReturn flow", async function () { 355 | let server = new HttpServer(getProto(), { 356 | logger: serverLogger, 357 | }) 358 | 359 | const flowExecResult: { -readonly [K in keyof BaseServer["flows"]]?: boolean } = {} 360 | 361 | server.implementApi("Test", async call => { 362 | call.succ({ reply: "xxxxxxxxxxxxxxxxxxxx" }) 363 | }) 364 | 365 | server.flows.preApiReturnFlow.push(v => { 366 | flowExecResult.preApiReturnFlow = true 367 | v.return = { 368 | isSucc: false, 369 | err: { ...new TsrpcError("Ret changed") }, 370 | } 371 | return v 372 | }) 373 | 374 | let ret = await server.inputJSON("Test", { name: "xxx" }) 375 | assert.strictEqual(flowExecResult.preApiReturnFlow, true) 376 | assert.deepStrictEqual(ret, { 377 | isSucc: false, 378 | err: { ...new TsrpcError("Ret changed") }, 379 | }) 380 | }) 381 | 382 | it("Extended JSON Types", async function () { 383 | let server = new HttpServer(getProto(), { 384 | logger: serverLogger, 385 | }) 386 | await server.autoImplementApi(path.resolve(__dirname, "../api")) 387 | 388 | let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]) 389 | let date = new Date("2021/11/17") 390 | 391 | // ObjectId 392 | let objId1 = new ObjectId() 393 | let ret = await server.inputJSON("ObjId", { 394 | id1: objId1, 395 | buf: buf, 396 | date: date, 397 | }) 398 | assert.deepStrictEqual(ret, { 399 | isSucc: true, 400 | res: { 401 | id2: objId1.toHexString(), 402 | buf: Base64Util.bufferToBase64(buf), 403 | date: date.toJSON(), 404 | }, 405 | }) 406 | }) 407 | }) 408 | -------------------------------------------------------------------------------- /test/proto/MsgChat.ts: -------------------------------------------------------------------------------- 1 | export interface MsgChat { 2 | channel: number 3 | userName: string 4 | content: string 5 | time: number 6 | } 7 | -------------------------------------------------------------------------------- /test/proto/MsgTest.ts: -------------------------------------------------------------------------------- 1 | export interface MsgTest { 2 | content: string 3 | } 4 | -------------------------------------------------------------------------------- /test/proto/PtlObjId.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ObjectId } from "mongodb" 3 | 4 | export interface ReqObjId { 5 | id1: ObjectId 6 | buf?: Uint8Array 7 | date?: Date 8 | } 9 | 10 | export interface ResObjId { 11 | id2: ObjectId 12 | buf?: Uint8Array 13 | date?: Date 14 | } 15 | -------------------------------------------------------------------------------- /test/proto/PtlTest.ts: -------------------------------------------------------------------------------- 1 | export interface ReqTest { 2 | name: string 3 | } 4 | 5 | export type ResTest = { 6 | reply: string 7 | } 8 | -------------------------------------------------------------------------------- /test/proto/a/b/c/PtlTest.ts: -------------------------------------------------------------------------------- 1 | import { MsgChat } from "../../../MsgChat" 2 | 3 | export interface ReqTest { 4 | name: string 5 | } 6 | 7 | export type ResTest = { 8 | reply: string 9 | chat?: MsgChat 10 | } 11 | -------------------------------------------------------------------------------- /test/proto/serviceProto.ts: -------------------------------------------------------------------------------- 1 | import { ServiceProto } from "tsrpc-proto" 2 | import { ReqTest, ResTest } from "./a/b/c/PtlTest" 3 | import { MsgChat } from "./MsgChat" 4 | import { MsgTest } from "./MsgTest" 5 | import { ReqObjId, ResObjId } from "./PtlObjId" 6 | import { ReqTest as ReqTest_1, ResTest as ResTest_1 } from "./PtlTest" 7 | 8 | export interface ServiceType { 9 | api: { 10 | "a/b/c/Test": { 11 | req: ReqTest 12 | res: ResTest 13 | } 14 | ObjId: { 15 | req: ReqObjId 16 | res: ResObjId 17 | } 18 | Test: { 19 | req: ReqTest_1 20 | res: ResTest_1 21 | } 22 | } 23 | msg: { 24 | Chat: MsgChat 25 | Test: MsgTest 26 | } 27 | } 28 | 29 | export const serviceProto: ServiceProto = { 30 | version: 2, 31 | services: [ 32 | { 33 | id: 0, 34 | name: "a/b/c/Test", 35 | type: "api", 36 | }, 37 | { 38 | id: 1, 39 | name: "Chat", 40 | type: "msg", 41 | }, 42 | { 43 | id: 4, 44 | name: "Test", 45 | type: "msg", 46 | }, 47 | { 48 | id: 2, 49 | name: "ObjId", 50 | type: "api", 51 | }, 52 | { 53 | id: 3, 54 | name: "Test", 55 | type: "api", 56 | }, 57 | ], 58 | types: { 59 | "a/b/c/PtlTest/ReqTest": { 60 | type: "Interface", 61 | properties: [ 62 | { 63 | id: 0, 64 | name: "name", 65 | type: { 66 | type: "String", 67 | }, 68 | }, 69 | ], 70 | }, 71 | "a/b/c/PtlTest/ResTest": { 72 | type: "Interface", 73 | properties: [ 74 | { 75 | id: 0, 76 | name: "reply", 77 | type: { 78 | type: "String", 79 | }, 80 | }, 81 | { 82 | id: 1, 83 | name: "chat", 84 | type: { 85 | type: "Reference", 86 | target: "MsgChat/MsgChat", 87 | }, 88 | optional: true, 89 | }, 90 | ], 91 | }, 92 | "MsgChat/MsgChat": { 93 | type: "Interface", 94 | properties: [ 95 | { 96 | id: 0, 97 | name: "channel", 98 | type: { 99 | type: "Number", 100 | }, 101 | }, 102 | { 103 | id: 1, 104 | name: "userName", 105 | type: { 106 | type: "String", 107 | }, 108 | }, 109 | { 110 | id: 2, 111 | name: "content", 112 | type: { 113 | type: "String", 114 | }, 115 | }, 116 | { 117 | id: 3, 118 | name: "time", 119 | type: { 120 | type: "Number", 121 | }, 122 | }, 123 | ], 124 | }, 125 | "MsgTest/MsgTest": { 126 | type: "Interface", 127 | properties: [ 128 | { 129 | id: 0, 130 | name: "content", 131 | type: { 132 | type: "String", 133 | }, 134 | }, 135 | ], 136 | }, 137 | "PtlObjId/ReqObjId": { 138 | type: "Interface", 139 | properties: [ 140 | { 141 | id: 0, 142 | name: "id1", 143 | type: { 144 | type: "Reference", 145 | target: "?mongodb/ObjectId", 146 | }, 147 | }, 148 | { 149 | id: 1, 150 | name: "buf", 151 | type: { 152 | type: "Buffer", 153 | arrayType: "Uint8Array", 154 | }, 155 | optional: true, 156 | }, 157 | { 158 | id: 2, 159 | name: "date", 160 | type: { 161 | type: "Date", 162 | }, 163 | optional: true, 164 | }, 165 | ], 166 | }, 167 | "PtlObjId/ResObjId": { 168 | type: "Interface", 169 | properties: [ 170 | { 171 | id: 0, 172 | name: "id2", 173 | type: { 174 | type: "Reference", 175 | target: "?mongodb/ObjectId", 176 | }, 177 | }, 178 | { 179 | id: 1, 180 | name: "buf", 181 | type: { 182 | type: "Buffer", 183 | arrayType: "Uint8Array", 184 | }, 185 | optional: true, 186 | }, 187 | { 188 | id: 2, 189 | name: "date", 190 | type: { 191 | type: "Date", 192 | }, 193 | optional: true, 194 | }, 195 | ], 196 | }, 197 | "PtlTest/ReqTest": { 198 | type: "Interface", 199 | properties: [ 200 | { 201 | id: 0, 202 | name: "name", 203 | type: { 204 | type: "String", 205 | }, 206 | }, 207 | ], 208 | }, 209 | "PtlTest/ResTest": { 210 | type: "Interface", 211 | properties: [ 212 | { 213 | id: 0, 214 | name: "reply", 215 | type: { 216 | type: "String", 217 | }, 218 | }, 219 | ], 220 | }, 221 | }, 222 | } 223 | -------------------------------------------------------------------------------- /test/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICbjCCAdcCFGYmSt2zR1oTSr2T6e1tb/bGM/C6MA0GCSqGSIb3DQEBCwUAMHYx 3 | CzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56 4 | aGVuMRIwEAYDVQQKDAlLaW5nd29ya3MxEjAQBgNVBAMMCTEyNy4wLjAuMTEYMBYG 5 | CSqGSIb3DQEJARYJbWVAazh3LmNuMB4XDTIyMDYwODA0Mjc0NloXDTMyMDYwNTA0 6 | Mjc0NlowdjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5nZG9uZzERMA8GA1UE 7 | BwwIU2hlbnpoZW4xEjAQBgNVBAoMCUtpbmd3b3JrczESMBAGA1UEAwwJMTI3LjAu 8 | MC4xMRgwFgYJKoZIhvcNAQkBFgltZUBrOHcuY24wgZ8wDQYJKoZIhvcNAQEBBQAD 9 | gY0AMIGJAoGBAMy3rzbsIc+v0kXnKEOdYixOHMxCnd0fFqt6NBb6pFa91mBq8Zoi 10 | 8rxJ80Ed89FJhYg4q2IUGx/HVhGF931jBQfvsVLaOLVIjlF2AqUx5LdvzM5Lq4GZ 11 | 49HmO3TLPvm8UPuCZ9TY8ZZDWnDTwGt1mOegHVtqVloHcI/bDIGDohEdAgMBAAEw 12 | DQYJKoZIhvcNAQELBQADgYEADr+ggxOiJVOImjlO/fHt4VzRAN1hEr2bWEgaEjfW 13 | p7fLkYe4o5ttO1RbEm5siGx0nnlCJQ9tNMva1iYYr2KXj5v7/SYpEHaS4B95w6/x 14 | hjZAsVJYlyaJhrv1uV5wkWJOIMccdh2eY4OCdsT6GWbD+jfQ4C6mYLUJRIlMmtRj 15 | aMs= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /test/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDMt6827CHPr9JF5yhDnWIsThzMQp3dHxarejQW+qRWvdZgavGa 3 | IvK8SfNBHfPRSYWIOKtiFBsfx1YRhfd9YwUH77FS2ji1SI5RdgKlMeS3b8zOS6uB 4 | mePR5jt0yz75vFD7gmfU2PGWQ1pw08BrdZjnoB1balZaB3CP2wyBg6IRHQIDAQAB 5 | AoGAONK3iMgsbmiANjT+gR4bVO7toWjQRsNNWJWYBdTWbtlMuwCURVN0Cv1/ztBQ 6 | kAQXU4NfVt771GtRIZYM5znn+BeNc8Rs9r03671cJ78hw6kUtiey2draANhgTjsI 7 | oC5ot1ThYAXI/K+f3BHLvo0nlNKi2MvYwHfsjr196OJXiiUCQQD2Gn3z2cO6IHtG 8 | CNxCxl74uCnUU5tx1jDTmOhJU69f26AQ3BymCMADfxz1lI5lfG7Rj2c3ZjvCpVAE 9 | 36QZ+QnLAkEA1PMlM5e2GIf4eqXhfLoUQYrH88aaSy5hjnXRENwITXGwFWaN6/CV 10 | njxoH6QcjBPU2sZA4vCe1ERH7YbwsGYTtwJBAKr+BytJz7tf3Cbx+xAOQmhvlOio 11 | 2qVCnBQ49pQUKBLjRxjPxrv58me7hwR+nl2XEmxaRe3xA26fa7SnKp69MPcCQQC4 12 | Y20j9kqThTDPqlDL+ifN9MhcOeyiCqAohbWofo2l2ToZ3bonwSMcZ7vVIfoiBI37 13 | fUzz9Fvi+ti5QG2qoEiTAkEAn9iPWsPDQE1iVTjP+B6v7ZcFn5r/u3zLRjzhz8Dx 14 | GxU+7IkcK6NUT1XuO1Oe2gNxwrlvDVju0rzy3bOEd6PeZA== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import { HttpServer } from "../src/server/http/HttpServer" 2 | import { serviceProto } from "./proto/serviceProto" 3 | 4 | let server = new HttpServer(serviceProto, { 5 | jsonEnabled: true, 6 | jsonRootPath: "api", 7 | }) 8 | 9 | server.implementApi("a/b/c/Test", call => { 10 | call.logger.log("xxx", call.req) 11 | call.succ({ 12 | reply: "xxxxxxxxxxx", 13 | aasdg: "d", 14 | b: "asdg", 15 | } as any) 16 | }) 17 | 18 | server.start() 19 | -------------------------------------------------------------------------------- /test/try/client/http.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from "../../src/client/http/HttpClient" 2 | import { serviceProto, ServiceType } from "../proto/serviceProto" 3 | let client = new HttpClient({ 4 | server: "http://localhost:3000", 5 | proto: serviceProto, 6 | }) 7 | 8 | async function main() { 9 | const P = 50, 10 | N = 1000 11 | let max = 0 12 | console.time(`test ${P}/${N}`) 13 | for (let i = 0, len = N / P; i < len; ++i) { 14 | let res = await Promise.all( 15 | Array.from({ length: P }, () => { 16 | let start = Date.now() 17 | return client.callApi("a/b/c/Test", { name: "123" }).then(() => Date.now() - start) 18 | }) 19 | ) 20 | max = Math.max(res.max(), max) 21 | } 22 | console.timeEnd(`test ${P}/${N}`) 23 | console.log("max", max) 24 | } 25 | main() 26 | -------------------------------------------------------------------------------- /test/try/client/ws.ts: -------------------------------------------------------------------------------- 1 | import { serviceProto, ServiceType } from "../proto/serviceProto" 2 | import { WsClient } from "../../src/client/ws/WsClient" 3 | import SuperPromise from "k8w-super-promise" 4 | import { Func } from "mocha" 5 | 6 | async function main() { 7 | let client = new WsClient({ 8 | server: "ws://127.0.0.1:3000", 9 | proto: serviceProto, 10 | onStatusChange: v => { 11 | console.log("StatusChange", v) 12 | }, 13 | // onLostConnection: () => { 14 | // console.log('连接断开,2秒后重连'); 15 | // setTimeout(() => { 16 | // client.connect().catch(() => { }); 17 | // }, 2000) 18 | // } 19 | }) 20 | 21 | await client.connect() 22 | 23 | let cancel = client.callApi("Test", { name: "XXXXXXXXXXXXX" }).catch(e => e) 24 | cancel.cancel() 25 | 26 | let res = await client.callApi("Test", { name: "小明同学" }).catch(e => e) 27 | console.log("Test Res", res) 28 | 29 | res = await client.callApi("a/b/c/Test", { name: "小明同学" }).catch(e => e) 30 | console.log("Test1 Res", res) 31 | 32 | // setInterval(async () => { 33 | // try { 34 | // let res = await client.callApi('Test', { name: '小明同学' }); 35 | // console.log('收到回复', res); 36 | // } 37 | // catch (e) { 38 | // if (e.info === 'NETWORK_ERR') { 39 | // return; 40 | // } 41 | // console.log('API错误', e) 42 | // } 43 | // }, 1000); 44 | 45 | // client.listenMsg('Chat', msg => { 46 | // console.log('收到MSG', msg); 47 | // }); 48 | 49 | // setInterval(() => { 50 | // try { 51 | // client.sendMsg('Chat', { 52 | // channel: 123, 53 | // userName: '王小明', 54 | // content: '你好', 55 | // time: Date.now() 56 | // }).catch(e => { 57 | // console.log('SendMsg Failed', e.message) 58 | // }) 59 | // } 60 | // catch{ } 61 | // }, 1000) 62 | 63 | // #region Benchmark 64 | // let maxTime = 0; 65 | // let done = 0; 66 | // let startTime = Date.now(); 67 | 68 | // setTimeout(() => { 69 | // console.log('done', maxTime, done); 70 | // process.exit(); 71 | // }, 3000); 72 | 73 | // for (let i = 0; i < 10000; ++i) { 74 | // client.callApi('Test', { name: '小明同学' }).then(() => { 75 | // ++done; 76 | // maxTime = Math.max(maxTime, Date.now() - startTime) 77 | // }) 78 | // } 79 | // #endregion 80 | } 81 | 82 | main() 83 | -------------------------------------------------------------------------------- /test/try/massive.ts: -------------------------------------------------------------------------------- 1 | import { TSRPCClient } from ".." 2 | import { serviceProto, ServiceType } from "./proto/serviceProto" 3 | 4 | async function main() { 5 | setInterval(() => { 6 | for (let i = 0; i < 100; ++i) { 7 | let client = new TSRPCClient({ 8 | server: "ws://127.0.0.1:3000", 9 | proto: serviceProto, 10 | }) 11 | 12 | client 13 | .connect() 14 | .then(() => { 15 | client 16 | .callApi("a/b/c/Test1", { name: "小明同学" }) 17 | .then(v => { 18 | // console.log('成功', v) 19 | }) 20 | .catch(e => { 21 | console.error("错误", e.message) 22 | }) 23 | .then(() => { 24 | client.disconnect() 25 | }) 26 | }) 27 | .catch(e => { 28 | console.error("连接错误", e) 29 | }) 30 | } 31 | }, 1000) 32 | } 33 | 34 | main() 35 | -------------------------------------------------------------------------------- /test/try/no-res-issue/client/index.ts: -------------------------------------------------------------------------------- 1 | import { TsrpcClient } from "../../.." 2 | import { serviceProto } from "../server/protocols/proto" 3 | 4 | let client = new TsrpcClient({ 5 | proto: serviceProto, 6 | }) 7 | 8 | client 9 | .callApi("Test", { name: "ssss" }) 10 | .then(v => { 11 | console.log("then", v) 12 | }) 13 | .catch(e => { 14 | console.log("catch", e) 15 | }) 16 | -------------------------------------------------------------------------------- /test/try/no-res-issue/server/index.ts: -------------------------------------------------------------------------------- 1 | import { TsrpcServer } from "../../../index" 2 | import { serviceProto } from "./protocols/proto" 3 | 4 | let server = new TsrpcServer({ 5 | proto: serviceProto, 6 | }) 7 | 8 | server.autoImplementApi("src/api") 9 | 10 | server.start() 11 | -------------------------------------------------------------------------------- /test/try/no-res-issue/server/protocols/PtlTest.ts: -------------------------------------------------------------------------------- 1 | export interface ReqTest { 2 | name: string 3 | } 4 | 5 | export interface ResTest { 6 | reply: string 7 | } 8 | -------------------------------------------------------------------------------- /test/try/no-res-issue/server/protocols/proto.ts: -------------------------------------------------------------------------------- 1 | import { ServiceProto } from "tsrpc-proto" 2 | import { ReqTest, ResTest } from "./PtlTest" 3 | 4 | export interface ServiceType { 5 | req: { 6 | Test: ReqTest 7 | } 8 | res: { 9 | Test: ResTest 10 | } 11 | msg: {} 12 | } 13 | 14 | export const serviceProto: ServiceProto = { 15 | services: [ 16 | { 17 | id: 0, 18 | name: "Test", 19 | type: "api", 20 | req: "PtlTest/ReqTest", 21 | res: "PtlTest/ResTest", 22 | }, 23 | ], 24 | types: { 25 | "PtlTest/ReqTest": { 26 | type: "Interface", 27 | properties: [ 28 | { 29 | id: 0, 30 | name: "name", 31 | type: { 32 | type: "String", 33 | }, 34 | }, 35 | ], 36 | }, 37 | "PtlTest/ResTest": { 38 | type: "Interface", 39 | properties: [ 40 | { 41 | id: 0, 42 | name: "reply", 43 | type: { 44 | type: "String", 45 | }, 46 | }, 47 | ], 48 | }, 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /test/try/no-res-issue/server/src/api/ApiTest.ts: -------------------------------------------------------------------------------- 1 | import { ReqTest, ResTest } from "../../protocols/PtlTest" 2 | import { ApiCall } from "../../../../.." 3 | 4 | export async function ApiTest(call: ApiCall) { 5 | await new Promise(rs => { 6 | let i = 5 7 | call.logger.log(i) 8 | let interval = setInterval(() => { 9 | call.logger.log(--i) 10 | if (i === 0) { 11 | clearInterval(interval) 12 | rs() 13 | } 14 | }, 1000) 15 | }) 16 | 17 | call.error("asdfasdf", { a: 1, b: 2 }) 18 | } 19 | -------------------------------------------------------------------------------- /test/try/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.0", 9 | "license": "ISC" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/try/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /test/try/proto/MsgChat.ts: -------------------------------------------------------------------------------- 1 | export interface MsgChat { 2 | channel: number 3 | userName: string 4 | content: string 5 | time: number 6 | } 7 | -------------------------------------------------------------------------------- /test/try/proto/PtlTest.ts: -------------------------------------------------------------------------------- 1 | export interface ReqTest { 2 | name: string 3 | } 4 | 5 | export type ResTest = { 6 | reply: string 7 | } 8 | -------------------------------------------------------------------------------- /test/try/proto/a/b/c/PtlTest.ts: -------------------------------------------------------------------------------- 1 | import { MsgChat } from "../../../MsgChat" 2 | 3 | export interface ReqTest { 4 | name: string 5 | } 6 | 7 | export type ResTest = { 8 | reply: string 9 | chat?: MsgChat 10 | } 11 | -------------------------------------------------------------------------------- /test/try/proto/serviceProto.ts: -------------------------------------------------------------------------------- 1 | import { ServiceProto } from "tsrpc-proto" 2 | import { ReqTest, ResTest } from "./a/b/c/PtlTest" 3 | import { MsgChat } from "./MsgChat" 4 | import { ReqTest as ReqTest_1, ResTest as ResTest_1 } from "./PtlTest" 5 | 6 | export interface ServiceType { 7 | req: { 8 | "a/b/c/Test": ReqTest 9 | Test: ReqTest_1 10 | } 11 | res: { 12 | "a/b/c/Test": ResTest 13 | Test: ResTest_1 14 | } 15 | msg: { 16 | Chat: MsgChat 17 | } 18 | } 19 | 20 | export const serviceProto: ServiceProto = { 21 | services: [ 22 | { 23 | id: 0, 24 | name: "a/b/c/Test", 25 | type: "api", 26 | req: "a/b/c/PtlTest/ReqTest", 27 | res: "a/b/c/PtlTest/ResTest", 28 | }, 29 | { 30 | id: 1, 31 | name: "Chat", 32 | type: "msg", 33 | msg: "MsgChat/MsgChat", 34 | }, 35 | { 36 | id: 2, 37 | name: "Test", 38 | type: "api", 39 | req: "PtlTest/ReqTest", 40 | res: "PtlTest/ResTest", 41 | }, 42 | ], 43 | types: { 44 | "a/b/c/PtlTest/ReqTest": { 45 | type: "Interface", 46 | properties: [ 47 | { 48 | id: 0, 49 | name: "name", 50 | type: { 51 | type: "String", 52 | }, 53 | }, 54 | ], 55 | }, 56 | "a/b/c/PtlTest/ResTest": { 57 | type: "Interface", 58 | properties: [ 59 | { 60 | id: 0, 61 | name: "reply", 62 | type: { 63 | type: "String", 64 | }, 65 | }, 66 | { 67 | id: 1, 68 | name: "chat", 69 | type: { 70 | type: "Reference", 71 | target: "MsgChat/MsgChat", 72 | }, 73 | optional: true, 74 | }, 75 | ], 76 | }, 77 | "MsgChat/MsgChat": { 78 | type: "Interface", 79 | properties: [ 80 | { 81 | id: 0, 82 | name: "channel", 83 | type: { 84 | type: "Number", 85 | }, 86 | }, 87 | { 88 | id: 1, 89 | name: "userName", 90 | type: { 91 | type: "String", 92 | }, 93 | }, 94 | { 95 | id: 2, 96 | name: "content", 97 | type: { 98 | type: "String", 99 | }, 100 | }, 101 | { 102 | id: 3, 103 | name: "time", 104 | type: { 105 | type: "Number", 106 | }, 107 | }, 108 | ], 109 | }, 110 | "PtlTest/ReqTest": { 111 | type: "Interface", 112 | properties: [ 113 | { 114 | id: 0, 115 | name: "name", 116 | type: { 117 | type: "String", 118 | }, 119 | }, 120 | ], 121 | }, 122 | "PtlTest/ResTest": { 123 | type: "Interface", 124 | properties: [ 125 | { 126 | id: 0, 127 | name: "reply", 128 | type: { 129 | type: "String", 130 | }, 131 | }, 132 | ], 133 | }, 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /test/try/proto/typeProto.json: -------------------------------------------------------------------------------- 1 | { 2 | "MsgChat/MsgChat": { 3 | "type": "Interface", 4 | "properties": [ 5 | { 6 | "id": 0, 7 | "name": "channel", 8 | "type": { 9 | "type": "String" 10 | } 11 | }, 12 | { 13 | "id": 1, 14 | "name": "userName", 15 | "type": { 16 | "type": "String" 17 | } 18 | }, 19 | { 20 | "id": 2, 21 | "name": "content", 22 | "type": { 23 | "type": "String" 24 | } 25 | }, 26 | { 27 | "id": 3, 28 | "name": "time", 29 | "type": { 30 | "type": "Number" 31 | } 32 | } 33 | ] 34 | }, 35 | "PtlTest/ReqTest": { 36 | "type": "Interface", 37 | "properties": [ 38 | { 39 | "id": 0, 40 | "name": "name", 41 | "type": { 42 | "type": "String" 43 | } 44 | } 45 | ] 46 | }, 47 | "PtlTest/ResTest": { 48 | "type": "Interface", 49 | "properties": [ 50 | { 51 | "id": 0, 52 | "name": "reply", 53 | "type": { 54 | "type": "String" 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/try/server/api/ApiTest.ts: -------------------------------------------------------------------------------- 1 | import { TsrpcError } from "tsrpc-proto" 2 | import { ApiCallHttp } from "../../../src/server/http/HttpCall" 3 | import { ReqTest } from "../../proto/PtlTest" 4 | import { ResTest } from "../../proto/a/b/c/PtlTest" 5 | 6 | export async function ApiTest(call: ApiCallHttp) { 7 | if (Math.random() > 0.75) { 8 | call.succ({ 9 | reply: "Hello, " + call.req.name, 10 | }) 11 | } else if (Math.random() > 0.5) { 12 | call.error("What the fuck??", { msg: "哈哈哈哈" }) 13 | } else if (Math.random() > 0.25) { 14 | throw new Error("这应该是InternalERROR") 15 | } else { 16 | throw new TsrpcError("返回到前台的错误", "ErrInfo") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/try/server/api/a/b/c/ApiTest.ts: -------------------------------------------------------------------------------- 1 | export async function ApiTest(call: any) { 2 | call.succ({ 3 | reply: "Api Test1 Succ", 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /test/try/server/http.ts: -------------------------------------------------------------------------------- 1 | import { serviceProto } from "../proto/serviceProto" 2 | import * as path from "path" 3 | import { TsrpcServer } from "../../index" 4 | 5 | let server = new TsrpcServer({ 6 | proto: serviceProto, 7 | }) 8 | 9 | server.dataFlow.push((data, conn) => { 10 | let httpReq = conn.options.httpReq 11 | if (httpReq.method === "GET") { 12 | conn.logger.log("url", httpReq.url) 13 | conn.logger.log("host", httpReq.headers.host) 14 | conn.options.httpRes.end("Hello~~~") 15 | return false 16 | } 17 | 18 | return true 19 | }) 20 | 21 | server.autoImplementApi(path.resolve(__dirname, "api")) 22 | server.start() 23 | -------------------------------------------------------------------------------- /test/try/server/ws.ts: -------------------------------------------------------------------------------- 1 | import { serviceProto, ServiceType } from "../proto/serviceProto" 2 | import * as path from "path" 3 | import { TsrpcServerWs } from "../../index" 4 | 5 | let server = new TsrpcServerWs({ 6 | proto: serviceProto, 7 | }) 8 | server.start() 9 | 10 | server.autoImplementApi(path.resolve(__dirname, "api")) 11 | server.listenMsg("Chat", v => { 12 | v.conn.sendMsg("Chat", { 13 | channel: v.msg.channel, 14 | userName: "SYSTEM", 15 | content: "收到", 16 | time: Date.now(), 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/try/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | /* Strict Type-Checking Options */ 25 | "strict": true /* Enable all strict type-checking options. */, 26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 43 | // "typeRoots": [], /* List of folders to include type definitions from. */ 44 | // "types": [], /* Type declaration files to be included in compilation. */ 45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 48 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 49 | /* Source Map Options */ 50 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 51 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 54 | /* Experimental Options */ 55 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 56 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 57 | "useUnknownInCatchVariables": false 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [ 7 | // "es5", 8 | // "dom" 9 | // ], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./lib", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "incremental": true, /* Enable incremental compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | "noEmit": true /* Do not emit outputs. */, 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true /* Do not resolve the real path of symlinks. */ 51 | /* Source Map Options */ 52 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | "useUnknownInCatchVariables": false, 60 | "skipLibCheck": true 61 | }, 62 | "include": [ 63 | "../src" 64 | // "test" 65 | // "benchmark" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [ 7 | // "es5", 8 | // "dom" 9 | // ], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./lib", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "incremental": true, /* Enable incremental compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true /* Do not resolve the real path of symlinks. */ 51 | /* Source Map Options */ 52 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | "useUnknownInCatchVariables": false, 60 | "skipLibCheck": true 61 | }, 62 | "include": [ 63 | "src" 64 | // "test" 65 | // "benchmark" 66 | ] 67 | } --------------------------------------------------------------------------------