[add] first
This commit is contained in:
		
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| dist | ||||
| node_modules | ||||
| yarn.lock | ||||
| logs | ||||
| .rpt2_cache | ||||
| .nyc_output | ||||
| coverage | ||||
| docs | ||||
| temp | ||||
| lib | ||||
| .ds_store | ||||
							
								
								
									
										22
									
								
								.mocharc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.mocharc.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| module.exports = { | ||||
|     require: [ | ||||
|         'ts-node/register', | ||||
|         './test/Base.ts' | ||||
|     ], | ||||
|     exit: true, | ||||
|     timeout: 999999, | ||||
|     'preserve-symlinks': true, | ||||
|     spec: [ | ||||
|         './test/cases/http.test.ts', | ||||
|         './test/cases/httpJSON.test.ts', | ||||
|         './test/cases/ws.test.ts', | ||||
|         './test/cases/wsJSON.test.ts', | ||||
|         './test/cases/inner.test.ts', | ||||
|         './test/cases/inputJSON.test.ts', | ||||
|         './test/cases/inputBuffer.test.ts', | ||||
|     ], | ||||
|     // parallel: false, | ||||
|  | ||||
|     // 'expose-gc': true, | ||||
|     // fgrep: 'throw type error in server' | ||||
| } | ||||
							
								
								
									
										34
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| { | ||||
|     // 使用 IntelliSense 了解相关属性。  | ||||
|     // 悬停以查看现有属性的描述。 | ||||
|     // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 | ||||
|     "version": "0.2.0", | ||||
|     "configurations": [ | ||||
|         { | ||||
|             "type": "node", | ||||
|             "request": "launch", | ||||
|             "name": "mocha current file", | ||||
|             "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", | ||||
|             "args": [ | ||||
|                 "${file}" | ||||
|             ], | ||||
|             "internalConsoleOptions": "openOnSessionStart", | ||||
|             "cwd": "${workspaceFolder}" | ||||
|         }, | ||||
|         { | ||||
|             "type": "node", | ||||
|             "request": "launch", | ||||
|             "name": "ts-node current file", | ||||
|             "protocol": "inspector", | ||||
|             "args": [ | ||||
|                 "${relativeFile}" | ||||
|             ], | ||||
|             "cwd": "${workspaceRoot}", | ||||
|             "runtimeArgs": [ | ||||
|                 "-r", | ||||
|                 "ts-node/register" | ||||
|             ], | ||||
|             "internalConsoleOptions": "openOnSessionStart" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|     "typescript.tsdk": "node_modules\\typescript\\lib" | ||||
| } | ||||
							
								
								
									
										161
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| # CHANGELOG | ||||
|  | ||||
| ## [3.3.1-dev.0] - 2022-04-27 | ||||
| ### Fixed | ||||
| - `HttpConnection.status` not correct when request aborted by client | ||||
|  | ||||
| ## [3.3.0] - 2022-04-15 | ||||
| ### Added | ||||
| - Builtin heartbeat support | ||||
| - New options `logLevel` | ||||
| ### Fixed | ||||
| - Add response header `Content-Type: application/json; charset=utf-8` for JSON mode under HttpServer, to fix the decoding issue in Chrome dev tools. | ||||
|  | ||||
| ## [3.2.5] - 2022-04-12 | ||||
| ### Added | ||||
| - New server options `corsMaxAge` to optimized preflight requests, default value is 3600. | ||||
| ### Fixed | ||||
| - `NonNullable` cannot be encoded and decoded when as a property in interface | ||||
|  | ||||
| ## [3.2.3] - 2022-03-25 | ||||
| ### Added | ||||
| - Print debug-level log when "pre flow" is canceled | ||||
| ### Changed | ||||
| - Log `[ResErr]` renamed to `[ApiErr]` to consist with client's. | ||||
| - Log `ApiRes` and `ApiErr` once they are ready to send, instead of after send them. | ||||
| ### Fixed | ||||
| - When `preSendDataFlow` return undefined, do not send "Internal Server Error". | ||||
| - Remove some unused code. | ||||
|  | ||||
| ## [3.2.2] - 2022-03-22 | ||||
| ### Fixed | ||||
| - `postDisconnectFlow` not executed when `disconnect()` manually | ||||
|  | ||||
|  | ||||
| ## [3.2.1] - 2022-03-21 | ||||
| ### Added | ||||
| - `preRecvDataFlow` add param `serviceName` | ||||
| - Support change `dataType` in `postConnectFlow` | ||||
| ### Fixed | ||||
| - Remark text error | ||||
|  | ||||
| ## [3.2.0] - 2022-02-26 | ||||
| ### Added | ||||
| - Support using `keyof` | ||||
| - Support type alias and `keyof` in `Pick` and `Omit` | ||||
| - Support `Pick<Intersection>` and `Omit<Intersection>` | ||||
| - Support `interface` extends Mapped Type, like `Pick` `Omit` | ||||
| - Support `Pick<XXX, keyof XXX>` | ||||
| - Support `Pick<XXX, TypeReference>` | ||||
| - Support `Pick<UnionType>` and `Pick<IntersectionType>`, the same to `Omit` | ||||
| - Support reference enum value as literal type,like: | ||||
|     ```ts | ||||
|     export enum Types { | ||||
|         Type1, | ||||
|         Type2 | ||||
|     } | ||||
|     export interface Obj { | ||||
|         type: Types.Type1, | ||||
|         value: string | ||||
|     } | ||||
|     ``` | ||||
| ### Changed | ||||
| - `SchemaType` switched to class | ||||
|  | ||||
| ## [3.1.9] - 2022-01-12 | ||||
| ### Added | ||||
| - `mongodb-polyfill.d.ts` to fixed mongodb type bug. | ||||
|  | ||||
| ## [3.1.6] - 2021-12-29 | ||||
| ### Changed | ||||
| - Return request type error detail when using JSON | ||||
|  | ||||
| ## [3.1.5] - 2021-12-23 | ||||
| ### Fixed | ||||
| - Optimize aliyun FC support of `server.inputJSON` | ||||
|  | ||||
| ## [3.1.4] - 2021-12-18 | ||||
| ### Added | ||||
| - `WsServer` now support client use `buffer` as transfering format when server set `json: true` | ||||
| ### Fixed | ||||
| - Type error when disable `skipLibChecks` | ||||
| - Cannot resolve JSON when `headers` is `application/json; charset=utf-8` | ||||
| - Cannot resolve serviceName when there is query string in the URL | ||||
|  | ||||
| ## [3.1.3] - 2021-12-04 | ||||
| ### Added | ||||
| - `conn.listenMsg` | ||||
| ### Fixed | ||||
| - Do not `broadcastMsg` when `conns.length` is `0` | ||||
|  | ||||
| ## [3.1.2] - 2021-11-17 | ||||
| ### Added | ||||
| - `server.inputJSON` and `server.inputBuffer` | ||||
| - Add new dataType `json` | ||||
|  | ||||
| ## [3.1.1] - 2021-11-09 | ||||
| ### Added | ||||
| - HTTP Text 传输模式下,区分 HTTP 状态码返回,不再统一返回 200 | ||||
|  | ||||
| ## [3.1.0] - 2021-11-08 | ||||
| ### Added | ||||
| - WebSocket 支持 JSON 格式传输 | ||||
| - JSON 格式传输支持 `ArrayBuffer`、`Date`、`ObjectId`,自动根据协议编解码为 `string` | ||||
| ### Changed | ||||
| - `jsonEnabled` -> `json` | ||||
|  | ||||
| ## [3.0.14] - 2021-10-25 | ||||
| ### Added | ||||
| - 增加 `server.autoImplementApi` 第二个参数 `delay`,用于延迟自动协议注册,加快冷启动速度。 | ||||
|  | ||||
| ## [3.0.13] - 2021-10-22 | ||||
| ### Added | ||||
| - 增加 `server.callApi` 的支持,以更方便的适配 Serverless 云函数等自定义传输场景。 | ||||
|  | ||||
| ## [3.0.12] - 2021-10-22 | ||||
| ### Fixed | ||||
| - 修复 `WsServer` 客户端断开连接后,日志显示的 `ActiveConn` 总是比实际多 1 的 BUG | ||||
|  | ||||
| ## [3.0.11] - 2021-10-18 | ||||
| ### Added | ||||
| - 增加对 `mongodb/ObjectId` 的支持 | ||||
|  | ||||
| ## [3.0.10] - 2021-10-13 | ||||
| ### Changed | ||||
| - `BaseConnection` 泛型参数默认为 `any`,便于扩展类型 | ||||
| - `HttpClient` and `WsClient` no longer have default type param | ||||
|  | ||||
| ## [3.0.9] - 2021-10-06 | ||||
| ### Changed | ||||
| - `strictNullChecks` 默认改为 `false` | ||||
|  | ||||
| ## [3.0.8] - 2021-10-06 | ||||
| ### Added | ||||
| - Optimize log level | ||||
|  | ||||
| ## [3.0.7] - 2021-10-06 | ||||
| ### Added | ||||
| - Optimize log color | ||||
| ## [3.0.6] - 2021-09-30 | ||||
| ### Added | ||||
| - "Server started at ..." 前增加 "ERROR:X API registered failed." | ||||
| ### Changed | ||||
| - `HttpServer.onInputBufferError` 改为 `call.error('InputBufferError')` | ||||
| - 替换 `colors` 为 `chalk` | ||||
|  | ||||
| ## [3.0.5] - 2021-08-14 | ||||
| ### Added | ||||
| - Optimize log for `sendMsg` and `broadcastMsg` | ||||
| - Return `Internal Server Error` when `SendReturnErr` occured | ||||
|  | ||||
| ### Changed | ||||
| - Remove error `API not return anything` | ||||
| - handler of `client.listenMsg` changed to `(msg, msgName, client)=>void`  | ||||
|  | ||||
| ### Fixed | ||||
| - NodeJS 12 compability issue (`Uint8Array` and `Buffer` is not treated samely) | ||||
|  | ||||
| ## [3.0.3] - 2021-06-27 | ||||
|  | ||||
| ### Added | ||||
| - `server.listenMsg` would return `handler` that passed in | ||||
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) King Wang. https://github.com/k8w | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||||
| IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | ||||
| DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||||
| OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE | ||||
| OR OTHER DEALINGS IN THE SOFTWARE. | ||||
							
								
								
									
										70
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| # TSRPC | ||||
|  | ||||
| EN / [中文](https://tsrpc.cn/docs/introduction.html) | ||||
|  | ||||
| A TypeScript RPC framework with runtime type checking and binary serialization. | ||||
|  | ||||
| Official site: https://tsrpc.cn (English version is on the way) | ||||
|  | ||||
| ## Features | ||||
| - Runtime type checking | ||||
| - Binary serialization | ||||
| - Pure TypeScript, without any decorater or other language | ||||
| - HTTP / WebSocket / and more protocols... | ||||
| - Optional backward-compatibility to JSON | ||||
| - High performance and reliable, verified by services over 100,000,000 users | ||||
|  | ||||
| ## Create Full-stack Project | ||||
| ``` | ||||
| npx create-tsrpc-app@latest | ||||
| ``` | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ### Define Protocol (Shared) | ||||
| ```ts | ||||
| export interface ReqHello { | ||||
|   name: string; | ||||
| } | ||||
|  | ||||
| export interface ResHello { | ||||
|   reply: string; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Implement API (Server) | ||||
| ```ts | ||||
| import { ApiCall } from "tsrpc"; | ||||
|  | ||||
| export async function ApiHello(call: ApiCall<ReqHello, ResHello>) { | ||||
|   call.succ({ | ||||
|     reply: 'Hello, ' + call.req.name | ||||
|   }); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Call API (Client) | ||||
| ```ts | ||||
| let ret = await client.callApi('Hello', { | ||||
|     name: 'World' | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| https://github.com/k8w/tsrpc-examples | ||||
|  | ||||
| ## Serialization Algorithm | ||||
| The best TypeScript serialization algorithm ever. | ||||
| Without any 3rd-party IDL language (like protobuf), it is fully based on TypeScript source file. Define the protocols directly by your code. | ||||
|  | ||||
| This is powered by [TSBuffer](https://github.com/tsbuffer), which is going to be open-source. | ||||
|  | ||||
| TypeScript has the best type system, with some unique advanced features like union type, intersection type, mapped type, etc. | ||||
|  | ||||
| TSBuffer may be the only serialization algorithm that support them all. | ||||
|  | ||||
|  | ||||
|  | ||||
| ## API Reference | ||||
| See [API Reference](./docs/api/tsrpc.md). | ||||
							
								
								
									
										345
									
								
								api-extractor.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								api-extractor.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,345 @@ | ||||
| /** | ||||
|  * Config file for API Extractor.  For more info, please visit: https://api-extractor.com | ||||
|  */ | ||||
| { | ||||
|   "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", | ||||
|   /** | ||||
|    * Optionally specifies another JSON config file that this file extends from.  This provides a way for | ||||
|    * standard settings to be shared across multiple projects. | ||||
|    * | ||||
|    * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains | ||||
|    * the "extends" field.  Otherwise, the first path segment is interpreted as an NPM package name, and will be | ||||
|    * resolved using NodeJS require(). | ||||
|    * | ||||
|    * SUPPORTED TOKENS: none | ||||
|    * DEFAULT VALUE: "" | ||||
|    */ | ||||
|   // "extends": "./shared/api-extractor-base.json" | ||||
|   // "extends": "my-package/include/api-extractor-base.json" | ||||
|   /** | ||||
|    * Determines the "<projectFolder>" token that can be used with other config file settings.  The project folder | ||||
|    * typically contains the tsconfig.json and package.json config files, but the path is user-defined. | ||||
|    * | ||||
|    * The path is resolved relative to the folder of the config file that contains the setting. | ||||
|    * | ||||
|    * The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing | ||||
|    * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder | ||||
|    * that contains a tsconfig.json file.  If a tsconfig.json file cannot be found in this way, then an error | ||||
|    * will be reported. | ||||
|    * | ||||
|    * SUPPORTED TOKENS: <lookup> | ||||
|    * DEFAULT VALUE: "<lookup>" | ||||
|    */ | ||||
|   // "projectFolder": "..", | ||||
|   /** | ||||
|    * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis.  API Extractor | ||||
|    * analyzes the symbols exported by this module. | ||||
|    * | ||||
|    * The file extension must be ".d.ts" and not ".ts". | ||||
|    * | ||||
|    * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|    * prepend a folder token such as "<projectFolder>". | ||||
|    * | ||||
|    * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|    */ | ||||
|   "mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts", | ||||
|   /** | ||||
|    * A list of NPM package names whose exports should be treated as part of this package. | ||||
|    * | ||||
|    * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", | ||||
|    * and another NPM package "library2" is embedded in this bundle.  Some types from library2 may become part | ||||
|    * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly | ||||
|    * imports library2.  To avoid this, we can specify: | ||||
|    * | ||||
|    *   "bundledPackages": [ "library2" ], | ||||
|    * | ||||
|    * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been | ||||
|    * local files for library1. | ||||
|    */ | ||||
|   "bundledPackages": [], | ||||
|   /** | ||||
|    * Determines how the TypeScript compiler engine will be invoked by API Extractor. | ||||
|    */ | ||||
|   "compiler": { | ||||
|     /** | ||||
|      * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * Note: This setting will be ignored if "overrideTsconfig" is used. | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "<projectFolder>/tsconfig.json" | ||||
|      */ | ||||
|     // "tsconfigFilePath": "<projectFolder>/tsconfig.json", | ||||
|     /** | ||||
|      * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. | ||||
|      * The object must conform to the TypeScript tsconfig schema: | ||||
|      * | ||||
|      * http://json.schemastore.org/tsconfig | ||||
|      * | ||||
|      * If omitted, then the tsconfig.json file will be read from the "projectFolder". | ||||
|      * | ||||
|      * DEFAULT VALUE: no overrideTsconfig section | ||||
|      */ | ||||
|     // "overrideTsconfig": { | ||||
|     //   . . . | ||||
|     // } | ||||
|     /** | ||||
|      * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended | ||||
|      * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when | ||||
|      * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses | ||||
|      * for its analysis.  Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. | ||||
|      * | ||||
|      * DEFAULT VALUE: false | ||||
|      */ | ||||
|     // "skipLibCheck": true, | ||||
|   }, | ||||
|   /** | ||||
|    * Configures how the API report file (*.api.md) will be generated. | ||||
|    */ | ||||
|   "apiReport": { | ||||
|     /** | ||||
|      * (REQUIRED) Whether to generate an API report. | ||||
|      */ | ||||
|     "enabled": false | ||||
|     /** | ||||
|      * The filename for the API report files.  It will be combined with "reportFolder" or "reportTempFolder" to produce | ||||
|      * a full file path. | ||||
|      * | ||||
|      * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "<unscopedPackageName>.api.md" | ||||
|      */ | ||||
|     // "reportFileName": "<unscopedPackageName>.api.md", | ||||
|     /** | ||||
|      * Specifies the folder where the API report file is written.  The file name portion is determined by | ||||
|      * the "reportFileName" setting. | ||||
|      * | ||||
|      * The API report file is normally tracked by Git.  Changes to it can be used to trigger a branch policy, | ||||
|      * e.g. for an API review. | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "<projectFolder>/etc/" | ||||
|      */ | ||||
|     // "reportFolder": "<projectFolder>/etc/", | ||||
|     /** | ||||
|      * Specifies the folder where the temporary report file is written.  The file name portion is determined by | ||||
|      * the "reportFileName" setting. | ||||
|      * | ||||
|      * After the temporary file is written to disk, it is compared with the file in the "reportFolder". | ||||
|      * If they are different, a production build will fail. | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "<projectFolder>/temp/" | ||||
|      */ | ||||
|     // "reportTempFolder": "<projectFolder>/temp/" | ||||
|   }, | ||||
|   /** | ||||
|    * Configures how the doc model file (*.api.json) will be generated. | ||||
|    */ | ||||
|   "docModel": { | ||||
|     /** | ||||
|      * (REQUIRED) Whether to generate a doc model file. | ||||
|      */ | ||||
|     "enabled": true | ||||
|     /** | ||||
|      * The output path for the doc model file.  The file extension should be ".api.json". | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json" | ||||
|      */ | ||||
|     // "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json" | ||||
|   }, | ||||
|   /** | ||||
|    * Configures how the .d.ts rollup file will be generated. | ||||
|    */ | ||||
|   "dtsRollup": { | ||||
|     /** | ||||
|      * (REQUIRED) Whether to generate the .d.ts rollup file. | ||||
|      */ | ||||
|     "enabled": true, | ||||
|     /** | ||||
|      * Specifies the output path for a .d.ts rollup file to be generated without any trimming. | ||||
|      * This file will include all declarations that are exported by the main entry point. | ||||
|      * | ||||
|      * If the path is an empty string, then this file will not be written. | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts" | ||||
|      */ | ||||
|     "untrimmedFilePath": "", | ||||
|     /** | ||||
|      * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. | ||||
|      * This file will include only declarations that are marked as "@public" or "@beta". | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "" | ||||
|      */ | ||||
|     // "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts", | ||||
|     /** | ||||
|      * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. | ||||
|      * This file will include only declarations that are marked as "@public". | ||||
|      * | ||||
|      * If the path is an empty string, then this file will not be written. | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "" | ||||
|      */ | ||||
|     "publicTrimmedFilePath": "<projectFolder>/dist/index.d.ts", | ||||
|     /** | ||||
|      * When a declaration is trimmed, by default it will be replaced by a code comment such as | ||||
|      * "Excluded from this release type: exampleMember".  Set "omitTrimmingComments" to true to remove the | ||||
|      * declaration completely. | ||||
|      * | ||||
|      * DEFAULT VALUE: false | ||||
|      */ | ||||
|     "omitTrimmingComments": true | ||||
|   }, | ||||
|   /** | ||||
|    * Configures how the tsdoc-metadata.json file will be generated. | ||||
|    */ | ||||
|   "tsdocMetadata": { | ||||
|     /** | ||||
|      * Whether to generate the tsdoc-metadata.json file. | ||||
|      * | ||||
|      * DEFAULT VALUE: true | ||||
|      */ | ||||
|     "enabled": false | ||||
|     /** | ||||
|      * Specifies where the TSDoc metadata file should be written. | ||||
|      * | ||||
|      * The path is resolved relative to the folder of the config file that contains the setting; to change this, | ||||
|      * prepend a folder token such as "<projectFolder>". | ||||
|      * | ||||
|      * The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata", | ||||
|      * "typings" or "main" fields of the project's package.json.  If none of these fields are set, the lookup | ||||
|      * falls back to "tsdoc-metadata.json" in the package folder. | ||||
|      * | ||||
|      * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> | ||||
|      * DEFAULT VALUE: "<lookup>" | ||||
|      */ | ||||
|     // "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json" | ||||
|   }, | ||||
|   /** | ||||
|    * Specifies what type of newlines API Extractor should use when writing output files.  By default, the output files | ||||
|    * will be written with Windows-style newlines.  To use POSIX-style newlines, specify "lf" instead. | ||||
|    * To use the OS's default newline kind, specify "os". | ||||
|    * | ||||
|    * DEFAULT VALUE: "crlf" | ||||
|    */ | ||||
|   // "newlineKind": "crlf", | ||||
|   /** | ||||
|    * Configures how API Extractor reports error and warning messages produced during analysis. | ||||
|    * | ||||
|    * There are three sources of messages:  compiler messages, API Extractor messages, and TSDoc messages. | ||||
|    */ | ||||
|   "messages": { | ||||
|     /** | ||||
|      * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing | ||||
|      * the input .d.ts files. | ||||
|      * | ||||
|      * TypeScript message identifiers start with "TS" followed by an integer.  For example: "TS2551" | ||||
|      * | ||||
|      * DEFAULT VALUE:  A single "default" entry with logLevel=warning. | ||||
|      */ | ||||
|     "compilerMessageReporting": { | ||||
|       /** | ||||
|        * Configures the default routing for messages that don't match an explicit rule in this table. | ||||
|        */ | ||||
|       "default": { | ||||
|         /** | ||||
|          * Specifies whether the message should be written to the the tool's output log.  Note that | ||||
|          * the "addToApiReportFile" property may supersede this option. | ||||
|          * | ||||
|          * Possible values: "error", "warning", "none" | ||||
|          * | ||||
|          * Errors cause the build to fail and return a nonzero exit code.  Warnings cause a production build fail | ||||
|          * and return a nonzero exit code.  For a non-production build (e.g. when "api-extractor run" includes | ||||
|          * the "--local" option), the warning is displayed but the build will not fail. | ||||
|          * | ||||
|          * DEFAULT VALUE: "warning" | ||||
|          */ | ||||
|         "logLevel": "warning" | ||||
|         /** | ||||
|          * When addToApiReportFile is true:  If API Extractor is configured to write an API report file (.api.md), | ||||
|          * then the message will be written inside that file; otherwise, the message is instead logged according to | ||||
|          * the "logLevel" option. | ||||
|          * | ||||
|          * DEFAULT VALUE: false | ||||
|          */ | ||||
|         // "addToApiReportFile": false | ||||
|       } | ||||
|       // "TS2551": { | ||||
|       //   "logLevel": "warning", | ||||
|       //   "addToApiReportFile": true | ||||
|       // }, | ||||
|       // | ||||
|       // . . . | ||||
|     }, | ||||
|     /** | ||||
|      * Configures handling of messages reported by API Extractor during its analysis. | ||||
|      * | ||||
|      * API Extractor message identifiers start with "ae-".  For example: "ae-extra-release-tag" | ||||
|      * | ||||
|      * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings | ||||
|      */ | ||||
|     "extractorMessageReporting": { | ||||
|       "default": { | ||||
|         "logLevel": "warning" | ||||
|         // "addToApiReportFile": false | ||||
|       }, | ||||
|       "ae-missing-release-tag": { | ||||
|         "logLevel": "none" | ||||
|       } | ||||
|       // "ae-extra-release-tag": { | ||||
|       //   "logLevel": "warning", | ||||
|       //   "addToApiReportFile": true | ||||
|       // }, | ||||
|       // | ||||
|       // . . . | ||||
|     }, | ||||
|     /** | ||||
|      * Configures handling of messages reported by the TSDoc parser when analyzing code comments. | ||||
|      * | ||||
|      * TSDoc message identifiers start with "tsdoc-".  For example: "tsdoc-link-tag-unescaped-text" | ||||
|      * | ||||
|      * DEFAULT VALUE:  A single "default" entry with logLevel=warning. | ||||
|      */ | ||||
|     "tsdocMessageReporting": { | ||||
|       "default": { | ||||
|         "logLevel": "warning" | ||||
|         // "addToApiReportFile": false | ||||
|       }, | ||||
|       "tsdoc-param-tag-missing-hyphen": { | ||||
|         "logLevel": "none" | ||||
|       } | ||||
|       // "tsdoc-link-tag-unescaped-text": { | ||||
|       //   "logLevel": "warning", | ||||
|       //   "addToApiReportFile": true | ||||
|       // }, | ||||
|       // | ||||
|       // . . . | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								benchmark/config/BenchmarkConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								benchmark/config/BenchmarkConfig.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| export const benchmarkConfig = { | ||||
|     /** 压测使用的APIServer */ | ||||
|     server: 'http://127.0.0.1:3000', | ||||
|  | ||||
|     /** 一共运行几次压测事务 */ | ||||
|     total: 200000, | ||||
|     /** 同时并发的请求数量 */ | ||||
|     concurrency: 100, | ||||
|     /** API请求的超时时间(超时将断开HTTP连接,释放资源,前端默认为10) */ | ||||
|     timeout: 10000, | ||||
|     /** 是否将错误的详情日志打印到Log */ | ||||
|     showError: false | ||||
| } | ||||
							
								
								
									
										13
									
								
								benchmark/http.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								benchmark/http.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { benchmarkConfig } from './config/BenchmarkConfig'; | ||||
| import { HttpRunner } from './models/HTTPRunner'; | ||||
|  | ||||
| const req = { | ||||
|     a: 123456, | ||||
|     b: 'Hello, World!', | ||||
|     c: true, | ||||
|     d: new Uint8Array(100000) | ||||
| } | ||||
|  | ||||
| new HttpRunner(async function () { | ||||
|     await this.callApi('Test', req); | ||||
| }, benchmarkConfig).start(); | ||||
							
								
								
									
										285
									
								
								benchmark/models/HTTPRunner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								benchmark/models/HTTPRunner.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | ||||
| import 'colors'; | ||||
| import * as http from "http"; | ||||
| import * as https from "https"; | ||||
| import 'k8w-extend-native'; | ||||
| import { TsrpcError, TsrpcErrorType } from "tsrpc-proto"; | ||||
| import { HttpClient } from '../../src/client/http/HttpClient'; | ||||
| import { benchmarkConfig } from "../config/BenchmarkConfig"; | ||||
| import { serviceProto } from '../protocols/proto'; | ||||
|  | ||||
| export interface HttpRunnerConfig { | ||||
|     total: number; | ||||
|     concurrency: number; | ||||
|     showError?: boolean; | ||||
| } | ||||
|  | ||||
| export class HttpRunner { | ||||
|  | ||||
|     private _config: HttpRunnerConfig; | ||||
|  | ||||
|     // 执行单个事务的方法 | ||||
|     private _single: (this: HttpRunner) => Promise<void>; | ||||
|  | ||||
|     // 执行进度信息 | ||||
|     private _progress?: { | ||||
|         startTime: number, | ||||
|         lastSuccTime?: number, | ||||
|         started: number, | ||||
|         finished: number, | ||||
|         succ: number, | ||||
|         fail: number | ||||
|     }; | ||||
|  | ||||
|     constructor(single: HttpRunner['_single'], config: HttpRunnerConfig) { | ||||
|         this._single = single.bind(this); | ||||
|         this._config = config; | ||||
|     } | ||||
|  | ||||
|     start() { | ||||
|         this._progress = { | ||||
|             startTime: Date.now(), | ||||
|             started: 0, | ||||
|             finished: 0, | ||||
|             succ: 0, | ||||
|             fail: 0 | ||||
|         } | ||||
|  | ||||
|         // 启动并发 | ||||
|         for (let i = 0; i < this._config.concurrency; ++i) { | ||||
|             this._doTrans(); | ||||
|         } | ||||
|  | ||||
|         console.log('Benchmark start!'); | ||||
|         this._startReport(); | ||||
|     } | ||||
|  | ||||
|     private _doTrans() { | ||||
|         if (this._isStoped || !this._progress) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (this._progress.started < this._config.total) { | ||||
|             ++this._progress.started; | ||||
|             let startTime = Date.now(); | ||||
|             this._single().then(v => { | ||||
|                 ++this._progress!.succ; | ||||
|                 this._progress!.lastSuccTime = Date.now(); | ||||
|             }).catch(e => { | ||||
|                 ++this._progress!.fail; | ||||
|                 if (this._config.showError) { | ||||
|                     console.error('[Error]', e.message); | ||||
|                 } | ||||
|             }).then(() => { | ||||
|                 ++this._progress!.finished; | ||||
|                 if (this._progress!.finished === this._config.total) { | ||||
|                     this._finish(); | ||||
|                 } | ||||
|                 else { | ||||
|                     this._doTrans(); | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private _reportInterval?: NodeJS.Timeout; | ||||
|     private _startReport() { | ||||
|         this._reportInterval = setInterval(() => { | ||||
|             this._report(); | ||||
|         }, 1000) | ||||
|     } | ||||
|  | ||||
|     private _isStoped = false; | ||||
|     stop() { | ||||
|         this._isStoped = true; | ||||
|     } | ||||
|  | ||||
|     private _finish() { | ||||
|         if (!this._progress) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this._reportInterval && clearInterval(this._reportInterval); | ||||
|  | ||||
|         console.log('\n\n-------------------------------\n  Benchmark finished!  \n-------------------------------'); | ||||
|  | ||||
|         let usedTime = Date.now() - this._progress.startTime; | ||||
|         console.log(`  Transaction Execution Result  `.bgBlue.white); | ||||
|         console.log(`Started=${this._progress.started}, Finished=${this._progress.finished}, UsedTime=${usedTime}ms`.green); | ||||
|         console.log(`Succ=${this._progress.succ}, Fail=${this._progress.fail}, TPS=${this._progress.succ / (this._progress.lastSuccTime! - this._progress.startTime) * 1000 | 0}\n`.green) | ||||
|  | ||||
|         // TIME TPS(完成的) | ||||
|         console.log(`  API Execution Result  `.bgBlue.white); | ||||
|  | ||||
|         // [KEY] RPS(完成的) AVG P95 P99 | ||||
|         for (let key in this._apiStat) { | ||||
|             let stat = this._apiStat[key]; | ||||
|             stat.resTime = stat.resTime.orderBy(v => v); | ||||
|  | ||||
|             let send = stat.sendReq; | ||||
|             let succ = stat.resTime.length; | ||||
|             let netErr = stat.networkError; | ||||
|             let apiErr = stat.otherError; | ||||
|             let avg = stat.resTime[stat.resTime.length >> 1] | 0; | ||||
|             let p95 = stat.resTime[stat.resTime.length * 0.95 | 0] | 0; | ||||
|             let p99 = stat.resTime[stat.resTime.length * 0.99 | 0] | 0; | ||||
|  | ||||
|             this._logTable([ | ||||
|                 [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr', 'AVG  ', 'P95  ', 'P99  '], | ||||
|                 ['', '' + send, | ||||
|                     { text: '' + succ, color: 'green' }, | ||||
|                     { text: '' + (succ / (stat.last.succTime - stat.startTime) * 1000 | 0), color: 'green' }, | ||||
|                     netErr ? { text: '' + netErr, color: 'red' } : '0', | ||||
|                     apiErr ? { text: '' + apiErr, color: 'red' } : '0', | ||||
|                     { text: avg ? avg + 'ms' : '-', color: 'yellow' }, | ||||
|                     { text: p95 ? p95 + 'ms' : '-', color: 'yellow' }, | ||||
|                     { text: p99 ? p99 + 'ms' : '-', color: 'yellow' } | ||||
|                 ] | ||||
|             ]) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private _apiStat: { | ||||
|         [key: string]: { | ||||
|             sendReq: number, | ||||
|             resTime: number[], | ||||
|             succ: number, | ||||
|             networkError: number, | ||||
|             otherError: number, | ||||
|             startTime: number, | ||||
|             last: { | ||||
|                 sendReq: number, | ||||
|                 resTime: number[], | ||||
|                 succ: number, | ||||
|                 networkError: number, | ||||
|                 otherError: number, | ||||
|                 startTime: number, | ||||
|                 succTime: number | ||||
|             } | ||||
|         } | ||||
|     } = {}; | ||||
|  | ||||
|     private _maxApiNameLength = 0; | ||||
|     /** | ||||
|      * callApi 并且计入统计 | ||||
|      */ | ||||
|     callApi: typeof benchmarkClient.callApi = async (apiName, req) => { | ||||
|         this._maxApiNameLength = Math.max(apiName.length, this._maxApiNameLength); | ||||
|  | ||||
|         if (!this._apiStat[apiName]) { | ||||
|             this._apiStat[apiName] = { | ||||
|                 sendReq: 0, | ||||
|                 resTime: [], | ||||
|                 succ: 0, | ||||
|                 networkError: 0, | ||||
|                 otherError: 0, | ||||
|                 startTime: Date.now(), | ||||
|                 last: { | ||||
|                     sendReq: 0, | ||||
|                     resTime: [], | ||||
|                     succ: 0, | ||||
|                     networkError: 0, | ||||
|                     otherError: 0, | ||||
|                     startTime: Date.now(), | ||||
|                     succTime: 0 | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         ++this._apiStat[apiName].sendReq; | ||||
|         ++this._apiStat[apiName].last.sendReq; | ||||
|  | ||||
|         let startTime = Date.now(); | ||||
|         let ret = await benchmarkClient.callApi(apiName, req); | ||||
|  | ||||
|         if (ret.isSucc) { | ||||
|             this._apiStat[apiName].last.succTime = Date.now(); | ||||
|             this._apiStat[apiName].resTime.push(Date.now() - startTime); | ||||
|             this._apiStat[apiName].last.resTime.push(Date.now() - startTime); | ||||
|             ++this._apiStat[apiName].succ; | ||||
|             ++this._apiStat[apiName].last.succ; | ||||
|         } | ||||
|         else { | ||||
|             if (ret.err.type === TsrpcErrorType.NetworkError) { | ||||
|                 ++this._apiStat[apiName].networkError; | ||||
|                 ++this._apiStat[apiName].last.networkError; | ||||
|             } | ||||
|             else { | ||||
|                 ++this._apiStat[apiName].otherError; | ||||
|                 ++this._apiStat[apiName].last.otherError; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
|     private _report() { | ||||
|         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']}`, | ||||
|             this._progress!.lastSuccTime ? `TPS=${this._progress!.succ / (this._progress!.lastSuccTime - this._progress!.startTime) * 1000 | 0}` : '') | ||||
|  | ||||
|         for (let key in this._apiStat) { | ||||
|             let stat = this._apiStat[key]; | ||||
|  | ||||
|             let send = stat.last.sendReq; | ||||
|             let succ = stat.last.resTime.length; | ||||
|             let netErr = stat.last.networkError; | ||||
|             let apiErr = stat.last.otherError; | ||||
|  | ||||
|             this._logTable([ | ||||
|                 [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr'], | ||||
|                 ['', '' + send, | ||||
|                     { text: '' + succ, color: 'green' }, | ||||
|                     { text: '' + (succ / (stat.last.succTime - stat.last.startTime) * 1000 | 0), color: 'green' }, | ||||
|                     netErr ? { text: '' + netErr, color: 'red' } : '0', | ||||
|                     apiErr ? { text: '' + apiErr, color: 'red' } : '0' | ||||
|                 ] | ||||
|             ]) | ||||
|  | ||||
|             Object.assign(stat.last, { | ||||
|                 sendReq: 0, | ||||
|                 resTime: [], | ||||
|                 succ: 0, | ||||
|                 networkError: 0, | ||||
|                 otherError: 0, | ||||
|                 startTime: Date.now(), | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private _logTable(rows: [TableCellItem[], TableCellItem[]]) { | ||||
|         let cellWidths: number[] = []; | ||||
|         for (let cell of rows[0]) { | ||||
|             cellWidths.push(typeof cell === 'string' ? cell.length + 4 : cell.text.length + 4); | ||||
|         } | ||||
|  | ||||
|         for (let row of rows) { | ||||
|             let line = ''; | ||||
|             for (let i = 0; i < row.length; ++i) { | ||||
|                 let cell = row[i]; | ||||
|                 let cellWidth = cellWidths[i]; | ||||
|                 if (typeof cell === 'string') { | ||||
|                     line += cell + ' '.repeat(cellWidth - cell.length); | ||||
|                 } | ||||
|                 else { | ||||
|                     line += cell.text[cell.color] + ' '.repeat(cellWidth - cell.text.length); | ||||
|                 } | ||||
|             } | ||||
|             console.log(line); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const benchmarkClient = new HttpClient(serviceProto, { | ||||
|     server: benchmarkConfig.server, | ||||
|     logger: { | ||||
|         debug: function () { }, | ||||
|         log: function () { }, | ||||
|         warn: function () { }, | ||||
|         error: function () { }, | ||||
|     }, | ||||
|     timeout: benchmarkConfig.timeout, | ||||
|     agent: new (benchmarkConfig.server.startsWith('https') ? https : http).Agent({ | ||||
|         keepAlive: true | ||||
|     }) | ||||
| }) | ||||
|  | ||||
| type TableCellItem = (string | { text: string, color: 'green' | 'red' | 'yellow' }); | ||||
							
								
								
									
										283
									
								
								benchmark/models/WsRunner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								benchmark/models/WsRunner.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | ||||
| import assert from 'assert'; | ||||
| import 'colors'; | ||||
| import 'k8w-extend-native'; | ||||
| import { TsrpcErrorType } from "tsrpc-proto"; | ||||
| import { WsClient } from '../../src/client/ws/WsClient'; | ||||
| import { benchmarkConfig } from "../config/BenchmarkConfig"; | ||||
| import { serviceProto } from '../protocols/proto'; | ||||
|  | ||||
| export interface WsRunnerConfig { | ||||
|     total: number; | ||||
|     concurrency: number; | ||||
|     showError?: boolean; | ||||
| } | ||||
|  | ||||
| export class WsRunner { | ||||
|  | ||||
|     private _config: WsRunnerConfig; | ||||
|  | ||||
|     // 执行单个事务的方法 | ||||
|     private _single: (this: WsRunner) => Promise<void>; | ||||
|  | ||||
|     // 执行进度信息 | ||||
|     private _progress?: { | ||||
|         startTime: number, | ||||
|         lastSuccTime?: number, | ||||
|         started: number, | ||||
|         finished: number, | ||||
|         succ: number, | ||||
|         fail: number | ||||
|     }; | ||||
|  | ||||
|     constructor(single: WsRunner['_single'], config: WsRunnerConfig) { | ||||
|         this._single = single.bind(this); | ||||
|         this._config = config; | ||||
|     } | ||||
|  | ||||
|     async start() { | ||||
|         this._progress = { | ||||
|             startTime: Date.now(), | ||||
|             started: 0, | ||||
|             finished: 0, | ||||
|             succ: 0, | ||||
|             fail: 0 | ||||
|         } | ||||
|  | ||||
|         assert.ok(await benchmarkClient.connect(), 'Connect failed'); | ||||
|  | ||||
|         // 启动并发 | ||||
|         for (let i = 0; i < this._config.concurrency; ++i) { | ||||
|             this._doTrans(); | ||||
|         } | ||||
|  | ||||
|         console.log('Benchmark start!'); | ||||
|         this._startReport(); | ||||
|     } | ||||
|  | ||||
|     private _doTrans() { | ||||
|         if (this._isStoped || !this._progress) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (this._progress.started < this._config.total) { | ||||
|             ++this._progress.started; | ||||
|             let startTime = Date.now(); | ||||
|             this._single().then(v => { | ||||
|                 ++this._progress!.succ; | ||||
|                 this._progress!.lastSuccTime = Date.now(); | ||||
|             }).catch(e => { | ||||
|                 ++this._progress!.fail; | ||||
|                 if (this._config.showError) { | ||||
|                     console.error('[Error]', e.message); | ||||
|                 } | ||||
|             }).then(() => { | ||||
|                 ++this._progress!.finished; | ||||
|                 if (this._progress!.finished === this._config.total) { | ||||
|                     this._finish(); | ||||
|                 } | ||||
|                 else { | ||||
|                     this._doTrans(); | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private _reportInterval?: NodeJS.Timeout; | ||||
|     private _startReport() { | ||||
|         this._reportInterval = setInterval(() => { | ||||
|             this._report(); | ||||
|         }, 1000) | ||||
|     } | ||||
|  | ||||
|     private _isStoped = false; | ||||
|     stop() { | ||||
|         this._isStoped = true; | ||||
|     } | ||||
|  | ||||
|     private _finish() { | ||||
|         if (!this._progress) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this._reportInterval && clearInterval(this._reportInterval); | ||||
|  | ||||
|         console.log('\n\n-------------------------------\n  Benchmark finished!  \n-------------------------------'); | ||||
|  | ||||
|         let usedTime = Date.now() - this._progress.startTime; | ||||
|         console.log(`  Transaction Execution Result  `.bgBlue.white); | ||||
|         console.log(`Started=${this._progress.started}, Finished=${this._progress.finished}, UsedTime=${usedTime}ms`.green); | ||||
|         console.log(`Succ=${this._progress.succ}, Fail=${this._progress.fail}, TPS=${this._progress.succ / (this._progress.lastSuccTime! - this._progress.startTime) * 1000 | 0}\n`.green) | ||||
|  | ||||
|         // TIME TPS(完成的) | ||||
|         console.log(`  API Execution Result  `.bgBlue.white); | ||||
|  | ||||
|         // [KEY] RPS(完成的) AVG P95 P99 | ||||
|         for (let key in this._apiStat) { | ||||
|             let stat = this._apiStat[key]; | ||||
|             stat.resTime = stat.resTime.orderBy(v => v); | ||||
|  | ||||
|             let send = stat.sendReq; | ||||
|             let succ = stat.resTime.length; | ||||
|             let netErr = stat.networkError; | ||||
|             let apiErr = stat.otherError; | ||||
|             let avg = stat.resTime[stat.resTime.length >> 1] | 0; | ||||
|             let p95 = stat.resTime[stat.resTime.length * 0.95 | 0] | 0; | ||||
|             let p99 = stat.resTime[stat.resTime.length * 0.99 | 0] | 0; | ||||
|  | ||||
|             this._logTable([ | ||||
|                 [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr', 'AVG  ', 'P95  ', 'P99  '], | ||||
|                 ['', '' + send, | ||||
|                     { text: '' + succ, color: 'green' }, | ||||
|                     { text: '' + (succ / (stat.last.succTime - stat.startTime) * 1000 | 0), color: 'green' }, | ||||
|                     netErr ? { text: '' + netErr, color: 'red' } : '0', | ||||
|                     apiErr ? { text: '' + apiErr, color: 'red' } : '0', | ||||
|                     { text: avg ? avg + 'ms' : '-', color: 'yellow' }, | ||||
|                     { text: p95 ? p95 + 'ms' : '-', color: 'yellow' }, | ||||
|                     { text: p99 ? p99 + 'ms' : '-', color: 'yellow' } | ||||
|                 ] | ||||
|             ]) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private _apiStat: { | ||||
|         [key: string]: { | ||||
|             sendReq: number, | ||||
|             resTime: number[], | ||||
|             succ: number, | ||||
|             networkError: number, | ||||
|             otherError: number, | ||||
|             startTime: number, | ||||
|             last: { | ||||
|                 sendReq: number, | ||||
|                 resTime: number[], | ||||
|                 succ: number, | ||||
|                 networkError: number, | ||||
|                 otherError: number, | ||||
|                 startTime: number, | ||||
|                 succTime: number | ||||
|             } | ||||
|         } | ||||
|     } = {}; | ||||
|  | ||||
|     private _maxApiNameLength = 0; | ||||
|     /** | ||||
|      * callApi 并且计入统计 | ||||
|      */ | ||||
|     callApi: typeof benchmarkClient.callApi = async (apiName, req) => { | ||||
|         this._maxApiNameLength = Math.max(apiName.length, this._maxApiNameLength); | ||||
|  | ||||
|         if (!this._apiStat[apiName]) { | ||||
|             this._apiStat[apiName] = { | ||||
|                 sendReq: 0, | ||||
|                 resTime: [], | ||||
|                 succ: 0, | ||||
|                 networkError: 0, | ||||
|                 otherError: 0, | ||||
|                 startTime: Date.now(), | ||||
|                 last: { | ||||
|                     sendReq: 0, | ||||
|                     resTime: [], | ||||
|                     succ: 0, | ||||
|                     networkError: 0, | ||||
|                     otherError: 0, | ||||
|                     startTime: Date.now(), | ||||
|                     succTime: 0 | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         ++this._apiStat[apiName].sendReq; | ||||
|         ++this._apiStat[apiName].last.sendReq; | ||||
|  | ||||
|         let startTime = Date.now(); | ||||
|         let ret = await benchmarkClient.callApi(apiName, req); | ||||
|  | ||||
|         if (ret.isSucc) { | ||||
|             this._apiStat[apiName].last.succTime = Date.now(); | ||||
|             this._apiStat[apiName].resTime.push(Date.now() - startTime); | ||||
|             this._apiStat[apiName].last.resTime.push(Date.now() - startTime); | ||||
|             ++this._apiStat[apiName].succ; | ||||
|             ++this._apiStat[apiName].last.succ; | ||||
|         } | ||||
|         else { | ||||
|             if (ret.err.type === TsrpcErrorType.NetworkError) { | ||||
|                 ++this._apiStat[apiName].networkError; | ||||
|                 ++this._apiStat[apiName].last.networkError; | ||||
|             } | ||||
|             else { | ||||
|                 ++this._apiStat[apiName].otherError; | ||||
|                 ++this._apiStat[apiName].last.otherError; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
|     private _report() { | ||||
|         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']}`, | ||||
|             this._progress!.lastSuccTime ? `TPS=${this._progress!.succ / (this._progress!.lastSuccTime - this._progress!.startTime) * 1000 | 0}` : '') | ||||
|  | ||||
|         for (let key in this._apiStat) { | ||||
|             let stat = this._apiStat[key]; | ||||
|  | ||||
|             let send = stat.last.sendReq; | ||||
|             let succ = stat.last.resTime.length; | ||||
|             let netErr = stat.last.networkError; | ||||
|             let apiErr = stat.last.otherError; | ||||
|  | ||||
|             this._logTable([ | ||||
|                 [{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr'], | ||||
|                 ['', '' + send, | ||||
|                     { text: '' + succ, color: 'green' }, | ||||
|                     { text: '' + (succ / (stat.last.succTime - stat.last.startTime) * 1000 | 0), color: 'green' }, | ||||
|                     netErr ? { text: '' + netErr, color: 'red' } : '0', | ||||
|                     apiErr ? { text: '' + apiErr, color: 'red' } : '0' | ||||
|                 ] | ||||
|             ]) | ||||
|  | ||||
|             Object.assign(stat.last, { | ||||
|                 sendReq: 0, | ||||
|                 resTime: [], | ||||
|                 succ: 0, | ||||
|                 networkError: 0, | ||||
|                 otherError: 0, | ||||
|                 startTime: Date.now(), | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private _logTable(rows: [TableCellItem[], TableCellItem[]]) { | ||||
|         let cellWidths: number[] = []; | ||||
|         for (let cell of rows[0]) { | ||||
|             cellWidths.push(typeof cell === 'string' ? cell.length + 4 : cell.text.length + 4); | ||||
|         } | ||||
|  | ||||
|         for (let row of rows) { | ||||
|             let line = ''; | ||||
|             for (let i = 0; i < row.length; ++i) { | ||||
|                 let cell = row[i]; | ||||
|                 let cellWidth = cellWidths[i]; | ||||
|                 if (typeof cell === 'string') { | ||||
|                     line += cell + ' '.repeat(cellWidth - cell.length); | ||||
|                 } | ||||
|                 else { | ||||
|                     line += cell.text[cell.color] + ' '.repeat(cellWidth - cell.text.length); | ||||
|                 } | ||||
|             } | ||||
|             console.log(line); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const benchmarkClient = new WsClient(serviceProto, { | ||||
|     server: benchmarkConfig.server, | ||||
|     logger: { | ||||
|         debug: function () { }, | ||||
|         log: function () { }, | ||||
|         warn: function () { }, | ||||
|         error: function () { }, | ||||
|     }, | ||||
|     timeout: benchmarkConfig.timeout | ||||
| }) | ||||
|  | ||||
| type TableCellItem = (string | { text: string, color: 'green' | 'red' | 'yellow' }); | ||||
							
								
								
									
										14
									
								
								benchmark/protocols/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								benchmark/protocols/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import { uint } from 'tsbuffer-schema'; | ||||
| export interface ReqTest { | ||||
|     a?: uint; | ||||
|     b?: string; | ||||
|     c?: boolean; | ||||
|     d?: Uint8Array; | ||||
| } | ||||
|  | ||||
| export interface ResTest { | ||||
|     a?: uint; | ||||
|     b?: string; | ||||
|     c?: boolean; | ||||
|     d?: Uint8Array; | ||||
| } | ||||
							
								
								
									
										104
									
								
								benchmark/protocols/proto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								benchmark/protocols/proto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| import { ServiceProto } from 'tsrpc-proto'; | ||||
| import { ReqTest, ResTest } from './PtlTest' | ||||
|  | ||||
| export interface ServiceType { | ||||
|     api: { | ||||
|         "Test": { | ||||
|             req: ReqTest, | ||||
|             res: ResTest | ||||
|         } | ||||
|     }, | ||||
|     msg: { | ||||
|  | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const serviceProto: ServiceProto<ServiceType> = { | ||||
|     "services": [ | ||||
|         { | ||||
|             "id": 0, | ||||
|             "name": "Test", | ||||
|             "type": "api" | ||||
|         } | ||||
|     ], | ||||
|     "types": { | ||||
|         "PtlTest/ReqTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "a", | ||||
|                     "type": { | ||||
|                         "type": "Number", | ||||
|                         "scalarType": "uint" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "b", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 2, | ||||
|                     "name": "c", | ||||
|                     "type": { | ||||
|                         "type": "Boolean" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 3, | ||||
|                     "name": "d", | ||||
|                     "type": { | ||||
|                         "type": "Buffer", | ||||
|                         "arrayType": "Uint8Array" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlTest/ResTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "a", | ||||
|                     "type": { | ||||
|                         "type": "Number", | ||||
|                         "scalarType": "uint" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "b", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 2, | ||||
|                     "name": "c", | ||||
|                     "type": { | ||||
|                         "type": "Boolean" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 3, | ||||
|                     "name": "d", | ||||
|                     "type": { | ||||
|                         "type": "Buffer", | ||||
|                         "arrayType": "Uint8Array" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										26
									
								
								benchmark/server/http.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								benchmark/server/http.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { HttpServer } from '../../src/index'; | ||||
| import { serviceProto } from "../protocols/proto"; | ||||
|  | ||||
| async function main() { | ||||
|     let server = new HttpServer(serviceProto, { | ||||
|         logger: { | ||||
|             debug: () => { }, | ||||
|             log: () => { }, | ||||
|             error: console.error.bind(console), | ||||
|             warn: console.warn.bind(console) | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     server.implementApi('Test', call => { | ||||
|         call.succ(call.req); | ||||
|     }); | ||||
|  | ||||
|     await server.start(); | ||||
|  | ||||
|     setInterval(() => { | ||||
|         let used = process.memoryUsage().heapUsed / 1024 / 1024; | ||||
|         console.log(`内存: ${Math.round(used * 100) / 100} MB`); | ||||
|     }, 2000) | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										26
									
								
								benchmark/server/ws.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								benchmark/server/ws.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { WsServer } from '../../src/index'; | ||||
| import { serviceProto } from "../protocols/proto"; | ||||
|  | ||||
| async function main() { | ||||
|     let server = new WsServer(serviceProto, { | ||||
|         logger: { | ||||
|             debug: () => { }, | ||||
|             log: () => { }, | ||||
|             error: console.error.bind(console), | ||||
|             warn: console.warn.bind(console) | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     server.implementApi('Test', call => { | ||||
|         call.succ(call.req); | ||||
|     }); | ||||
|  | ||||
|     await server.start(); | ||||
|  | ||||
|     setInterval(() => { | ||||
|         let used = process.memoryUsage().heapUsed / 1024 / 1024; | ||||
|         console.log(`内存: ${Math.round(used * 100) / 100} MB`); | ||||
|     }, 2000) | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										63
									
								
								benchmark/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								benchmark/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     /* Basic Options */ | ||||
|     // "incremental": true,                   /* Enable incremental compilation */ | ||||
|     "target": "es6",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ | ||||
|     "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ | ||||
|     // "lib": [],                             /* Specify library files to be included in the compilation. */ | ||||
|     // "allowJs": true,                       /* Allow javascript files to be compiled. */ | ||||
|     // "checkJs": true,                       /* Report errors in .js files. */ | ||||
|     // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ | ||||
|     // "declaration": true,                   /* Generates corresponding '.d.ts' file. */ | ||||
|     // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */ | ||||
|     // "sourceMap": true,                     /* Generates corresponding '.map' file. */ | ||||
|     // "outFile": "./",                       /* Concatenate and emit output to single file. */ | ||||
|     // "outDir": "./",                        /* Redirect output structure to the directory. */ | ||||
|     // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ | ||||
|     // "composite": true,                     /* Enable project compilation */ | ||||
|     // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */ | ||||
|     // "removeComments": true,                /* Do not emit comments to output. */ | ||||
|     // "noEmit": true,                        /* Do not emit outputs. */ | ||||
|     // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */ | ||||
|     // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ | ||||
|     // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ | ||||
|  | ||||
|     /* Strict Type-Checking Options */ | ||||
|     "strict": true,                           /* Enable all strict type-checking options. */ | ||||
|     // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */ | ||||
|     // "strictNullChecks": true,              /* Enable strict null checks. */ | ||||
|     // "strictFunctionTypes": true,           /* Enable strict checking of function types. */ | ||||
|     // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ | ||||
|     // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */ | ||||
|     // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */ | ||||
|     // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */ | ||||
|  | ||||
|     /* Additional Checks */ | ||||
|     // "noUnusedLocals": true,                /* Report errors on unused locals. */ | ||||
|     // "noUnusedParameters": true,            /* Report errors on unused parameters. */ | ||||
|     // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */ | ||||
|     // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */ | ||||
|  | ||||
|     /* Module Resolution Options */ | ||||
|     // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ | ||||
|     // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */ | ||||
|     // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ | ||||
|     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */ | ||||
|     // "typeRoots": [],                       /* List of folders to include type definitions from. */ | ||||
|     // "types": [],                           /* Type declaration files to be included in compilation. */ | ||||
|     // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ | ||||
|     "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ | ||||
|     // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */ | ||||
|     // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */ | ||||
|  | ||||
|     /* Source Map Options */ | ||||
|     // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */ | ||||
|     // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */ | ||||
|     // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */ | ||||
|     // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ | ||||
|  | ||||
|     /* Experimental Options */ | ||||
|     // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */ | ||||
|     // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */ | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								benchmark/ws.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								benchmark/ws.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { benchmarkConfig } from './config/BenchmarkConfig'; | ||||
| import { WsRunner } from './models/WsRunner'; | ||||
|  | ||||
| const req = { | ||||
|     a: 123456, | ||||
|     b: 'Hello, World!', | ||||
|     c: true, | ||||
|     d: new Uint8Array(100000) | ||||
| } | ||||
|  | ||||
| new WsRunner(async function () { | ||||
|     await this.callApi('Test', req); | ||||
| }, benchmarkConfig).start(); | ||||
							
								
								
									
										6105
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6105
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										77
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| { | ||||
|   "name": "tsrpc", | ||||
|   "version": "3.3.1-dev.0", | ||||
|   "description": "A TypeScript RPC Framework, with runtime type checking and built-in serialization, support both HTTP and WebSocket.", | ||||
|   "main": "index.js", | ||||
|   "exports": { | ||||
|     "require": "./index.js", | ||||
|     "import": "./index.mjs" | ||||
|   }, | ||||
|   "typings": "index.d.ts", | ||||
|   "directories": { | ||||
|     "doc": "docs" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "test": "npx mocha", | ||||
|     "genTestProto": "npx tsrpc-cli@latest proto --input test/proto --output test/proto/serviceProto.ts", | ||||
|     "coverage": "nyc mocha test/**/*.test.ts && start coverage\\index.html", | ||||
|     "build": "npm run build:js && npm run build:dts && npm run build:doc && node scripts/postBuild && cp package.json LICENSE README.md dist/", | ||||
|     "build:js": "rm -rf dist && npx rollup -c", | ||||
|     "build:dts": "rm -rf lib && npx tsc && npx api-extractor run --local --verbose && rm -rf lib", | ||||
|     "build:doc": "rm -rf docs/api && npx api-documenter markdown --input temp --output docs/api" | ||||
|   }, | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "https://github.com/k8w/tsrpc.git" | ||||
|   }, | ||||
|   "keywords": [ | ||||
|     "k8w", | ||||
|     "ts", | ||||
|     "rpc", | ||||
|     "grpc", | ||||
|     "tsbuffer", | ||||
|     "fullstack", | ||||
|     "websocket", | ||||
|     "protobuf", | ||||
|     "socket.io" | ||||
|   ], | ||||
|   "author": "k8w", | ||||
|   "license": "MIT", | ||||
|   "devDependencies": { | ||||
|     "@microsoft/api-documenter": "^7.17.9", | ||||
|     "@microsoft/api-extractor": "^7.22.2", | ||||
|     "@types/chai": "^4.3.1", | ||||
|     "@types/mocha": "^8.2.3", | ||||
|     "@types/node": "^15.14.9", | ||||
|     "@types/uuid": "^8.3.4", | ||||
|     "chai": "^4.3.6", | ||||
|     "mocha": "^9.2.2", | ||||
|     "nyc": "^15.1.0", | ||||
|     "rollup": "^2.70.2", | ||||
|     "rollup-plugin-typescript2": "^0.31.2", | ||||
|     "ts-node": "^10.7.0", | ||||
|     "typescript": "^4.6.3" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@types/ws": "^7.4.7", | ||||
|     "bson": "*", | ||||
|     "chalk": "^4.1.2", | ||||
|     "tsbuffer": "^2.2.2", | ||||
|     "tsrpc-base-client": "^2.0.5", | ||||
|     "tsrpc-proto": "^1.4.1", | ||||
|     "uuid": "^8.3.2", | ||||
|     "ws": "^7.5.7" | ||||
|   }, | ||||
|   "nyc": { | ||||
|     "extension": [ | ||||
|       ".ts" | ||||
|     ], | ||||
|     "include": [ | ||||
|       "src/**/*.ts" | ||||
|     ], | ||||
|     "reporter": [ | ||||
|       "html" | ||||
|     ], | ||||
|     "all": true | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								res/mongodb-polyfill.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								res/mongodb-polyfill.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { ObjectId } from 'bson'; | ||||
| import { OmitUnion } from 'k8w-extend-native'; | ||||
|  | ||||
| type InsertOneResult<T> = any; | ||||
| type OptionalId<T> = any; | ||||
| type Document = any; | ||||
|  | ||||
| declare module 'mongodb' { | ||||
|     export interface Collection<TSchema extends Document = Document> { | ||||
|         insertOne(doc: OptionalUnlessRequiredId_1<TSchema>): Promise<InsertOneResult<TSchema>>; | ||||
|     } | ||||
|     export type OptionalUnlessRequiredId_1<TSchema> = TSchema extends { | ||||
|         _id: ObjectId; | ||||
|     } ? (OmitUnion<TSchema, '_id'> & { _id?: ObjectId }) : TSchema extends { | ||||
|         _id: any; | ||||
|     } ? TSchema : OptionalId<TSchema>; | ||||
| } | ||||
							
								
								
									
										42
									
								
								rollup.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								rollup.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import typescript from 'rollup-plugin-typescript2'; | ||||
|  | ||||
| export default [ | ||||
|     { | ||||
|         input: './src/index.ts', | ||||
|         output: [{ | ||||
|             format: 'cjs', | ||||
|             file: './dist/index.js', | ||||
|             banner: require('./scripts/copyright') | ||||
|         }], | ||||
|         plugins: [ | ||||
|             typescript({ | ||||
|                 tsconfigOverride: { | ||||
|                     compilerOptions: { | ||||
|                         declaration: false, | ||||
|                         declarationMap: false, | ||||
|                         module: "esnext" | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         input: './src/index.ts', | ||||
|         output: [{ | ||||
|             format: 'es', | ||||
|             file: './dist/index.mjs', | ||||
|             banner: require('./scripts/copyright') | ||||
|         }], | ||||
|         plugins: [ | ||||
|             typescript({ | ||||
|                 tsconfigOverride: { | ||||
|                     compilerOptions: { | ||||
|                         declaration: false, | ||||
|                         declarationMap: false, | ||||
|                         module: "esnext" | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|         ] | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										7
									
								
								scripts/copyright.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								scripts/copyright.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| module.exports = `/*! | ||||
|  * TSRPC v${require('../package.json').version} | ||||
|  * ----------------------------------------- | ||||
|  * Copyright (c) King Wang. | ||||
|  * MIT License | ||||
|  * https://github.com/k8w/tsrpc | ||||
|  */` | ||||
							
								
								
									
										26
									
								
								scripts/postBuild.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								scripts/postBuild.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
|  | ||||
| // remove private / protected index.d.ts | ||||
| (() => { | ||||
|     let content = fs.readFileSync(path.resolve(__dirname, '../dist/index.d.ts'), 'utf-8'); | ||||
|     content = content.replace(/^\s*(private|protected)\s+\_.+;/g, ''); | ||||
|     content = require('./copyright') + '\n' + content; | ||||
|     fs.writeFileSync(path.resolve(__dirname, '../dist/index.d.ts'), content, 'utf-8'); | ||||
| })(); | ||||
|  | ||||
| // replace __TSRPC_VERSION__from index.js/mjs | ||||
| [ | ||||
|     path.resolve(__dirname, '../dist/index.js'), | ||||
|     path.resolve(__dirname, '../dist/index.mjs') | ||||
| ].forEach(filepath => { | ||||
|     let content = fs.readFileSync(filepath, 'utf-8'); | ||||
|     content = content.replace('__TSRPC_VERSION__', require('../package.json').version);; | ||||
|     fs.writeFileSync(filepath, content, 'utf-8'); | ||||
| }); | ||||
|  | ||||
| // mongodb-polyfill | ||||
| fs.copyFileSync(path.resolve(__dirname, '../res/mongodb-polyfill.d.ts'), path.resolve(__dirname, '../dist/mongodb-polyfill.d.ts')); | ||||
| let content = fs.readFileSync(path.resolve(__dirname, '../dist/index.d.ts'), 'utf-8'); | ||||
| content = content.replace(`/// <reference types="node" />`, `/// <reference types="node" />\n/// <reference path="mongodb-polyfill.d.ts" />`) | ||||
| fs.writeFileSync(path.resolve(__dirname, '../dist/index.d.ts'), content, 'utf-8'); | ||||
							
								
								
									
										37
									
								
								src/client/http/HttpClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/client/http/HttpClient.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import { ObjectId } from "bson"; | ||||
| import http from "http"; | ||||
| import https from "https"; | ||||
| import { BaseHttpClient, BaseHttpClientOptions, defaultBaseHttpClientOptions } from "tsrpc-base-client"; | ||||
| import { BaseServiceType, ServiceProto } from "tsrpc-proto"; | ||||
| import { HttpProxy } from "./HttpProxy"; | ||||
|  | ||||
| /** | ||||
|  * Client for TSRPC HTTP Server. | ||||
|  * It uses native http module of NodeJS. | ||||
|  * @typeParam ServiceType - `ServiceType` from generated `proto.ts` | ||||
|  */ | ||||
| export class HttpClient<ServiceType extends BaseServiceType> extends BaseHttpClient<ServiceType> { | ||||
|  | ||||
|     readonly options!: Readonly<HttpClientOptions>; | ||||
|  | ||||
|     constructor(proto: ServiceProto<ServiceType>, options?: Partial<HttpClientOptions>) { | ||||
|         let httpProxy = new HttpProxy; | ||||
|         super(proto, httpProxy, { | ||||
|             customObjectIdClass: ObjectId, | ||||
|             ...defaultHttpClientOptions, | ||||
|             ...options | ||||
|         }); | ||||
|  | ||||
|         httpProxy.agent = this.options.agent; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| const defaultHttpClientOptions: HttpClientOptions = { | ||||
|     ...defaultBaseHttpClientOptions | ||||
| } | ||||
|  | ||||
| export interface HttpClientOptions extends BaseHttpClientOptions { | ||||
|     /** NodeJS HTTP Agent */ | ||||
|     agent?: http.Agent | https.Agent; | ||||
| } | ||||
							
								
								
									
										69
									
								
								src/client/http/HttpProxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/client/http/HttpProxy.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| import http from "http"; | ||||
| import https from "https"; | ||||
| import { IHttpProxy } from "tsrpc-base-client"; | ||||
| import { TsrpcError } from "tsrpc-proto"; | ||||
|  | ||||
| /** @internal */ | ||||
| export class HttpProxy implements IHttpProxy { | ||||
|  | ||||
|     /** NodeJS HTTP Agent */ | ||||
|     agent?: http.Agent | https.Agent; | ||||
|  | ||||
|     fetch(options: Parameters<IHttpProxy['fetch']>[0]): ReturnType<IHttpProxy['fetch']> { | ||||
|         let nodeHttp = options.url.startsWith('https://') ? https : http; | ||||
|  | ||||
|         let rs!: (v: { isSucc: true, res: string | Uint8Array } | { isSucc: false, err: TsrpcError }) => void; | ||||
|         let promise: ReturnType<IHttpProxy['fetch']>['promise'] = new Promise(_rs => { | ||||
|             rs = _rs; | ||||
|         }) | ||||
|  | ||||
|         let httpReq: http.ClientRequest; | ||||
|         httpReq = nodeHttp.request(options.url, { | ||||
|             method: options.method, | ||||
|             agent: this.agent, | ||||
|             timeout: options.timeout, | ||||
|             headers: options.headers, | ||||
|         }, httpRes => { | ||||
|             let data: Buffer[] = []; | ||||
|             httpRes.on('data', (v: Buffer) => { | ||||
|                 data.push(v) | ||||
|             }); | ||||
|             httpRes.on('end', () => { | ||||
|                 let buf: Uint8Array = Buffer.concat(data); | ||||
|                 if (options.responseType === 'text') { | ||||
|                     rs({ | ||||
|                         isSucc: true, | ||||
|                         res: buf.toString() | ||||
|                     }) | ||||
|                 } | ||||
|                 else { | ||||
|                     rs({ | ||||
|                         isSucc: true, | ||||
|                         res: buf | ||||
|                     }) | ||||
|                 } | ||||
|             }) | ||||
|         }); | ||||
|  | ||||
|         httpReq.on('error', e => { | ||||
|             rs({ | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError(e.message, { | ||||
|                     type: TsrpcError.Type.NetworkError, | ||||
|                     code: (e as any).code | ||||
|                 }) | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         let buf = options.data; | ||||
|         httpReq.end(typeof buf === 'string' ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength)); | ||||
|  | ||||
|         let abort = httpReq.abort.bind(httpReq); | ||||
|  | ||||
|         return { | ||||
|             promise: promise, | ||||
|             abort: abort | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										58
									
								
								src/client/ws/WebSocketProxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/client/ws/WebSocketProxy.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import { IWebSocketProxy } from "tsrpc-base-client"; | ||||
| import { TsrpcError } from "tsrpc-proto"; | ||||
| import WebSocket from 'ws'; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  */ | ||||
| export class WebSocketProxy implements IWebSocketProxy { | ||||
|  | ||||
|     options!: IWebSocketProxy['options'] | ||||
|  | ||||
|     private _ws?: WebSocket; | ||||
|     connect(server: string, protocols?: string[]): void { | ||||
|         this._ws = new WebSocket(server, protocols); | ||||
|         this._ws.onopen = this.options.onOpen; | ||||
|         this._ws.onclose = e => { | ||||
|             this.options.onClose(e.code, e.reason); | ||||
|             this._ws = undefined; | ||||
|         } | ||||
|         this._ws.onerror = e => { | ||||
|             this.options.onError(e.error); | ||||
|         } | ||||
|         this._ws.onmessage = e => { | ||||
|             if (e.data instanceof ArrayBuffer) { | ||||
|                 this.options.onMessage(new Uint8Array(e.data)); | ||||
|             } | ||||
|             else if (Array.isArray(e.data)) { | ||||
|                 this.options.onMessage(Buffer.concat(e.data)); | ||||
|             } | ||||
|             else { | ||||
|                 this.options.onMessage(e.data); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     close(code?: number, reason?: string): void { | ||||
|         this._ws?.close(code, reason); | ||||
|         this._ws = undefined; | ||||
|     } | ||||
|     send(data: string | Uint8Array): Promise<{ err?: TsrpcError | undefined; }> { | ||||
|         return new Promise(rs => { | ||||
|             this._ws?.send(data, err => { | ||||
|                 if (err) { | ||||
|                     this.options.logger?.error('WebSocket Send Error:', err); | ||||
|                     rs({ | ||||
|                         err: new TsrpcError('Network Error', { | ||||
|                             code: 'SEND_BUF_ERR', | ||||
|                             type: TsrpcError.Type.NetworkError, | ||||
|                             innerErr: err | ||||
|                         }) | ||||
|                     }); | ||||
|                     return; | ||||
|                 } | ||||
|                 rs({}); | ||||
|             }); | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/client/ws/WsClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/client/ws/WsClient.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { ObjectId } from "bson"; | ||||
| import { BaseWsClient, BaseWsClientOptions, defaultBaseWsClientOptions } from "tsrpc-base-client"; | ||||
| import { BaseServiceType, ServiceProto } from "tsrpc-proto"; | ||||
| import { WebSocketProxy } from "./WebSocketProxy"; | ||||
|  | ||||
| /** | ||||
|  * Client for TSRPC WebSocket Server. | ||||
|  * @typeParam ServiceType - `ServiceType` from generated `proto.ts` | ||||
|  */ | ||||
| export class WsClient<ServiceType extends BaseServiceType> extends BaseWsClient<ServiceType> { | ||||
|  | ||||
|     readonly options!: Readonly<WsClientOptions>; | ||||
|  | ||||
|     constructor(proto: ServiceProto<ServiceType>, options?: Partial<WsClientOptions>) { | ||||
|         let wsp = new WebSocketProxy(); | ||||
|         super(proto, wsp, { | ||||
|             customObjectIdClass: ObjectId, | ||||
|             ...defaultWsClientOptions, | ||||
|             ...options | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| const defaultWsClientOptions: WsClientOptions = { | ||||
|     ...defaultBaseWsClientOptions, | ||||
| } | ||||
|  | ||||
| export interface WsClientOptions extends BaseWsClientOptions { | ||||
|  | ||||
| } | ||||
							
								
								
									
										27
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import 'k8w-extend-native'; | ||||
|  | ||||
| // Common | ||||
| export * from 'tsrpc-base-client'; | ||||
| export * from 'tsrpc-proto'; | ||||
| export * from './client/http/HttpClient'; | ||||
| export * from './client/ws/WsClient'; | ||||
| export * from './models/version'; | ||||
| export * from './server/base/ApiCall'; | ||||
| // Base | ||||
| export * from './server/base/BaseCall'; | ||||
| export * from './server/base/BaseConnection'; | ||||
| export * from './server/base/BaseServer'; | ||||
| export * from './server/base/MsgCall'; | ||||
| export * from './server/http/ApiCallHttp'; | ||||
| // Http | ||||
| export * from './server/http/HttpConnection'; | ||||
| export * from './server/http/HttpServer'; | ||||
| export * from './server/http/MsgCallHttp'; | ||||
| export * from './server/models/PrefixLogger'; | ||||
| export * from './server/models/TerminalColorLogger'; | ||||
| // WebSocket | ||||
| export * from './server/ws/ApiCallWs'; | ||||
| export * from './server/ws/MsgCallWs'; | ||||
| export * from './server/ws/WsConnection'; | ||||
| export * from './server/ws/WsServer'; | ||||
|  | ||||
							
								
								
									
										22
									
								
								src/models/HttpUtil.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/models/HttpUtil.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import * as http from "http"; | ||||
|  | ||||
| export class HttpUtil { | ||||
|     static getClientIp(req: http.IncomingMessage) { | ||||
|         var ipAddress; | ||||
|         // The request may be forwarded from local web server. | ||||
|         var forwardedIpsStr = req.headers['x-forwarded-for'] as string | undefined; | ||||
|         if (forwardedIpsStr) { | ||||
|             // 'x-forwarded-for' header may return multiple IP addresses in | ||||
|             // the format: "client IP, proxy 1 IP, proxy 2 IP" so take the | ||||
|             // the first one | ||||
|             var forwardedIps = forwardedIpsStr.split(','); | ||||
|             ipAddress = forwardedIps[0]; | ||||
|         } | ||||
|         if (!ipAddress) { | ||||
|             // If request was not forwarded | ||||
|             ipAddress = req.connection.remoteAddress; | ||||
|         } | ||||
|         // Remove prefix ::ffff: | ||||
|         return ipAddress ? ipAddress.replace(/^::ffff:/, '') : ''; | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/models/Pool.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/models/Pool.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| export class Pool<ItemClass extends PoolItem> { | ||||
|  | ||||
|     private _pools: ItemClass[] = []; | ||||
|     private _itemClass: { new(): ItemClass }; | ||||
|     enabled: boolean; | ||||
|  | ||||
|     constructor(itemClass: { new(): ItemClass }, enabled: boolean) { | ||||
|         this._itemClass = itemClass; | ||||
|         this.enabled = enabled; | ||||
|     } | ||||
|  | ||||
|     get() { | ||||
|         let item = this.enabled && this._pools.pop(); | ||||
|         if (!item) { | ||||
|             item = new this._itemClass(); | ||||
|         } | ||||
|         item.reuse?.(); | ||||
|         return item; | ||||
|     } | ||||
|  | ||||
|     put(item: ItemClass) { | ||||
|         if (!this.enabled || this._pools.indexOf(item) > -1) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         item.unuse?.(); | ||||
|         this._pools.push(item); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export interface PoolItem { | ||||
|     reuse: () => void; | ||||
|     unuse: () => void; | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/models/version.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/models/version.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| /** Version of TSRPC */ | ||||
| export const TSRPC_VERSION = '__TSRPC_VERSION__'; | ||||
							
								
								
									
										215
									
								
								src/server/base/ApiCall.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/server/base/ApiCall.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| import { TSBuffer } from 'tsbuffer'; | ||||
| import { ApiService, TransportDataUtil } from "tsrpc-base-client"; | ||||
| import { ApiReturn, BaseServiceType, ServerOutputData, TsrpcError, TsrpcErrorData, TsrpcErrorType } from "tsrpc-proto"; | ||||
| import { PrefixLogger } from "../models/PrefixLogger"; | ||||
| import { BaseCall, BaseCallOptions } from "./BaseCall"; | ||||
| import { BaseConnection } from './BaseConnection'; | ||||
|  | ||||
| export interface ApiCallOptions<Req, ServiceType extends BaseServiceType> extends BaseCallOptions<ServiceType> { | ||||
|     /** Which service the Call is belong to */ | ||||
|     service: ApiService, | ||||
|     /** Only exists in long connection, it is used to associate request and response. | ||||
|      * It is created by the client, and the server would return the same value in `ApiReturn`. | ||||
|      */ | ||||
|     sn?: number, | ||||
|     /** Request Data */ | ||||
|     req: Req | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A call request by `client.callApi()` | ||||
|  * @typeParam Req - Type of request | ||||
|  * @typeParam Res - Type of response | ||||
|  * @typeParam ServiceType - The same `ServiceType` to server, it is used for code auto hint. | ||||
|  */ | ||||
| export abstract class ApiCall<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends BaseCall<ServiceType> { | ||||
|     readonly type = 'api' as const; | ||||
|  | ||||
|     /** | ||||
|      * Which `ApiService` the request is calling for | ||||
|      */ | ||||
|     readonly service!: ApiService; | ||||
|     /** Only exists in long connection, it is used to associate request and response. | ||||
|      * It is created by the client, and the server would return the same value in `ApiReturn`. | ||||
|      */ | ||||
|     readonly sn?: number; | ||||
|     /** | ||||
|      * Request data from the client, type of it is checked by the framework already. | ||||
|      */ | ||||
|     readonly req: Req; | ||||
|  | ||||
|     constructor(options: ApiCallOptions<Req, ServiceType>, logger?: PrefixLogger) { | ||||
|         super(options, logger ?? new PrefixLogger({ | ||||
|             logger: options.conn.logger, | ||||
|             prefixs: [`[Api:${options.service.name}]${options.sn !== undefined ? ` SN=${options.sn}` : ''}`] | ||||
|         })); | ||||
|  | ||||
|         this.sn = options.sn; | ||||
|         this.req = options.req; | ||||
|     } | ||||
|  | ||||
|     protected _return?: ApiReturn<Res>; | ||||
|     /** | ||||
|      * Response Data that sent already. | ||||
|      * `undefined` means no return data is sent yet. (Never `call.succ()` and `call.error()`) | ||||
|      */ | ||||
|     public get return(): ApiReturn<Res> | undefined { | ||||
|         return this._return; | ||||
|     } | ||||
|  | ||||
|     protected _usedTime: number | undefined; | ||||
|     /** Time from received req to send return data */ | ||||
|     public get usedTime(): number | undefined { | ||||
|         return this._usedTime; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send a successful `ApiReturn` with response data | ||||
|      * @param res - Response data | ||||
|      * @returns Promise resolved means the buffer is sent to kernel | ||||
|      */ | ||||
|     succ(res: Res): Promise<void> { | ||||
|         return this._prepareReturn({ | ||||
|             isSucc: true, | ||||
|             res: res | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send a error `ApiReturn` with a `TsrpcError` | ||||
|      * @returns Promise resolved means the buffer is sent to kernel | ||||
|      */ | ||||
|     error(message: string, info?: Partial<TsrpcErrorData>): Promise<void>; | ||||
|     error(err: TsrpcError): Promise<void>; | ||||
|     error(errOrMsg: string | TsrpcError, data?: Partial<TsrpcErrorData>): Promise<void> { | ||||
|         let error: TsrpcError = typeof errOrMsg === 'string' ? new TsrpcError(errOrMsg, data) : errOrMsg; | ||||
|         return this._prepareReturn({ | ||||
|             isSucc: false, | ||||
|             err: error | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     protected async _prepareReturn(ret: ApiReturn<Res>): Promise<void> { | ||||
|         if (this._return) { | ||||
|             return; | ||||
|         } | ||||
|         this._return = ret; | ||||
|  | ||||
|         // Pre Flow | ||||
|         let preFlow = await this.server.flows.preApiReturnFlow.exec({ call: this, return: ret }, this.logger); | ||||
|         // Stopped! | ||||
|         if (!preFlow) { | ||||
|             this.logger.debug('[preApiReturnFlow]', 'Canceled') | ||||
|             return; | ||||
|         } | ||||
|         ret = preFlow.return; | ||||
|  | ||||
|         // record & log ret | ||||
|         this._usedTime = Date.now() - this.startTime; | ||||
|         if (ret.isSucc) { | ||||
|             this.logger.log('[ApiRes]', `${this.usedTime}ms`, this.server.options.logResBody ? ret.res : ''); | ||||
|         } | ||||
|         else { | ||||
|             if (ret.err.type === TsrpcErrorType.ApiError) { | ||||
|                 this.logger.log('[ApiErr]', `${this.usedTime}ms`, ret.err, 'req=', this.req); | ||||
|             } | ||||
|             else { | ||||
|                 this.logger.error(`[ApiErr]`, `${this.usedTime}ms`, ret.err, 'req=', this.req) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Do send! | ||||
|         this._return = ret; | ||||
|         let opSend = await this._sendReturn(ret); | ||||
|         if (!opSend.isSucc) { | ||||
|             if (opSend.canceledByFlow) { | ||||
|                 this.logger.debug(`[${opSend.canceledByFlow}]`, 'Canceled'); | ||||
|             } | ||||
|             else { | ||||
|                 this.logger.error('[SendDataErr]', opSend.errMsg); | ||||
|                 if (ret.isSucc || ret.err.type === TsrpcErrorType.ApiError) { | ||||
|                     this._return = undefined; | ||||
|                     this.server.onInternalServerError({ message: opSend.errMsg, name: 'SendReturnErr' }, this) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Post Flow | ||||
|         await this.server.flows.postApiReturnFlow.exec(preFlow, this.logger); | ||||
|     } | ||||
|  | ||||
|     protected async _sendReturn(ret: ApiReturn<Res>): ReturnType<SendReturnMethod<Res>> { | ||||
|         // Encode | ||||
|         let opServerOutput = ApiCall.encodeApiReturn(this.server.tsbuffer, this.service, ret, this.conn.dataType, this.sn);; | ||||
|         if (!opServerOutput.isSucc) { | ||||
|             this.server.onInternalServerError({ message: opServerOutput.errMsg, stack: '  |- TransportDataUtil.encodeApiReturn\n  |- ApiCall._sendReturn' }, this); | ||||
|             return opServerOutput; | ||||
|         } | ||||
|  | ||||
|         let opSend = await this.conn.sendData(opServerOutput.output); | ||||
|         if (!opSend.isSucc) { | ||||
|             return opSend; | ||||
|         } | ||||
|         return opSend; | ||||
|     } | ||||
|  | ||||
|     static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'text', sn?: number): EncodeApiReturnOutput<string> | ||||
|     static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'buffer', sn?: number): EncodeApiReturnOutput<Uint8Array> | ||||
|     static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'json', sn?: number): EncodeApiReturnOutput<object> | ||||
|     static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'text' | 'buffer' | 'json', sn?: number): EncodeApiReturnOutput<Uint8Array> | EncodeApiReturnOutput<string> | EncodeApiReturnOutput<object>; | ||||
|     static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'text' | 'buffer' | 'json', sn?: number): EncodeApiReturnOutput<Uint8Array> | EncodeApiReturnOutput<string> | EncodeApiReturnOutput<object> { | ||||
|         if (type === 'buffer') { | ||||
|             let serverOutputData: ServerOutputData = { | ||||
|                 sn: sn, | ||||
|                 serviceId: sn !== undefined ? service.id : undefined | ||||
|             }; | ||||
|             if (apiReturn.isSucc) { | ||||
|                 let op = tsbuffer.encode(apiReturn.res, service.resSchemaId); | ||||
|                 if (!op.isSucc) { | ||||
|                     return op; | ||||
|                 } | ||||
|                 serverOutputData.buffer = op.buf; | ||||
|             } | ||||
|             else { | ||||
|                 serverOutputData.error = apiReturn.err; | ||||
|             } | ||||
|  | ||||
|             let op = TransportDataUtil.tsbuffer.encode(serverOutputData, 'ServerOutputData'); | ||||
|             return op.isSucc ? { isSucc: true, output: op.buf } : { isSucc: false, errMsg: op.errMsg }; | ||||
|         } | ||||
|         else { | ||||
|             apiReturn = { ...apiReturn }; | ||||
|             if (apiReturn.isSucc) { | ||||
|                 let op = tsbuffer.encodeJSON(apiReturn.res, service.resSchemaId); | ||||
|                 if (!op.isSucc) { | ||||
|                     return op; | ||||
|                 } | ||||
|                 apiReturn.res = op.json; | ||||
|             } | ||||
|             else { | ||||
|                 apiReturn.err = { | ||||
|                     ...apiReturn.err | ||||
|                 } | ||||
|             } | ||||
|             let json = sn == undefined ? apiReturn : [service.name, apiReturn, sn]; | ||||
|             return { isSucc: true, output: type === 'json' ? json : JSON.stringify(json) }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export type SendReturnMethod<Res> = (ret: ApiReturn<Res>) => ReturnType<BaseConnection['sendData']>; | ||||
|  | ||||
| export declare type EncodeApiReturnOutput<T> = { | ||||
|     isSucc: true; | ||||
|     /** Encoded binary buffer */ | ||||
|     output: T; | ||||
|     errMsg?: undefined; | ||||
| } | { | ||||
|     isSucc: false; | ||||
|     /** Error message */ | ||||
|     errMsg: string; | ||||
|     output?: undefined; | ||||
| }; | ||||
							
								
								
									
										30
									
								
								src/server/base/BaseCall.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/server/base/BaseCall.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { ApiService, MsgService } from 'tsrpc-base-client'; | ||||
| import { BaseServiceType } from 'tsrpc-proto'; | ||||
| import { PrefixLogger } from '../models/PrefixLogger'; | ||||
| import { BaseConnection } from './BaseConnection'; | ||||
|  | ||||
| export interface BaseCallOptions<ServiceType extends BaseServiceType> { | ||||
|     /** Connection */ | ||||
|     conn: BaseConnection<ServiceType>, | ||||
|     /** Which service the call is belong to */ | ||||
|     service: ApiService | MsgService | ||||
| } | ||||
|  | ||||
| export abstract class BaseCall<ServiceType extends BaseServiceType> { | ||||
|     readonly conn: BaseConnection<ServiceType>; | ||||
|     readonly service: ApiService | MsgService; | ||||
|     /** Time that server created the call */ | ||||
|     readonly startTime: number; | ||||
|     readonly logger: PrefixLogger; | ||||
|  | ||||
|     constructor(options: BaseCallOptions<ServiceType>, logger: PrefixLogger) { | ||||
|         this.conn = options.conn; | ||||
|         this.service = options.service; | ||||
|         this.startTime = Date.now(); | ||||
|         this.logger = logger; | ||||
|     } | ||||
|  | ||||
|     get server(): this['conn']['server'] { | ||||
|         return this.conn.server; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										182
									
								
								src/server/base/BaseConnection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/server/base/BaseConnection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| import { MsgHandlerManager, ParsedServerInput, TransportDataUtil } from "tsrpc-base-client"; | ||||
| import { BaseServiceType } from "tsrpc-proto"; | ||||
| import { PrefixLogger } from "../models/PrefixLogger"; | ||||
| import { ApiCall } from "./ApiCall"; | ||||
| import { BaseServer, MsgHandler } from "./BaseServer"; | ||||
| import { MsgCall } from "./MsgCall"; | ||||
|  | ||||
| export interface BaseConnectionOptions<ServiceType extends BaseServiceType = any> { | ||||
|     /** Created by server, each Call has a unique id. */ | ||||
|     id: string; | ||||
|     /** Client IP address */ | ||||
|     ip: string, | ||||
|     server: BaseServer<ServiceType>, | ||||
|     dataType: 'text' | 'buffer' | 'json' | ||||
| } | ||||
|  | ||||
| export abstract class BaseConnection<ServiceType extends BaseServiceType = any> { | ||||
|     /** It is long connection or short connection */ | ||||
|     abstract readonly type: 'LONG' | 'SHORT'; | ||||
|  | ||||
|     protected abstract readonly ApiCallClass: { new(options: any): ApiCall }; | ||||
|     protected abstract readonly MsgCallClass: { new(options: any): MsgCall }; | ||||
|  | ||||
|     /** Connection unique ID */ | ||||
|     readonly id: string; | ||||
|     /** Client IP address */ | ||||
|     readonly ip: string; | ||||
|     readonly server: BaseServer<ServiceType>; | ||||
|     readonly logger: PrefixLogger; | ||||
|     dataType: BaseConnectionOptions['dataType']; | ||||
|  | ||||
|     constructor(options: BaseConnectionOptions<ServiceType>, logger: PrefixLogger) { | ||||
|         this.id = options.id; | ||||
|         this.ip = options.ip; | ||||
|         this.server = options.server; | ||||
|         this.logger = logger; | ||||
|         this.dataType = options.dataType; | ||||
|     } | ||||
|  | ||||
|     abstract get status(): ConnectionStatus; | ||||
|     /** Close the connection */ | ||||
|     abstract close(reason?: string): void; | ||||
|  | ||||
|     /** Send buffer (with pre-flow and post-flow) */ | ||||
|     async sendData(data: string | Uint8Array | object, call?: ApiCall): Promise<{ isSucc: true } | { isSucc: false, errMsg: string, canceledByFlow?: string }> { | ||||
|         // Pre Flow | ||||
|         let pre = await this.server.flows.preSendDataFlow.exec({ conn: this, data: data, call: call }, call?.logger || this.logger); | ||||
|         if (!pre) { | ||||
|             return { isSucc: false, errMsg: 'Canceled by preSendDataFlow', canceledByFlow: 'preSendDataFlow' }; | ||||
|         } | ||||
|         data = pre.data; | ||||
|  | ||||
|         // @deprecated Pre Buffer Flow | ||||
|         if (data instanceof Uint8Array) { | ||||
|             let preBuf = await this.server.flows.preSendBufferFlow.exec({ conn: this, buf: data, call: call }, call?.logger || this.logger); | ||||
|             if (!preBuf) { | ||||
|                 return { isSucc: false, errMsg: 'Canceled by preSendBufferFlow', canceledByFlow: 'preSendBufferFlow' }; | ||||
|             } | ||||
|             data = preBuf.buf; | ||||
|         } | ||||
|  | ||||
|         // debugBuf log | ||||
|         if (this.server.options.debugBuf) { | ||||
|             if (typeof data === 'string') { | ||||
|                 (call?.logger ?? this.logger)?.debug(`[SendText] length=${data.length}`, data); | ||||
|             } | ||||
|             else if (data instanceof Uint8Array) { | ||||
|                 (call?.logger ?? this.logger)?.debug(`[SendBuf] length=${data.length}`, data); | ||||
|             } | ||||
|             else { | ||||
|                 (call?.logger ?? this.logger)?.debug('[SendJSON]', data); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return this.doSendData(data, call); | ||||
|     } | ||||
|     protected abstract doSendData(data: string | Uint8Array | object, call?: ApiCall): Promise<{ isSucc: true } | { isSucc: false, errMsg: string }>; | ||||
|  | ||||
|     makeCall(input: ParsedServerInput): ApiCall | MsgCall { | ||||
|         if (input.type === 'api') { | ||||
|             return new this.ApiCallClass({ | ||||
|                 conn: this, | ||||
|                 service: input.service, | ||||
|                 req: input.req, | ||||
|                 sn: input.sn, | ||||
|             }) | ||||
|         } | ||||
|         else { | ||||
|             return new this.MsgCallClass({ | ||||
|                 conn: this, | ||||
|                 service: input.service, | ||||
|                 msg: input.msg | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send message to the client, only be available when it is long connection. | ||||
|      * @param msgName  | ||||
|      * @param msg - Message body | ||||
|      * @returns Promise resolved when the buffer is sent to kernel, it not represents the server received it. | ||||
|      */ | ||||
|     async sendMsg<T extends keyof ServiceType['msg']>(msgName: T, msg: ServiceType['msg'][T]): ReturnType<BaseConnection['sendData']> { | ||||
|         if (this.type === 'SHORT') { | ||||
|             this.logger.warn('[SendMsgErr]', `[${msgName}]`, 'Short connection cannot sendMsg'); | ||||
|             return { isSucc: false, errMsg: 'Short connection cannot sendMsg' } | ||||
|         } | ||||
|  | ||||
|         let service = this.server.serviceMap.msgName2Service[msgName as string]; | ||||
|         if (!service) { | ||||
|             this.logger.warn('[SendMsgErr]', `[${msgName}]`, `Invalid msg name: ${msgName}`); | ||||
|             return { isSucc: false, errMsg: `Invalid msg name: ${msgName}` } | ||||
|         } | ||||
|  | ||||
|         // Pre Flow | ||||
|         let pre = await this.server.flows.preSendMsgFlow.exec({ conn: this, service: service, msg: msg }, this.logger); | ||||
|         if (!pre) { | ||||
|             this.logger.debug('[preSendMsgFlow]', 'Canceled'); | ||||
|             return { isSucc: false, errMsg: 'Canceled by preSendMsgFlow', canceledByFlow: 'preSendMsgFlow' }; | ||||
|         } | ||||
|         msg = pre.msg; | ||||
|  | ||||
|         // Encode | ||||
|         let opServerOutput = TransportDataUtil.encodeServerMsg(this.server.tsbuffer, service, msg, this.dataType, this.type); | ||||
|         if (!opServerOutput.isSucc) { | ||||
|             this.logger.warn('[SendMsgErr]', `[${msgName}]`, opServerOutput.errMsg); | ||||
|             return opServerOutput; | ||||
|         } | ||||
|  | ||||
|         // Do send! | ||||
|         this.server.options.logMsg && this.logger.log('[SendMsg]', `[${msgName}]`, msg); | ||||
|         let opSend = await this.sendData(opServerOutput.output); | ||||
|         if (!opSend.isSucc) { | ||||
|             return opSend; | ||||
|         } | ||||
|  | ||||
|         // Post Flow | ||||
|         await this.server.flows.postSendMsgFlow.exec(pre, this.logger); | ||||
|  | ||||
|         return { isSucc: true }; | ||||
|     } | ||||
|  | ||||
|     // 多个Handler将异步并行执行 | ||||
|     private _msgHandlers?: MsgHandlerManager; | ||||
|     /** | ||||
|      * Add a message handler, | ||||
|      * duplicate handlers to the same `msgName` would be ignored. | ||||
|      * @param msgName | ||||
|      * @param handler | ||||
|      */ | ||||
|     listenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: MsgHandler<Call>): MsgHandler<Call> { | ||||
|         if (!this._msgHandlers) { | ||||
|             this._msgHandlers = new MsgHandlerManager(); | ||||
|         } | ||||
|         this._msgHandlers.addHandler(msgName as string, handler); | ||||
|         return handler; | ||||
|     }; | ||||
|     /** | ||||
|      * Remove a message handler | ||||
|      */ | ||||
|     unlistenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: Function): void { | ||||
|         if (!this._msgHandlers) { | ||||
|             this._msgHandlers = new MsgHandlerManager(); | ||||
|         } | ||||
|         this._msgHandlers.removeHandler(msgName as string, handler); | ||||
|     }; | ||||
|     /** | ||||
|      * Remove all handlers from a message | ||||
|      */ | ||||
|     unlistenMsgAll<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg): void { | ||||
|         if (!this._msgHandlers) { | ||||
|             this._msgHandlers = new MsgHandlerManager(); | ||||
|         } | ||||
|         this._msgHandlers.removeAllHandlers(msgName as string); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export enum ConnectionStatus { | ||||
|     Opened = 'OPENED', | ||||
|     Closing = 'CLOSING', | ||||
|     Closed = 'CLOSED' | ||||
| } | ||||
							
								
								
									
										937
									
								
								src/server/base/BaseServer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										937
									
								
								src/server/base/BaseServer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,937 @@ | ||||
| import { ObjectId } from "bson"; | ||||
| import chalk from "chalk"; | ||||
| import * as path from "path"; | ||||
| import { TSBuffer } from 'tsbuffer'; | ||||
| import { ApiService, Counter, Flow, getCustomObjectIdTypes, MsgHandlerManager, MsgService, ParsedServerInput, ServiceMap, ServiceMapUtil, TransportDataUtil } from 'tsrpc-base-client'; | ||||
| import { ApiReturn, ApiServiceDef, BaseServiceType, Logger, LogLevel, ServerInputData, ServiceProto, setLogLevel, TsrpcError, TsrpcErrorType } from 'tsrpc-proto'; | ||||
| import { ApiCallInner } from "../inner/ApiCallInner"; | ||||
| import { InnerConnection } from "../inner/InnerConnection"; | ||||
| import { TerminalColorLogger } from '../models/TerminalColorLogger'; | ||||
| import { ApiCall } from './ApiCall'; | ||||
| import { BaseConnection } from './BaseConnection'; | ||||
| import { MsgCall } from './MsgCall'; | ||||
|  | ||||
| /** | ||||
|  * Abstract base class for TSRPC Server. | ||||
|  * Implement on a transportation protocol (like HTTP WebSocket) by extend it. | ||||
|  * @typeParam ServiceType - `ServiceType` from generated `proto.ts` | ||||
|  */ | ||||
| export abstract class BaseServer<ServiceType extends BaseServiceType = BaseServiceType>{ | ||||
|     /** | ||||
|      * Start the server | ||||
|      * @throws | ||||
|      */ | ||||
|     abstract start(): Promise<void>; | ||||
|  | ||||
|     /** | ||||
|      * Stop server immediately, not waiting for the requests ending. | ||||
|      */ | ||||
|     abstract stop(): Promise<void>; | ||||
|  | ||||
|     protected _status: ServerStatus = ServerStatus.Closed; | ||||
|     get status(): ServerStatus { | ||||
|         return this._status; | ||||
|     } | ||||
|  | ||||
|     // 配置及其衍生项 | ||||
|     readonly proto: ServiceProto<ServiceType>; | ||||
|     readonly options: BaseServerOptions<ServiceType>; | ||||
|     readonly tsbuffer: TSBuffer; | ||||
|     readonly serviceMap: ServiceMap; | ||||
|     readonly logger: Logger; | ||||
|  | ||||
|     protected _connIdCounter = new Counter(1); | ||||
|  | ||||
|     /**  | ||||
|      * Flow is a specific concept created by TSRPC family. | ||||
|      * All pre-flow can interrupt latter behaviours. | ||||
|      * All post-flow can NOT interrupt latter behaviours. | ||||
|      */ | ||||
|     readonly flows = { | ||||
|         // Conn Flows | ||||
|         /** After the connection is created */ | ||||
|         postConnectFlow: new Flow<BaseConnection<ServiceType>>(), | ||||
|         /** After the connection is disconnected */ | ||||
|         postDisconnectFlow: new Flow<{ conn: BaseConnection<ServiceType>, reason?: string }>(), | ||||
|  | ||||
|         // Buffer Flows | ||||
|         /** | ||||
|          * Before processing the received data, usually be used to encryption / decryption. | ||||
|          * Return `null | undefined` would ignore the buffer. | ||||
|          */ | ||||
|         preRecvDataFlow: new Flow<{ conn: BaseConnection<ServiceType>, data: string | Uint8Array | object, serviceName?: string }>(), | ||||
|         /** | ||||
|          * Before send out data to network, usually be used to encryption / decryption. | ||||
|          * Return `null | undefined` would not send the buffer. | ||||
|          */ | ||||
|         preSendDataFlow: new Flow<{ conn: BaseConnection<ServiceType>, data: string | Uint8Array | object, call?: ApiCall }>(), | ||||
|         /** | ||||
|          * @deprecated Use `preRecvDataFlow` instead. | ||||
|          */ | ||||
|         preRecvBufferFlow: new Flow<{ conn: BaseConnection<ServiceType>, buf: Uint8Array }>(), | ||||
|         /** | ||||
|          * @deprecated Use `preSendDataFlow` instead. | ||||
|          */ | ||||
|         preSendBufferFlow: new Flow<{ conn: BaseConnection<ServiceType>, buf: Uint8Array, call?: ApiCall }>(), | ||||
|  | ||||
|         // ApiCall Flows | ||||
|         /** | ||||
|          * Before a API request is send. | ||||
|          * Return `null | undefined` would cancel the request. | ||||
|          */ | ||||
|         preApiCallFlow: new Flow<ApiCall>(), | ||||
|         /** | ||||
|          * Before return the `ApiReturn` to the client. | ||||
|          * It may be used to change the return value, or return `null | undefined` to abort the request. | ||||
|          */ | ||||
|         preApiReturnFlow: new Flow<{ call: ApiCall, return: ApiReturn<any> }>(), | ||||
|         /**  | ||||
|          * After the `ApiReturn` is send. | ||||
|          * return `null | undefined` would NOT interrupt latter behaviours. | ||||
|          */ | ||||
|         postApiReturnFlow: new Flow<{ call: ApiCall, return: ApiReturn<any> }>(), | ||||
|         /** | ||||
|          * After the api handler is executed. | ||||
|          * return `null | undefined` would NOT interrupt latter behaviours. | ||||
|          */ | ||||
|         postApiCallFlow: new Flow<ApiCall>(), | ||||
|  | ||||
|         // MsgCall Flows | ||||
|         /** | ||||
|          * Before handle a `MsgCall` | ||||
|          */ | ||||
|         preMsgCallFlow: new Flow<MsgCall>(), | ||||
|         /** | ||||
|          * After handlers of a `MsgCall` are executed. | ||||
|          * return `null | undefined` would NOT interrupt latter behaviours. | ||||
|          */ | ||||
|         postMsgCallFlow: new Flow<MsgCall>(), | ||||
|         /** | ||||
|          * Before send out a message. | ||||
|          * return `null | undefined` would NOT interrupt latter behaviours. | ||||
|          */ | ||||
|         preSendMsgFlow: new Flow<{ conn: BaseConnection<ServiceType>, service: MsgService, msg: any }>(), | ||||
|         /** | ||||
|          * After send out a message. | ||||
|          * return `null | undefined` would NOT interrupt latter behaviours. | ||||
|          */ | ||||
|         postSendMsgFlow: new Flow<{ conn: BaseConnection<ServiceType>, service: MsgService, msg: any }>(), | ||||
|     } as const; | ||||
|  | ||||
|     // Handlers | ||||
|     private _apiHandlers: { [apiName: string]: ApiHandler<any> | undefined } = {}; | ||||
|     // 多个Handler将异步并行执行 | ||||
|     private _msgHandlers: MsgHandlerManager = new MsgHandlerManager(); | ||||
|  | ||||
|     private static _isUncaughtExceptionProcessed = false; | ||||
|     /** | ||||
|      * It makes the `uncaughtException` and `unhandledRejection` not lead to the server stopping. | ||||
|      * @param logger  | ||||
|      * @returns  | ||||
|      */ | ||||
|     static processUncaughtException(logger: Logger) { | ||||
|         if (this._isUncaughtExceptionProcessed) { | ||||
|             return; | ||||
|         } | ||||
|         this._isUncaughtExceptionProcessed = true; | ||||
|  | ||||
|         process.on('uncaughtException', e => { | ||||
|             logger.error('[uncaughtException]', e); | ||||
|         }); | ||||
|  | ||||
|         process.on('unhandledRejection', e => { | ||||
|             logger.error('[unhandledRejection]', e); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     constructor(proto: ServiceProto<ServiceType>, options: BaseServerOptions<ServiceType>) { | ||||
|         this.proto = proto; | ||||
|         this.options = options; | ||||
|  | ||||
|         // @deprecated jsonEnabled | ||||
|         if (this.options.json) { | ||||
|             this.options.jsonEnabled = true; | ||||
|         } | ||||
|  | ||||
|         this.tsbuffer = new TSBuffer({ | ||||
|             ...proto.types, | ||||
|             // Support mongodb/ObjectId | ||||
|             ...getCustomObjectIdTypes(ObjectId) | ||||
|         }, { | ||||
|             strictNullChecks: this.options.strictNullChecks | ||||
|         }); | ||||
|         this.serviceMap = ServiceMapUtil.getServiceMap(proto); | ||||
|         this.logger = this.options.logger; | ||||
|         setLogLevel(this.logger, this.options.logLevel); | ||||
|  | ||||
|         // Process uncaught exception, so that Node.js process would not exit easily | ||||
|         BaseServer.processUncaughtException(this.logger); | ||||
|  | ||||
|         // default flows onError handler | ||||
|         this._setDefaultFlowOnError(); | ||||
|     } | ||||
|  | ||||
|     protected _setDefaultFlowOnError() { | ||||
|         // API Flow Error: return [InternalServerError] | ||||
|         this.flows.preApiCallFlow.onError = (e, call) => { | ||||
|             if (e instanceof TsrpcError) { | ||||
|                 call.error(e) | ||||
|             } | ||||
|             else { | ||||
|                 this.onInternalServerError(e, call) | ||||
|             } | ||||
|         }; | ||||
|         this.flows.postApiCallFlow.onError = (e, call) => { | ||||
|             if (!call.return) { | ||||
|                 if (e instanceof TsrpcError) { | ||||
|                     call.error(e) | ||||
|                 } | ||||
|                 else { | ||||
|                     this.onInternalServerError(e, call) | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 call.logger.error('postApiCallFlow Error:', e); | ||||
|             } | ||||
|         }; | ||||
|         this.flows.preApiReturnFlow.onError = (e, last) => { | ||||
|             last.call['_return'] = undefined; | ||||
|             if (e instanceof TsrpcError) { | ||||
|                 last.call.error(e) | ||||
|             } | ||||
|             else { | ||||
|                 this.onInternalServerError(e, last.call) | ||||
|             } | ||||
|         } | ||||
|         this.flows.postApiReturnFlow.onError = (e, last) => { | ||||
|             if (!last.call.return) { | ||||
|                 if (e instanceof TsrpcError) { | ||||
|                     last.call.error(e) | ||||
|                 } | ||||
|                 else { | ||||
|                     this.onInternalServerError(e, last.call) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected _pendingApiCallNum = 0; | ||||
|  | ||||
|     // #region receive buffer process flow | ||||
|     /** | ||||
|      * Process the buffer, after the `preRecvBufferFlow`. | ||||
|      */ | ||||
|     async _onRecvData(conn: BaseConnection<ServiceType>, data: string | Uint8Array | object, serviceName?: string) { | ||||
|         // 非 OPENED 状态 停止接受新的请求 | ||||
|         if (!(conn instanceof InnerConnection) && this.status !== ServerStatus.Opened) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // debugBuf log | ||||
|         if (this.options.debugBuf) { | ||||
|             if (typeof data === 'string') { | ||||
|                 conn.logger?.debug(`[RecvText] length=${data.length}`, data); | ||||
|             } | ||||
|             else if (data instanceof Uint8Array) { | ||||
|                 conn.logger?.debug(`[RecvBuf] length=${data.length}`, data); | ||||
|             } | ||||
|             else { | ||||
|                 conn.logger?.debug('[RecvJSON]', data); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // jsonEnabled 未启用,不支持文本请求 | ||||
|         if (typeof data === 'string' && !this.options.jsonEnabled) { | ||||
|             this.onInputDataError('JSON mode is not enabled, please use binary instead.', conn, data); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let pre = await this.flows.preRecvDataFlow.exec({ conn: conn, data: data, serviceName: serviceName }, conn.logger); | ||||
|         if (!pre) { | ||||
|             conn.logger.debug('[preRecvDataFlow] Canceled'); | ||||
|             return; | ||||
|         } | ||||
|         data = pre.data; | ||||
|         serviceName = pre.serviceName; | ||||
|  | ||||
|         // @deprecated preRecvBuffer | ||||
|         if (data instanceof Uint8Array) { | ||||
|             let preBuf = await this.flows.preRecvBufferFlow.exec({ conn: conn, buf: data }, conn.logger); | ||||
|             if (!preBuf) { | ||||
|                 conn.logger.debug('[preRecvBufferFlow] Canceled'); | ||||
|                 return; | ||||
|             } | ||||
|             data = preBuf.buf; | ||||
|         } | ||||
|  | ||||
|         // Parse Call | ||||
|         let opInput = this._parseServerInput(this.tsbuffer, this.serviceMap, data, serviceName); | ||||
|         if (!opInput.isSucc) { | ||||
|             this.onInputDataError(opInput.errMsg, conn, data); | ||||
|             return; | ||||
|         } | ||||
|         let call = conn.makeCall(opInput.result); | ||||
|  | ||||
|         if (call.type === 'api') { | ||||
|             await this._handleApiCall(call); | ||||
|         } | ||||
|         else { | ||||
|             await this._onMsgCall(call); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected async _handleApiCall(call: ApiCall) { | ||||
|         ++this._pendingApiCallNum; | ||||
|         await this._onApiCall(call); | ||||
|         if (--this._pendingApiCallNum === 0) { | ||||
|             this._gracefulStop?.rs(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected async _onApiCall(call: ApiCall) { | ||||
|         let timeoutTimer = this.options.apiTimeout ? setTimeout(() => { | ||||
|             if (!call.return) { | ||||
|                 call.error('Server Timeout', { | ||||
|                     code: 'SERVER_TIMEOUT', | ||||
|                     type: TsrpcErrorType.ServerError | ||||
|                 }) | ||||
|             } | ||||
|             timeoutTimer = undefined; | ||||
|         }, this.options.apiTimeout) : undefined; | ||||
|  | ||||
|         // Pre Flow | ||||
|         let preFlow = await this.flows.preApiCallFlow.exec(call, call.logger); | ||||
|         if (!preFlow) { | ||||
|             if (timeoutTimer) { | ||||
|                 clearTimeout(timeoutTimer); | ||||
|                 timeoutTimer = undefined; | ||||
|             } | ||||
|             call.logger.debug('[preApiCallFlow] Canceled'); | ||||
|             return; | ||||
|         } | ||||
|         call = preFlow; | ||||
|  | ||||
|         // exec ApiCall | ||||
|         call.logger.log('[ApiReq]', this.options.logReqBody ? call.req : ''); | ||||
|         let { handler } = await this.getApiHandler(call.service, this._delayImplementApiPath, call.logger); | ||||
|         // exec API handler | ||||
|         if (handler) { | ||||
|             try { | ||||
|                 await handler(call); | ||||
|             } | ||||
|             catch (e: any) { | ||||
|                 if (e instanceof TsrpcError) { | ||||
|                     call.error(e); | ||||
|                 } | ||||
|                 else { | ||||
|                     this.onInternalServerError(e, call); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // 未找到ApiHandler,且未进行任何输出 | ||||
|         else { | ||||
|             call.error(`Unhandled API: ${call.service.name}`, { code: 'UNHANDLED_API', type: TsrpcErrorType.ServerError }); | ||||
|         } | ||||
|  | ||||
|         // Post Flow | ||||
|         await this.flows.postApiCallFlow.exec(call, call.logger); | ||||
|  | ||||
|         if (timeoutTimer) { | ||||
|             clearTimeout(timeoutTimer); | ||||
|             timeoutTimer = undefined; | ||||
|         } | ||||
|  | ||||
|         // Destroy call | ||||
|         // if (!call.return) { | ||||
|         //     this.onInternalServerError({ message: 'API not return anything' }, call); | ||||
|         // } | ||||
|     } | ||||
|  | ||||
|     protected async _onMsgCall(call: MsgCall) { | ||||
|         // 收到Msg即可断开连接(短连接) | ||||
|         if (call.conn.type === 'SHORT') { | ||||
|             call.conn.close(); | ||||
|         } | ||||
|  | ||||
|         // Pre Flow | ||||
|         let preFlow = await this.flows.preMsgCallFlow.exec(call, call.logger); | ||||
|         if (!preFlow) { | ||||
|             call.logger.debug('[preMsgCallFlow]', 'Canceled') | ||||
|             return; | ||||
|         } | ||||
|         call = preFlow; | ||||
|  | ||||
|         // MsgHandler | ||||
|         this.options.logMsg && call.logger.log('[RecvMsg]', call.msg); | ||||
|         let promises = [ | ||||
|             // Conn Handlers | ||||
|             ...(call.conn['_msgHandlers']?.forEachHandler(call.service.name, call.logger, call) ?? []), | ||||
|             // Server Handlers | ||||
|             this._msgHandlers.forEachHandler(call.service.name, call.logger, call) | ||||
|         ]; | ||||
|         if (!promises.length) { | ||||
|             this.logger.debug('[UNHANDLED_MSG]', call.service.name); | ||||
|         } | ||||
|         else { | ||||
|             await Promise.all(promises); | ||||
|         } | ||||
|  | ||||
|         // Post Flow | ||||
|         await this.flows.postMsgCallFlow.exec(call, call.logger); | ||||
|     } | ||||
|     // #endregion     | ||||
|  | ||||
|     // #region Api/Msg handler register | ||||
|     /** | ||||
|      * Associate a `ApiHandler` to a specific `apiName`. | ||||
|      * So that when `ApiCall` is receiving, it can be handled correctly. | ||||
|      * @param apiName  | ||||
|      * @param handler  | ||||
|      */ | ||||
|     implementApi<Api extends keyof ServiceType['api'], Call extends ApiCall<ServiceType['api'][Api]['req'], ServiceType['api'][Api]['res']>>(apiName: Api, handler: ApiHandler<Call>): void { | ||||
|         if (this._apiHandlers[apiName as string]) { | ||||
|             throw new Error('Already exist handler for API: ' + apiName); | ||||
|         } | ||||
|         this._apiHandlers[apiName as string] = handler; | ||||
|         this.logger.log(`API implemented succ: [${apiName}]`); | ||||
|     }; | ||||
|  | ||||
|     /** 用于延迟注册 API */ | ||||
|     protected _delayImplementApiPath?: string; | ||||
|  | ||||
|     /** | ||||
|      * Auto call `imeplementApi` by traverse the `apiPath` and find all matched `PtlXXX` and `ApiXXX`. | ||||
|      * It is matched by checking whether the relative path and name of an API is consistent to the service name in `serviceProto`. | ||||
|      * Notice that the name prefix of protocol is `Ptl`, of API is `Api`. | ||||
|      * For example, `protocols/a/b/c/PtlTest` is matched to `api/a/b/c/ApiTest`. | ||||
|      * @param apiPath Absolute path or relative path to `process.cwd()`. | ||||
|      * @returns  | ||||
|      */ | ||||
|     async autoImplementApi(apiPath: string, delay?: boolean): Promise<{ succ: string[], fail: string[] }> { | ||||
|         let apiServices = Object.values(this.serviceMap.apiName2Service) as ApiServiceDef[]; | ||||
|         let output: { succ: string[], fail: string[] } = { succ: [], fail: [] }; | ||||
|  | ||||
|         if (delay) { | ||||
|             this._delayImplementApiPath = apiPath; | ||||
|             return output; | ||||
|         } | ||||
|  | ||||
|         for (let svc of apiServices) { | ||||
|             //get api handler | ||||
|             let { handler } = await this.getApiHandler(svc, apiPath, this.logger) | ||||
|  | ||||
|             if (!handler) { | ||||
|                 output.fail.push(svc.name); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             this.implementApi(svc.name, handler); | ||||
|             output.succ.push(svc.name); | ||||
|         } | ||||
|  | ||||
|         if (output.fail.length) { | ||||
|             this.logger.error(chalk.red(`${output.fail.length} API implemented failed: ` + output.fail.map(v => chalk.cyan.underline(v)).join(' '))) | ||||
|         } | ||||
|  | ||||
|         return output; | ||||
|     } | ||||
|  | ||||
|     async getApiHandler(svc: ApiServiceDef, apiPath?: string, logger?: Logger): Promise<{ handler: ApiHandler, errMsg?: undefined } | { handler?: undefined, errMsg: string }> { | ||||
|         if (this._apiHandlers[svc.name]) { | ||||
|             return { handler: this._apiHandlers[svc.name]! }; | ||||
|         } | ||||
|  | ||||
|         if (!apiPath) { | ||||
|             return { errMsg: `Api not implemented: ${svc.name}` }; | ||||
|         } | ||||
|  | ||||
|         // get api last name | ||||
|         let match = svc.name.match(/^(.+\/)*(.+)$/); | ||||
|         if (!match) { | ||||
|             logger?.error('Invalid apiName: ' + svc.name); | ||||
|             return { errMsg: `Invalid api name: ${svc.name}` }; | ||||
|         } | ||||
|         let handlerPath = match[1] || ''; | ||||
|         let handlerName = match[2]; | ||||
|  | ||||
|         // try import | ||||
|         let modulePath = path.resolve(apiPath, handlerPath, 'Api' + handlerName); | ||||
|         try { | ||||
|             var handlerModule = await import(modulePath); | ||||
|         } | ||||
|         catch (e: unknown) { | ||||
|             this.logger.error(chalk.red(`Implement API ${chalk.cyan.underline(`${svc.name}`)} failed:`), e); | ||||
|             return { errMsg: (e as Error).message }; | ||||
|         } | ||||
|  | ||||
|         // 优先 default,其次 ApiName 同名 | ||||
|         let handler = handlerModule.default ?? handlerModule['Api' + handlerName]; | ||||
|         if (handler) { | ||||
|             return { handler: handler }; | ||||
|         } | ||||
|         else { | ||||
|             return { errMsg: `Missing 'export Api${handlerName}' or 'export default' in: ${modulePath}` } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Add a message handler, | ||||
|      * duplicate handlers to the same `msgName` would be ignored. | ||||
|      * @param msgName | ||||
|      * @param handler | ||||
|      */ | ||||
|     listenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: MsgHandler<Call>): MsgHandler<Call> { | ||||
|         this._msgHandlers.addHandler(msgName as string, handler); | ||||
|         return handler; | ||||
|     }; | ||||
|     /** | ||||
|      * Remove a message handler | ||||
|      */ | ||||
|     unlistenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: Function): void { | ||||
|         this._msgHandlers.removeHandler(msgName as string, handler); | ||||
|     }; | ||||
|     /** | ||||
|      * Remove all handlers from a message | ||||
|      */ | ||||
|     unlistenMsgAll<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg): void { | ||||
|         this._msgHandlers.removeAllHandlers(msgName as string); | ||||
|     }; | ||||
|     // #endregion    | ||||
|  | ||||
|     /** | ||||
|      * Event when the server cannot parse input buffer to api/msg call. | ||||
|      * By default, it will return "Input Data Error" . | ||||
|      */ | ||||
|     async onInputDataError(errMsg: string, conn: BaseConnection<ServiceType>, data: string | Uint8Array | object) { | ||||
|         if (this.options.debugBuf) { | ||||
|             if (typeof data === 'string') { | ||||
|                 conn.logger.error(`[InputDataError] ${errMsg} length = ${data.length}`, data) | ||||
|             } | ||||
|             else if (data instanceof Uint8Array) { | ||||
|                 conn.logger.error(`[InputBufferError] ${errMsg} length = ${data.length}`, data.subarray(0, 16)) | ||||
|             } | ||||
|             else { | ||||
|                 conn.logger.error(`[InputJsonError] ${errMsg} `, data) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const message = data instanceof Uint8Array ? `Invalid request buffer, please check the version of service proto.` : errMsg; | ||||
|  | ||||
|         // Short conn, send apiReturn with error | ||||
|         if (conn.type === 'SHORT') { | ||||
|             // Return API Error | ||||
|             let opEncode = ApiCall.encodeApiReturn(this.tsbuffer, { | ||||
|                 type: 'api', | ||||
|                 name: '?', | ||||
|                 id: 0, | ||||
|                 reqSchemaId: '?', | ||||
|                 resSchemaId: '?' | ||||
|             }, { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError({ | ||||
|                     message: message, | ||||
|                     type: TsrpcErrorType.ServerError, | ||||
|                     code: 'INPUT_DATA_ERR' | ||||
|                 }) | ||||
|             }, conn.dataType) | ||||
|             if (opEncode.isSucc) { | ||||
|                 let opSend = await conn.sendData(opEncode.output); | ||||
|                 if (opSend.isSucc) { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         conn.close(message); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Event when a uncaught error (except `TsrpcError`) is throwed. | ||||
|      * By default, it will return a `TsrpcError` with message "Internal server error". | ||||
|      * If `returnInnerError` is `true`, the original error would be returned as `innerErr` property. | ||||
|      */ | ||||
|     onInternalServerError(err: { message: string, stack?: string, name?: string }, call: ApiCall) { | ||||
|         call.logger.error(err); | ||||
|         call.error('Internal Server Error', { | ||||
|             code: 'INTERNAL_ERR', | ||||
|             type: TsrpcErrorType.ServerError, | ||||
|             innerErr: call.conn.server.options.returnInnerError ? err.message : undefined | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     protected _gracefulStop?: { | ||||
|         rs: () => void | ||||
|     }; | ||||
|     /** | ||||
|      * Stop the server gracefully. | ||||
|      * Wait all API requests finished and then stop the server. | ||||
|      * @param maxWaitTime - The max time(ms) to wait before force stop the server. | ||||
|      * `undefined` and `0` means unlimited time. | ||||
|      */ | ||||
|     async gracefulStop(maxWaitTime?: number) { | ||||
|         if (this._status !== ServerStatus.Opened) { | ||||
|             throw new Error(`Cannot gracefulStop when server status is '${this._status}'.`); | ||||
|         } | ||||
|  | ||||
|         this.logger.log('[GracefulStop] Start graceful stop, waiting all ApiCall finished...') | ||||
|         this._status = ServerStatus.Closing; | ||||
|         let promiseWaitApi = new Promise<void>(rs => { | ||||
|             this._gracefulStop = { | ||||
|                 rs: rs | ||||
|             }; | ||||
|         }); | ||||
|  | ||||
|         return new Promise<void>(rs => { | ||||
|             let maxWaitTimer: ReturnType<typeof setTimeout> | undefined; | ||||
|             if (maxWaitTime) { | ||||
|                 maxWaitTimer = setTimeout(() => { | ||||
|                     maxWaitTimer = undefined; | ||||
|                     if (this._gracefulStop) { | ||||
|                         this._gracefulStop = undefined; | ||||
|                         this.logger.log('Graceful stop timeout, stop the server directly.'); | ||||
|                         this.stop().then(() => { rs() }); | ||||
|                     } | ||||
|                 }, maxWaitTime); | ||||
|             } | ||||
|  | ||||
|             promiseWaitApi.then(() => { | ||||
|                 this.logger.log('All ApiCall finished, continue stop server.'); | ||||
|                 if (maxWaitTimer) { | ||||
|                     clearTimeout(maxWaitTimer); | ||||
|                     maxWaitTimer = undefined; | ||||
|                 } | ||||
|                 if (this._gracefulStop) { | ||||
|                     this._gracefulStop = undefined; | ||||
|                     this.stop().then(() => { rs() }); | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Execute API function through the inner connection, which is useful for unit test. | ||||
|      *  | ||||
|      * **NOTICE** | ||||
|      * The `req` and return value is native JavaScript object which is not compatible to JSON. (etc. ArrayBuffer, Date, ObjectId) | ||||
|      * If you are using pure JSON as transfering, you may need use `callApiByJSON`. | ||||
|      * @param apiName  | ||||
|      * @param req  | ||||
|      * @param options  | ||||
|      */ | ||||
|     callApi<T extends keyof ServiceType['api']>(apiName: T, req: ServiceType['api'][T]['req']): Promise<ApiReturn<ServiceType['api'][T]['res']>> { | ||||
|         return new Promise(rs => { | ||||
|             // 确认是哪个Service | ||||
|             let service = this.serviceMap.apiName2Service[apiName as string]; | ||||
|             if (!service) { | ||||
|                 let errMsg = `Cannot find service: ${apiName}`; | ||||
|                 this.logger.warn(`[callApi]`, errMsg); | ||||
|                 rs({ isSucc: false, err: new TsrpcError(errMsg, { type: TsrpcErrorType.ServerError, code: 'ERR_API_NAME' }) }); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let conn = new InnerConnection({ | ||||
|                 dataType: 'json', | ||||
|                 server: this, | ||||
|                 id: '' + this._connIdCounter.getNext(), | ||||
|                 ip: '', | ||||
|                 return: { | ||||
|                     type: 'raw', | ||||
|                     rs: rs | ||||
|                 } | ||||
|             }); | ||||
|             let call = new ApiCallInner({ | ||||
|                 conn: conn, | ||||
|                 req: req, | ||||
|                 service: service | ||||
|             }); | ||||
|             this._handleApiCall(call); | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Like `server.callApi`, but both input and output are pure JSON object, | ||||
|      * which can be `JSON.stringify()` and `JSON.parse()` directly. | ||||
|      * Types that not compatible to JSON, would be encoded and decoded automatically. | ||||
|      * @param apiName - The same with `server.callApi`, may be parsed from the URL. | ||||
|      * @param jsonReq - Request data in pure JSON | ||||
|      * @returns Encoded `ApiReturn<Res>` in pure JSON | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Process JSON request by inner proxy, this is useful when you are porting to cloud function services. | ||||
|      * Both the input and output is pure JSON, ArrayBuffer/Date/ObjectId are encoded to string automatically. | ||||
|      * @param apiName - Parsed from URL | ||||
|      * @param req - Pure JSON | ||||
|      * @returns - Pure JSON | ||||
|      */ | ||||
|     async inputJSON(apiName: string, req: object): Promise<ApiReturn<object>> { | ||||
|         if (apiName.startsWith('/')) { | ||||
|             apiName = apiName.slice(1); | ||||
|         } | ||||
|         if (!this.serviceMap.apiName2Service[apiName]) { | ||||
|             return { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError(`Invalid service name: ${apiName}`, { | ||||
|                     type: TsrpcErrorType.ServerError, | ||||
|                     code: 'INPUT_DATA_ERR' | ||||
|                 }) | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return new Promise(rs => { | ||||
|             let conn = new InnerConnection({ | ||||
|                 dataType: 'json', | ||||
|                 server: this, | ||||
|                 id: '' + this._connIdCounter.getNext(), | ||||
|                 ip: '', | ||||
|                 return: { | ||||
|                     type: 'json', | ||||
|                     rs: rs | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             this._onRecvData(conn, req, apiName); | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Process input buffer by inner proxy, this is useful when you are porting to cloud function services. | ||||
|      * @param buf Input buffer (may be sent by TSRPC client) | ||||
|      * @returns Response buffer | ||||
|      */ | ||||
|     inputBuffer(buf: Uint8Array): Promise<Uint8Array> { | ||||
|         return new Promise(rs => { | ||||
|             let conn = new InnerConnection({ | ||||
|                 dataType: 'buffer', | ||||
|                 server: this, | ||||
|                 id: '' + this._connIdCounter.getNext(), | ||||
|                 ip: '', | ||||
|                 return: { | ||||
|                     type: 'buffer', | ||||
|                     rs: rs | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             this._onRecvData(conn, buf); | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     protected _parseServerInput(tsbuffer: TSBuffer, serviceMap: ServiceMap, data: string | Uint8Array | object, serviceName?: string): { isSucc: true, result: ParsedServerInput } | { isSucc: false, errMsg: string } { | ||||
|         if (data instanceof Uint8Array) { | ||||
|             let opServerInputData = TransportDataUtil.tsbuffer.decode(data, 'ServerInputData'); | ||||
|  | ||||
|             if (!opServerInputData.isSucc) { | ||||
|                 return opServerInputData; | ||||
|             } | ||||
|             let serverInput = opServerInputData.value as ServerInputData; | ||||
|  | ||||
|             // 确认是哪个Service | ||||
|             let service = serviceMap.id2Service[serverInput.serviceId]; | ||||
|             if (!service) { | ||||
|                 return { isSucc: false, errMsg: `Cannot find service ID: ${serverInput.serviceId}` } | ||||
|             } | ||||
|  | ||||
|             // 解码Body | ||||
|             if (service.type === 'api') { | ||||
|                 let opReq = tsbuffer.decode(serverInput.buffer, service.reqSchemaId); | ||||
|                 return opReq.isSucc ? { | ||||
|                     isSucc: true, | ||||
|                     result: { | ||||
|                         type: 'api', | ||||
|                         service: service, | ||||
|                         req: opReq.value, | ||||
|                         sn: serverInput.sn | ||||
|                     } | ||||
|                 } : opReq | ||||
|             } | ||||
|             else { | ||||
|                 let opMsg = tsbuffer.decode(serverInput.buffer, service.msgSchemaId); | ||||
|                 return opMsg.isSucc ? { | ||||
|                     isSucc: true, | ||||
|                     result: { | ||||
|                         type: 'msg', | ||||
|                         service: service, | ||||
|                         msg: opMsg.value | ||||
|                     } | ||||
|                 } : opMsg; | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             let json: object; | ||||
|             if (typeof data === 'string') { | ||||
|                 try { | ||||
|                     json = JSON.parse(data); | ||||
|                 } | ||||
|                 catch (e: any) { | ||||
|                     return { isSucc: false, errMsg: `Input is not a valid JSON string: ${e.message}` }; | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 json = data; | ||||
|             } | ||||
|  | ||||
|             let body: any; | ||||
|             let sn: number | undefined; | ||||
|  | ||||
|             // Parse serviceName / body / sn | ||||
|             let service: ApiService | MsgService | undefined; | ||||
|             const oriServiceName = serviceName; | ||||
|             if (serviceName == undefined) { | ||||
|                 if (!Array.isArray(json)) { | ||||
|                     return { isSucc: false, errMsg: `Invalid request format: unresolved service name.` }; | ||||
|                 } | ||||
|                 serviceName = json[0] as string; | ||||
|                 body = json[1]; | ||||
|                 sn = json[2]; | ||||
|             } | ||||
|             else { | ||||
|                 body = json; | ||||
|             } | ||||
|  | ||||
|             // Get Service | ||||
|             service = serviceMap.apiName2Service[serviceName] ?? serviceMap.msgName2Service[serviceName]; | ||||
|             if (!service) { | ||||
|                 let errMsg = `Invalid service name: ${chalk.cyan.underline(serviceName)}`; | ||||
|  | ||||
|                 // 可能是 JSON 模式下,jsonHostPath 未设置妥当的原因,此时给予友好提示 | ||||
|                 if (oriServiceName) { | ||||
|                     // TODO | ||||
|                 } | ||||
|  | ||||
|                 return { isSucc: false, errMsg: errMsg }; | ||||
|             } | ||||
|  | ||||
|             // Decode | ||||
|             if (service.type === 'api') { | ||||
|                 let op = tsbuffer.decodeJSON(body, service.reqSchemaId); | ||||
|                 if (!op.isSucc) { | ||||
|                     return op; | ||||
|                 } | ||||
|                 return { | ||||
|                     isSucc: true, | ||||
|                     result: { | ||||
|                         type: 'api', | ||||
|                         service: service, | ||||
|                         sn: sn, | ||||
|                         req: op.value | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|             else { | ||||
|                 let op = tsbuffer.decodeJSON(body, service.msgSchemaId); | ||||
|                 if (!op.isSucc) { | ||||
|                     return op; | ||||
|                 } | ||||
|                 return { | ||||
|                     isSucc: true, | ||||
|                     result: { | ||||
|                         type: 'msg', | ||||
|                         service: service, | ||||
|                         msg: op.value | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export interface BaseServerOptions<ServiceType extends BaseServiceType> { | ||||
|     /** | ||||
|      * Whether to enable JSON compatible mode. | ||||
|      * When it is true, it can be compatible with typical HTTP JSON request (like RESTful API). | ||||
|      *  | ||||
|      * @remarks | ||||
|      * The JSON request methods are: | ||||
|      *  | ||||
|      * 1. Add `Content-type: application/json` to request header. | ||||
|      * 2. HTTP request is: `POST /{jsonUrlPath}/{apiName}`. | ||||
|      * 3. POST body is JSON string. | ||||
|      * 4. The response body is JSON string also. | ||||
|      *  | ||||
|      * NOTICE: Buffer type are not supported due to JSON not support them. | ||||
|      * For security and efficient reason, we strongly recommend you use binary encoded transportation. | ||||
|      *  | ||||
|      * @defaultValue `false` | ||||
|      */ | ||||
|     json: boolean, | ||||
|     /** @deprecated Use `json` instead. */ | ||||
|     jsonEnabled?: boolean, | ||||
|  | ||||
|     // TSBuffer相关 | ||||
|     /** | ||||
|      * Whether to strictly distinguish between `null` and `undefined` when encoding, decoding, and type checking. | ||||
|      * @defaultValue false | ||||
|      */ | ||||
|     strictNullChecks: boolean, | ||||
|  | ||||
|     /** | ||||
|      * Timeout for processing an `ApiCall`(ms) | ||||
|      * `0` and `undefined` means unlimited time | ||||
|      * @defaultValue 30000 | ||||
|      */ | ||||
|     apiTimeout: number | undefined, | ||||
|  | ||||
|     // LOG相关 | ||||
|     /** | ||||
|      * Logger for processing log | ||||
|      * @defaultValue `new TerminalColorLogger()` (print to console with color) | ||||
|      */ | ||||
|     logger: Logger; | ||||
|     /** | ||||
|      * The minimum log level of `logger` | ||||
|      * @defaultValue `debug` | ||||
|      */ | ||||
|     logLevel: LogLevel; | ||||
|     /**  | ||||
|      * Whethere to print API request body into log (may increase log size) | ||||
|      * @defaultValue `true` | ||||
|      */ | ||||
|     logReqBody: boolean; | ||||
|     /**  | ||||
|      * Whethere to print API response body into log (may increase log size) | ||||
|      * @defaultValue `true` | ||||
|      */ | ||||
|     logResBody: boolean; | ||||
|     /** | ||||
|      * Whethere to print `[SendMsg]` and `[RecvMsg]` log into log | ||||
|      * @defaultValue `true` | ||||
|      */ | ||||
|     logMsg: boolean; | ||||
|  | ||||
|     /** | ||||
|      * If `true`, all sent and received raw buffer would be print into the log. | ||||
|      * It may be useful when you do something for buffer encryption/decryption, and want to debug them. | ||||
|      */ | ||||
|     debugBuf?: boolean; | ||||
|  | ||||
|     /** | ||||
|      * When uncaught error throwed, | ||||
|      * whether to return the original error as a property `innerErr`.  | ||||
|      * (May include some sensitive information, suggests set to `false` in production environment.) | ||||
|      * @defaultValue It depends on environment variable `NODE_ENV`. | ||||
|      * If `NODE_ENV` equals to `production`, the default value is `false`, otherwise is `true`. | ||||
|      */ | ||||
|     returnInnerError: boolean; | ||||
| } | ||||
|  | ||||
| export const defaultBaseServerOptions: BaseServerOptions<any> = { | ||||
|     json: false, | ||||
|     strictNullChecks: false, | ||||
|     apiTimeout: 30000, | ||||
|     logger: new TerminalColorLogger, | ||||
|     logLevel: 'debug', | ||||
|     logReqBody: true, | ||||
|     logResBody: true, | ||||
|     logMsg: true, | ||||
|     returnInnerError: process.env['NODE_ENV'] !== 'production' | ||||
| } | ||||
|  | ||||
| export type ApiHandler<Call extends ApiCall = ApiCall> = (call: Call) => void | Promise<void>; | ||||
| export type MsgHandler<Call extends MsgCall = MsgCall> = (call: Call) => void | Promise<void>; | ||||
|  | ||||
| export enum ServerStatus { | ||||
|     Opening = 'OPENING', | ||||
|     Opened = 'OPENED', | ||||
|     Closing = 'CLOSING', | ||||
|     Closed = 'CLOSED', | ||||
| } | ||||
							
								
								
									
										30
									
								
								src/server/base/MsgCall.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/server/base/MsgCall.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { MsgService } from "tsrpc-base-client"; | ||||
| import { BaseServiceType } from "tsrpc-proto"; | ||||
| import { PrefixLogger } from "../models/PrefixLogger"; | ||||
| import { BaseCall, BaseCallOptions } from "./BaseCall"; | ||||
|  | ||||
| export interface MsgCallOptions<Msg, ServiceType extends BaseServiceType> extends BaseCallOptions<ServiceType> { | ||||
|     service: MsgService, | ||||
|     msg: Msg | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A call request by `client.sendMsg()` | ||||
|  * @typeParam Msg - Type of the message | ||||
|  * @typeParam ServiceType - The same `ServiceType` to server, it is used for code auto hint. | ||||
|  */ | ||||
| export abstract class MsgCall<Msg = any, ServiceType extends BaseServiceType = any> extends BaseCall<ServiceType> { | ||||
|     readonly type = 'msg' as const; | ||||
|  | ||||
|     readonly service!: MsgService; | ||||
|     readonly msg: Msg; | ||||
|  | ||||
|     constructor(options: MsgCallOptions<Msg, ServiceType>, logger?: PrefixLogger) { | ||||
|         super(options, logger ?? new PrefixLogger({ | ||||
|             logger: options.conn.logger, | ||||
|             prefixs: [`[Msg:${options.service.name}]`] | ||||
|         })); | ||||
|  | ||||
|         this.msg = options.msg; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/server/http/ApiCallHttp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/server/http/ApiCallHttp.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { ApiReturn, BaseServiceType, TsrpcErrorType } from 'tsrpc-proto'; | ||||
| import { ApiCall, ApiCallOptions } from '../base/ApiCall'; | ||||
| import { HttpConnection } from './HttpConnection'; | ||||
|  | ||||
| export interface ApiCallHttpOptions<Req, ServiceType extends BaseServiceType> extends ApiCallOptions<Req, ServiceType> { | ||||
|     conn: HttpConnection<ServiceType>; | ||||
| } | ||||
| export class ApiCallHttp<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends ApiCall<Req, Res, ServiceType> { | ||||
|  | ||||
|     readonly conn!: HttpConnection<ServiceType>; | ||||
|  | ||||
|     constructor(options: ApiCallHttpOptions<Req, ServiceType>) { | ||||
|         super(options); | ||||
|     } | ||||
|  | ||||
|     protected async _sendReturn(ret: ApiReturn<Res>): Promise<{ isSucc: true } | { isSucc: false, errMsg: string }> { | ||||
|         if (this.conn.dataType === 'text') { | ||||
|             if (ret.isSucc) { | ||||
|                 this.conn.httpRes.statusCode = 200; | ||||
|             } | ||||
|             else { | ||||
|                 this.conn.httpRes.statusCode = ret.err.type === TsrpcErrorType.ApiError ? 200 : 500; | ||||
|             } | ||||
|         } | ||||
|         return super._sendReturn(ret); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										90
									
								
								src/server/http/HttpConnection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/server/http/HttpConnection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| import * as http from "http"; | ||||
| import { ParsedServerInput } from "tsrpc-base-client"; | ||||
| import { BaseServiceType } from "tsrpc-proto"; | ||||
| import { ApiCall } from "../base/ApiCall"; | ||||
| import { BaseConnection, BaseConnectionOptions, ConnectionStatus } from '../base/BaseConnection'; | ||||
| import { PrefixLogger } from "../models/PrefixLogger"; | ||||
| import { ApiCallHttp } from "./ApiCallHttp"; | ||||
| import { HttpServer } from './HttpServer'; | ||||
| import { MsgCallHttp } from "./MsgCallHttp"; | ||||
|  | ||||
| export interface HttpConnectionOptions<ServiceType extends BaseServiceType> extends BaseConnectionOptions<ServiceType> { | ||||
|     server: HttpServer<ServiceType>, | ||||
|     httpReq: http.IncomingMessage, | ||||
|     httpRes: http.ServerResponse, | ||||
| } | ||||
|  | ||||
| export class HttpConnection<ServiceType extends BaseServiceType = any> extends BaseConnection<ServiceType> { | ||||
|     readonly type = 'SHORT'; | ||||
|  | ||||
|     protected readonly ApiCallClass = ApiCallHttp; | ||||
|     protected readonly MsgCallClass = MsgCallHttp; | ||||
|  | ||||
|     readonly httpReq: http.IncomingMessage; | ||||
|     readonly httpRes: http.ServerResponse; | ||||
|     readonly server!: HttpServer<ServiceType>; | ||||
|     /** | ||||
|      * Whether the transportation of the connection is JSON encoded instead of binary encoded. | ||||
|      */ | ||||
|     readonly isJSON: boolean | undefined; | ||||
|  | ||||
|     /**  | ||||
|      * In short connection, one connection correspond one call. | ||||
|      * It may be `undefined` when the request data is not fully received yet. | ||||
|      */ | ||||
|     call?: ApiCallHttp | MsgCallHttp; | ||||
|  | ||||
|     constructor(options: HttpConnectionOptions<ServiceType>) { | ||||
|         super(options, new PrefixLogger({ | ||||
|             logger: options.server.logger, | ||||
|             prefixs: [`${options.ip} #${options.id}`] | ||||
|         })); | ||||
|  | ||||
|         this.httpReq = options.httpReq; | ||||
|         this.httpRes = options.httpRes; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public get status(): ConnectionStatus { | ||||
|         if (this.httpRes.socket?.writableFinished) { | ||||
|             return ConnectionStatus.Closed; | ||||
|         } | ||||
|         else if (this.httpRes.socket?.writableEnded) { | ||||
|             return ConnectionStatus.Closing; | ||||
|         } | ||||
|         else { | ||||
|             return ConnectionStatus.Opened; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected async doSendData(data: string | Uint8Array, call?: ApiCall): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> { | ||||
|         if (typeof data === 'string') { | ||||
|             this.httpRes.setHeader('Content-Type', 'application/json; charset=utf-8'); | ||||
|         } | ||||
|         this.httpRes.end(typeof data === 'string' ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength)); | ||||
|         return { isSucc: true } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Close the connection, the reason would be attached to response header `X-TSRPC-Close-Reason`. | ||||
|      */ | ||||
|     close(reason?: string) { | ||||
|         if (this.status !== ConnectionStatus.Opened) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 有Reason代表是异常关闭 | ||||
|         if (reason) { | ||||
|             this.logger.warn(this.httpReq.method, this.httpReq.url, reason); | ||||
|         } | ||||
|         reason && this.httpRes.setHeader('X-TSRPC-Close-Reason', reason); | ||||
|         this.httpRes.end(); | ||||
|     } | ||||
|  | ||||
|     // HTTP Server 一个conn只有一个call,对应关联之 | ||||
|     makeCall(input: ParsedServerInput): ApiCallHttp | MsgCallHttp { | ||||
|         let call = super.makeCall(input) as ApiCallHttp | MsgCallHttp; | ||||
|         this.call = call; | ||||
|         return call; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										241
									
								
								src/server/http/HttpServer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								src/server/http/HttpServer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| import * as http from "http"; | ||||
| import { BaseServiceType, ServiceProto } from 'tsrpc-proto'; | ||||
| import { HttpUtil } from '../../models/HttpUtil'; | ||||
| import { TSRPC_VERSION } from "../../models/version"; | ||||
| import { BaseServer, BaseServerOptions, defaultBaseServerOptions, ServerStatus } from '../base/BaseServer'; | ||||
| import { HttpConnection } from './HttpConnection'; | ||||
|  | ||||
| /** | ||||
|  * TSRPC Server, based on HTTP connection. | ||||
|  * @typeParam ServiceType - `ServiceType` from generated `proto.ts` | ||||
|  */ | ||||
| export class HttpServer<ServiceType extends BaseServiceType = any> extends BaseServer<ServiceType>{ | ||||
|     readonly options!: HttpServerOptions<ServiceType>; | ||||
|  | ||||
|     constructor(proto: ServiceProto<ServiceType>, options?: Partial<HttpServerOptions<ServiceType>>) { | ||||
|         super(proto, { | ||||
|             ...defaultHttpServerOptions, | ||||
|             ...options | ||||
|         }); | ||||
|  | ||||
|         // 确保 jsonHostPath 以 / 开头和结尾 | ||||
|         this.options.jsonHostPath = this.options.jsonHostPath ? | ||||
|             (this.options.jsonHostPath.startsWith('/') ? '' : '/') + this.options.jsonHostPath + (this.options.jsonHostPath.endsWith('/') ? '' : '/') | ||||
|             : '/'; | ||||
|     } | ||||
|  | ||||
|     /** Native `http.Server` of NodeJS */ | ||||
|     httpServer?: http.Server; | ||||
|     /** | ||||
|      * {@inheritDoc BaseServer.start} | ||||
|      */ | ||||
|     start(): Promise<void> { | ||||
|         if (this.httpServer) { | ||||
|             throw new Error('Server already started'); | ||||
|         } | ||||
|  | ||||
|         return new Promise(rs => { | ||||
|             this._status = ServerStatus.Opening; | ||||
|             this.logger.log(`Starting HTTP server ...`); | ||||
|             this.httpServer = http.createServer((httpReq, httpRes) => { | ||||
|                 if (this.status !== ServerStatus.Opened) { | ||||
|                     httpRes.statusCode = 503; | ||||
|                     httpRes.end(); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 let ip = HttpUtil.getClientIp(httpReq); | ||||
|  | ||||
|                 httpRes.statusCode = 200; | ||||
|                 httpRes.setHeader('X-Powered-By', `TSRPC ${TSRPC_VERSION}`); | ||||
|                 if (this.options.cors) { | ||||
|                     httpRes.setHeader('Access-Control-Allow-Origin', this.options.cors); | ||||
|                     httpRes.setHeader('Access-Control-Allow-Headers', 'Content-Type,*'); | ||||
|                     if (this.options.corsMaxAge) { | ||||
|                         httpRes.setHeader('Access-Control-Max-Age', '' + this.options.corsMaxAge); | ||||
|                     } | ||||
|                     if (httpReq.method === 'OPTIONS') { | ||||
|                         httpRes.writeHead(200); | ||||
|                         httpRes.end(); | ||||
|                         return; | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 let chunks: Buffer[] = []; | ||||
|                 httpReq.on('data', data => { | ||||
|                     chunks.push(data); | ||||
|                 }); | ||||
|  | ||||
|                 let conn: HttpConnection<ServiceType> | undefined; | ||||
|                 httpReq.on('end', async () => { | ||||
|                     let isJSON = this.options.jsonEnabled && httpReq.headers["content-type"]?.toLowerCase().includes('application/json') | ||||
|                         && httpReq.method === 'POST' && httpReq.url?.startsWith(this.options.jsonHostPath); | ||||
|                     conn = new HttpConnection({ | ||||
|                         server: this, | ||||
|                         id: '' + this._connIdCounter.getNext(), | ||||
|                         ip: ip, | ||||
|                         httpReq: httpReq, | ||||
|                         httpRes: httpRes, | ||||
|                         dataType: isJSON ? 'text' : 'buffer' | ||||
|                     }); | ||||
|                     await this.flows.postConnectFlow.exec(conn, conn.logger); | ||||
|  | ||||
|                     let buf = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); | ||||
|  | ||||
|                     if (conn.dataType === 'text') { | ||||
|                         let url = conn.httpReq.url!; | ||||
|  | ||||
|                         let urlEndPos = url.indexOf('?'); | ||||
|                         if (urlEndPos > -1) { | ||||
|                             url = url.slice(0, urlEndPos); | ||||
|                         } | ||||
|  | ||||
|                         let serviceName = url.slice(this.options.jsonHostPath.length); | ||||
|                         this._onRecvData(conn, buf.toString(), serviceName); | ||||
|                     } | ||||
|                     else { | ||||
|                         this._onRecvData(conn, buf); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 // 处理连接异常关闭的情况 | ||||
|                 httpRes.on('close', async () => { | ||||
|                     // 客户端Abort | ||||
|                     if (httpReq.aborted) { | ||||
|                         if (conn) { | ||||
|                             if (conn.call) { | ||||
|                                 conn.call.logger.log('[ReqAborted]'); | ||||
|                             } | ||||
|                             else { | ||||
|                                 conn.logger.log('[ReqAborted]'); | ||||
|                             } | ||||
|                         } | ||||
|                         else { | ||||
|                             this.logger.log('[ReqAborted]', { | ||||
|                                 url: httpReq.url, | ||||
|                                 method: httpReq.method, | ||||
|                                 ip: ip, | ||||
|                                 chunksLength: chunks.length, | ||||
|                                 chunksSize: chunks.sum(v => v.byteLength), | ||||
|                                 reqComplete: httpReq.complete, | ||||
|                                 headers: httpReq.rawHeaders | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                     // 非Abort,异常中断:直到连接关闭,Client也未end(Conn未生成) | ||||
|                     else if (!conn) { | ||||
|                         this.logger.warn('Socket closed before request end', { | ||||
|                             url: httpReq.url, | ||||
|                             method: httpReq.method, | ||||
|                             ip: ip, | ||||
|                             chunksLength: chunks.length, | ||||
|                             chunksSize: chunks.sum(v => v.byteLength), | ||||
|                             reqComplete: httpReq.complete, | ||||
|                             headers: httpReq.rawHeaders | ||||
|                         }); | ||||
|                     } | ||||
|                     // 有Conn,但连接非正常end:直到连接关闭,也未调用过 httpRes.end 方法 | ||||
|                     else if (!httpRes.writableEnded) { | ||||
|                         (conn.call?.logger || conn.logger).warn('Socket closed without response') | ||||
|                     } | ||||
|  | ||||
|                     // Post Flow | ||||
|                     if (conn) { | ||||
|                         await this.flows.postDisconnectFlow.exec({ conn: conn }, conn.logger) | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             if (this.options.socketTimeout) { | ||||
|                 this.httpServer.timeout = this.options.socketTimeout; | ||||
|             } | ||||
|             if (this.options.keepAliveTimeout) { | ||||
|                 this.httpServer.keepAliveTimeout = this.options.keepAliveTimeout; | ||||
|             } | ||||
|  | ||||
|             this.httpServer.listen(this.options.port, () => { | ||||
|                 this._status = ServerStatus.Opened; | ||||
|                 this.logger.log(`Server started at ${this.options.port}.`); | ||||
|                 rs(); | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * {@inheritDoc BaseServer.stop} | ||||
|      */ | ||||
|     async stop(): Promise<void> { | ||||
|         if (!this.httpServer) { | ||||
|             return; | ||||
|         } | ||||
|         this.logger.log('Stopping server...'); | ||||
|  | ||||
|         return new Promise<void>((rs) => { | ||||
|             this._status = ServerStatus.Closing; | ||||
|  | ||||
|             // 立即close,不再接受新请求 | ||||
|             // 等所有连接都断开后rs | ||||
|             this.httpServer?.close(err => { | ||||
|                 this._status = ServerStatus.Closed; | ||||
|                 this.httpServer = undefined; | ||||
|  | ||||
|                 if (err) { | ||||
|                     this.logger.error(err); | ||||
|                 } | ||||
|                 this.logger.log('Server stopped'); | ||||
|                 rs(); | ||||
|             }); | ||||
|         }) | ||||
|  | ||||
|     } | ||||
| } | ||||
|  | ||||
| export interface HttpServerOptions<ServiceType extends BaseServiceType> extends BaseServerOptions<ServiceType> { | ||||
|     /** Which port the HTTP server listen to */ | ||||
|     port: number, | ||||
|     /**  | ||||
|      * Passed to the `timeout` property to the native `http.Server` of NodeJS, in milliseconds. | ||||
|      * `0` and `undefined` will disable the socket timeout behavior. | ||||
|      * NOTICE: this `socketTimeout` be `undefined` only means disabling of the socket timeout, the `apiTimeout` is still working. | ||||
|      * `socketTimeout` should always greater than `apiTimeout`. | ||||
|      * @defaultValue `undefined` | ||||
|      * @see {@link https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_server_timeout} | ||||
|      */ | ||||
|     socketTimeout?: number, | ||||
|     /** | ||||
|      * Passed to the `keepAliveTimeout` property to the native `http.Server` of NodeJS, in milliseconds. | ||||
|      * It means keep-alive timeout of HTTP socket connection. | ||||
|      * @defaultValue 5000 (from NodeJS) | ||||
|      * @see {@link https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_server_keepalivetimeout} | ||||
|      */ | ||||
|     keepAliveTimeout?: number, | ||||
|     /**  | ||||
|      * Response header value of `Access-Control-Allow-Origin`. | ||||
|      * If this has any value, it would also set `Access-Control-Allow-Headers` as `*`. | ||||
|      * `undefined` means no CORS header. | ||||
|      * @defaultValue `*` | ||||
|      */ | ||||
|     cors?: string, | ||||
|     /** | ||||
|      * Response header value of `Access-Control-Allow-Origin`. | ||||
|      * @defaultValue `3600` | ||||
|      */ | ||||
|     corsMaxAge?: number, | ||||
|  | ||||
|     /** | ||||
|      * Actual URL path is `${jsonHostPath}/${apiName}`. | ||||
|      * For example, if `jsonHostPath` is `'/api'`, then you can send `POST /api/a/b/c/Test` to call API `a/b/c/Test`. | ||||
|      * @defaultValue `'/'` | ||||
|      */ | ||||
|     jsonHostPath: string | ||||
| } | ||||
|  | ||||
| export const defaultHttpServerOptions: HttpServerOptions<any> = { | ||||
|     ...defaultBaseServerOptions, | ||||
|     port: 3000, | ||||
|     cors: '*', | ||||
|     corsMaxAge: 3600, | ||||
|     jsonHostPath: '/', | ||||
|  | ||||
|     // TODO: keep-alive time (to SLB) | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/server/http/MsgCallHttp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/server/http/MsgCallHttp.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import { BaseServiceType } from "tsrpc-proto"; | ||||
| import { MsgCall, MsgCallOptions } from "../base/MsgCall"; | ||||
| import { HttpConnection } from "./HttpConnection"; | ||||
|  | ||||
| export interface MsgCallHttpOptions<Msg, ServiceType extends BaseServiceType> extends MsgCallOptions<Msg, ServiceType> { | ||||
|     conn: HttpConnection<ServiceType>; | ||||
| } | ||||
| export class MsgCallHttp<Msg = any, ServiceType extends BaseServiceType = any> extends MsgCall<Msg, ServiceType> { | ||||
|  | ||||
|     readonly conn!: HttpConnection<ServiceType>; | ||||
|      | ||||
|     constructor(options: MsgCallHttpOptions<Msg, ServiceType>) { | ||||
|         super(options); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/server/inner/ApiCallInner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/server/inner/ApiCallInner.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { ApiReturn, BaseServiceType } from 'tsrpc-proto'; | ||||
| import { ApiCall, ApiCallOptions } from '../base/ApiCall'; | ||||
| import { InnerConnection } from './InnerConnection'; | ||||
|  | ||||
| export interface ApiCallInnerOptions<Req, ServiceType extends BaseServiceType> extends ApiCallOptions<Req, ServiceType> { | ||||
|     conn: InnerConnection<ServiceType>; | ||||
| } | ||||
| export class ApiCallInner<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends ApiCall<Req, Res, ServiceType> { | ||||
|  | ||||
|     readonly conn!: InnerConnection<ServiceType>; | ||||
|  | ||||
|     constructor(options: ApiCallInnerOptions<Req, ServiceType>) { | ||||
|         super(options); | ||||
|     } | ||||
|  | ||||
|     protected async _sendReturn(ret: ApiReturn<Res>): Promise<{ isSucc: true } | { isSucc: false, errMsg: string }> { | ||||
|         if (this.conn.return.type === 'raw') { | ||||
|             // Validate Res | ||||
|             if (ret.isSucc) { | ||||
|                 let resValidate = this.server.tsbuffer.validate(ret.res, this.service.resSchemaId); | ||||
|                 if (!resValidate.isSucc) { | ||||
|                     return resValidate; | ||||
|                 } | ||||
|             } | ||||
|             return this.conn.sendData(ret); | ||||
|         } | ||||
|  | ||||
|         return super._sendReturn(ret); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										79
									
								
								src/server/inner/InnerConnection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/server/inner/InnerConnection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| import { ApiReturn, TsrpcError, TsrpcErrorType } from "tsrpc-proto"; | ||||
| import { ApiCall, BaseConnection, BaseServiceType, PrefixLogger, TransportDataUtil } from "../.."; | ||||
| import { BaseConnectionOptions, ConnectionStatus } from "../base/BaseConnection"; | ||||
| import { ApiCallInner } from "./ApiCallInner"; | ||||
|  | ||||
| export interface InnerConnectionOptions<ServiceType extends BaseServiceType> extends BaseConnectionOptions<ServiceType> { | ||||
|     return: { | ||||
|         type: 'raw' | 'json', | ||||
|         rs: (ret: ApiReturn<any>) => void; | ||||
|     } | { | ||||
|         type: 'buffer', | ||||
|         rs: (ret: Uint8Array) => void; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Server can `callApi` it self by using this inner connection | ||||
|  */ | ||||
| export class InnerConnection<ServiceType extends BaseServiceType = any> extends BaseConnection<ServiceType> { | ||||
|     readonly type = 'SHORT'; | ||||
|  | ||||
|     protected readonly ApiCallClass = ApiCallInner; | ||||
|     protected readonly MsgCallClass = null as any; | ||||
|  | ||||
|     return!: InnerConnectionOptions<any>['return']; | ||||
|  | ||||
|     constructor(options: InnerConnectionOptions<ServiceType>) { | ||||
|         super(options, new PrefixLogger({ | ||||
|             logger: options.server.logger, | ||||
|             prefixs: [`Inner #${options.id}`] | ||||
|         })); | ||||
|  | ||||
|         this.return = options.return; | ||||
|     } | ||||
|  | ||||
|     private _status: ConnectionStatus = ConnectionStatus.Opened; | ||||
|     get status(): ConnectionStatus { | ||||
|         return this._status; | ||||
|     } | ||||
|  | ||||
|     close(reason?: string): void { | ||||
|         this.doSendData({ | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError(reason ?? 'Internal Server Error', { | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 code: 'CONN_CLOSED', | ||||
|                 reason: reason | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     protected async doSendData(data: Uint8Array | ApiReturn<any>, call?: ApiCall): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> { | ||||
|         this._status = ConnectionStatus.Closed; | ||||
|  | ||||
|         if (this.return.type === 'buffer') { | ||||
|             if (!(data instanceof Uint8Array)) { | ||||
|                 // encode tsrpc error | ||||
|                 if (!data.isSucc) { | ||||
|                     let op = TransportDataUtil.tsbuffer.encode({ | ||||
|                         error: data.err | ||||
|                     }, 'ServerOutputData'); | ||||
|                     if (op.isSucc) { | ||||
|                         return this.doSendData(op.buf, call); | ||||
|                     } | ||||
|                 } | ||||
|                 return { isSucc: false, errMsg: 'Error data type' }; | ||||
|             } | ||||
|             this.return.rs(data); | ||||
|             return { isSucc: true } | ||||
|         } | ||||
|         else { | ||||
|             if (data instanceof Uint8Array) { | ||||
|                 return { isSucc: false, errMsg: 'Error data type' }; | ||||
|             } | ||||
|             this.return.rs(data); | ||||
|             return { isSucc: true } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/server/models/PrefixLogger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/server/models/PrefixLogger.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { Logger } from 'tsrpc-proto'; | ||||
|  | ||||
| export interface PrefixLoggerOptions { | ||||
|     logger: Logger | ||||
|     prefixs: (string | (() => string))[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Auto add prefix using existed `Logger` | ||||
|  */ | ||||
| export class PrefixLogger implements Logger { | ||||
|  | ||||
|     readonly logger: PrefixLoggerOptions['logger']; | ||||
|     readonly prefixs: PrefixLoggerOptions['prefixs']; | ||||
|  | ||||
|     constructor(options: PrefixLoggerOptions) { | ||||
|         this.logger = options.logger; | ||||
|         this.prefixs = options.prefixs; | ||||
|     } | ||||
|  | ||||
|     getPrefix(): string[] { | ||||
|         return this.prefixs.map(v => typeof v === 'string' ? v : v()); | ||||
|     } | ||||
|  | ||||
|     log(...args: any[]) { | ||||
|         this.logger.log(...this.getPrefix().concat(args)); | ||||
|     } | ||||
|  | ||||
|     debug(...args: any[]) { | ||||
|         this.logger.debug(...this.getPrefix().concat(args)); | ||||
|     } | ||||
|  | ||||
|     warn(...args: any[]) { | ||||
|         this.logger.warn(...this.getPrefix().concat(args)); | ||||
|     } | ||||
|  | ||||
|     error(...args: any[]) { | ||||
|         this.logger.error(...this.getPrefix().concat(args)); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										54
									
								
								src/server/models/TerminalColorLogger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/server/models/TerminalColorLogger.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| import chalk from "chalk"; | ||||
| import { Logger } from "tsrpc-proto"; | ||||
|  | ||||
| export interface TerminalColorLoggerOptions { | ||||
|     /** | ||||
|      * Process ID prefix | ||||
|      * @defaultValue `process.pid` | ||||
|      */ | ||||
|     pid: string, | ||||
|  | ||||
|     /** | ||||
|      * `undefined` represents not print time | ||||
|      * @defaultValue 'yyyy-MM-dd hh:mm:ss' | ||||
|      */ | ||||
|     timeFormat?: string | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Print log to terminal, with color. | ||||
|  */ | ||||
| export class TerminalColorLogger implements Logger { | ||||
|  | ||||
|     options: TerminalColorLoggerOptions = { | ||||
|         pid: process.pid.toString(), | ||||
|         timeFormat: 'yyyy-MM-dd hh:mm:ss' | ||||
|     } | ||||
|  | ||||
|     private _pid: string; | ||||
|     constructor(options?: Partial<TerminalColorLoggerOptions>) { | ||||
|         Object.assign(this.options, options); | ||||
|         this._pid = this.options.pid ? `<${this.options.pid}> ` : ''; | ||||
|     } | ||||
|  | ||||
|     private _time(): string { | ||||
|         return this.options.timeFormat ? new Date().format(this.options.timeFormat) : ''; | ||||
|     } | ||||
|  | ||||
|     debug(...args: any[]) { | ||||
|         console.debug.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.cyan('[DEBUG]'), ...args); | ||||
|     } | ||||
|  | ||||
|     log(...args: any[]) { | ||||
|         console.log.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.green('[INFO]'), ...args); | ||||
|     } | ||||
|  | ||||
|     warn(...args: any[]) { | ||||
|         console.warn.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.yellow('[WARN]'), ...args); | ||||
|     } | ||||
|  | ||||
|     error(...args: any[]) { | ||||
|         console.error.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.red('[ERROR]'), ...args); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										27
									
								
								src/server/ws/ApiCallWs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/server/ws/ApiCallWs.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { ApiReturn, BaseServiceType } from 'tsrpc-proto'; | ||||
| import { ApiCall, ApiCallOptions, SendReturnMethod } from '../base/ApiCall'; | ||||
| import { ConnectionStatus } from '../base/BaseConnection'; | ||||
| import { WsConnection } from './WsConnection'; | ||||
|  | ||||
| export interface ApiCallWsOptions<Req, ServiceType extends BaseServiceType> extends ApiCallOptions<Req, ServiceType> { | ||||
|     conn: WsConnection<ServiceType> | ||||
| } | ||||
|  | ||||
| export class ApiCallWs<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends ApiCall<Req, Res, ServiceType> { | ||||
|  | ||||
|     readonly conn!: WsConnection<ServiceType>; | ||||
|  | ||||
|     constructor(options: ApiCallWsOptions<Req, ServiceType>) { | ||||
|         super(options); | ||||
|     } | ||||
|  | ||||
|     protected async _prepareReturn(ret: ApiReturn<Res>): Promise<void> { | ||||
|         if (this.conn.status !== ConnectionStatus.Opened) { | ||||
|             this.logger.error('[SendReturnErr]', 'WebSocket is not opened', ret); | ||||
|             this._return = ret; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         return super._prepareReturn(ret); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/server/ws/MsgCallWs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/server/ws/MsgCallWs.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import { BaseServiceType } from "tsrpc-proto"; | ||||
| import { MsgCall, MsgCallOptions } from "../base/MsgCall"; | ||||
| import { WsConnection } from "./WsConnection"; | ||||
|  | ||||
| export interface MsgCallWsOptions<Msg, ServiceType extends BaseServiceType> extends MsgCallOptions<Msg, ServiceType> { | ||||
|     conn: WsConnection<ServiceType>; | ||||
| } | ||||
| export class MsgCallWs<Msg = any, ServiceType extends BaseServiceType = any> extends MsgCall<Msg, ServiceType> { | ||||
|  | ||||
|     readonly conn!: WsConnection<ServiceType>; | ||||
|  | ||||
|     constructor(options: MsgCallWsOptions<Msg, ServiceType>) { | ||||
|         super(options); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										141
									
								
								src/server/ws/WsConnection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/server/ws/WsConnection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| import * as http from "http"; | ||||
| import { TransportDataUtil } from "tsrpc-base-client"; | ||||
| import { BaseServiceType } from "tsrpc-proto"; | ||||
| import * as WebSocket from "ws"; | ||||
| import { BaseConnection, BaseConnectionOptions, ConnectionStatus } from "../base/BaseConnection"; | ||||
| import { PrefixLogger } from "../models/PrefixLogger"; | ||||
| import { ApiCallWs } from "./ApiCallWs"; | ||||
| import { MsgCallWs } from "./MsgCallWs"; | ||||
| import { WsServer } from "./WsServer"; | ||||
|  | ||||
| export interface WsConnectionOptions<ServiceType extends BaseServiceType> extends BaseConnectionOptions<ServiceType> { | ||||
|     server: WsServer<ServiceType>, | ||||
|     ws: WebSocket, | ||||
|     httpReq: http.IncomingMessage, | ||||
|     onClose: (conn: WsConnection<ServiceType>, code: number, reason: string) => Promise<void>, | ||||
|     dataType: 'text' | 'buffer', | ||||
|     isDataTypeConfirmed?: boolean | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Connected client | ||||
|  */ | ||||
| export class WsConnection<ServiceType extends BaseServiceType = any> extends BaseConnection<ServiceType> { | ||||
|     readonly type = "LONG"; | ||||
|  | ||||
|     protected readonly ApiCallClass = ApiCallWs; | ||||
|     protected readonly MsgCallClass = MsgCallWs; | ||||
|  | ||||
|     readonly ws: WebSocket; | ||||
|     readonly httpReq: http.IncomingMessage; | ||||
|     readonly server!: WsServer<ServiceType>; | ||||
|     dataType!: 'text' | 'buffer'; | ||||
|     // 是否已经收到了客户端的第一条消息,以确认了客户端的 dataType | ||||
|     isDataTypeConfirmed?: boolean; | ||||
|  | ||||
|     constructor(options: WsConnectionOptions<ServiceType>) { | ||||
|         super(options, new PrefixLogger({ | ||||
|             logger: options.server.logger, | ||||
|             prefixs: [`${options.ip} Conn#${options.id}`] | ||||
|         })); | ||||
|         this.ws = options.ws; | ||||
|         this.httpReq = options.httpReq; | ||||
|         this.isDataTypeConfirmed = options.isDataTypeConfirmed; | ||||
|  | ||||
|         if (this.server.options.heartbeatWaitTime) { | ||||
|             const timeout = this.server.options.heartbeatWaitTime; | ||||
|             this._heartbeatInterval = setInterval(() => { | ||||
|                 if (Date.now() - this._lastHeartbeatTime > timeout) { | ||||
|                     this.ws.close(3001, 'Receive heartbeat timeout'); | ||||
|                 } | ||||
|             }, timeout); | ||||
|         } | ||||
|  | ||||
|         // Init WS | ||||
|         this.ws.onclose = async e => { | ||||
|             if (this._heartbeatInterval) { | ||||
|                 clearInterval(this._heartbeatInterval); | ||||
|                 this._heartbeatInterval = undefined; | ||||
|             } | ||||
|             await options.onClose(this, e.code, e.reason); | ||||
|             this._rsClose?.(); | ||||
|         }; | ||||
|         this.ws.onerror = e => { this.logger.warn('[ClientErr]', e.error) }; | ||||
|         this.ws.onmessage = e => { | ||||
|             let data: Buffer | string; | ||||
|             if (e.data instanceof ArrayBuffer) { | ||||
|                 data = Buffer.from(e.data); | ||||
|             } | ||||
|             else if (Array.isArray(e.data)) { | ||||
|                 data = Buffer.concat(e.data) | ||||
|             } | ||||
|             else if (Buffer.isBuffer(e.data)) { | ||||
|                 data = e.data; | ||||
|             } | ||||
|             else { | ||||
|                 data = e.data; | ||||
|             } | ||||
|  | ||||
|             // 心跳包,直接回复 | ||||
|             if (data instanceof Buffer && data.equals(TransportDataUtil.HeartbeatPacket)) { | ||||
|                 this.server.options.debugBuf && this.logger.log('[Heartbeat] Recv ping and send pong', TransportDataUtil.HeartbeatPacket); | ||||
|                 this._lastHeartbeatTime = Date.now(); | ||||
|                 this.ws.send(TransportDataUtil.HeartbeatPacket); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // dataType 尚未确认,自动检测 | ||||
|             if (!this.isDataTypeConfirmed) { | ||||
|                 if (this.server.options.jsonEnabled && typeof data === 'string') { | ||||
|                     this.dataType = 'text'; | ||||
|                 } | ||||
|                 else { | ||||
|                     this.dataType = 'buffer'; | ||||
|                 } | ||||
|  | ||||
|                 this.isDataTypeConfirmed = true; | ||||
|             } | ||||
|  | ||||
|             // dataType 已确认 | ||||
|             this.server._onRecvData(this, data) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private _lastHeartbeatTime = 0; | ||||
|     private _heartbeatInterval?: ReturnType<typeof setInterval>; | ||||
|  | ||||
|     get status(): ConnectionStatus { | ||||
|         if (this.ws.readyState === WebSocket.CLOSED) { | ||||
|             return ConnectionStatus.Closed; | ||||
|         } | ||||
|         if (this.ws.readyState === WebSocket.CLOSING) { | ||||
|             return ConnectionStatus.Closing; | ||||
|         } | ||||
|         return ConnectionStatus.Opened; | ||||
|     } | ||||
|  | ||||
|     protected async doSendData(data: string | Uint8Array, call?: ApiCallWs): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> { | ||||
|         let opSend = await new Promise<{ isSucc: true } | { isSucc: false, errMsg: string }>((rs) => { | ||||
|             this.ws.send(data, e => { | ||||
|                 e ? rs({ isSucc: false, errMsg: e.message || 'Send buffer error' }) : rs({ isSucc: true }); | ||||
|             }) | ||||
|         }); | ||||
|         if (!opSend.isSucc) { | ||||
|             return opSend; | ||||
|         } | ||||
|  | ||||
|         return { isSucc: true } | ||||
|     } | ||||
|  | ||||
|     protected _rsClose?: () => void; | ||||
|     /** Close WebSocket connection */ | ||||
|     close(reason?: string): Promise<void> { | ||||
|         // 已连接 Close之 | ||||
|         return new Promise<void>(rs => { | ||||
|             this._rsClose = rs; | ||||
|             this.ws.close(1000, reason); | ||||
|         }).finally(() => { | ||||
|             this._rsClose = undefined | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										241
									
								
								src/server/ws/WsServer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								src/server/ws/WsServer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| import * as http from "http"; | ||||
| import { EncodeOutput, TransportDataUtil } from "tsrpc-base-client"; | ||||
| import { BaseServiceType, ServiceProto } from 'tsrpc-proto'; | ||||
| import * as WebSocket from 'ws'; | ||||
| import { Server as WebSocketServer } from 'ws'; | ||||
| import { HttpUtil } from '../../models/HttpUtil'; | ||||
| import { BaseServer, BaseServerOptions, defaultBaseServerOptions, ServerStatus } from '../base/BaseServer'; | ||||
| import { WsConnection } from './WsConnection'; | ||||
|  | ||||
| /** | ||||
|  * TSRPC Server, based on WebSocket connection. | ||||
|  * It can support realtime cases. | ||||
|  * @typeParam ServiceType - `ServiceType` from generated `proto.ts` | ||||
|  */ | ||||
| export class WsServer<ServiceType extends BaseServiceType = any> extends BaseServer<ServiceType> { | ||||
|     readonly options!: WsServerOptions<ServiceType>; | ||||
|  | ||||
|     readonly connections: WsConnection<ServiceType>[] = []; | ||||
|     private readonly _id2Conn: { [connId: string]: WsConnection<ServiceType> | undefined } = {}; | ||||
|  | ||||
|     constructor(proto: ServiceProto<ServiceType>, options?: Partial<WsServerOptions<ServiceType>>) { | ||||
|         super(proto, { | ||||
|             ...defaultWsServerOptions, | ||||
|             ...options | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private _wsServer?: WebSocketServer; | ||||
|  | ||||
|     /** | ||||
|      * {@inheritDoc BaseServer.start} | ||||
|      */ | ||||
|     start(): Promise<void> { | ||||
|         if (this._wsServer) { | ||||
|             throw new Error('Server already started'); | ||||
|         } | ||||
|         this._status = ServerStatus.Opening; | ||||
|         return new Promise((rs, rj) => { | ||||
|             this.logger.log('Starting WebSocket server...'); | ||||
|             this._wsServer = new WebSocketServer({ | ||||
|                 port: this.options.port | ||||
|             }, () => { | ||||
|                 this.logger.log(`Server started at ${this.options.port}...`); | ||||
|                 this._status = ServerStatus.Opened; | ||||
|                 rs(); | ||||
|             }); | ||||
|  | ||||
|             this._wsServer.on('connection', this._onClientConnect); | ||||
|             this._wsServer.on('error', e => { | ||||
|                 this.logger.error('[ServerError]', e); | ||||
|                 rj(e); | ||||
|             }); | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * {@inheritDoc BaseServer.stop} | ||||
|      */ | ||||
|     async stop(): Promise<void> { | ||||
|         // Closed Already | ||||
|         if (!this._wsServer) { | ||||
|             throw new Error('Server has not been started') | ||||
|         } | ||||
|         if (this._status === ServerStatus.Closed) { | ||||
|             throw new Error('Server is closed already'); | ||||
|         } | ||||
|  | ||||
|         this._status = ServerStatus.Closing; | ||||
|  | ||||
|         return new Promise<void>(async (rs, rj) => { | ||||
|             await Promise.all(this.connections.map(v => v.close('Server Stop'))); | ||||
|             this._wsServer!.close(err => { err ? rj(err) : rs() }) | ||||
|         }).then(() => { | ||||
|             this.logger.log('Server stopped'); | ||||
|             this._status = ServerStatus.Closed; | ||||
|             this._wsServer = undefined; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private _onClientConnect = (ws: WebSocket, httpReq: http.IncomingMessage) => { | ||||
|         // 停止中 不再接受新的连接 | ||||
|         if (this._status !== ServerStatus.Opened) { | ||||
|             ws.close(1012); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 推测 dataType 和 isDataTypeConfirmed | ||||
|         let isDataTypeConfirmed = true; | ||||
|         let dataType: 'text' | 'buffer'; | ||||
|         let protocols = httpReq.headers['sec-websocket-protocol']?.split(',').map(v => v.trim()).filter(v => !!v); | ||||
|         if (protocols?.includes('text')) { | ||||
|             dataType = 'text'; | ||||
|         } | ||||
|         else if (protocols?.includes('buffer')) { | ||||
|             dataType = 'buffer'; | ||||
|         } | ||||
|         else { | ||||
|             dataType = this.options.jsonEnabled ? 'text' : 'buffer'; | ||||
|             isDataTypeConfirmed = false; | ||||
|         } | ||||
|  | ||||
|         // Create Active Connection | ||||
|         let conn = new WsConnection({ | ||||
|             id: '' + this._connIdCounter.getNext(), | ||||
|             ip: HttpUtil.getClientIp(httpReq), | ||||
|             server: this, | ||||
|             ws: ws, | ||||
|             httpReq: httpReq, | ||||
|             onClose: this._onClientClose, | ||||
|             dataType: dataType, | ||||
|             isDataTypeConfirmed: isDataTypeConfirmed | ||||
|         }); | ||||
|         this.connections.push(conn); | ||||
|         this._id2Conn[conn.id] = conn; | ||||
|  | ||||
|         conn.logger.log('[Connected]', `ActiveConn=${this.connections.length}`); | ||||
|         this.flows.postConnectFlow.exec(conn, conn.logger); | ||||
|     }; | ||||
|  | ||||
|     private _onClientClose = async (conn: WsConnection<ServiceType>, code: number, reason: string) => { | ||||
|         this.connections.removeOne(v => v.id === conn.id); | ||||
|         delete this._id2Conn[conn.id]; | ||||
|         conn.logger.log('[Disconnected]', `Code=${code} ${reason ? `Reason=${reason} ` : ''}ActiveConn=${this.connections.length}`) | ||||
|  | ||||
|         await this.flows.postDisconnectFlow.exec({ conn: conn, reason: reason }, conn.logger); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send the same message to many connections. | ||||
|      * No matter how many target connections are, the message would be only encoded once. | ||||
|      * @param msgName  | ||||
|      * @param msg - Message body | ||||
|      * @param connIds - `id` of target connections, `undefined` means broadcast to every connections. | ||||
|      * @returns Send result, `isSucc: true` means the message buffer is sent to kernel, not represents the clients received. | ||||
|      */ | ||||
|     async broadcastMsg<T extends keyof ServiceType['msg']>(msgName: T, msg: ServiceType['msg'][T], conns?: WsConnection<ServiceType>[]): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> { | ||||
|         let connStr: string; | ||||
|         if (!conns) { | ||||
|             conns = this.connections; | ||||
|             connStr = '*'; | ||||
|         } | ||||
|         else { | ||||
|             connStr = conns ? conns.map(v => v.id).join(',') : '*'; | ||||
|         } | ||||
|  | ||||
|         if (!conns.length) { | ||||
|             return { isSucc: true }; | ||||
|         } | ||||
|  | ||||
|         if (this.status !== ServerStatus.Opened) { | ||||
|             this.logger.warn('[BroadcastMsgErr]', `[${msgName}]`, `[To:${connStr}]`, 'Server not open'); | ||||
|             return { isSucc: false, errMsg: 'Server not open' }; | ||||
|         } | ||||
|  | ||||
|         // GetService | ||||
|         let service = this.serviceMap.msgName2Service[msgName as string]; | ||||
|         if (!service) { | ||||
|             this.logger.warn('[BroadcastMsgErr]', `[${msgName}]`, `[To:${connStr}]`, 'Invalid msg name: ' + msgName); | ||||
|             return { isSucc: false, errMsg: 'Invalid msg name: ' + msgName }; | ||||
|         } | ||||
|  | ||||
|         // Encode group by dataType | ||||
|         let _opEncodeBuf: EncodeOutput<Uint8Array> | undefined; | ||||
|         let _opEncodeText: EncodeOutput<string> | undefined; | ||||
|         const getOpEncodeBuf = () => { | ||||
|             if (!_opEncodeBuf) { | ||||
|                 _opEncodeBuf = TransportDataUtil.encodeServerMsg(this.tsbuffer, service!, msg, 'buffer', 'LONG'); | ||||
|             } | ||||
|             return _opEncodeBuf; | ||||
|         } | ||||
|         const getOpEncodeText = () => { | ||||
|             if (!_opEncodeText) { | ||||
|                 _opEncodeText = TransportDataUtil.encodeServerMsg(this.tsbuffer, service!, msg, 'text', 'LONG'); | ||||
|             } | ||||
|             return _opEncodeText; | ||||
|         } | ||||
|  | ||||
|         // 测试一下编码可以通过 | ||||
|         let op = conns.some(v => v.dataType === 'buffer') ? getOpEncodeBuf() : getOpEncodeText(); | ||||
|         if (!op.isSucc) { | ||||
|             this.logger.warn('[BroadcastMsgErr]', `[${msgName}]`, `[To:${connStr}]`, op.errMsg); | ||||
|             return op; | ||||
|         } | ||||
|  | ||||
|         this.options.logMsg && this.logger.log(`[BroadcastMsg]`, `[${msgName}]`, `[To:${connStr}]`, msg); | ||||
|  | ||||
|         // Batch send | ||||
|         let errMsgs: string[] = []; | ||||
|         return Promise.all(conns.map(async conn => { | ||||
|             // Pre Flow | ||||
|             let pre = await this.flows.preSendMsgFlow.exec({ conn: conn, service: service!, msg: msg }, this.logger); | ||||
|             if (!pre) { | ||||
|                 conn.logger.debug('[preSendMsgFlow]', 'Canceled'); | ||||
|                 return { isSucc: false, errMsg: 'Prevented by preSendMsgFlow' }; | ||||
|             } | ||||
|             msg = pre.msg; | ||||
|  | ||||
|             // Do send! | ||||
|             let opSend = await conn.sendData((conn.dataType === 'buffer' ? getOpEncodeBuf() : getOpEncodeText())!.output!); | ||||
|             if (!opSend.isSucc) { | ||||
|                 return opSend; | ||||
|             } | ||||
|  | ||||
|             // Post Flow | ||||
|             this.flows.postSendMsgFlow.exec(pre, this.logger); | ||||
|  | ||||
|             return { isSucc: true }; | ||||
|         })).then(results => { | ||||
|             for (let i = 0; i < results.length; ++i) { | ||||
|                 let op = results[i]; | ||||
|                 if (!op.isSucc) { | ||||
|                     errMsgs.push(`Conn#conns[i].id: ${op.errMsg}`) | ||||
|                 }; | ||||
|             } | ||||
|             if (errMsgs.length) { | ||||
|                 return { isSucc: false, errMsg: errMsgs.join('\n') } | ||||
|             } | ||||
|             else { | ||||
|                 return { isSucc: true } | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export interface WsServerOptions<ServiceType extends BaseServiceType> extends BaseServerOptions<ServiceType> { | ||||
|     /** Which port the WebSocket server is listen to */ | ||||
|     port: number; | ||||
|  | ||||
|     /**  | ||||
|      * Close a connection if not receive heartbeat after the time (ms). | ||||
|      * This value should be greater than `client.heartbeat.interval`, for exmaple 2x of it. | ||||
|      * `undefined` or `0` represent disable this feature. | ||||
|      * @defaultValue `undefined` | ||||
|      */ | ||||
|     heartbeatWaitTime?: number; | ||||
| }; | ||||
|  | ||||
| const defaultWsServerOptions: WsServerOptions<any> = { | ||||
|     ...defaultBaseServerOptions, | ||||
|     port: 3000 | ||||
| } | ||||
							
								
								
									
										1
									
								
								test/Base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/Base.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| import 'k8w-extend-native'; | ||||
							
								
								
									
										10
									
								
								test/api/ApiObjId.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								test/api/ApiObjId.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { ApiCall } from "../../src/server/base/ApiCall"; | ||||
| import { ReqObjId, ResObjId } from "../proto/PtlObjId"; | ||||
|  | ||||
| export async function ApiObjId(call: ApiCall<ReqObjId, ResObjId>) { | ||||
|     call.succ({ | ||||
|         id2: call.req.id1, | ||||
|         buf: call.req.buf, | ||||
|         date: call.req.date | ||||
|     }) | ||||
| } | ||||
							
								
								
									
										27
									
								
								test/api/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								test/api/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { TsrpcError } from "tsrpc-proto"; | ||||
| import { ApiCall } from "../../src/server/base/ApiCall"; | ||||
| import { ReqTest, ResTest } from "../proto/PtlTest"; | ||||
|  | ||||
| export async function ApiTest(call: ApiCall<ReqTest, ResTest>) { | ||||
|     if (call.req.name === 'InnerError') { | ||||
|         await new Promise(rs => { setTimeout(rs, 50) }) | ||||
|         throw new Error('Test InnerError') | ||||
|     } | ||||
|     else if (call.req.name === 'TsrpcError') { | ||||
|         await new Promise(rs => { setTimeout(rs, 50) }) | ||||
|         throw new TsrpcError('Test TsrpcError', { | ||||
|             code: 'CODE_TEST', | ||||
|             info: 'ErrInfo Test' | ||||
|         }); | ||||
|     } | ||||
|     else if (call.req.name === 'error') { | ||||
|         await new Promise(rs => { setTimeout(rs, 50) }) | ||||
|         call.error('Got an error') | ||||
|     } | ||||
|     else { | ||||
|         await new Promise(rs => { setTimeout(rs, 50) }) | ||||
|         call.succ({ | ||||
|             reply: 'Test reply: ' + call.req.name | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								test/api/a/b/c/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								test/api/a/b/c/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { TsrpcError } from "tsrpc-proto"; | ||||
|  | ||||
| export async function ApiTest(call: any) { | ||||
|     if (call.req.name === 'InnerError') { | ||||
|         throw new Error('a/b/c/Test InnerError') | ||||
|     } | ||||
|     else if (call.req.name === 'TsrpcError') { | ||||
|         throw new TsrpcError('a/b/c/Test TsrpcError', { | ||||
|             code: 'CODE_TEST', | ||||
|             info: 'ErrInfo a/b/c/Test' | ||||
|         }); | ||||
|     } | ||||
|     else if (call.req.name === 'error') { | ||||
|         call.error('Got an error') | ||||
|     } | ||||
|     else { | ||||
|         call.succ({ | ||||
|             reply: 'a/b/c/Test reply: ' + call.req.name | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										907
									
								
								test/cases/http.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										907
									
								
								test/cases/http.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,907 @@ | ||||
| import { ObjectId } from 'bson'; | ||||
| import { assert } from 'chai'; | ||||
| import chalk from 'chalk'; | ||||
| import * as path from "path"; | ||||
| import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto'; | ||||
| import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src'; | ||||
| import { HttpClient } from '../../src/client/http/HttpClient'; | ||||
| import { HttpServer } from '../../src/server/http/HttpServer'; | ||||
| import { PrefixLogger } from '../../src/server/models/PrefixLogger'; | ||||
| import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest'; | ||||
| import { ApiTest } from '../api/ApiTest'; | ||||
| import { MsgChat } from '../proto/MsgChat'; | ||||
| import { ReqTest, ResTest } from '../proto/PtlTest'; | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
|  | ||||
| const serverLogger = new PrefixLogger({ | ||||
|     prefixs: [chalk.bgGreen.white(' Server ')], | ||||
|     logger: new TerminalColorLogger({ pid: 'Server' }) | ||||
| }); | ||||
| const clientLogger = new PrefixLogger({ | ||||
|     prefixs: [chalk.bgBlue.white(' Client ')], | ||||
|     logger: new TerminalColorLogger({ pid: 'Client' }) | ||||
| }) | ||||
|  | ||||
| const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>; | ||||
|  | ||||
| async function testApi(server: HttpServer<ServiceType>, client: HttpClient<ServiceType>) { | ||||
|     // Succ | ||||
|     assert.deepStrictEqual(await client.callApi('Test', { | ||||
|         name: 'Req1' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'Test reply: Req1' | ||||
|         } | ||||
|     }); | ||||
|     assert.deepStrictEqual(await client.callApi('a/b/c/Test', { | ||||
|         name: 'Req2' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'a/b/c/Test reply: Req2' | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Inner error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await client.callApi(v as any, { | ||||
|             name: 'InnerError' | ||||
|         }); | ||||
|         delete ret.err!.innerErr.stack; | ||||
|  | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 code: 'INTERNAL_ERR', | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: `${v} InnerError` | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // TsrpcError | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await client.callApi(v as any, { | ||||
|             name: 'TsrpcError' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError(`${v} TsrpcError`, { | ||||
|                 code: 'CODE_TEST', | ||||
|                 type: TsrpcErrorType.ApiError, | ||||
|                 info: 'ErrInfo ' + v | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // call.error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await client.callApi(v as any, { | ||||
|             name: 'error' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Got an error', { | ||||
|                 type: TsrpcErrorType.ApiError | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| describe('HTTP Server & Client basic', function () { | ||||
|     it('implement API manually', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.implementApi('Test', ApiTest); | ||||
|         server.implementApi('a/b/c/Test', ApiAbcTest); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger, | ||||
|             debugBuf: true | ||||
|         }) | ||||
|  | ||||
|         await testApi(server, client); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('extend call in handler', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|  | ||||
|         server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => { | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }); | ||||
|         server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => { | ||||
|             call.msg.content; | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('extend call in flow', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyConn = HttpConnection<any> & { | ||||
|             currentUser: { | ||||
|                 uid: string, | ||||
|                 nickName: string | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         server.flows.postConnectFlow.push((conn: MyConn) => { | ||||
|             conn.currentUser.nickName = 'asdf'; | ||||
|             return conn; | ||||
|         }); | ||||
|         server.flows.postConnectFlow.exec(null as any as MyConn, console); | ||||
|         server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => { | ||||
|             call.value2 = 'x'; | ||||
|             return call; | ||||
|         }); | ||||
|         server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => { | ||||
|             call.value2 = 'f'; | ||||
|             return call; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('autoImplementApi', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         await testApi(server, client); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('sendMsg', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             port: 3001, | ||||
|             logger: serverLogger, | ||||
|             // debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             server: 'http://127.0.0.1:3001', | ||||
|             logger: clientLogger, | ||||
|             // debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         return new Promise(rs => { | ||||
|             let msg: MsgChat = { | ||||
|                 channel: 123, | ||||
|                 userName: 'fff', | ||||
|                 content: '666', | ||||
|                 time: Date.now() | ||||
|             }; | ||||
|  | ||||
|             server.listenMsg('Chat', async v => { | ||||
|                 assert.deepStrictEqual(v.msg, msg); | ||||
|                 await server.stop(); | ||||
|                 rs(); | ||||
|             }); | ||||
|  | ||||
|             client.sendMsg('Chat', msg); | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('abort', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let result: any | undefined; | ||||
|         let promise = client.callApi('Test', { name: 'aaaaaaaa' }); | ||||
|         let sn = client.lastSN; | ||||
|         setTimeout(() => { | ||||
|             client.abort(sn) | ||||
|         }, 10); | ||||
|         promise.then(v => { | ||||
|             result = v; | ||||
|         }); | ||||
|  | ||||
|         await new Promise<void>(rs => { | ||||
|             setTimeout(() => { | ||||
|                 assert.strictEqual(result, undefined); | ||||
|                 rs(); | ||||
|             }, 150) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('abortByKey', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let result: any | undefined; | ||||
|         let result1: any | undefined; | ||||
|  | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|  | ||||
|         client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; }); | ||||
|  | ||||
|         setTimeout(() => { | ||||
|             client.abortByKey('XXX') | ||||
|         }, 10); | ||||
|  | ||||
|         await new Promise<void>(rs => { | ||||
|             setTimeout(() => { | ||||
|                 assert.strictEqual(result, undefined); | ||||
|                 assert.deepStrictEqual(result1, { | ||||
|                     isSucc: true, | ||||
|                     res: { | ||||
|                         reply: 'Test reply: bbbbbb' | ||||
|                     } | ||||
|                 }) | ||||
|                 rs(); | ||||
|             }, 150) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('abortAll', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let result: any | undefined; | ||||
|         let result1: any | undefined; | ||||
|  | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|  | ||||
|         client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; }); | ||||
|  | ||||
|         setTimeout(() => { | ||||
|             client.abortAll() | ||||
|         }, 10); | ||||
|  | ||||
|         await new Promise<void>(rs => { | ||||
|             setTimeout(() => { | ||||
|                 assert.strictEqual(result, undefined); | ||||
|                 assert.strictEqual(result1, undefined); | ||||
|                 rs(); | ||||
|             }, 150) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('pendingApis', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         for (let i = 0; i < 10; ++i) { | ||||
|             let promise = Promise.all(Array.from({ length: 10 }, () => new Promise<void>(rs => { | ||||
|                 let name = ['Req', 'InnerError', 'TsrpcError', 'error'][Math.random() * 4 | 0]; | ||||
|                 let ret: any | undefined; | ||||
|                 let promise = client.callApi('Test', { name: name }); | ||||
|                 let sn = client.lastSN; | ||||
|                 let abort = Math.random() > 0.5; | ||||
|                 if (abort) { | ||||
|                     setTimeout(() => { | ||||
|                         client.abort(sn) | ||||
|                     }, 0); | ||||
|                 } | ||||
|                 promise.then(v => { | ||||
|                     ret = v; | ||||
|                 }); | ||||
|  | ||||
|                 setTimeout(() => { | ||||
|                     client.logger?.log('sn', sn, name, abort, ret) | ||||
|                     if (abort) { | ||||
|                         assert.strictEqual(ret, undefined); | ||||
|                     } | ||||
|                     else { | ||||
|                         assert.notEqual(ret, undefined); | ||||
|                         if (name === 'Req') { | ||||
|                             assert.strictEqual(ret.isSucc, true); | ||||
|                         } | ||||
|                         else { | ||||
|                             assert.strictEqual(ret.isSucc, false) | ||||
|                         } | ||||
|                     } | ||||
|                     rs(); | ||||
|                 }, 300) | ||||
|             }))); | ||||
|             assert.strictEqual(client['_pendingApis'].length, 10); | ||||
|             await promise; | ||||
|             assert.strictEqual(client['_pendingApis'].length, 0); | ||||
|         } | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         let client1 = new HttpClient(getProto(), { | ||||
|             server: 'http://localhost:80', | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let ret = await client1.callApi('Test', { name: 'xx' }); | ||||
|         console.log(ret); | ||||
|         assert.strictEqual(ret.isSucc, false); | ||||
|         assert.strictEqual(ret.err?.code, 'ECONNREFUSED'); | ||||
|         assert.strictEqual(ret.err?.type, TsrpcErrorType.NetworkError); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('server timeout', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 100 | ||||
|         }); | ||||
|         server.implementApi('Test', call => { | ||||
|             return new Promise(rs => { | ||||
|                 setTimeout(() => { | ||||
|                     call.req && call.succ({ | ||||
|                         reply: 'Hi, ' + call.req.name | ||||
|                     }); | ||||
|                     rs(); | ||||
|                 }, 200) | ||||
|             }) | ||||
|         }) | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|         let ret = await client.callApi('Test', { name: 'Jack' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Server Timeout', { | ||||
|                 code: 'SERVER_TIMEOUT', | ||||
|                 type: TsrpcErrorType.ServerError | ||||
|             }) | ||||
|         }); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('client timeout', async function () { | ||||
|         let server1 = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         server1.implementApi('Test', call => { | ||||
|             return new Promise(rs => { | ||||
|                 setTimeout(() => { | ||||
|                     call.succ({ | ||||
|                         reply: 'Hello, ' + call.req.name | ||||
|                     }); | ||||
|                     rs(); | ||||
|                 }, 2000) | ||||
|             }) | ||||
|         }) | ||||
|         await server1.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             timeout: 100, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'Jack123' }); | ||||
|         // SERVER TIMEOUT的call还没执行完,但是call却被放入Pool了,导致这个BUG | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError({ | ||||
|                 message: 'Request Timeout', | ||||
|                 code: 'TIMEOUT', | ||||
|                 type: TsrpcErrorType.NetworkError | ||||
|             }) | ||||
|         }); | ||||
|         await server1.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('Graceful stop', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         let reqNum = 0; | ||||
|         server.implementApi('Test', async call => { | ||||
|             if (++reqNum === 10) { | ||||
|                 server.gracefulStop(); | ||||
|             } | ||||
|             await new Promise(rs => setTimeout(rs, parseInt(call.req.name))); | ||||
|             call.succ({ reply: 'OK' }); | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|         let isStopped = false; | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let succNum = 0; | ||||
|         await Promise.all(Array.from({ length: 10 }, (v, i) => client.callApi('Test', { name: '' + (i * 100) }).then(v => { | ||||
|             if (v.res?.reply === 'OK') { | ||||
|                 ++succNum; | ||||
|             } | ||||
|         }))) | ||||
|         assert.strictEqual(succNum, 10); | ||||
|     }) | ||||
| }) | ||||
|  | ||||
| describe('HTTP Flows', function () { | ||||
|     it('Server conn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             assert.strictEqual((call.conn as any).xxxx, 'asdfasdf') | ||||
|             assert.strictEqual(flowExecResult.postConnectFlow, true); | ||||
|             assert.strictEqual(flowExecResult.postDisconnectFlow, undefined); | ||||
|             call.succ({ reply: 'ok' }); | ||||
|             assert.strictEqual(flowExecResult.postDisconnectFlow, undefined); | ||||
|         }); | ||||
|  | ||||
|         server.flows.postConnectFlow.push(v => { | ||||
|             flowExecResult.postConnectFlow = true; | ||||
|             (v as any).xxxx = 'asdfasdf'; | ||||
|             return v; | ||||
|         }); | ||||
|         server.flows.postDisconnectFlow.push(v => { | ||||
|             flowExecResult.postDisconnectFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         assert.strictEqual(flowExecResult.postConnectFlow, undefined); | ||||
|         assert.strictEqual(flowExecResult.postDisconnectFlow, undefined); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|         await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postConnectFlow, true); | ||||
|         assert.strictEqual(flowExecResult.postDisconnectFlow, true); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('Buffer enc/dec flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'Enc&Dec' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preRecvBufferFlow.push(v => { | ||||
|             flowExecResult.preRecvBufferFlow = true; | ||||
|             for (let i = 0; i < v.buf.length; ++i) { | ||||
|                 v.buf[i] ^= 128; | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|         server.flows.preSendBufferFlow.push(v => { | ||||
|             flowExecResult.preSendBufferFlow = true; | ||||
|             for (let i = 0; i < v.buf.length; ++i) { | ||||
|                 v.buf[i] ^= 128; | ||||
|             } | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preSendBufferFlow.push(v => { | ||||
|             for (let i = 0; i < v.buf.length; ++i) { | ||||
|                 v.buf[i] ^= 128; | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         client.flows.preRecvBufferFlow.push(v => { | ||||
|             for (let i = 0; i < v.buf.length; ++i) { | ||||
|                 v.buf[i] ^= 128; | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preRecvBufferFlow, true); | ||||
|         assert.strictEqual(flowExecResult.preSendBufferFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: true, | ||||
|             res: { | ||||
|                 reply: 'Enc&Dec' | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             call.error('You need login'); | ||||
|             return call; | ||||
|         }); | ||||
|         server.flows.postApiCallFlow.push(v => { | ||||
|             flowExecResult.postApiCallFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preCallApiFlow.push(v => { | ||||
|             if (v.apiName !== 'ObjId') { | ||||
|                 v.req.name = 'Changed' | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postApiCallFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow break', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             call.error('You need login'); | ||||
|             return undefined; | ||||
|         }); | ||||
|         server.flows.postApiCallFlow.push(v => { | ||||
|             flowExecResult.postApiCallFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preCallApiFlow.push(v => { | ||||
|             if (v.apiName !== 'ObjId') { | ||||
|                 v.req.name = 'Changed' | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postApiCallFlow, undefined); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             throw new Error('ASDFASDF') | ||||
|         }); | ||||
|         server.flows.postApiCallFlow.push(v => { | ||||
|             flowExecResult.postApiCallFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preCallApiFlow.push(v => { | ||||
|             if (v.apiName !== 'ObjId') { | ||||
|                 v.req.name = 'Changed' | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postApiCallFlow, undefined); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: 'ASDFASDF', | ||||
|                 code: 'INTERNAL_ERR' | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('server ApiReturn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiReturnFlow.push(v => { | ||||
|             flowExecResult.preApiReturnFlow = true; | ||||
|             v.return = { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError('Ret changed') | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|         server.flows.postApiReturnFlow.push(v => { | ||||
|             flowExecResult.postApiReturnFlow = true; | ||||
|             v.call.logger.log('RETTT', v.return); | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preApiReturnFlow, true); | ||||
|         assert.strictEqual(flowExecResult.postApiReturnFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Ret changed') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('client ApiReturn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof HttpClient<any>['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preApiReturnFlow.push(v => { | ||||
|             flowExecResult.preApiReturnFlow = true; | ||||
|             v.return = { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError('Ret changed') | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|         client.flows.postApiReturnFlow.push(v => { | ||||
|             flowExecResult.postApiReturnFlow = true; | ||||
|             client.logger?.log('RETTT', v.return); | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preApiReturnFlow, true); | ||||
|         assert.strictEqual(flowExecResult.postApiReturnFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Ret changed') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('client SendBufferFlow prevent', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         // const flowExecResult: { [K in (keyof BaseClient<any>['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preSendBufferFlow.push(v => { | ||||
|             return undefined | ||||
|         }); | ||||
|  | ||||
|         let ret: any; | ||||
|         client.callApi('Test', { name: 'xxx' }).then(v => { ret = v }); | ||||
|         await new Promise(rs => { setTimeout(rs, 200) }); | ||||
|         assert.strictEqual(ret, undefined) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('onInputBufferError', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|         client.flows.preSendBufferFlow.push(v => { | ||||
|             for (let i = 0; i < v.buf.length; ++i) { | ||||
|                 v.buf[i] += 1; | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'XXX' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Invalid request buffer, please check the version of service proto.', { | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 code: 'INPUT_DATA_ERR' | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('ObjectId', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         // ObjectId | ||||
|         let objId1 = new ObjectId(); | ||||
|         let ret = await client.callApi('ObjId', { | ||||
|             id1: objId1 | ||||
|         }); | ||||
|         assert.strictEqual(ret.isSucc, true, ret.err?.message); | ||||
|         assert.strictEqual(objId1.toString(), ret.res!.id2.toString()); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
| }) | ||||
							
								
								
									
										997
									
								
								test/cases/httpJSON.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										997
									
								
								test/cases/httpJSON.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,997 @@ | ||||
| import { ObjectId } from 'bson'; | ||||
| import { assert } from 'chai'; | ||||
| import chalk from 'chalk'; | ||||
| import * as path from "path"; | ||||
| import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto'; | ||||
| import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src'; | ||||
| import { HttpClient } from '../../src/client/http/HttpClient'; | ||||
| import { HttpProxy } from '../../src/client/http/HttpProxy'; | ||||
| import { HttpServer } from '../../src/server/http/HttpServer'; | ||||
| import { PrefixLogger } from '../../src/server/models/PrefixLogger'; | ||||
| import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest'; | ||||
| import { ApiTest } from '../api/ApiTest'; | ||||
| import { MsgChat } from '../proto/MsgChat'; | ||||
| import { ReqTest, ResTest } from '../proto/PtlTest'; | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
|  | ||||
| const serverLogger = new PrefixLogger({ | ||||
|     prefixs: [chalk.bgGreen.white(' Server ')], | ||||
|     logger: new TerminalColorLogger({ pid: 'Server' }) | ||||
| }); | ||||
| const clientLogger = new PrefixLogger({ | ||||
|     prefixs: [chalk.bgBlue.white(' Client ')], | ||||
|     logger: new TerminalColorLogger({ pid: 'Client' }) | ||||
| }) | ||||
|  | ||||
| const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>; | ||||
|  | ||||
| async function testApi(server: HttpServer<ServiceType>, client: HttpClient<ServiceType>) { | ||||
|     // Succ | ||||
|     assert.deepStrictEqual(await client.callApi('Test', { | ||||
|         name: 'Req1' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'Test reply: Req1' | ||||
|         } | ||||
|     }); | ||||
|     assert.deepStrictEqual(await client.callApi('a/b/c/Test', { | ||||
|         name: 'Req2' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'a/b/c/Test reply: Req2' | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Inner error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await client.callApi(v as any, { | ||||
|             name: 'InnerError' | ||||
|         }); | ||||
|         delete ret.err!.innerErr.stack; | ||||
|  | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 code: 'INTERNAL_ERR', | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: `${v} InnerError` | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // TsrpcError | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await client.callApi(v as any, { | ||||
|             name: 'TsrpcError' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError(`${v} TsrpcError`, { | ||||
|                 code: 'CODE_TEST', | ||||
|                 type: TsrpcErrorType.ApiError, | ||||
|                 info: 'ErrInfo ' + v | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // call.error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await client.callApi(v as any, { | ||||
|             name: 'error' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Got an error', { | ||||
|                 type: TsrpcErrorType.ApiError | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| describe('HTTP Server & Client basic', function () { | ||||
|     it('implement API manually', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.implementApi('Test', ApiTest); | ||||
|         server.implementApi('a/b/c/Test', ApiAbcTest); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger, | ||||
|             debugBuf: true | ||||
|         }) | ||||
|  | ||||
|         await testApi(server, client); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('extend call in handler', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|  | ||||
|         server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => { | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }); | ||||
|         server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => { | ||||
|             call.msg.content; | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('extend call in flow', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyConn = HttpConnection<any> & { | ||||
|             currentUser: { | ||||
|                 uid: string, | ||||
|                 nickName: string | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         server.flows.postConnectFlow.push((conn: MyConn) => { | ||||
|             conn.currentUser.nickName = 'asdf'; | ||||
|             return conn; | ||||
|         }); | ||||
|         server.flows.postConnectFlow.exec(null as any as MyConn, console); | ||||
|         server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => { | ||||
|             call.value2 = 'x'; | ||||
|             return call; | ||||
|         }); | ||||
|         server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => { | ||||
|             call.value2 = 'f'; | ||||
|             return call; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('autoImplementApi', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         await testApi(server, client); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('sendMsg', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             port: 3001, | ||||
|             logger: serverLogger, | ||||
|             // debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             server: 'http://127.0.0.1:3001', | ||||
|             logger: clientLogger, | ||||
|             // debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         return new Promise(rs => { | ||||
|             let msg: MsgChat = { | ||||
|                 channel: 123, | ||||
|                 userName: 'fff', | ||||
|                 content: '666', | ||||
|                 time: Date.now() | ||||
|             }; | ||||
|  | ||||
|             server.listenMsg('Chat', async v => { | ||||
|                 assert.deepStrictEqual(v.msg, msg); | ||||
|                 await server.stop(); | ||||
|                 rs(); | ||||
|             }); | ||||
|  | ||||
|             client.sendMsg('Chat', msg); | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('abort', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let result: any | undefined; | ||||
|         let promise = client.callApi('Test', { name: 'aaaaaaaa' }); | ||||
|         let sn = client.lastSN; | ||||
|         setTimeout(() => { | ||||
|             client.abort(sn) | ||||
|         }, 10); | ||||
|         promise.then(v => { | ||||
|             result = v; | ||||
|         }); | ||||
|  | ||||
|         await new Promise<void>(rs => { | ||||
|             setTimeout(() => { | ||||
|                 assert.strictEqual(result, undefined); | ||||
|                 rs(); | ||||
|             }, 150) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('abortByKey', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let result: any | undefined; | ||||
|         let result1: any | undefined; | ||||
|  | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|  | ||||
|         client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; }); | ||||
|  | ||||
|         setTimeout(() => { | ||||
|             client.abortByKey('XXX') | ||||
|         }, 10); | ||||
|  | ||||
|         await new Promise<void>(rs => { | ||||
|             setTimeout(() => { | ||||
|                 assert.strictEqual(result, undefined); | ||||
|                 assert.deepStrictEqual(result1, { | ||||
|                     isSucc: true, | ||||
|                     res: { | ||||
|                         reply: 'Test reply: bbbbbb' | ||||
|                     } | ||||
|                 }) | ||||
|                 rs(); | ||||
|             }, 150) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('abortAll', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let result: any | undefined; | ||||
|         let result1: any | undefined; | ||||
|  | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|         client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; }); | ||||
|  | ||||
|         client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; }); | ||||
|  | ||||
|         setTimeout(() => { | ||||
|             client.abortAll() | ||||
|         }, 10); | ||||
|  | ||||
|         await new Promise<void>(rs => { | ||||
|             setTimeout(() => { | ||||
|                 assert.strictEqual(result, undefined); | ||||
|                 assert.strictEqual(result1, undefined); | ||||
|                 rs(); | ||||
|             }, 150) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('pendingApis', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         for (let i = 0; i < 10; ++i) { | ||||
|             let promise = Promise.all(Array.from({ length: 10 }, () => new Promise<void>(rs => { | ||||
|                 let name = ['Req', 'InnerError', 'TsrpcError', 'error'][Math.random() * 4 | 0]; | ||||
|                 let ret: any | undefined; | ||||
|                 let promise = client.callApi('Test', { name: name }); | ||||
|                 let sn = client.lastSN; | ||||
|                 let abort = Math.random() > 0.5; | ||||
|                 if (abort) { | ||||
|                     setTimeout(() => { | ||||
|                         client.abort(sn) | ||||
|                     }, 0); | ||||
|                 } | ||||
|                 promise.then(v => { | ||||
|                     ret = v; | ||||
|                 }); | ||||
|  | ||||
|                 setTimeout(() => { | ||||
|                     client.logger?.log('sn', sn, name, abort, ret) | ||||
|                     if (abort) { | ||||
|                         assert.strictEqual(ret, undefined); | ||||
|                     } | ||||
|                     else { | ||||
|                         assert.notEqual(ret, undefined); | ||||
|                         if (name === 'Req') { | ||||
|                             assert.strictEqual(ret.isSucc, true); | ||||
|                         } | ||||
|                         else { | ||||
|                             assert.strictEqual(ret.isSucc, false) | ||||
|                         } | ||||
|                     } | ||||
|                     rs(); | ||||
|                 }, 300) | ||||
|             }))); | ||||
|             assert.strictEqual(client['_pendingApis'].length, 10); | ||||
|             await promise; | ||||
|             assert.strictEqual(client['_pendingApis'].length, 0); | ||||
|         } | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         let client1 = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             server: 'http://localhost:80', | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let ret = await client1.callApi('Test', { name: 'xx' }); | ||||
|         console.log(ret); | ||||
|         assert.strictEqual(ret.isSucc, false); | ||||
|         assert.strictEqual(ret.err?.code, 'ECONNREFUSED'); | ||||
|         assert.strictEqual(ret.err?.type, TsrpcErrorType.NetworkError); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('server timeout', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 100 | ||||
|         }); | ||||
|         server.implementApi('Test', call => { | ||||
|             return new Promise(rs => { | ||||
|                 setTimeout(() => { | ||||
|                     call.req && call.succ({ | ||||
|                         reply: 'Hi, ' + call.req.name | ||||
|                     }); | ||||
|                     rs(); | ||||
|                 }, 200) | ||||
|             }) | ||||
|         }) | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|         let ret = await client.callApi('Test', { name: 'Jack' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Server Timeout', { | ||||
|                 code: 'SERVER_TIMEOUT', | ||||
|                 type: TsrpcErrorType.ServerError | ||||
|             }) | ||||
|         }); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('client timeout', async function () { | ||||
|         let server1 = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         server1.implementApi('Test', call => { | ||||
|             return new Promise(rs => { | ||||
|                 setTimeout(() => { | ||||
|                     call.succ({ | ||||
|                         reply: 'Hello, ' + call.req.name | ||||
|                     }); | ||||
|                     rs(); | ||||
|                 }, 2000) | ||||
|             }) | ||||
|         }) | ||||
|         await server1.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             timeout: 100, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'Jack123' }); | ||||
|         // SERVER TIMEOUT的call还没执行完,但是call却被放入Pool了,导致这个BUG | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError({ | ||||
|                 message: 'Request Timeout', | ||||
|                 code: 'TIMEOUT', | ||||
|                 type: TsrpcErrorType.NetworkError | ||||
|             }) | ||||
|         }); | ||||
|         await server1.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('Graceful stop', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         let reqNum = 0; | ||||
|         server.implementApi('Test', async call => { | ||||
|             if (++reqNum === 10) { | ||||
|                 server.gracefulStop(); | ||||
|             } | ||||
|             await new Promise(rs => setTimeout(rs, parseInt(call.req.name))); | ||||
|             call.succ({ reply: 'OK' }); | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|         let isStopped = false; | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }) | ||||
|  | ||||
|         let succNum = 0; | ||||
|         await Promise.all(Array.from({ length: 10 }, (v, i) => client.callApi('Test', { name: '' + (i * 100) }).then(v => { | ||||
|             if (v.res?.reply === 'OK') { | ||||
|                 ++succNum; | ||||
|             } | ||||
|         }))) | ||||
|         assert.strictEqual(succNum, 10); | ||||
|     }) | ||||
| }) | ||||
|  | ||||
| describe('HTTP Flows', function () { | ||||
|     it('Server conn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             assert.strictEqual((call.conn as any).xxxx, 'asdfasdf') | ||||
|             assert.strictEqual(flowExecResult.postConnectFlow, true); | ||||
|             assert.strictEqual(flowExecResult.postDisconnectFlow, undefined); | ||||
|             call.succ({ reply: 'ok' }); | ||||
|             assert.strictEqual(flowExecResult.postDisconnectFlow, undefined); | ||||
|         }); | ||||
|  | ||||
|         server.flows.postConnectFlow.push(v => { | ||||
|             flowExecResult.postConnectFlow = true; | ||||
|             (v as any).xxxx = 'asdfasdf'; | ||||
|             return v; | ||||
|         }); | ||||
|         server.flows.postDisconnectFlow.push(v => { | ||||
|             flowExecResult.postDisconnectFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         assert.strictEqual(flowExecResult.postConnectFlow, undefined); | ||||
|         assert.strictEqual(flowExecResult.postDisconnectFlow, undefined); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|         await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postConnectFlow, true); | ||||
|         assert.strictEqual(flowExecResult.postDisconnectFlow, true); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('Buffer enc/dec flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'Enc&Dec' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preRecvDataFlow.push(v => { | ||||
|             flowExecResult.preRecvDataFlow = true; | ||||
|             v.data = (v.data as string).split('').reverse().join(''); | ||||
|             return v; | ||||
|         }); | ||||
|         server.flows.preSendDataFlow.push(v => { | ||||
|             flowExecResult.preSendDataFlow = true; | ||||
|             v.data = (v.data as string).split('').reverse().join(''); | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preSendDataFlow.push(v => { | ||||
|             v.data = (v.data as string).split('').reverse().join(''); | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         client.flows.preRecvDataFlow.push(v => { | ||||
|             v.data = (v.data as string).split('').reverse().join(''); | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preRecvDataFlow, true); | ||||
|         assert.strictEqual(flowExecResult.preSendDataFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: true, | ||||
|             res: { | ||||
|                 reply: 'Enc&Dec' | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             call.error('You need login'); | ||||
|             return call; | ||||
|         }); | ||||
|         server.flows.postApiCallFlow.push(v => { | ||||
|             flowExecResult.postApiCallFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preCallApiFlow.push(v => { | ||||
|             if (v.apiName !== 'ObjId') { | ||||
|                 v.req.name = 'Changed' | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postApiCallFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow break', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             call.error('You need login'); | ||||
|             return undefined; | ||||
|         }); | ||||
|         server.flows.postApiCallFlow.push(v => { | ||||
|             flowExecResult.postApiCallFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preCallApiFlow.push(v => { | ||||
|             if (v.apiName !== 'ObjId') { | ||||
|                 v.req.name = 'Changed' | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postApiCallFlow, undefined); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             throw new Error('ASDFASDF') | ||||
|         }); | ||||
|         server.flows.postApiCallFlow.push(v => { | ||||
|             flowExecResult.postApiCallFlow = true; | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preCallApiFlow.push(v => { | ||||
|             if (v.apiName !== 'ObjId') { | ||||
|                 v.req.name = 'Changed' | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.postApiCallFlow, undefined); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: 'ASDFASDF', | ||||
|                 code: 'INTERNAL_ERR' | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('server ApiReturn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiReturnFlow.push(v => { | ||||
|             flowExecResult.preApiReturnFlow = true; | ||||
|             v.return = { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError('Ret changed') | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|         server.flows.postApiReturnFlow.push(v => { | ||||
|             flowExecResult.postApiReturnFlow = true; | ||||
|             v.call.logger.log('RETTT', v.return); | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preApiReturnFlow, true); | ||||
|         assert.strictEqual(flowExecResult.postApiReturnFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Ret changed') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('client ApiReturn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof HttpClient<any>['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preApiReturnFlow.push(v => { | ||||
|             flowExecResult.preApiReturnFlow = true; | ||||
|             v.return = { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError('Ret changed') | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|         client.flows.postApiReturnFlow.push(v => { | ||||
|             flowExecResult.postApiReturnFlow = true; | ||||
|             client.logger?.log('RETTT', v.return); | ||||
|             return v; | ||||
|         }) | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preApiReturnFlow, true); | ||||
|         assert.strictEqual(flowExecResult.postApiReturnFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Ret changed') | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('client SendBufferFlow prevent', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         // const flowExecResult: { [K in (keyof BaseClient<any>['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         client.flows.preSendDataFlow.push(v => { | ||||
|             return undefined | ||||
|         }); | ||||
|  | ||||
|         let ret: any; | ||||
|         client.callApi('Test', { name: 'xxx' }).then(v => { ret = v }); | ||||
|         await new Promise(rs => { setTimeout(rs, 200) }); | ||||
|         assert.strictEqual(ret, undefined) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }); | ||||
|  | ||||
|     it('onInputBufferError', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|         client.flows.preSendDataFlow.push(v => { | ||||
|             v.data = (v.data as string).split('').reverse().join(''); | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 'XXX' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Input is not a valid JSON string: Unexpected token } in JSON at position 0', { | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 code: 'INPUT_DATA_ERR' | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('throw type error in client', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         let ret = await client.callApi('Test', { name: 23456 } as any); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError({ | ||||
|                 "code": "INPUT_DATA_ERR", | ||||
|                 "message": "Property `name`: Expected type to be `string`, actually `number`.", | ||||
|                 "type": TsrpcErrorType.ClientError | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('throw type error in server', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.start(); | ||||
|  | ||||
|         let retHttp = await new HttpProxy().fetch({ | ||||
|             url: 'http://127.0.0.1:3000/Test', | ||||
|             data: JSON.stringify({ name: 12345 }), | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'content-type': 'application/json; charset=utf-8' | ||||
|             }, | ||||
|             transportOptions: {}, | ||||
|             responseType: 'text' | ||||
|         }).promise; | ||||
|  | ||||
|         assert.deepStrictEqual(JSON.parse((retHttp as any).res), { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 "code": "INPUT_DATA_ERR", | ||||
|                 "message": "Property `name`: Expected type to be `string`, actually `number`.", | ||||
|                 "type": TsrpcErrorType.ServerError | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
|  | ||||
|     it('ObjectId', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             json: true, | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|         await server.start(); | ||||
|  | ||||
|         let client = new HttpClient(getProto(), { | ||||
|             json: true, | ||||
|             logger: clientLogger | ||||
|         }); | ||||
|  | ||||
|         // ObjectId | ||||
|         let objId1 = new ObjectId(); | ||||
|         let ret = await client.callApi('ObjId', { | ||||
|             id1: objId1 | ||||
|         }); | ||||
|         assert.strictEqual(ret.isSucc, true, ret.err?.message); | ||||
|         assert.strictEqual(objId1.toString(), ret.res!.id2.toString()); | ||||
|  | ||||
|         await server.stop(); | ||||
|     }) | ||||
| }) | ||||
							
								
								
									
										396
									
								
								test/cases/inner.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										396
									
								
								test/cases/inner.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,396 @@ | ||||
| import { ObjectId } from 'bson'; | ||||
| import { assert } from 'chai'; | ||||
| import chalk from 'chalk'; | ||||
| import * as path from "path"; | ||||
| import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto'; | ||||
| import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src'; | ||||
| import { HttpServer } from '../../src/server/http/HttpServer'; | ||||
| import { PrefixLogger } from '../../src/server/models/PrefixLogger'; | ||||
| import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest'; | ||||
| import { ApiTest } from '../api/ApiTest'; | ||||
| import { MsgChat } from '../proto/MsgChat'; | ||||
| import { ReqTest, ResTest } from '../proto/PtlTest'; | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
|  | ||||
| const serverLogger = new PrefixLogger({ | ||||
|     prefixs: [chalk.bgGreen.white(' Server ')], | ||||
|     logger: new TerminalColorLogger({ pid: 'Server' }) | ||||
| }); | ||||
|  | ||||
| const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>; | ||||
|  | ||||
| async function testApi(server: HttpServer<ServiceType>) { | ||||
|     // Succ | ||||
|     assert.deepStrictEqual(await server.callApi('Test', { | ||||
|         name: 'Req1' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'Test reply: Req1' | ||||
|         } | ||||
|     }); | ||||
|     assert.deepStrictEqual(await server.callApi('a/b/c/Test', { | ||||
|         name: 'Req2' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'a/b/c/Test reply: Req2' | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Inner error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await server.callApi(v as any, { | ||||
|             name: 'InnerError' | ||||
|         }); | ||||
|         delete ret.err!.innerErr.stack; | ||||
|  | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 code: 'INTERNAL_ERR', | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: `${v} InnerError` | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // TsrpcError | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await server.callApi(v as any, { | ||||
|             name: 'TsrpcError' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError(`${v} TsrpcError`, { | ||||
|                 code: 'CODE_TEST', | ||||
|                 type: TsrpcErrorType.ApiError, | ||||
|                 info: 'ErrInfo ' + v | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // call.error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await server.callApi(v as any, { | ||||
|             name: 'error' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Got an error', { | ||||
|                 type: TsrpcErrorType.ApiError | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| describe('HTTP Server & Client basic', function () { | ||||
|     it('implement API manually', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         server.implementApi('Test', ApiTest); | ||||
|         server.implementApi('a/b/c/Test', ApiAbcTest); | ||||
|  | ||||
|         await testApi(server); | ||||
|  | ||||
|  | ||||
|     }) | ||||
|  | ||||
|     it('extend call in handler', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|  | ||||
|         server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => { | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }); | ||||
|         server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => { | ||||
|             call.msg.content; | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('extend call in flow', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyConn = HttpConnection<any> & { | ||||
|             currentUser: { | ||||
|                 uid: string, | ||||
|                 nickName: string | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         server.flows.postConnectFlow.push((conn: MyConn) => { | ||||
|             conn.currentUser.nickName = 'asdf'; | ||||
|             return conn; | ||||
|         }); | ||||
|         server.flows.postConnectFlow.exec(null as any as MyConn, console); | ||||
|         server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => { | ||||
|             call.value2 = 'x'; | ||||
|             return call; | ||||
|         }); | ||||
|         server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => { | ||||
|             call.value2 = 'f'; | ||||
|             return call; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('autoImplementApi', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|  | ||||
|         await server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         await testApi(server); | ||||
|     }); | ||||
|  | ||||
|     it('autoImplementApi delay', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api'), true) | ||||
|  | ||||
|         await testApi(server); | ||||
|     }); | ||||
|  | ||||
|     it('error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let ret = await server.callApi('TesASFt' as any, { name: 'xx' } as any); | ||||
|         console.log(ret); | ||||
|         assert.strictEqual(ret.isSucc, false); | ||||
|         assert.strictEqual(ret.err?.code, 'ERR_API_NAME'); | ||||
|         assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError); | ||||
|  | ||||
|  | ||||
|     }) | ||||
|  | ||||
|     it('server timeout', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 100 | ||||
|         }); | ||||
|         server.implementApi('Test', call => { | ||||
|             return new Promise(rs => { | ||||
|                 setTimeout(() => { | ||||
|                     call.req && call.succ({ | ||||
|                         reply: 'Hi, ' + call.req.name | ||||
|                     }); | ||||
|                     rs(); | ||||
|                 }, 200) | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|  | ||||
|         let ret = await server.callApi('Test', { name: 'Jack' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Server Timeout', { | ||||
|                 code: 'SERVER_TIMEOUT', | ||||
|                 type: TsrpcErrorType.ServerError | ||||
|             }) | ||||
|         }); | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('Graceful stop', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         let reqNum = 0; | ||||
|         server.implementApi('Test', async call => { | ||||
|             if (++reqNum === 10) { | ||||
|                 server.gracefulStop(); | ||||
|             } | ||||
|             await new Promise(rs => setTimeout(rs, parseInt(call.req.name))); | ||||
|             call.succ({ reply: 'OK' }); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let isStopped = false; | ||||
|  | ||||
|         let succNum = 0; | ||||
|         await Promise.all(Array.from({ length: 10 }, (v, i) => server.callApi('Test', { name: '' + (i * 100) }).then(v => { | ||||
|             if (v.res?.reply === 'OK') { | ||||
|                 ++succNum; | ||||
|             } | ||||
|         }))) | ||||
|         assert.strictEqual(succNum, 10); | ||||
|     }) | ||||
| }) | ||||
|  | ||||
| describe('HTTP Flows', function () { | ||||
|     it('ApiCall flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             if (call.req.apiName !== 'ObjId') { | ||||
|                 call.req.name = 'Changed' | ||||
|             } | ||||
|             return call; | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             call.error('You need login'); | ||||
|             return call; | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.callApi('Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow break', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             call.error('You need login'); | ||||
|             return undefined; | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.callApi('Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             throw new Error('ASDFASDF') | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.callApi('Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: 'ASDFASDF', | ||||
|                 code: 'INTERNAL_ERR' | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('server ApiReturn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiReturnFlow.push(v => { | ||||
|             flowExecResult.preApiReturnFlow = true; | ||||
|             v.return = { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError('Ret changed') | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.callApi('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preApiReturnFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Ret changed') | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('Extended JSON Types', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]); | ||||
|         let date = new Date('2021/11/17'); | ||||
|  | ||||
|         // ObjectId | ||||
|         let objId1 = new ObjectId(); | ||||
|         let ret = await server.callApi('ObjId', { | ||||
|             id1: objId1, | ||||
|             buf: buf, | ||||
|             date: date | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: true, | ||||
|             res: { | ||||
|                 id2: objId1, | ||||
|                 buf: buf, | ||||
|                 date: date | ||||
|             } | ||||
|         }); | ||||
|     }) | ||||
| }) | ||||
							
								
								
									
										414
									
								
								test/cases/inputBuffer.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										414
									
								
								test/cases/inputBuffer.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,414 @@ | ||||
| import { ObjectId } from 'bson'; | ||||
| import { assert } from 'chai'; | ||||
| import chalk from 'chalk'; | ||||
| import * as path from "path"; | ||||
| import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto'; | ||||
| import { ApiCall, ApiService, BaseServer, HttpConnection, MsgCall, TerminalColorLogger, TransportDataUtil } from '../../src'; | ||||
| import { HttpServer } from '../../src/server/http/HttpServer'; | ||||
| import { PrefixLogger } from '../../src/server/models/PrefixLogger'; | ||||
| import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest'; | ||||
| import { ApiTest } from '../api/ApiTest'; | ||||
| import { MsgChat } from '../proto/MsgChat'; | ||||
| import { ReqTest, ResTest } from '../proto/PtlTest'; | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
|  | ||||
| const serverLogger = new PrefixLogger({ | ||||
|     prefixs: [chalk.bgGreen.white(' Server ')], | ||||
|     logger: new TerminalColorLogger({ pid: 'Server' }) | ||||
| }); | ||||
|  | ||||
| async function inputBuffer(server: BaseServer<any>, apiName: string, req: any) { | ||||
|     let apiSvc: ApiService | undefined = server.serviceMap.apiName2Service[apiName]; | ||||
|     let inputBuf = apiSvc ? (await TransportDataUtil.encodeApiReq(server.tsbuffer, apiSvc, req, 'buffer')).output : new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]); | ||||
|     if (!inputBuf) { | ||||
|         throw new Error('failed to encode inputBuf') | ||||
|     } | ||||
|  | ||||
|     let retBuf = await server.inputBuffer(inputBuf); | ||||
|     assert.ok(retBuf instanceof Uint8Array) | ||||
|  | ||||
|     let opDecode = await TransportDataUtil.parseServerOutout(server.tsbuffer, server.serviceMap, retBuf, apiSvc?.id ?? 0); | ||||
|     if (!opDecode.isSucc) { | ||||
|         throw new Error('decode buf failed') | ||||
|     } | ||||
|     if (opDecode.result.type !== 'api') { | ||||
|         throw new Error('decode result is not api') | ||||
|     } | ||||
|     return opDecode.result.ret; | ||||
| } | ||||
|  | ||||
| const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>; | ||||
|  | ||||
| async function testApi(server: HttpServer<ServiceType>) { | ||||
|     // Succ | ||||
|     assert.deepStrictEqual(await inputBuffer(server, 'Test', { | ||||
|         name: 'Req1' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'Test reply: Req1' | ||||
|         } | ||||
|     }); | ||||
|     assert.deepStrictEqual(await inputBuffer(server, 'a/b/c/Test', { | ||||
|         name: 'Req2' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'a/b/c/Test reply: Req2' | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Inner error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await inputBuffer(server, v as any, { | ||||
|             name: 'InnerError' | ||||
|         }); | ||||
|         delete ret.err!.innerErr.stack; | ||||
|  | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 code: 'INTERNAL_ERR', | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: `${v} InnerError` | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // TsrpcError | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await inputBuffer(server, v as any, { | ||||
|             name: 'TsrpcError' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError(`${v} TsrpcError`, { | ||||
|                 code: 'CODE_TEST', | ||||
|                 type: TsrpcErrorType.ApiError, | ||||
|                 info: 'ErrInfo ' + v | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // call.error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await inputBuffer(server, v as any, { | ||||
|             name: 'error' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Got an error', { | ||||
|                 type: TsrpcErrorType.ApiError | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| describe('HTTP Server & Client basic', function () { | ||||
|     it('implement API manually', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         server.implementApi('Test', ApiTest); | ||||
|         server.implementApi('a/b/c/Test', ApiAbcTest); | ||||
|  | ||||
|         await testApi(server); | ||||
|  | ||||
|  | ||||
|     }) | ||||
|  | ||||
|     it('extend call in handler', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|  | ||||
|         server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => { | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }); | ||||
|         server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => { | ||||
|             call.msg.content; | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('extend call in flow', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyConn = HttpConnection<any> & { | ||||
|             currentUser: { | ||||
|                 uid: string, | ||||
|                 nickName: string | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         server.flows.postConnectFlow.push((conn: MyConn) => { | ||||
|             conn.currentUser.nickName = 'asdf'; | ||||
|             return conn; | ||||
|         }); | ||||
|         server.flows.postConnectFlow.exec(null as any as MyConn, console); | ||||
|         server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => { | ||||
|             call.value2 = 'x'; | ||||
|             return call; | ||||
|         }); | ||||
|         server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => { | ||||
|             call.value2 = 'f'; | ||||
|             return call; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('autoImplementApi', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|  | ||||
|         await server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         await testApi(server); | ||||
|     }); | ||||
|  | ||||
|     it('autoImplementApi delay', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api'), true) | ||||
|  | ||||
|         await testApi(server); | ||||
|     }); | ||||
|  | ||||
|     it('error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let ret = await inputBuffer(server, 'TesASFt' as any, { name: 'xx' } as any); | ||||
|         console.log(ret); | ||||
|         assert.strictEqual(ret.isSucc, false); | ||||
|         assert.strictEqual(ret.err?.code, 'INPUT_DATA_ERR'); | ||||
|         assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError); | ||||
|     }) | ||||
|  | ||||
|     it('server timeout', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 100 | ||||
|         }); | ||||
|         server.implementApi('Test', call => { | ||||
|             return new Promise(rs => { | ||||
|                 setTimeout(() => { | ||||
|                     call.req && call.succ({ | ||||
|                         reply: 'Hi, ' + call.req.name | ||||
|                     }); | ||||
|                     rs(); | ||||
|                 }, 200) | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|  | ||||
|         let ret = await inputBuffer(server, 'Test', { name: 'Jack' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Server Timeout', { | ||||
|                 code: 'SERVER_TIMEOUT', | ||||
|                 type: TsrpcErrorType.ServerError | ||||
|             }) | ||||
|         }); | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('Graceful stop', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         let reqNum = 0; | ||||
|         server.implementApi('Test', async call => { | ||||
|             if (++reqNum === 10) { | ||||
|                 server.gracefulStop(); | ||||
|             } | ||||
|             await new Promise(rs => setTimeout(rs, parseInt(call.req.name))); | ||||
|             call.succ({ reply: 'OK' }); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let isStopped = false; | ||||
|  | ||||
|         let succNum = 0; | ||||
|         await Promise.all(Array.from({ length: 10 }, (v, i) => inputBuffer(server, 'Test', { name: '' + (i * 100) }).then((v: any) => { | ||||
|             if (v.res?.reply === 'OK') { | ||||
|                 ++succNum; | ||||
|             } | ||||
|         }))) | ||||
|         assert.strictEqual(succNum, 10); | ||||
|     }) | ||||
| }) | ||||
|  | ||||
| describe('HTTP Flows', function () { | ||||
|     it('ApiCall flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             if (call.req.apiName !== 'ObjId') { | ||||
|                 call.req.name = 'Changed' | ||||
|             } | ||||
|             return call; | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             call.error('You need login'); | ||||
|             return call; | ||||
|         }); | ||||
|  | ||||
|         let ret = await inputBuffer(server, 'Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow break', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             call.error('You need login'); | ||||
|             return undefined; | ||||
|         }); | ||||
|  | ||||
|         let ret = await inputBuffer(server, 'Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('You need login') | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             throw new Error('ASDFASDF') | ||||
|         }); | ||||
|  | ||||
|         let ret = await inputBuffer(server, 'Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Internal Server Error', { | ||||
|                 type: TsrpcErrorType.ServerError, | ||||
|                 innerErr: 'ASDFASDF', | ||||
|                 code: 'INTERNAL_ERR' | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('server ApiReturn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiReturnFlow.push(v => { | ||||
|             flowExecResult.preApiReturnFlow = true; | ||||
|             v.return = { | ||||
|                 isSucc: false, | ||||
|                 err: new TsrpcError('Ret changed') | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await inputBuffer(server, 'Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preApiReturnFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: new TsrpcError('Ret changed') | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('Extended JSON Types', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]); | ||||
|         let date = new Date('2021/11/17'); | ||||
|  | ||||
|         // ObjectId | ||||
|         let objId1 = new ObjectId(); | ||||
|         let ret = await inputBuffer(server, 'ObjId', { | ||||
|             id1: objId1, | ||||
|             buf: buf, | ||||
|             date: date | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: true, | ||||
|             res: { | ||||
|                 id2: objId1, | ||||
|                 buf: buf, | ||||
|                 date: date | ||||
|             } | ||||
|         }); | ||||
|     }) | ||||
| }) | ||||
							
								
								
									
										410
									
								
								test/cases/inputJSON.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								test/cases/inputJSON.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,410 @@ | ||||
| import { ObjectId } from 'bson'; | ||||
| import { assert } from 'chai'; | ||||
| import chalk from 'chalk'; | ||||
| import * as path from "path"; | ||||
| import { Base64Util } from 'tsbuffer'; | ||||
| import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto'; | ||||
| import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src'; | ||||
| import { HttpServer } from '../../src/server/http/HttpServer'; | ||||
| import { PrefixLogger } from '../../src/server/models/PrefixLogger'; | ||||
| import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest'; | ||||
| import { ApiTest } from '../api/ApiTest'; | ||||
| import { MsgChat } from '../proto/MsgChat'; | ||||
| import { ReqTest, ResTest } from '../proto/PtlTest'; | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
|  | ||||
| const serverLogger = new PrefixLogger({ | ||||
|     prefixs: [chalk.bgGreen.white(' Server ')], | ||||
|     logger: new TerminalColorLogger({ pid: 'Server' }) | ||||
| }); | ||||
|  | ||||
| const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>; | ||||
|  | ||||
| async function testApi(server: HttpServer<ServiceType>) { | ||||
|     // Succ | ||||
|     assert.deepStrictEqual(await server.inputJSON('Test', { | ||||
|         name: 'Req1' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'Test reply: Req1' | ||||
|         } | ||||
|     }); | ||||
|     assert.deepStrictEqual(await server.inputJSON('/a/b/c/Test', { | ||||
|         name: 'Req2' | ||||
|     }), { | ||||
|         isSucc: true, | ||||
|         res: { | ||||
|             reply: 'a/b/c/Test reply: Req2' | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Inner error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await server.inputJSON(v as any, { | ||||
|             name: 'InnerError' | ||||
|         }); | ||||
|         delete ret.err!.innerErr.stack; | ||||
|  | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 ...new TsrpcError('Internal Server Error', { | ||||
|                     code: 'INTERNAL_ERR', | ||||
|                     type: TsrpcErrorType.ServerError, | ||||
|                     innerErr: `${v} InnerError` | ||||
|                 }) | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // TsrpcError | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await server.inputJSON(v as any, { | ||||
|             name: 'TsrpcError' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 ...new TsrpcError(`${v} TsrpcError`, { | ||||
|                     code: 'CODE_TEST', | ||||
|                     type: TsrpcErrorType.ApiError, | ||||
|                     info: 'ErrInfo ' + v | ||||
|                 }) | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // call.error | ||||
|     for (let v of ['Test', 'a/b/c/Test']) { | ||||
|         let ret = await server.inputJSON(v as any, { | ||||
|             name: 'error' | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 ...new TsrpcError('Got an error', { | ||||
|                     type: TsrpcErrorType.ApiError | ||||
|                 }) | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| describe('HTTP Server & Client basic', function () { | ||||
|     it('implement API manually', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         server.implementApi('Test', ApiTest); | ||||
|         server.implementApi('a/b/c/Test', ApiAbcTest); | ||||
|  | ||||
|         await testApi(server); | ||||
|  | ||||
|  | ||||
|     }) | ||||
|  | ||||
|     it('extend call in handler', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|  | ||||
|         server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => { | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }); | ||||
|         server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => { | ||||
|             call.msg.content; | ||||
|             call.value1 = 'xxx'; | ||||
|             call.value2 = 'xxx'; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('extend call in flow', function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             debugBuf: true | ||||
|         }); | ||||
|  | ||||
|         type MyApiCall<Req, Res> = ApiCall<Req, Res> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyMsgCall<Msg> = MsgCall<Msg> & { | ||||
|             value1?: string; | ||||
|             value2: string; | ||||
|         } | ||||
|         type MyConn = HttpConnection<any> & { | ||||
|             currentUser: { | ||||
|                 uid: string, | ||||
|                 nickName: string | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         server.flows.postConnectFlow.push((conn: MyConn) => { | ||||
|             conn.currentUser.nickName = 'asdf'; | ||||
|             return conn; | ||||
|         }); | ||||
|         server.flows.postConnectFlow.exec(null as any as MyConn, console); | ||||
|         server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => { | ||||
|             call.value2 = 'x'; | ||||
|             return call; | ||||
|         }); | ||||
|         server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => { | ||||
|             call.value2 = 'f'; | ||||
|             return call; | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     it('autoImplementApi', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|  | ||||
|         await server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         await testApi(server); | ||||
|     }); | ||||
|  | ||||
|     it('autoImplementApi delay', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 5000 | ||||
|         }); | ||||
|  | ||||
|         server.autoImplementApi(path.resolve(__dirname, '../api'), true) | ||||
|  | ||||
|         await testApi(server); | ||||
|     }); | ||||
|  | ||||
|     it('error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let ret = await server.inputJSON('TesASFt' as any, { name: 'xx' } as any); | ||||
|         console.log(ret); | ||||
|         assert.strictEqual(ret.isSucc, false); | ||||
|         assert.strictEqual(ret.err?.message, 'Invalid service name: TesASFt'); | ||||
|         assert.strictEqual(ret.err?.code, 'INPUT_DATA_ERR'); | ||||
|         assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError); | ||||
|     }) | ||||
|  | ||||
|     it('server timeout', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger, | ||||
|             apiTimeout: 100 | ||||
|         }); | ||||
|         server.implementApi('Test', call => { | ||||
|             return new Promise(rs => { | ||||
|                 setTimeout(() => { | ||||
|                     call.req && call.succ({ | ||||
|                         reply: 'Hi, ' + call.req.name | ||||
|                     }); | ||||
|                     rs(); | ||||
|                 }, 200) | ||||
|             }) | ||||
|         }) | ||||
|  | ||||
|  | ||||
|         let ret = await server.inputJSON('Test', { name: 'Jack' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 ...new TsrpcError('Server Timeout', { | ||||
|                     code: 'SERVER_TIMEOUT', | ||||
|                     type: TsrpcErrorType.ServerError | ||||
|                 }) | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('Graceful stop', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         let reqNum = 0; | ||||
|         server.implementApi('Test', async call => { | ||||
|             if (++reqNum === 10) { | ||||
|                 server.gracefulStop(); | ||||
|             } | ||||
|             await new Promise(rs => setTimeout(rs, parseInt(call.req.name))); | ||||
|             call.succ({ reply: 'OK' }); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         let isStopped = false; | ||||
|  | ||||
|         let succNum = 0; | ||||
|         await Promise.all(Array.from({ length: 10 }, (v, i) => server.inputJSON('Test', { name: '' + (i * 100) }).then((v: any) => { | ||||
|             if (v.res?.reply === 'OK') { | ||||
|                 ++succNum; | ||||
|             } | ||||
|         }))) | ||||
|         assert.strictEqual(succNum, 10); | ||||
|     }) | ||||
| }) | ||||
|  | ||||
| describe('HTTP Flows', function () { | ||||
|     it('ApiCall flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             if (call.req.apiName !== 'ObjId') { | ||||
|                 call.req.name = 'Changed' | ||||
|             } | ||||
|             return call; | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             assert.strictEqual(call.req.name, 'Changed') | ||||
|             call.error('You need login'); | ||||
|             return call; | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.inputJSON('Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 ...new TsrpcError('You need login') | ||||
|             } | ||||
|         }) | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow break', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             call.error('You need login'); | ||||
|             return undefined; | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.inputJSON('Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 ...new TsrpcError('You need login') | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('ApiCall flow error', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'asdgasdgasdgasdg' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiCallFlow.push(call => { | ||||
|             throw new Error('ASDFASDF') | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.inputJSON('Test', { name: 'xxx' }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { | ||||
|                 ...new TsrpcError('Internal Server Error', { | ||||
|                     type: TsrpcErrorType.ServerError, | ||||
|                     innerErr: 'ASDFASDF', | ||||
|                     code: 'INTERNAL_ERR' | ||||
|                 }) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('server ApiReturn flow', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|  | ||||
|         const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {}; | ||||
|  | ||||
|         server.implementApi('Test', async call => { | ||||
|             call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' }); | ||||
|         }); | ||||
|  | ||||
|         server.flows.preApiReturnFlow.push(v => { | ||||
|             flowExecResult.preApiReturnFlow = true; | ||||
|             v.return = { | ||||
|                 isSucc: false, | ||||
|                 err: { ...new TsrpcError('Ret changed') } | ||||
|             } | ||||
|             return v; | ||||
|         }); | ||||
|  | ||||
|         let ret = await server.inputJSON('Test', { name: 'xxx' }); | ||||
|         assert.strictEqual(flowExecResult.preApiReturnFlow, true); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: false, | ||||
|             err: { ...new TsrpcError('Ret changed') } | ||||
|         }) | ||||
|  | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('Extended JSON Types', async function () { | ||||
|         let server = new HttpServer(getProto(), { | ||||
|             logger: serverLogger | ||||
|         }); | ||||
|         await server.autoImplementApi(path.resolve(__dirname, '../api')) | ||||
|  | ||||
|         let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]); | ||||
|         let date = new Date('2021/11/17'); | ||||
|  | ||||
|         // ObjectId | ||||
|         let objId1 = new ObjectId(); | ||||
|         let ret = await server.inputJSON('ObjId', { | ||||
|             id1: objId1, | ||||
|             buf: buf, | ||||
|             date: date | ||||
|         }); | ||||
|         assert.deepStrictEqual(ret, { | ||||
|             isSucc: true, | ||||
|             res: { | ||||
|                 id2: objId1.toHexString(), | ||||
|                 buf: Base64Util.bufferToBase64(buf), | ||||
|                 date: date.toJSON() | ||||
|             } | ||||
|         }); | ||||
|     }) | ||||
| }) | ||||
							
								
								
									
										1051
									
								
								test/cases/ws.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1051
									
								
								test/cases/ws.test.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1278
									
								
								test/cases/wsJSON.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1278
									
								
								test/cases/wsJSON.test.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										6
									
								
								test/proto/MsgChat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								test/proto/MsgChat.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| export interface MsgChat { | ||||
|     channel: number, | ||||
|     userName: string, | ||||
|     content: string, | ||||
|     time: number | ||||
| } | ||||
							
								
								
									
										14
									
								
								test/proto/PtlObjId.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								test/proto/PtlObjId.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // @ts-ignore | ||||
| import { ObjectId } from "mongodb"; | ||||
|  | ||||
| export interface ReqObjId { | ||||
|     id1: ObjectId; | ||||
|     buf?: Uint8Array, | ||||
|     date?: Date | ||||
| } | ||||
|  | ||||
| export interface ResObjId { | ||||
|     id2: ObjectId; | ||||
|     buf?: Uint8Array, | ||||
|     date?: Date | ||||
| } | ||||
							
								
								
									
										7
									
								
								test/proto/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								test/proto/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export interface ReqTest { | ||||
|     name: string | ||||
| }; | ||||
|  | ||||
| export type ResTest = { | ||||
|     reply: string | ||||
| }; | ||||
							
								
								
									
										10
									
								
								test/proto/a/b/c/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								test/proto/a/b/c/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { MsgChat } from '../../../MsgChat'; | ||||
|  | ||||
| export interface ReqTest { | ||||
|     name: string | ||||
| }; | ||||
|  | ||||
| export type ResTest = { | ||||
|     reply: string, | ||||
|     chat?: MsgChat | ||||
| }; | ||||
							
								
								
									
										203
									
								
								test/proto/serviceProto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								test/proto/serviceProto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | ||||
| import { ServiceProto } from 'tsrpc-proto'; | ||||
| import { ReqTest, ResTest } from './a/b/c/PtlTest'; | ||||
| import { MsgChat } from './MsgChat'; | ||||
| import { ReqObjId, ResObjId } from './PtlObjId'; | ||||
| import { ReqTest as ReqTest_1, ResTest as ResTest_1 } from './PtlTest'; | ||||
|  | ||||
| export interface ServiceType { | ||||
|     api: { | ||||
|         "a/b/c/Test": { | ||||
|             req: ReqTest, | ||||
|             res: ResTest | ||||
|         }, | ||||
|         "ObjId": { | ||||
|             req: ReqObjId, | ||||
|             res: ResObjId | ||||
|         }, | ||||
|         "Test": { | ||||
|             req: ReqTest_1, | ||||
|             res: ResTest_1 | ||||
|         } | ||||
|     }, | ||||
|     msg: { | ||||
|         "Chat": MsgChat | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const serviceProto: ServiceProto<ServiceType> = { | ||||
|     "version": 1, | ||||
|     "services": [ | ||||
|         { | ||||
|             "id": 0, | ||||
|             "name": "a/b/c/Test", | ||||
|             "type": "api" | ||||
|         }, | ||||
|         { | ||||
|             "id": 1, | ||||
|             "name": "Chat", | ||||
|             "type": "msg" | ||||
|         }, | ||||
|         { | ||||
|             "id": 2, | ||||
|             "name": "ObjId", | ||||
|             "type": "api" | ||||
|         }, | ||||
|         { | ||||
|             "id": 3, | ||||
|             "name": "Test", | ||||
|             "type": "api" | ||||
|         } | ||||
|     ], | ||||
|     "types": { | ||||
|         "a/b/c/PtlTest/ReqTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "name", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "a/b/c/PtlTest/ResTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "reply", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "chat", | ||||
|                     "type": { | ||||
|                         "type": "Reference", | ||||
|                         "target": "MsgChat/MsgChat" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "MsgChat/MsgChat": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "channel", | ||||
|                     "type": { | ||||
|                         "type": "Number" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "userName", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 2, | ||||
|                     "name": "content", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 3, | ||||
|                     "name": "time", | ||||
|                     "type": { | ||||
|                         "type": "Number" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlObjId/ReqObjId": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "id1", | ||||
|                     "type": { | ||||
|                         "type": "Reference", | ||||
|                         "target": "?mongodb/ObjectId" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "buf", | ||||
|                     "type": { | ||||
|                         "type": "Buffer", | ||||
|                         "arrayType": "Uint8Array" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 2, | ||||
|                     "name": "date", | ||||
|                     "type": { | ||||
|                         "type": "Date" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlObjId/ResObjId": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "id2", | ||||
|                     "type": { | ||||
|                         "type": "Reference", | ||||
|                         "target": "?mongodb/ObjectId" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "buf", | ||||
|                     "type": { | ||||
|                         "type": "Buffer", | ||||
|                         "arrayType": "Uint8Array" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 2, | ||||
|                     "name": "date", | ||||
|                     "type": { | ||||
|                         "type": "Date" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlTest/ReqTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "name", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlTest/ResTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "reply", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										18
									
								
								test/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								test/test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { HttpServer } from '../src/server/http/HttpServer'; | ||||
| import { serviceProto } from './proto/serviceProto'; | ||||
|  | ||||
| let server = new HttpServer(serviceProto, { | ||||
|     jsonEnabled: true, | ||||
|     jsonRootPath: 'api' | ||||
| }); | ||||
|  | ||||
| server.implementApi('a/b/c/Test', call => { | ||||
|     call.logger.log('xxx', call.req); | ||||
|     call.succ({ | ||||
|         reply: 'xxxxxxxxxxx', | ||||
|         aasdg: 'd', | ||||
|         b: 'asdg' | ||||
|     } as any) | ||||
| }); | ||||
|  | ||||
| server.start(); | ||||
							
								
								
									
										22
									
								
								test/try/client/http.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								test/try/client/http.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { HttpClient } from '../../src/client/http/HttpClient'; | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
| let client = new HttpClient<ServiceType>({ | ||||
|     server: 'http://localhost:3000', | ||||
|     proto: serviceProto | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
|     const P = 50, N = 1000; | ||||
|     let max = 0; | ||||
|     console.time(`test ${P}/${N}`); | ||||
|     for (let i = 0, len = N / P; i < len; ++i) { | ||||
|         let res = await Promise.all(Array.from({ length: P }, () => { | ||||
|             let start = Date.now(); | ||||
|             return client.callApi('a/b/c/Test', { name: '123' }).then(() => Date.now() - start); | ||||
|         })); | ||||
|         max = Math.max(res.max(), max) | ||||
|     } | ||||
|     console.timeEnd(`test ${P}/${N}`); | ||||
|     console.log('max', max) | ||||
| } | ||||
| main(); | ||||
							
								
								
									
										82
									
								
								test/try/client/ws.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								test/try/client/ws.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
| import { WsClient } from '../../src/client/ws/WsClient'; | ||||
| import SuperPromise from 'k8w-super-promise'; | ||||
| import { Func } from 'mocha'; | ||||
|  | ||||
| async function main() { | ||||
|     let client = new WsClient({ | ||||
|         server: 'ws://127.0.0.1:3000', | ||||
|         proto: serviceProto, | ||||
|         onStatusChange: v => { | ||||
|             console.log('StatusChange', v); | ||||
|         }, | ||||
|         // onLostConnection: () => { | ||||
|         //     console.log('连接断开,2秒后重连'); | ||||
|         //     setTimeout(() => { | ||||
|         //         client.connect().catch(() => { }); | ||||
|         //     }, 2000) | ||||
|         // } | ||||
|     }); | ||||
|  | ||||
|     await client.connect(); | ||||
|  | ||||
|     let cancel = client.callApi('Test', { name: 'XXXXXXXXXXXXX' }).catch(e => e); | ||||
|     cancel.cancel(); | ||||
|  | ||||
|     let res = await client.callApi('Test', { name: '小明同学' }).catch(e => e); | ||||
|     console.log('Test Res', res); | ||||
|  | ||||
|     res = await client.callApi('a/b/c/Test', { name: '小明同学' }).catch(e => e); | ||||
|     console.log('Test1 Res', res); | ||||
|  | ||||
|     // setInterval(async () => { | ||||
|     //     try { | ||||
|     //         let res = await client.callApi('Test', { name: '小明同学' }); | ||||
|     //         console.log('收到回复', res); | ||||
|     //     } | ||||
|     //     catch (e) { | ||||
|     //         if (e.info === 'NETWORK_ERR') { | ||||
|     //             return; | ||||
|     //         } | ||||
|     //         console.log('API错误', e) | ||||
|     //     } | ||||
|     // }, 1000); | ||||
|  | ||||
|     // client.listenMsg('Chat', msg => { | ||||
|     //     console.log('收到MSG', msg); | ||||
|     // }); | ||||
|  | ||||
|     // setInterval(() => { | ||||
|     //     try { | ||||
|     //         client.sendMsg('Chat', { | ||||
|     //             channel: 123, | ||||
|     //             userName: '王小明', | ||||
|     //             content: '你好', | ||||
|     //             time: Date.now() | ||||
|     //         }).catch(e => { | ||||
|     //             console.log('SendMsg Failed', e.message) | ||||
|     //         }) | ||||
|     //     } | ||||
|     //     catch{ } | ||||
|     // }, 1000) | ||||
|  | ||||
|     // #region Benchmark | ||||
|     // let maxTime = 0; | ||||
|     // let done = 0; | ||||
|     // let startTime = Date.now(); | ||||
|  | ||||
|     // setTimeout(() => { | ||||
|     //     console.log('done', maxTime, done); | ||||
|     //     process.exit(); | ||||
|     // }, 3000); | ||||
|  | ||||
|     // for (let i = 0; i < 10000; ++i) { | ||||
|     //     client.callApi('Test', { name: '小明同学' }).then(() => { | ||||
|     //         ++done; | ||||
|     //         maxTime = Math.max(maxTime, Date.now() - startTime) | ||||
|     //     }) | ||||
|     // } | ||||
|     // #endregion     | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										27
									
								
								test/try/massive.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								test/try/massive.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { TSRPCClient } from ".."; | ||||
| import { serviceProto, ServiceType } from './proto/serviceProto'; | ||||
|  | ||||
| async function main() { | ||||
|     setInterval(() => { | ||||
|         for (let i = 0; i < 100; ++i) { | ||||
|             let client = new TSRPCClient<ServiceType>({ | ||||
|                 server: 'ws://127.0.0.1:3000', | ||||
|                 proto: serviceProto | ||||
|             }); | ||||
|  | ||||
|             client.connect().then(() => { | ||||
|                 client.callApi('a/b/c/Test1', { name: '小明同学' }).then(v => { | ||||
|                     // console.log('成功', v) | ||||
|                 }).catch(e => { | ||||
|                     console.error('错误', e.message) | ||||
|                 }).then(() => { | ||||
|                     client.disconnect(); | ||||
|                 }); | ||||
|             }).catch(e => { | ||||
|                 console.error('连接错误', e) | ||||
|             }) | ||||
|         } | ||||
|     }, 1000) | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										12
									
								
								test/try/no-res-issue/client/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								test/try/no-res-issue/client/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { TsrpcClient } from "../../.."; | ||||
| import { serviceProto } from '../server/protocols/proto'; | ||||
|  | ||||
| let client = new TsrpcClient({ | ||||
|     proto: serviceProto | ||||
| }); | ||||
|  | ||||
| client.callApi('Test', { name: 'ssss' }).then(v => { | ||||
|     console.log('then', v) | ||||
| }).catch(e => { | ||||
|     console.log('catch', e) | ||||
| }) | ||||
							
								
								
									
										10
									
								
								test/try/no-res-issue/server/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								test/try/no-res-issue/server/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { TsrpcServer } from '../../../index'; | ||||
| import { serviceProto } from './protocols/proto'; | ||||
|  | ||||
| let server = new TsrpcServer({ | ||||
|     proto: serviceProto, | ||||
| }); | ||||
|  | ||||
| server.autoImplementApi('src/api'); | ||||
|  | ||||
| server.start(); | ||||
							
								
								
									
										7
									
								
								test/try/no-res-issue/server/protocols/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								test/try/no-res-issue/server/protocols/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export interface ReqTest { | ||||
|     name: string | ||||
| } | ||||
|  | ||||
| export interface ResTest { | ||||
|     reply: string | ||||
| } | ||||
							
								
								
									
										52
									
								
								test/try/no-res-issue/server/protocols/proto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								test/try/no-res-issue/server/protocols/proto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| import { ServiceProto } from 'tsrpc-proto'; | ||||
| import { ReqTest, ResTest } from './PtlTest' | ||||
|  | ||||
| export interface ServiceType { | ||||
|     req: { | ||||
|         "Test": ReqTest | ||||
|     }, | ||||
|     res: { | ||||
|         "Test": ResTest | ||||
|     }, | ||||
|     msg: { | ||||
|  | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const serviceProto: ServiceProto<ServiceType> = { | ||||
|     "services": [ | ||||
|         { | ||||
|             "id": 0, | ||||
|             "name": "Test", | ||||
|             "type": "api", | ||||
|             "req": "PtlTest/ReqTest", | ||||
|             "res": "PtlTest/ResTest" | ||||
|         } | ||||
|     ], | ||||
|     "types": { | ||||
|         "PtlTest/ReqTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "name", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlTest/ResTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "reply", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										18
									
								
								test/try/no-res-issue/server/src/api/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								test/try/no-res-issue/server/src/api/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { ReqTest, ResTest } from "../../protocols/PtlTest"; | ||||
| import { ApiCall } from "../../../../.."; | ||||
|  | ||||
| export async function ApiTest(call: ApiCall<ReqTest, ResTest>) { | ||||
|     await new Promise(rs => { | ||||
|         let i = 5; | ||||
|         call.logger.log(i); | ||||
|         let interval = setInterval(() => { | ||||
|             call.logger.log(--i); | ||||
|             if (i === 0) { | ||||
|                 clearInterval(interval); | ||||
|                 rs(); | ||||
|             } | ||||
|         }, 1000); | ||||
|     }); | ||||
|  | ||||
|     call.error('asdfasdf', { a: 1, b: 2 }) | ||||
| } | ||||
							
								
								
									
										12
									
								
								test/try/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								test/try/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| { | ||||
|   "name": "try", | ||||
|   "version": "1.0.0", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "version": "1.0.0", | ||||
|       "license": "ISC" | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								test/try/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								test/try/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "name": "try", | ||||
|   "version": "1.0.0", | ||||
|   "description": "", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "test": "echo \"Error: no test specified\" && exit 1" | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "ISC" | ||||
| } | ||||
							
								
								
									
										6
									
								
								test/try/proto/MsgChat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								test/try/proto/MsgChat.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| export interface MsgChat { | ||||
|     channel: number, | ||||
|     userName: string, | ||||
|     content: string, | ||||
|     time: number | ||||
| } | ||||
							
								
								
									
										7
									
								
								test/try/proto/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								test/try/proto/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export interface ReqTest { | ||||
|     name: string | ||||
| }; | ||||
|  | ||||
| export type ResTest = { | ||||
|     reply: string | ||||
| }; | ||||
							
								
								
									
										10
									
								
								test/try/proto/a/b/c/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								test/try/proto/a/b/c/PtlTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { MsgChat } from '../../../MsgChat'; | ||||
|  | ||||
| export interface ReqTest { | ||||
|     name: string | ||||
| }; | ||||
|  | ||||
| export type ResTest = { | ||||
|     reply: string, | ||||
|     chat?: MsgChat | ||||
| }; | ||||
							
								
								
									
										135
									
								
								test/try/proto/serviceProto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								test/try/proto/serviceProto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| import { ServiceProto } from 'tsrpc-proto'; | ||||
| import { ReqTest, ResTest } from './a/b/c/PtlTest' | ||||
| import { MsgChat } from './MsgChat' | ||||
| import { ReqTest as ReqTest_1, ResTest as ResTest_1 } from './PtlTest' | ||||
|  | ||||
| export interface ServiceType { | ||||
|     req: { | ||||
|         "a/b/c/Test": ReqTest, | ||||
|         "Test": ReqTest_1 | ||||
|     }, | ||||
|     res: { | ||||
|         "a/b/c/Test": ResTest, | ||||
|         "Test": ResTest_1 | ||||
|     }, | ||||
|     msg: { | ||||
|         "Chat": MsgChat | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const serviceProto: ServiceProto<ServiceType> = { | ||||
|     "services": [ | ||||
|         { | ||||
|             "id": 0, | ||||
|             "name": "a/b/c/Test", | ||||
|             "type": "api", | ||||
|             "req": "a/b/c/PtlTest/ReqTest", | ||||
|             "res": "a/b/c/PtlTest/ResTest" | ||||
|         }, | ||||
|         { | ||||
|             "id": 1, | ||||
|             "name": "Chat", | ||||
|             "type": "msg", | ||||
|             "msg": "MsgChat/MsgChat" | ||||
|         }, | ||||
|         { | ||||
|             "id": 2, | ||||
|             "name": "Test", | ||||
|             "type": "api", | ||||
|             "req": "PtlTest/ReqTest", | ||||
|             "res": "PtlTest/ResTest" | ||||
|         } | ||||
|     ], | ||||
|     "types": { | ||||
|         "a/b/c/PtlTest/ReqTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "name", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "a/b/c/PtlTest/ResTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "reply", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "chat", | ||||
|                     "type": { | ||||
|                         "type": "Reference", | ||||
|                         "target": "MsgChat/MsgChat" | ||||
|                     }, | ||||
|                     "optional": true | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "MsgChat/MsgChat": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "channel", | ||||
|                     "type": { | ||||
|                         "type": "Number" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 1, | ||||
|                     "name": "userName", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 2, | ||||
|                     "name": "content", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "id": 3, | ||||
|                     "name": "time", | ||||
|                     "type": { | ||||
|                         "type": "Number" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlTest/ReqTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "name", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "PtlTest/ResTest": { | ||||
|             "type": "Interface", | ||||
|             "properties": [ | ||||
|                 { | ||||
|                     "id": 0, | ||||
|                     "name": "reply", | ||||
|                     "type": { | ||||
|                         "type": "String" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										59
									
								
								test/try/proto/typeProto.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								test/try/proto/typeProto.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| { | ||||
|   "MsgChat/MsgChat": { | ||||
|     "type": "Interface", | ||||
|     "properties": [ | ||||
|       { | ||||
|         "id": 0, | ||||
|         "name": "channel", | ||||
|         "type": { | ||||
|           "type": "String" | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         "id": 1, | ||||
|         "name": "userName", | ||||
|         "type": { | ||||
|           "type": "String" | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         "id": 2, | ||||
|         "name": "content", | ||||
|         "type": { | ||||
|           "type": "String" | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         "id": 3, | ||||
|         "name": "time", | ||||
|         "type": { | ||||
|           "type": "Number" | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "PtlTest/ReqTest": { | ||||
|     "type": "Interface", | ||||
|     "properties": [ | ||||
|       { | ||||
|         "id": 0, | ||||
|         "name": "name", | ||||
|         "type": { | ||||
|           "type": "String" | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "PtlTest/ResTest": { | ||||
|     "type": "Interface", | ||||
|     "properties": [ | ||||
|       { | ||||
|         "id": 0, | ||||
|         "name": "reply", | ||||
|         "type": { | ||||
|           "type": "String" | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										21
									
								
								test/try/server/api/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								test/try/server/api/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { TsrpcError } from "tsrpc-proto"; | ||||
| import { ApiCallHttp } from '../../../src/server/http/HttpCall'; | ||||
| import { ReqTest } from "../../proto/PtlTest"; | ||||
| import { ResTest } from '../../proto/a/b/c/PtlTest'; | ||||
|  | ||||
| export async function ApiTest(call: ApiCallHttp<ReqTest, ResTest>) { | ||||
|     if (Math.random() > 0.75) { | ||||
|         call.succ({ | ||||
|             reply: 'Hello, ' + call.req.name | ||||
|         }) | ||||
|     } | ||||
|     else if (Math.random() > 0.5) { | ||||
|         call.error('What the fuck??', { msg: '哈哈哈哈' }) | ||||
|     } | ||||
|     else if (Math.random() > 0.25) { | ||||
|         throw new Error('这应该是InternalERROR') | ||||
|     } | ||||
|     else { | ||||
|         throw new TsrpcError('返回到前台的错误', 'ErrInfo'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										5
									
								
								test/try/server/api/a/b/c/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/try/server/api/a/b/c/ApiTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export async function ApiTest(call: any) { | ||||
|     call.succ({ | ||||
|         reply: 'Api Test1 Succ' | ||||
|     }) | ||||
| } | ||||
							
								
								
									
										22
									
								
								test/try/server/http.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								test/try/server/http.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { serviceProto } from '../proto/serviceProto'; | ||||
| import * as path from "path"; | ||||
| import { TsrpcServer } from '../../index'; | ||||
|  | ||||
| let server = new TsrpcServer({ | ||||
|     proto: serviceProto | ||||
| }); | ||||
|  | ||||
| server.dataFlow.push((data, conn) => { | ||||
|     let httpReq = conn.options.httpReq; | ||||
|     if (httpReq.method === 'GET') { | ||||
|         conn.logger.log('url', httpReq.url); | ||||
|         conn.logger.log('host', httpReq.headers.host); | ||||
|         conn.options.httpRes.end('Hello~~~'); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| }) | ||||
|  | ||||
| server.autoImplementApi(path.resolve(__dirname, 'api')); | ||||
| server.start(); | ||||
							
								
								
									
										18
									
								
								test/try/server/ws.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								test/try/server/ws.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { serviceProto, ServiceType } from '../proto/serviceProto'; | ||||
| import * as path from "path"; | ||||
| import { TsrpcServerWs } from '../../index'; | ||||
|  | ||||
| let server = new TsrpcServerWs<ServiceType>({ | ||||
|     proto: serviceProto | ||||
| }); | ||||
| server.start(); | ||||
|  | ||||
| server.autoImplementApi(path.resolve(__dirname, 'api')); | ||||
| server.listenMsg('Chat', v => { | ||||
|     v.conn.sendMsg('Chat', { | ||||
|         channel: v.msg.channel, | ||||
|         userName: 'SYSTEM', | ||||
|         content: '收到', | ||||
|         time: Date.now() | ||||
|     }) | ||||
| }) | ||||
							
								
								
									
										59
									
								
								test/try/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								test/try/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     /* Basic Options */ | ||||
|     // "incremental": true,                   /* Enable incremental compilation */ | ||||
|     "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ | ||||
|     "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ | ||||
|     // "lib": [],                             /* Specify library files to be included in the compilation. */ | ||||
|     // "allowJs": true,                       /* Allow javascript files to be compiled. */ | ||||
|     // "checkJs": true,                       /* Report errors in .js files. */ | ||||
|     // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ | ||||
|     // "declaration": true,                   /* Generates corresponding '.d.ts' file. */ | ||||
|     // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */ | ||||
|     // "sourceMap": true,                     /* Generates corresponding '.map' file. */ | ||||
|     // "outFile": "./",                       /* Concatenate and emit output to single file. */ | ||||
|     "outDir": "./dist", /* Redirect output structure to the directory. */ | ||||
|     // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ | ||||
|     // "composite": true,                     /* Enable project compilation */ | ||||
|     // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */ | ||||
|     // "removeComments": true,                /* Do not emit comments to output. */ | ||||
|     // "noEmit": true,                        /* Do not emit outputs. */ | ||||
|     // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */ | ||||
|     // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ | ||||
|     // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ | ||||
|     /* Strict Type-Checking Options */ | ||||
|     "strict": true, /* Enable all strict type-checking options. */ | ||||
|     // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */ | ||||
|     // "strictNullChecks": true,              /* Enable strict null checks. */ | ||||
|     // "strictFunctionTypes": true,           /* Enable strict checking of function types. */ | ||||
|     // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ | ||||
|     // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */ | ||||
|     // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */ | ||||
|     // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */ | ||||
|     /* Additional Checks */ | ||||
|     // "noUnusedLocals": true,                /* Report errors on unused locals. */ | ||||
|     // "noUnusedParameters": true,            /* Report errors on unused parameters. */ | ||||
|     // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */ | ||||
|     // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */ | ||||
|     /* Module Resolution Options */ | ||||
|     // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ | ||||
|     // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */ | ||||
|     // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ | ||||
|     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */ | ||||
|     // "typeRoots": [],                       /* List of folders to include type definitions from. */ | ||||
|     // "types": [],                           /* Type declaration files to be included in compilation. */ | ||||
|     // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ | ||||
|     "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ | ||||
|     // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */ | ||||
|     // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */ | ||||
|     /* Source Map Options */ | ||||
|     // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */ | ||||
|     // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */ | ||||
|     // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */ | ||||
|     // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ | ||||
|     /* Experimental Options */ | ||||
|     // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */ | ||||
|     // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */ | ||||
|     "useUnknownInCatchVariables": false | ||||
|   } | ||||
| } | ||||
							
								
								
									
										66
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     /* Basic Options */ | ||||
|     "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ | ||||
|     "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ | ||||
|     // "lib": [ | ||||
|     //   "es5", | ||||
|     //   "dom" | ||||
|     // ], /* Specify library files to be included in the compilation. */ | ||||
|     // "allowJs": true,                       /* Allow javascript files to be compiled. */ | ||||
|     // "checkJs": true,                       /* Report errors in .js files. */ | ||||
|     // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ | ||||
|     "declaration": true, /* Generates corresponding '.d.ts' file. */ | ||||
|     "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ | ||||
|     // "sourceMap": true,                     /* Generates corresponding '.map' file. */ | ||||
|     // "outFile": "./",                       /* Concatenate and emit output to single file. */ | ||||
|     "outDir": "./lib", /* Redirect output structure to the directory. */ | ||||
|     // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ | ||||
|     // "composite": true,                     /* Enable project compilation */ | ||||
|     // "incremental": true,                   /* Enable incremental compilation */ | ||||
|     // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */ | ||||
|     // "removeComments": true,                /* Do not emit comments to output. */ | ||||
|     // "noEmit": true,                        /* Do not emit outputs. */ | ||||
|     // "importHelpers": true, /* Import emit helpers from 'tslib'. */ | ||||
|     // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ | ||||
|     // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ | ||||
|     /* Strict Type-Checking Options */ | ||||
|     "strict": true, /* Enable all strict type-checking options. */ | ||||
|     // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */ | ||||
|     // "strictNullChecks": true,              /* Enable strict null checks. */ | ||||
|     // "strictFunctionTypes": true,           /* Enable strict checking of function types. */ | ||||
|     // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ | ||||
|     // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */ | ||||
|     // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */ | ||||
|     // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */ | ||||
|     /* Additional Checks */ | ||||
|     // "noUnusedLocals": true, /* Report errors on unused locals. */ | ||||
|     // "noUnusedParameters": true,            /* Report errors on unused parameters. */ | ||||
|     // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */ | ||||
|     // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */ | ||||
|     /* Module Resolution Options */ | ||||
|     "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ | ||||
|     // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */ | ||||
|     // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ | ||||
|     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */ | ||||
|     // "typeRoots": [],                       /* List of folders to include type definitions from. */ | ||||
|     // "types": [],                           /* Type declaration files to be included in compilation. */ | ||||
|     // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ | ||||
|     "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ | ||||
|     // "preserveSymlinks": true              /* Do not resolve the real path of symlinks. */ | ||||
|     /* Source Map Options */ | ||||
|     // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */ | ||||
|     // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */ | ||||
|     // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */ | ||||
|     // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ | ||||
|     /* Experimental Options */ | ||||
|     // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */ | ||||
|     // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */ | ||||
|     "useUnknownInCatchVariables": false | ||||
|   }, | ||||
|   "include": [ | ||||
|     "src" | ||||
|     // "test" | ||||
|     // "benchmark" | ||||
|   ] | ||||
| } | ||||
		Reference in New Issue
	
	Block a user