[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