[add] first
This commit is contained in:
commit
da8024fc30
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"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user