[mod]
This commit is contained in:
parent
da8024fc30
commit
4e027e2e98
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,11 +1,3 @@
|
|||||||
dist
|
|
||||||
node_modules
|
node_modules
|
||||||
yarn.lock
|
dist
|
||||||
logs
|
.DS_STORE
|
||||||
.rpt2_cache
|
|
||||||
.nyc_output
|
|
||||||
coverage
|
|
||||||
docs
|
|
||||||
temp
|
|
||||||
lib
|
|
||||||
.ds_store
|
|
17
.mocharc.js
17
.mocharc.js
@ -1,22 +1,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
require: [
|
require: [
|
||||||
'ts-node/register',
|
'ts-node/register',
|
||||||
'./test/Base.ts'
|
|
||||||
],
|
],
|
||||||
exit: true,
|
|
||||||
timeout: 999999,
|
timeout: 999999,
|
||||||
'preserve-symlinks': true,
|
exit: true,
|
||||||
spec: [
|
spec: [
|
||||||
'./test/cases/http.test.ts',
|
'./test/**/*.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,
|
'preserve-symlinks': true
|
||||||
|
|
||||||
// 'expose-gc': true,
|
|
||||||
// fgrep: 'throw type error in server'
|
|
||||||
}
|
}
|
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
// 使用 IntelliSense 了解相关属性。
|
|
||||||
// 悬停以查看现有属性的描述。
|
|
||||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
161
CHANGELOG.md
161
CHANGELOG.md
@ -1,161 +0,0 @@
|
|||||||
# 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
|
|
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
FROM node
|
||||||
|
|
||||||
|
# 使用淘宝 NPM 镜像(国内机器构建推荐启用)
|
||||||
|
# RUN npm config set registry https://registry.npm.taobao.org/
|
||||||
|
|
||||||
|
# npm install
|
||||||
|
ADD package*.json /src/
|
||||||
|
WORKDIR /src
|
||||||
|
RUN npm i
|
||||||
|
|
||||||
|
# build
|
||||||
|
ADD . /src
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# clean
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
|
# move
|
||||||
|
RUN rm -rf /app \
|
||||||
|
&& mv dist /app \
|
||||||
|
&& mv node_modules /app/ \
|
||||||
|
&& rm -rf /src
|
||||||
|
|
||||||
|
# ENV
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
CMD node index.js
|
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
|||||||
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.
|
|
79
README.md
79
README.md
@ -1,70 +1,31 @@
|
|||||||
# TSRPC
|
# TSRPC Server
|
||||||
|
|
||||||
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
|
## Usage
|
||||||
|
### Local dev server
|
||||||
|
|
||||||
### Define Protocol (Shared)
|
Dev server would restart automatically when code changed.
|
||||||
```ts
|
|
||||||
export interface ReqHello {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResHello {
|
```
|
||||||
reply: string;
|
npm run dev
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Implement API (Server)
|
### Build
|
||||||
```ts
|
```
|
||||||
import { ApiCall } from "tsrpc";
|
npm run build
|
||||||
|
|
||||||
export async function ApiHello(call: ApiCall<ReqHello, ResHello>) {
|
|
||||||
call.succ({
|
|
||||||
reply: 'Hello, ' + call.req.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Call API (Client)
|
### Generate API document
|
||||||
```ts
|
|
||||||
let ret = await client.callApi('Hello', {
|
Generate API document in swagger/openapi and markdown format.
|
||||||
name: 'World'
|
|
||||||
});
|
```shell
|
||||||
|
npm run doc
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
### Run unit Test
|
||||||
|
Execute `npm run dev` first, then execute:
|
||||||
|
```
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
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).
|
|
@ -1,345 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// . . .
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
export const benchmarkConfig = {
|
|
||||||
/** 压测使用的APIServer */
|
|
||||||
server: 'http://127.0.0.1:3000',
|
|
||||||
|
|
||||||
/** 一共运行几次压测事务 */
|
|
||||||
total: 200000,
|
|
||||||
/** 同时并发的请求数量 */
|
|
||||||
concurrency: 100,
|
|
||||||
/** API请求的超时时间(超时将断开HTTP连接,释放资源,前端默认为10) */
|
|
||||||
timeout: 10000,
|
|
||||||
/** 是否将错误的详情日志打印到Log */
|
|
||||||
showError: false
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
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();
|
|
@ -1,285 +0,0 @@
|
|||||||
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' });
|
|
@ -1,283 +0,0 @@
|
|||||||
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' });
|
|
@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
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();
|
|
@ -1,26 +0,0 @@
|
|||||||
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();
|
|
@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"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. */
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
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();
|
|
5015
package-lock.json
generated
5015
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@ -1,77 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "tsrpc",
|
"name": "tsrpc_test-.",
|
||||||
"version": "3.3.1-dev.0",
|
"version": "0.1.0",
|
||||||
"description": "A TypeScript RPC Framework, with runtime type checking and built-in serialization, support both HTTP and WebSocket.",
|
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"exports": {
|
"private": true,
|
||||||
"require": "./index.js",
|
|
||||||
"import": "./index.mjs"
|
|
||||||
},
|
|
||||||
"typings": "index.d.ts",
|
|
||||||
"directories": {
|
|
||||||
"doc": "docs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npx mocha",
|
"dev": "tsrpc-cli dev",
|
||||||
"genTestProto": "npx tsrpc-cli@latest proto --input test/proto --output test/proto/serviceProto.ts",
|
"build": "tsrpc-cli build",
|
||||||
"coverage": "nyc mocha test/**/*.test.ts && start coverage\\index.html",
|
"doc": "tsrpc-cli doc",
|
||||||
"build": "npm run build:js && npm run build:dts && npm run build:doc && node scripts/postBuild && cp package.json LICENSE README.md dist/",
|
"test": "mocha test/**/*.test.ts",
|
||||||
"build:js": "rm -rf dist && npx rollup -c",
|
"proto": "tsrpc-cli proto",
|
||||||
"build:dts": "rm -rf lib && npx tsc && npx api-extractor run --local --verbose && rm -rf lib",
|
"sync": "tsrpc-cli sync",
|
||||||
"build:doc": "rm -rf docs/api && npx api-documenter markdown --input temp --output docs/api"
|
"api": "tsrpc-cli 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": {
|
"devDependencies": {
|
||||||
"@microsoft/api-documenter": "^7.17.9",
|
|
||||||
"@microsoft/api-extractor": "^7.22.2",
|
|
||||||
"@types/chai": "^4.3.1",
|
|
||||||
"@types/mocha": "^8.2.3",
|
"@types/mocha": "^8.2.3",
|
||||||
"@types/node": "^15.14.9",
|
"@types/node": "^15.14.9",
|
||||||
"@types/uuid": "^8.3.4",
|
|
||||||
"chai": "^4.3.6",
|
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"nyc": "^15.1.0",
|
"onchange": "^7.1.0",
|
||||||
"rollup": "^2.70.2",
|
|
||||||
"rollup-plugin-typescript2": "^0.31.2",
|
|
||||||
"ts-node": "^10.7.0",
|
"ts-node": "^10.7.0",
|
||||||
"typescript": "^4.6.3"
|
"tsrpc-cli": "^2.4.3",
|
||||||
|
"typescript": "^4.6.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/ws": "^7.4.7",
|
"tsrpc": "^3.3.0"
|
||||||
"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
17
res/mongodb-polyfill.d.ts
vendored
@ -1,17 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,7 +0,0 @@
|
|||||||
module.exports = `/*!
|
|
||||||
* TSRPC v${require('../package.json').version}
|
|
||||||
* -----------------------------------------
|
|
||||||
* Copyright (c) King Wang.
|
|
||||||
* MIT License
|
|
||||||
* https://github.com/k8w/tsrpc
|
|
||||||
*/`
|
|
@ -1,26 +0,0 @@
|
|||||||
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');
|
|
26
src/api/ApiSend.ts
Normal file
26
src/api/ApiSend.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { ApiCall } from "tsrpc";
|
||||||
|
import { server } from "..";
|
||||||
|
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
|
||||||
|
|
||||||
|
// This is a demo code file
|
||||||
|
// Feel free to delete it
|
||||||
|
|
||||||
|
export async function ApiSend(call: ApiCall<ReqSend, ResSend>) {
|
||||||
|
// Error
|
||||||
|
if (call.req.content.length === 0) {
|
||||||
|
call.error('Content is empty')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success
|
||||||
|
let time = new Date();
|
||||||
|
call.succ({
|
||||||
|
time: time
|
||||||
|
});
|
||||||
|
|
||||||
|
// Broadcast
|
||||||
|
server.broadcastMsg('Chat', {
|
||||||
|
content: call.req.content,
|
||||||
|
time: time
|
||||||
|
})
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
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({});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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 {
|
|
||||||
|
|
||||||
}
|
|
48
src/index.ts
48
src/index.ts
@ -1,27 +1,25 @@
|
|||||||
import 'k8w-extend-native';
|
import * as path from "path";
|
||||||
|
import { WsServer } from "tsrpc";
|
||||||
|
import { serviceProto } from './shared/protocols/serviceProto';
|
||||||
|
|
||||||
// Common
|
// Create the Server
|
||||||
export * from 'tsrpc-base-client';
|
export const server = new WsServer(serviceProto, {
|
||||||
export * from 'tsrpc-proto';
|
port: 3000,
|
||||||
export * from './client/http/HttpClient';
|
// Remove this to use binary mode (remove from the client too)
|
||||||
export * from './client/ws/WsClient';
|
json: true
|
||||||
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';
|
|
||||||
|
|
||||||
|
// Initialize before server start
|
||||||
|
async function init() {
|
||||||
|
await server.autoImplementApi(path.resolve(__dirname, 'api'));
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// Prepare something... (e.g. connect the db)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Entry function
|
||||||
|
async function main() {
|
||||||
|
await init();
|
||||||
|
await server.start();
|
||||||
|
}
|
||||||
|
main();
|
@ -1,22 +0,0 @@
|
|||||||
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:/, '') : '';
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
/** Version of TSRPC */
|
|
||||||
export const TSRPC_VERSION = '__TSRPC_VERSION__';
|
|
@ -1,215 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
@ -1,30 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,182 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
@ -1,937 +0,0 @@
|
|||||||
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',
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,241 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,141 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,241 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
7
src/shared/protocols/MsgChat.ts
Normal file
7
src/shared/protocols/MsgChat.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// This is a demo code file
|
||||||
|
// Feel free to delete it
|
||||||
|
|
||||||
|
export interface MsgChat {
|
||||||
|
content: string,
|
||||||
|
time: Date
|
||||||
|
}
|
10
src/shared/protocols/PtlSend.ts
Normal file
10
src/shared/protocols/PtlSend.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// This is a demo code file
|
||||||
|
// Feel free to delete it
|
||||||
|
|
||||||
|
export interface ReqSend {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResSend {
|
||||||
|
time: Date
|
||||||
|
}
|
15
src/shared/protocols/base.ts
Normal file
15
src/shared/protocols/base.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export interface BaseRequest {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseResponse {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseConf {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseMessage {
|
||||||
|
|
||||||
|
}
|
78
src/shared/protocols/serviceProto.ts
Normal file
78
src/shared/protocols/serviceProto.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { ServiceProto } from 'tsrpc-proto';
|
||||||
|
import { MsgChat } from './MsgChat';
|
||||||
|
import { ReqSend, ResSend } from './PtlSend';
|
||||||
|
|
||||||
|
// This is a demo service proto file (auto generated)
|
||||||
|
// Feel free to delete it
|
||||||
|
|
||||||
|
export interface ServiceType {
|
||||||
|
api: {
|
||||||
|
"Send": {
|
||||||
|
req: ReqSend,
|
||||||
|
res: ResSend
|
||||||
|
}
|
||||||
|
},
|
||||||
|
msg: {
|
||||||
|
"Chat": MsgChat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serviceProto: ServiceProto<ServiceType> = {
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"name": "Chat",
|
||||||
|
"type": "msg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Send",
|
||||||
|
"type": "api"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"types": {
|
||||||
|
"MsgChat/MsgChat": {
|
||||||
|
"type": "Interface",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"name": "content",
|
||||||
|
"type": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "time",
|
||||||
|
"type": {
|
||||||
|
"type": "Date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PtlSend/ReqSend": {
|
||||||
|
"type": "Interface",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"name": "content",
|
||||||
|
"type": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PtlSend/ResSend": {
|
||||||
|
"type": "Interface",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"name": "time",
|
||||||
|
"type": {
|
||||||
|
"type": "Date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -1 +0,0 @@
|
|||||||
import 'k8w-extend-native';
|
|
@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
40
test/api/ApiSend.test.ts
Normal file
40
test/api/ApiSend.test.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import { TsrpcError, WsClient } from 'tsrpc';
|
||||||
|
import { serviceProto } from '../../src/shared/protocols/serviceProto';
|
||||||
|
|
||||||
|
// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
|
||||||
|
// 2. EXECUTE `npm test` TO START UNIT TEST
|
||||||
|
|
||||||
|
describe('ApiSend', function () {
|
||||||
|
let client = new WsClient(serviceProto, {
|
||||||
|
server: 'ws://127.0.0.1:3000',
|
||||||
|
json: true,
|
||||||
|
logger: console
|
||||||
|
});
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
let res = await client.connect();
|
||||||
|
assert.strictEqual(res.isSucc, true, 'Failed to connect to server, have you executed `npm run dev` already?');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Success', async function () {
|
||||||
|
let ret = await client.callApi('Send', {
|
||||||
|
content: 'Test'
|
||||||
|
});
|
||||||
|
assert.ok(ret.isSucc)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check content is empty', async function () {
|
||||||
|
let ret = await client.callApi('Send', {
|
||||||
|
content: ''
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(ret, {
|
||||||
|
isSucc: false,
|
||||||
|
err: new TsrpcError('Content is empty')
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await client.disconnect();
|
||||||
|
})
|
||||||
|
})
|
@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,907 +0,0 @@
|
|||||||
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();
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,997 +0,0 @@
|
|||||||
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();
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,396 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,414 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,410 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
})
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,6 +0,0 @@
|
|||||||
export interface MsgChat {
|
|
||||||
channel: number,
|
|
||||||
userName: string,
|
|
||||||
content: string,
|
|
||||||
time: number
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
import { ObjectId } from "mongodb";
|
|
||||||
|
|
||||||
export interface ReqObjId {
|
|
||||||
id1: ObjectId;
|
|
||||||
buf?: Uint8Array,
|
|
||||||
date?: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResObjId {
|
|
||||||
id2: ObjectId;
|
|
||||||
buf?: Uint8Array,
|
|
||||||
date?: Date
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export interface ReqTest {
|
|
||||||
name: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResTest = {
|
|
||||||
reply: string
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import { MsgChat } from '../../../MsgChat';
|
|
||||||
|
|
||||||
export interface ReqTest {
|
|
||||||
name: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResTest = {
|
|
||||||
reply: string,
|
|
||||||
chat?: MsgChat
|
|
||||||
};
|
|
@ -1,203 +0,0 @@
|
|||||||
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
18
test/test.ts
@ -1,18 +0,0 @@
|
|||||||
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();
|
|
@ -1,22 +0,0 @@
|
|||||||
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();
|
|
@ -1,82 +0,0 @@
|
|||||||
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();
|
|
@ -1,27 +0,0 @@
|
|||||||
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();
|
|
@ -1,12 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
@ -1,10 +0,0 @@
|
|||||||
import { TsrpcServer } from '../../../index';
|
|
||||||
import { serviceProto } from './protocols/proto';
|
|
||||||
|
|
||||||
let server = new TsrpcServer({
|
|
||||||
proto: serviceProto,
|
|
||||||
});
|
|
||||||
|
|
||||||
server.autoImplementApi('src/api');
|
|
||||||
|
|
||||||
server.start();
|
|
@ -1,7 +0,0 @@
|
|||||||
export interface ReqTest {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResTest {
|
|
||||||
reply: string
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,18 +0,0 @@
|
|||||||
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
12
test/try/package-lock.json
generated
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "try",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "try",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC"
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
export interface MsgChat {
|
|
||||||
channel: number,
|
|
||||||
userName: string,
|
|
||||||
content: string,
|
|
||||||
time: number
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export interface ReqTest {
|
|
||||||
name: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResTest = {
|
|
||||||
reply: string
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import { MsgChat } from '../../../MsgChat';
|
|
||||||
|
|
||||||
export interface ReqTest {
|
|
||||||
name: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResTest = {
|
|
||||||
reply: string,
|
|
||||||
chat?: MsgChat
|
|
||||||
};
|
|
@ -1,135 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export async function ApiTest(call: any) {
|
|
||||||
call.succ({
|
|
||||||
reply: 'Api Test1 Succ'
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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();
|
|
@ -1,18 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
19
test/tsconfig.json
Normal file
19
test/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"es2018"
|
||||||
|
],
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2018",
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
".",
|
||||||
|
"../src"
|
||||||
|
]
|
||||||
|
}
|
@ -1,66 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
"lib": [
|
||||||
"target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
"es2018"
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
],
|
||||||
// "lib": [
|
"module": "commonjs",
|
||||||
// "es5",
|
"target": "es2018",
|
||||||
// "dom"
|
"outDir": "dist",
|
||||||
// ], /* Specify library files to be included in the compilation. */
|
"strict": true,
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"esModuleInterop": true,
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
"skipLibCheck": true,
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
"forceConsistentCasingInFileNames": true,
|
||||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
"moduleResolution": "node"
|
||||||
"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": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
// "test"
|
|
||||||
// "benchmark"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
39
tsrpc.config.ts
Normal file
39
tsrpc.config.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { CodeTemplate, TsrpcConfig } from 'tsrpc-cli';
|
||||||
|
|
||||||
|
const tsrpcConf: TsrpcConfig = {
|
||||||
|
// Generate ServiceProto
|
||||||
|
proto: [
|
||||||
|
{
|
||||||
|
ptlDir: 'src/shared/protocols', // Protocol dir
|
||||||
|
output: 'src/shared/protocols/serviceProto.ts', // Path for generated ServiceProto
|
||||||
|
apiDir: 'src/api', // API dir
|
||||||
|
docDir: 'docs', // API documents dir
|
||||||
|
ptlTemplate: CodeTemplate.getExtendedPtl(),
|
||||||
|
// msgTemplate: CodeTemplate.getExtendedMsg(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// Sync shared code
|
||||||
|
sync: [
|
||||||
|
// {
|
||||||
|
// from: 'src/shared',
|
||||||
|
// to: '../frontend/src/shared',
|
||||||
|
// type: 'symlink' // Change this to 'copy' if your environment not support symlink
|
||||||
|
// }
|
||||||
|
],
|
||||||
|
// Dev server
|
||||||
|
dev: {
|
||||||
|
autoProto: true, // Auto regenerate proto
|
||||||
|
autoSync: true, // Auto sync when file changed
|
||||||
|
autoApi: true, // Auto create API when ServiceProto updated
|
||||||
|
watch: 'src', // Restart dev server when these files changed
|
||||||
|
entry: 'src/index.ts', // Dev server command: node -r ts-node/register {entry}
|
||||||
|
},
|
||||||
|
// Build config
|
||||||
|
build: {
|
||||||
|
autoProto: true, // Auto generate proto before build
|
||||||
|
autoSync: true, // Auto sync before build
|
||||||
|
autoApi: true, // Auto generate API before build
|
||||||
|
outDir: 'dist', // Clean this dir before build
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default tsrpcConf;
|
Loading…
Reference in New Issue
Block a user