[add] first

This commit is contained in:
建喵 2022-04-29 15:25:10 +08:00
commit da8024fc30
87 changed files with 16892 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
dist
node_modules
yarn.lock
logs
.rpt2_cache
.nyc_output
coverage
docs
temp
lib
.ds_store

22
.mocharc.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
require: [
'ts-node/register',
'./test/Base.ts'
],
exit: true,
timeout: 999999,
'preserve-symlinks': true,
spec: [
'./test/cases/http.test.ts',
'./test/cases/httpJSON.test.ts',
'./test/cases/ws.test.ts',
'./test/cases/wsJSON.test.ts',
'./test/cases/inner.test.ts',
'./test/cases/inputJSON.test.ts',
'./test/cases/inputBuffer.test.ts',
],
// parallel: false,
// 'expose-gc': true,
// fgrep: 'throw type error in server'
}

34
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,34 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "mocha current file",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
"${file}"
],
"internalConsoleOptions": "openOnSessionStart",
"cwd": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "ts-node current file",
"protocol": "inspector",
"args": [
"${relativeFile}"
],
"cwd": "${workspaceRoot}",
"runtimeArgs": [
"-r",
"ts-node/register"
],
"internalConsoleOptions": "openOnSessionStart"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

161
CHANGELOG.md Normal file
View File

@ -0,0 +1,161 @@
# CHANGELOG
## [3.3.1-dev.0] - 2022-04-27
### Fixed
- `HttpConnection.status` not correct when request aborted by client
## [3.3.0] - 2022-04-15
### Added
- Builtin heartbeat support
- New options `logLevel`
### Fixed
- Add response header `Content-Type: application/json; charset=utf-8` for JSON mode under HttpServer, to fix the decoding issue in Chrome dev tools.
## [3.2.5] - 2022-04-12
### Added
- New server options `corsMaxAge` to optimized preflight requests, default value is 3600.
### Fixed
- `NonNullable` cannot be encoded and decoded when as a property in interface
## [3.2.3] - 2022-03-25
### Added
- Print debug-level log when "pre flow" is canceled
### Changed
- Log `[ResErr]` renamed to `[ApiErr]` to consist with client's.
- Log `ApiRes` and `ApiErr` once they are ready to send, instead of after send them.
### Fixed
- When `preSendDataFlow` return undefined, do not send "Internal Server Error".
- Remove some unused code.
## [3.2.2] - 2022-03-22
### Fixed
- `postDisconnectFlow` not executed when `disconnect()` manually
## [3.2.1] - 2022-03-21
### Added
- `preRecvDataFlow` add param `serviceName`
- Support change `dataType` in `postConnectFlow`
### Fixed
- Remark text error
## [3.2.0] - 2022-02-26
### Added
- Support using `keyof`
- Support type alias and `keyof` in `Pick` and `Omit`
- Support `Pick<Intersection>` and `Omit<Intersection>`
- Support `interface` extends Mapped Type, like `Pick` `Omit`
- Support `Pick<XXX, keyof XXX>`
- Support `Pick<XXX, TypeReference>`
- Support `Pick<UnionType>` and `Pick<IntersectionType>`, the same to `Omit`
- Support reference enum value as literal type,like:
```ts
export enum Types {
Type1,
Type2
}
export interface Obj {
type: Types.Type1,
value: string
}
```
### Changed
- `SchemaType` switched to class
## [3.1.9] - 2022-01-12
### Added
- `mongodb-polyfill.d.ts` to fixed mongodb type bug.
## [3.1.6] - 2021-12-29
### Changed
- Return request type error detail when using JSON
## [3.1.5] - 2021-12-23
### Fixed
- Optimize aliyun FC support of `server.inputJSON`
## [3.1.4] - 2021-12-18
### Added
- `WsServer` now support client use `buffer` as transfering format when server set `json: true`
### Fixed
- Type error when disable `skipLibChecks`
- Cannot resolve JSON when `headers` is `application/json; charset=utf-8`
- Cannot resolve serviceName when there is query string in the URL
## [3.1.3] - 2021-12-04
### Added
- `conn.listenMsg`
### Fixed
- Do not `broadcastMsg` when `conns.length` is `0`
## [3.1.2] - 2021-11-17
### Added
- `server.inputJSON` and `server.inputBuffer`
- Add new dataType `json`
## [3.1.1] - 2021-11-09
### Added
- HTTP Text 传输模式下,区分 HTTP 状态码返回,不再统一返回 200
## [3.1.0] - 2021-11-08
### Added
- WebSocket 支持 JSON 格式传输
- JSON 格式传输支持 `ArrayBuffer`、`Date`、`ObjectId`,自动根据协议编解码为 `string`
### Changed
- `jsonEnabled` -> `json`
## [3.0.14] - 2021-10-25
### Added
- 增加 `server.autoImplementApi` 第二个参数 `delay`,用于延迟自动协议注册,加快冷启动速度。
## [3.0.13] - 2021-10-22
### Added
- 增加 `server.callApi` 的支持,以更方便的适配 Serverless 云函数等自定义传输场景。
## [3.0.12] - 2021-10-22
### Fixed
- 修复 `WsServer` 客户端断开连接后,日志显示的 `ActiveConn` 总是比实际多 1 的 BUG
## [3.0.11] - 2021-10-18
### Added
- 增加对 `mongodb/ObjectId` 的支持
## [3.0.10] - 2021-10-13
### Changed
- `BaseConnection` 泛型参数默认为 `any`,便于扩展类型
- `HttpClient` and `WsClient` no longer have default type param
## [3.0.9] - 2021-10-06
### Changed
- `strictNullChecks` 默认改为 `false`
## [3.0.8] - 2021-10-06
### Added
- Optimize log level
## [3.0.7] - 2021-10-06
### Added
- Optimize log color
## [3.0.6] - 2021-09-30
### Added
- "Server started at ..." 前增加 "ERRORX API registered failed."
### Changed
- `HttpServer.onInputBufferError` 改为 `call.error('InputBufferError')`
- 替换 `colors``chalk`
## [3.0.5] - 2021-08-14
### Added
- Optimize log for `sendMsg` and `broadcastMsg`
- Return `Internal Server Error` when `SendReturnErr` occured
### Changed
- Remove error `API not return anything`
- handler of `client.listenMsg` changed to `(msg, msgName, client)=>void`
### Fixed
- NodeJS 12 compability issue (`Uint8Array` and `Buffer` is not treated samely)
## [3.0.3] - 2021-06-27
### Added
- `server.listenMsg` would return `handler` that passed in

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) King Wang. https://github.com/k8w
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# TSRPC
EN / [中文](https://tsrpc.cn/docs/introduction.html)
A TypeScript RPC framework with runtime type checking and binary serialization.
Official site: https://tsrpc.cn (English version is on the way)
## Features
- Runtime type checking
- Binary serialization
- Pure TypeScript, without any decorater or other language
- HTTP / WebSocket / and more protocols...
- Optional backward-compatibility to JSON
- High performance and reliable, verified by services over 100,000,000 users
## Create Full-stack Project
```
npx create-tsrpc-app@latest
```
## Usage
### Define Protocol (Shared)
```ts
export interface ReqHello {
name: string;
}
export interface ResHello {
reply: string;
}
```
### Implement API (Server)
```ts
import { ApiCall } from "tsrpc";
export async function ApiHello(call: ApiCall<ReqHello, ResHello>) {
call.succ({
reply: 'Hello, ' + call.req.name
});
}
```
### Call API (Client)
```ts
let ret = await client.callApi('Hello', {
name: 'World'
});
```
## Examples
https://github.com/k8w/tsrpc-examples
## Serialization Algorithm
The best TypeScript serialization algorithm ever.
Without any 3rd-party IDL language (like protobuf), it is fully based on TypeScript source file. Define the protocols directly by your code.
This is powered by [TSBuffer](https://github.com/tsbuffer), which is going to be open-source.
TypeScript has the best type system, with some unique advanced features like union type, intersection type, mapped type, etc.
TSBuffer may be the only serialization algorithm that support them all.
## API Reference
See [API Reference](./docs/api/tsrpc.md).

345
api-extractor.json Normal file
View File

@ -0,0 +1,345 @@
/**
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
/**
* Optionally specifies another JSON config file that this file extends from. This provides a way for
* standard settings to be shared across multiple projects.
*
* If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
* the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be
* resolved using NodeJS require().
*
* SUPPORTED TOKENS: none
* DEFAULT VALUE: ""
*/
// "extends": "./shared/api-extractor-base.json"
// "extends": "my-package/include/api-extractor-base.json"
/**
* Determines the "<projectFolder>" token that can be used with other config file settings. The project folder
* typically contains the tsconfig.json and package.json config files, but the path is user-defined.
*
* The path is resolved relative to the folder of the config file that contains the setting.
*
* The default value for "projectFolder" is the token "<lookup>", which means the folder is determined by traversing
* parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
* that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error
* will be reported.
*
* SUPPORTED TOKENS: <lookup>
* DEFAULT VALUE: "<lookup>"
*/
// "projectFolder": "..",
/**
* (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor
* analyzes the symbols exported by this module.
*
* The file extension must be ".d.ts" and not ".ts".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
*/
"mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",
/**
* A list of NPM package names whose exports should be treated as part of this package.
*
* For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
* and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part
* of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
* imports library2. To avoid this, we can specify:
*
* "bundledPackages": [ "library2" ],
*
* This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
* local files for library1.
*/
"bundledPackages": [],
/**
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
*/
"compiler": {
/**
* Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* Note: This setting will be ignored if "overrideTsconfig" is used.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
*/
// "tsconfigFilePath": "<projectFolder>/tsconfig.json",
/**
* Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
* The object must conform to the TypeScript tsconfig schema:
*
* http://json.schemastore.org/tsconfig
*
* If omitted, then the tsconfig.json file will be read from the "projectFolder".
*
* DEFAULT VALUE: no overrideTsconfig section
*/
// "overrideTsconfig": {
// . . .
// }
/**
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
* and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
* dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
* for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
*
* DEFAULT VALUE: false
*/
// "skipLibCheck": true,
},
/**
* Configures how the API report file (*.api.md) will be generated.
*/
"apiReport": {
/**
* (REQUIRED) Whether to generate an API report.
*/
"enabled": false
/**
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
* a full file path.
*
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
*
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
*/
// "reportFileName": "<unscopedPackageName>.api.md",
/**
* Specifies the folder where the API report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy,
* e.g. for an API review.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/etc/"
*/
// "reportFolder": "<projectFolder>/etc/",
/**
* Specifies the folder where the temporary report file is written. The file name portion is determined by
* the "reportFileName" setting.
*
* After the temporary file is written to disk, it is compared with the file in the "reportFolder".
* If they are different, a production build will fail.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
// "reportTempFolder": "<projectFolder>/temp/"
},
/**
* Configures how the doc model file (*.api.json) will be generated.
*/
"docModel": {
/**
* (REQUIRED) Whether to generate a doc model file.
*/
"enabled": true
/**
* The output path for the doc model file. The file extension should be ".api.json".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
*/
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.api.json"
},
/**
* Configures how the .d.ts rollup file will be generated.
*/
"dtsRollup": {
/**
* (REQUIRED) Whether to generate the .d.ts rollup file.
*/
"enabled": true,
/**
* Specifies the output path for a .d.ts rollup file to be generated without any trimming.
* This file will include all declarations that are exported by the main entry point.
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
*/
"untrimmedFilePath": "",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
* This file will include only declarations that are marked as "@public" or "@beta".
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-beta.d.ts",
/**
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
* This file will include only declarations that are marked as "@public".
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
"publicTrimmedFilePath": "<projectFolder>/dist/index.d.ts",
/**
* When a declaration is trimmed, by default it will be replaced by a code comment such as
* "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the
* declaration completely.
*
* DEFAULT VALUE: false
*/
"omitTrimmingComments": true
},
/**
* Configures how the tsdoc-metadata.json file will be generated.
*/
"tsdocMetadata": {
/**
* Whether to generate the tsdoc-metadata.json file.
*
* DEFAULT VALUE: true
*/
"enabled": false
/**
* Specifies where the TSDoc metadata file should be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
* The default value is "<lookup>", which causes the path to be automatically inferred from the "tsdocMetadata",
* "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup
* falls back to "tsdoc-metadata.json" in the package folder.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<lookup>"
*/
// "tsdocMetadataFilePath": "<projectFolder>/dist/tsdoc-metadata.json"
},
/**
* Specifies what type of newlines API Extractor should use when writing output files. By default, the output files
* will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead.
* To use the OS's default newline kind, specify "os".
*
* DEFAULT VALUE: "crlf"
*/
// "newlineKind": "crlf",
/**
* Configures how API Extractor reports error and warning messages produced during analysis.
*
* There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages.
*/
"messages": {
/**
* Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
* the input .d.ts files.
*
* TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"compilerMessageReporting": {
/**
* Configures the default routing for messages that don't match an explicit rule in this table.
*/
"default": {
/**
* Specifies whether the message should be written to the the tool's output log. Note that
* the "addToApiReportFile" property may supersede this option.
*
* Possible values: "error", "warning", "none"
*
* Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail
* and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes
* the "--local" option), the warning is displayed but the build will not fail.
*
* DEFAULT VALUE: "warning"
*/
"logLevel": "warning"
/**
* When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md),
* then the message will be written inside that file; otherwise, the message is instead logged according to
* the "logLevel" option.
*
* DEFAULT VALUE: false
*/
// "addToApiReportFile": false
}
// "TS2551": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by API Extractor during its analysis.
*
* API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag"
*
* DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
*/
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
},
"ae-missing-release-tag": {
"logLevel": "none"
}
// "ae-extra-release-tag": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
},
/**
* Configures handling of messages reported by the TSDoc parser when analyzing code comments.
*
* TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text"
*
* DEFAULT VALUE: A single "default" entry with logLevel=warning.
*/
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
// "addToApiReportFile": false
},
"tsdoc-param-tag-missing-hyphen": {
"logLevel": "none"
}
// "tsdoc-link-tag-unescaped-text": {
// "logLevel": "warning",
// "addToApiReportFile": true
// },
//
// . . .
}
}
}

View File

@ -0,0 +1,13 @@
export const benchmarkConfig = {
/** 压测使用的APIServer */
server: 'http://127.0.0.1:3000',
/** 一共运行几次压测事务 */
total: 200000,
/** 同时并发的请求数量 */
concurrency: 100,
/** API请求的超时时间超时将断开HTTP连接释放资源前端默认为10 */
timeout: 10000,
/** 是否将错误的详情日志打印到Log */
showError: false
}

13
benchmark/http.ts Normal file
View File

@ -0,0 +1,13 @@
import { benchmarkConfig } from './config/BenchmarkConfig';
import { HttpRunner } from './models/HTTPRunner';
const req = {
a: 123456,
b: 'Hello, World!',
c: true,
d: new Uint8Array(100000)
}
new HttpRunner(async function () {
await this.callApi('Test', req);
}, benchmarkConfig).start();

View File

@ -0,0 +1,285 @@
import 'colors';
import * as http from "http";
import * as https from "https";
import 'k8w-extend-native';
import { TsrpcError, TsrpcErrorType } from "tsrpc-proto";
import { HttpClient } from '../../src/client/http/HttpClient';
import { benchmarkConfig } from "../config/BenchmarkConfig";
import { serviceProto } from '../protocols/proto';
export interface HttpRunnerConfig {
total: number;
concurrency: number;
showError?: boolean;
}
export class HttpRunner {
private _config: HttpRunnerConfig;
// 执行单个事务的方法
private _single: (this: HttpRunner) => Promise<void>;
// 执行进度信息
private _progress?: {
startTime: number,
lastSuccTime?: number,
started: number,
finished: number,
succ: number,
fail: number
};
constructor(single: HttpRunner['_single'], config: HttpRunnerConfig) {
this._single = single.bind(this);
this._config = config;
}
start() {
this._progress = {
startTime: Date.now(),
started: 0,
finished: 0,
succ: 0,
fail: 0
}
// 启动并发
for (let i = 0; i < this._config.concurrency; ++i) {
this._doTrans();
}
console.log('Benchmark start!');
this._startReport();
}
private _doTrans() {
if (this._isStoped || !this._progress) {
return;
}
if (this._progress.started < this._config.total) {
++this._progress.started;
let startTime = Date.now();
this._single().then(v => {
++this._progress!.succ;
this._progress!.lastSuccTime = Date.now();
}).catch(e => {
++this._progress!.fail;
if (this._config.showError) {
console.error('[Error]', e.message);
}
}).then(() => {
++this._progress!.finished;
if (this._progress!.finished === this._config.total) {
this._finish();
}
else {
this._doTrans();
}
})
}
}
private _reportInterval?: NodeJS.Timeout;
private _startReport() {
this._reportInterval = setInterval(() => {
this._report();
}, 1000)
}
private _isStoped = false;
stop() {
this._isStoped = true;
}
private _finish() {
if (!this._progress) {
return;
}
this._reportInterval && clearInterval(this._reportInterval);
console.log('\n\n-------------------------------\n Benchmark finished! \n-------------------------------');
let usedTime = Date.now() - this._progress.startTime;
console.log(` Transaction Execution Result `.bgBlue.white);
console.log(`Started=${this._progress.started}, Finished=${this._progress.finished}, UsedTime=${usedTime}ms`.green);
console.log(`Succ=${this._progress.succ}, Fail=${this._progress.fail}, TPS=${this._progress.succ / (this._progress.lastSuccTime! - this._progress.startTime) * 1000 | 0}\n`.green)
// TIME TPS(完成的)
console.log(` API Execution Result `.bgBlue.white);
// [KEY] RPS(完成的) AVG P95 P99
for (let key in this._apiStat) {
let stat = this._apiStat[key];
stat.resTime = stat.resTime.orderBy(v => v);
let send = stat.sendReq;
let succ = stat.resTime.length;
let netErr = stat.networkError;
let apiErr = stat.otherError;
let avg = stat.resTime[stat.resTime.length >> 1] | 0;
let p95 = stat.resTime[stat.resTime.length * 0.95 | 0] | 0;
let p99 = stat.resTime[stat.resTime.length * 0.99 | 0] | 0;
this._logTable([
[{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr', 'AVG ', 'P95 ', 'P99 '],
['', '' + send,
{ text: '' + succ, color: 'green' },
{ text: '' + (succ / (stat.last.succTime - stat.startTime) * 1000 | 0), color: 'green' },
netErr ? { text: '' + netErr, color: 'red' } : '0',
apiErr ? { text: '' + apiErr, color: 'red' } : '0',
{ text: avg ? avg + 'ms' : '-', color: 'yellow' },
{ text: p95 ? p95 + 'ms' : '-', color: 'yellow' },
{ text: p99 ? p99 + 'ms' : '-', color: 'yellow' }
]
])
}
}
private _apiStat: {
[key: string]: {
sendReq: number,
resTime: number[],
succ: number,
networkError: number,
otherError: number,
startTime: number,
last: {
sendReq: number,
resTime: number[],
succ: number,
networkError: number,
otherError: number,
startTime: number,
succTime: number
}
}
} = {};
private _maxApiNameLength = 0;
/**
* callApi
*/
callApi: typeof benchmarkClient.callApi = async (apiName, req) => {
this._maxApiNameLength = Math.max(apiName.length, this._maxApiNameLength);
if (!this._apiStat[apiName]) {
this._apiStat[apiName] = {
sendReq: 0,
resTime: [],
succ: 0,
networkError: 0,
otherError: 0,
startTime: Date.now(),
last: {
sendReq: 0,
resTime: [],
succ: 0,
networkError: 0,
otherError: 0,
startTime: Date.now(),
succTime: 0
}
};
}
++this._apiStat[apiName].sendReq;
++this._apiStat[apiName].last.sendReq;
let startTime = Date.now();
let ret = await benchmarkClient.callApi(apiName, req);
if (ret.isSucc) {
this._apiStat[apiName].last.succTime = Date.now();
this._apiStat[apiName].resTime.push(Date.now() - startTime);
this._apiStat[apiName].last.resTime.push(Date.now() - startTime);
++this._apiStat[apiName].succ;
++this._apiStat[apiName].last.succ;
}
else {
if (ret.err.type === TsrpcErrorType.NetworkError) {
++this._apiStat[apiName].networkError;
++this._apiStat[apiName].last.networkError;
}
else {
++this._apiStat[apiName].otherError;
++this._apiStat[apiName].last.otherError;
}
}
return ret;
}
private _report() {
console.log(new Date().format('hh:mm:ss').gray, `Started=${this._progress!.started}/${this._config.total}, Finished=${this._progress!.finished}, Succ=${this._progress!.succ.toString().green}, Fail=${this._progress!.fail.toString()[this._progress!.fail > 0 ? 'red' : 'white']}`,
this._progress!.lastSuccTime ? `TPS=${this._progress!.succ / (this._progress!.lastSuccTime - this._progress!.startTime) * 1000 | 0}` : '')
for (let key in this._apiStat) {
let stat = this._apiStat[key];
let send = stat.last.sendReq;
let succ = stat.last.resTime.length;
let netErr = stat.last.networkError;
let apiErr = stat.last.otherError;
this._logTable([
[{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr'],
['', '' + send,
{ text: '' + succ, color: 'green' },
{ text: '' + (succ / (stat.last.succTime - stat.last.startTime) * 1000 | 0), color: 'green' },
netErr ? { text: '' + netErr, color: 'red' } : '0',
apiErr ? { text: '' + apiErr, color: 'red' } : '0'
]
])
Object.assign(stat.last, {
sendReq: 0,
resTime: [],
succ: 0,
networkError: 0,
otherError: 0,
startTime: Date.now(),
})
}
}
private _logTable(rows: [TableCellItem[], TableCellItem[]]) {
let cellWidths: number[] = [];
for (let cell of rows[0]) {
cellWidths.push(typeof cell === 'string' ? cell.length + 4 : cell.text.length + 4);
}
for (let row of rows) {
let line = '';
for (let i = 0; i < row.length; ++i) {
let cell = row[i];
let cellWidth = cellWidths[i];
if (typeof cell === 'string') {
line += cell + ' '.repeat(cellWidth - cell.length);
}
else {
line += cell.text[cell.color] + ' '.repeat(cellWidth - cell.text.length);
}
}
console.log(line);
}
}
}
export const benchmarkClient = new HttpClient(serviceProto, {
server: benchmarkConfig.server,
logger: {
debug: function () { },
log: function () { },
warn: function () { },
error: function () { },
},
timeout: benchmarkConfig.timeout,
agent: new (benchmarkConfig.server.startsWith('https') ? https : http).Agent({
keepAlive: true
})
})
type TableCellItem = (string | { text: string, color: 'green' | 'red' | 'yellow' });

View File

@ -0,0 +1,283 @@
import assert from 'assert';
import 'colors';
import 'k8w-extend-native';
import { TsrpcErrorType } from "tsrpc-proto";
import { WsClient } from '../../src/client/ws/WsClient';
import { benchmarkConfig } from "../config/BenchmarkConfig";
import { serviceProto } from '../protocols/proto';
export interface WsRunnerConfig {
total: number;
concurrency: number;
showError?: boolean;
}
export class WsRunner {
private _config: WsRunnerConfig;
// 执行单个事务的方法
private _single: (this: WsRunner) => Promise<void>;
// 执行进度信息
private _progress?: {
startTime: number,
lastSuccTime?: number,
started: number,
finished: number,
succ: number,
fail: number
};
constructor(single: WsRunner['_single'], config: WsRunnerConfig) {
this._single = single.bind(this);
this._config = config;
}
async start() {
this._progress = {
startTime: Date.now(),
started: 0,
finished: 0,
succ: 0,
fail: 0
}
assert.ok(await benchmarkClient.connect(), 'Connect failed');
// 启动并发
for (let i = 0; i < this._config.concurrency; ++i) {
this._doTrans();
}
console.log('Benchmark start!');
this._startReport();
}
private _doTrans() {
if (this._isStoped || !this._progress) {
return;
}
if (this._progress.started < this._config.total) {
++this._progress.started;
let startTime = Date.now();
this._single().then(v => {
++this._progress!.succ;
this._progress!.lastSuccTime = Date.now();
}).catch(e => {
++this._progress!.fail;
if (this._config.showError) {
console.error('[Error]', e.message);
}
}).then(() => {
++this._progress!.finished;
if (this._progress!.finished === this._config.total) {
this._finish();
}
else {
this._doTrans();
}
})
}
}
private _reportInterval?: NodeJS.Timeout;
private _startReport() {
this._reportInterval = setInterval(() => {
this._report();
}, 1000)
}
private _isStoped = false;
stop() {
this._isStoped = true;
}
private _finish() {
if (!this._progress) {
return;
}
this._reportInterval && clearInterval(this._reportInterval);
console.log('\n\n-------------------------------\n Benchmark finished! \n-------------------------------');
let usedTime = Date.now() - this._progress.startTime;
console.log(` Transaction Execution Result `.bgBlue.white);
console.log(`Started=${this._progress.started}, Finished=${this._progress.finished}, UsedTime=${usedTime}ms`.green);
console.log(`Succ=${this._progress.succ}, Fail=${this._progress.fail}, TPS=${this._progress.succ / (this._progress.lastSuccTime! - this._progress.startTime) * 1000 | 0}\n`.green)
// TIME TPS(完成的)
console.log(` API Execution Result `.bgBlue.white);
// [KEY] RPS(完成的) AVG P95 P99
for (let key in this._apiStat) {
let stat = this._apiStat[key];
stat.resTime = stat.resTime.orderBy(v => v);
let send = stat.sendReq;
let succ = stat.resTime.length;
let netErr = stat.networkError;
let apiErr = stat.otherError;
let avg = stat.resTime[stat.resTime.length >> 1] | 0;
let p95 = stat.resTime[stat.resTime.length * 0.95 | 0] | 0;
let p99 = stat.resTime[stat.resTime.length * 0.99 | 0] | 0;
this._logTable([
[{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr', 'AVG ', 'P95 ', 'P99 '],
['', '' + send,
{ text: '' + succ, color: 'green' },
{ text: '' + (succ / (stat.last.succTime - stat.startTime) * 1000 | 0), color: 'green' },
netErr ? { text: '' + netErr, color: 'red' } : '0',
apiErr ? { text: '' + apiErr, color: 'red' } : '0',
{ text: avg ? avg + 'ms' : '-', color: 'yellow' },
{ text: p95 ? p95 + 'ms' : '-', color: 'yellow' },
{ text: p99 ? p99 + 'ms' : '-', color: 'yellow' }
]
])
}
}
private _apiStat: {
[key: string]: {
sendReq: number,
resTime: number[],
succ: number,
networkError: number,
otherError: number,
startTime: number,
last: {
sendReq: number,
resTime: number[],
succ: number,
networkError: number,
otherError: number,
startTime: number,
succTime: number
}
}
} = {};
private _maxApiNameLength = 0;
/**
* callApi
*/
callApi: typeof benchmarkClient.callApi = async (apiName, req) => {
this._maxApiNameLength = Math.max(apiName.length, this._maxApiNameLength);
if (!this._apiStat[apiName]) {
this._apiStat[apiName] = {
sendReq: 0,
resTime: [],
succ: 0,
networkError: 0,
otherError: 0,
startTime: Date.now(),
last: {
sendReq: 0,
resTime: [],
succ: 0,
networkError: 0,
otherError: 0,
startTime: Date.now(),
succTime: 0
}
};
}
++this._apiStat[apiName].sendReq;
++this._apiStat[apiName].last.sendReq;
let startTime = Date.now();
let ret = await benchmarkClient.callApi(apiName, req);
if (ret.isSucc) {
this._apiStat[apiName].last.succTime = Date.now();
this._apiStat[apiName].resTime.push(Date.now() - startTime);
this._apiStat[apiName].last.resTime.push(Date.now() - startTime);
++this._apiStat[apiName].succ;
++this._apiStat[apiName].last.succ;
}
else {
if (ret.err.type === TsrpcErrorType.NetworkError) {
++this._apiStat[apiName].networkError;
++this._apiStat[apiName].last.networkError;
}
else {
++this._apiStat[apiName].otherError;
++this._apiStat[apiName].last.otherError;
}
}
return ret;
}
private _report() {
console.log(new Date().format('hh:mm:ss').gray, `Started=${this._progress!.started}/${this._config.total}, Finished=${this._progress!.finished}, Succ=${this._progress!.succ.toString().green}, Fail=${this._progress!.fail.toString()[this._progress!.fail > 0 ? 'red' : 'white']}`,
this._progress!.lastSuccTime ? `TPS=${this._progress!.succ / (this._progress!.lastSuccTime - this._progress!.startTime) * 1000 | 0}` : '')
for (let key in this._apiStat) {
let stat = this._apiStat[key];
let send = stat.last.sendReq;
let succ = stat.last.resTime.length;
let netErr = stat.last.networkError;
let apiErr = stat.last.otherError;
this._logTable([
[{ text: 'Api' + key + ' '.repeat(this._maxApiNameLength - key.length), color: 'green' }, 'Send', 'Succ', 'QPS', 'NetErr', 'ApiErr'],
['', '' + send,
{ text: '' + succ, color: 'green' },
{ text: '' + (succ / (stat.last.succTime - stat.last.startTime) * 1000 | 0), color: 'green' },
netErr ? { text: '' + netErr, color: 'red' } : '0',
apiErr ? { text: '' + apiErr, color: 'red' } : '0'
]
])
Object.assign(stat.last, {
sendReq: 0,
resTime: [],
succ: 0,
networkError: 0,
otherError: 0,
startTime: Date.now(),
})
}
}
private _logTable(rows: [TableCellItem[], TableCellItem[]]) {
let cellWidths: number[] = [];
for (let cell of rows[0]) {
cellWidths.push(typeof cell === 'string' ? cell.length + 4 : cell.text.length + 4);
}
for (let row of rows) {
let line = '';
for (let i = 0; i < row.length; ++i) {
let cell = row[i];
let cellWidth = cellWidths[i];
if (typeof cell === 'string') {
line += cell + ' '.repeat(cellWidth - cell.length);
}
else {
line += cell.text[cell.color] + ' '.repeat(cellWidth - cell.text.length);
}
}
console.log(line);
}
}
}
export const benchmarkClient = new WsClient(serviceProto, {
server: benchmarkConfig.server,
logger: {
debug: function () { },
log: function () { },
warn: function () { },
error: function () { },
},
timeout: benchmarkConfig.timeout
})
type TableCellItem = (string | { text: string, color: 'green' | 'red' | 'yellow' });

View File

@ -0,0 +1,14 @@
import { uint } from 'tsbuffer-schema';
export interface ReqTest {
a?: uint;
b?: string;
c?: boolean;
d?: Uint8Array;
}
export interface ResTest {
a?: uint;
b?: string;
c?: boolean;
d?: Uint8Array;
}

View File

@ -0,0 +1,104 @@
import { ServiceProto } from 'tsrpc-proto';
import { ReqTest, ResTest } from './PtlTest'
export interface ServiceType {
api: {
"Test": {
req: ReqTest,
res: ResTest
}
},
msg: {
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"services": [
{
"id": 0,
"name": "Test",
"type": "api"
}
],
"types": {
"PtlTest/ReqTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "a",
"type": {
"type": "Number",
"scalarType": "uint"
},
"optional": true
},
{
"id": 1,
"name": "b",
"type": {
"type": "String"
},
"optional": true
},
{
"id": 2,
"name": "c",
"type": {
"type": "Boolean"
},
"optional": true
},
{
"id": 3,
"name": "d",
"type": {
"type": "Buffer",
"arrayType": "Uint8Array"
},
"optional": true
}
]
},
"PtlTest/ResTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "a",
"type": {
"type": "Number",
"scalarType": "uint"
},
"optional": true
},
{
"id": 1,
"name": "b",
"type": {
"type": "String"
},
"optional": true
},
{
"id": 2,
"name": "c",
"type": {
"type": "Boolean"
},
"optional": true
},
{
"id": 3,
"name": "d",
"type": {
"type": "Buffer",
"arrayType": "Uint8Array"
},
"optional": true
}
]
}
}
};

26
benchmark/server/http.ts Normal file
View File

@ -0,0 +1,26 @@
import { HttpServer } from '../../src/index';
import { serviceProto } from "../protocols/proto";
async function main() {
let server = new HttpServer(serviceProto, {
logger: {
debug: () => { },
log: () => { },
error: console.error.bind(console),
warn: console.warn.bind(console)
}
});
server.implementApi('Test', call => {
call.succ(call.req);
});
await server.start();
setInterval(() => {
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`内存: ${Math.round(used * 100) / 100} MB`);
}, 2000)
}
main();

26
benchmark/server/ws.ts Normal file
View File

@ -0,0 +1,26 @@
import { WsServer } from '../../src/index';
import { serviceProto } from "../protocols/proto";
async function main() {
let server = new WsServer(serviceProto, {
logger: {
debug: () => { },
log: () => { },
error: console.error.bind(console),
warn: console.warn.bind(console)
}
});
server.implementApi('Test', call => {
call.succ(call.req);
});
await server.start();
setInterval(() => {
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`内存: ${Math.round(used * 100) / 100} MB`);
}, 2000)
}
main();

63
benchmark/tsconfig.json Normal file
View File

@ -0,0 +1,63 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}

13
benchmark/ws.ts Normal file
View File

@ -0,0 +1,13 @@
import { benchmarkConfig } from './config/BenchmarkConfig';
import { WsRunner } from './models/WsRunner';
const req = {
a: 123456,
b: 'Hello, World!',
c: true,
d: new Uint8Array(100000)
}
new WsRunner(async function () {
await this.callApi('Test', req);
}, benchmarkConfig).start();

6105
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

77
package.json Normal file
View File

@ -0,0 +1,77 @@
{
"name": "tsrpc",
"version": "3.3.1-dev.0",
"description": "A TypeScript RPC Framework, with runtime type checking and built-in serialization, support both HTTP and WebSocket.",
"main": "index.js",
"exports": {
"require": "./index.js",
"import": "./index.mjs"
},
"typings": "index.d.ts",
"directories": {
"doc": "docs"
},
"scripts": {
"test": "npx mocha",
"genTestProto": "npx tsrpc-cli@latest proto --input test/proto --output test/proto/serviceProto.ts",
"coverage": "nyc mocha test/**/*.test.ts && start coverage\\index.html",
"build": "npm run build:js && npm run build:dts && npm run build:doc && node scripts/postBuild && cp package.json LICENSE README.md dist/",
"build:js": "rm -rf dist && npx rollup -c",
"build:dts": "rm -rf lib && npx tsc && npx api-extractor run --local --verbose && rm -rf lib",
"build:doc": "rm -rf docs/api && npx api-documenter markdown --input temp --output docs/api"
},
"repository": {
"type": "git",
"url": "https://github.com/k8w/tsrpc.git"
},
"keywords": [
"k8w",
"ts",
"rpc",
"grpc",
"tsbuffer",
"fullstack",
"websocket",
"protobuf",
"socket.io"
],
"author": "k8w",
"license": "MIT",
"devDependencies": {
"@microsoft/api-documenter": "^7.17.9",
"@microsoft/api-extractor": "^7.22.2",
"@types/chai": "^4.3.1",
"@types/mocha": "^8.2.3",
"@types/node": "^15.14.9",
"@types/uuid": "^8.3.4",
"chai": "^4.3.6",
"mocha": "^9.2.2",
"nyc": "^15.1.0",
"rollup": "^2.70.2",
"rollup-plugin-typescript2": "^0.31.2",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"
},
"dependencies": {
"@types/ws": "^7.4.7",
"bson": "*",
"chalk": "^4.1.2",
"tsbuffer": "^2.2.2",
"tsrpc-base-client": "^2.0.5",
"tsrpc-proto": "^1.4.1",
"uuid": "^8.3.2",
"ws": "^7.5.7"
},
"nyc": {
"extension": [
".ts"
],
"include": [
"src/**/*.ts"
],
"reporter": [
"html"
],
"all": true
}
}

17
res/mongodb-polyfill.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import { ObjectId } from 'bson';
import { OmitUnion } from 'k8w-extend-native';
type InsertOneResult<T> = any;
type OptionalId<T> = any;
type Document = any;
declare module 'mongodb' {
export interface Collection<TSchema extends Document = Document> {
insertOne(doc: OptionalUnlessRequiredId_1<TSchema>): Promise<InsertOneResult<TSchema>>;
}
export type OptionalUnlessRequiredId_1<TSchema> = TSchema extends {
_id: ObjectId;
} ? (OmitUnion<TSchema, '_id'> & { _id?: ObjectId }) : TSchema extends {
_id: any;
} ? TSchema : OptionalId<TSchema>;
}

42
rollup.config.js Normal file
View File

@ -0,0 +1,42 @@
import typescript from 'rollup-plugin-typescript2';
export default [
{
input: './src/index.ts',
output: [{
format: 'cjs',
file: './dist/index.js',
banner: require('./scripts/copyright')
}],
plugins: [
typescript({
tsconfigOverride: {
compilerOptions: {
declaration: false,
declarationMap: false,
module: "esnext"
}
}
})
]
},
{
input: './src/index.ts',
output: [{
format: 'es',
file: './dist/index.mjs',
banner: require('./scripts/copyright')
}],
plugins: [
typescript({
tsconfigOverride: {
compilerOptions: {
declaration: false,
declarationMap: false,
module: "esnext"
}
}
})
]
}
]

7
scripts/copyright.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = `/*!
* TSRPC v${require('../package.json').version}
* -----------------------------------------
* Copyright (c) King Wang.
* MIT License
* https://github.com/k8w/tsrpc
*/`

26
scripts/postBuild.js Normal file
View File

@ -0,0 +1,26 @@
const fs = require('fs');
const path = require('path');
// remove private / protected index.d.ts
(() => {
let content = fs.readFileSync(path.resolve(__dirname, '../dist/index.d.ts'), 'utf-8');
content = content.replace(/^\s*(private|protected)\s+\_.+;/g, '');
content = require('./copyright') + '\n' + content;
fs.writeFileSync(path.resolve(__dirname, '../dist/index.d.ts'), content, 'utf-8');
})();
// replace __TSRPC_VERSION__from index.js/mjs
[
path.resolve(__dirname, '../dist/index.js'),
path.resolve(__dirname, '../dist/index.mjs')
].forEach(filepath => {
let content = fs.readFileSync(filepath, 'utf-8');
content = content.replace('__TSRPC_VERSION__', require('../package.json').version);;
fs.writeFileSync(filepath, content, 'utf-8');
});
// mongodb-polyfill
fs.copyFileSync(path.resolve(__dirname, '../res/mongodb-polyfill.d.ts'), path.resolve(__dirname, '../dist/mongodb-polyfill.d.ts'));
let content = fs.readFileSync(path.resolve(__dirname, '../dist/index.d.ts'), 'utf-8');
content = content.replace(`/// <reference types="node" />`, `/// <reference types="node" />\n/// <reference path="mongodb-polyfill.d.ts" />`)
fs.writeFileSync(path.resolve(__dirname, '../dist/index.d.ts'), content, 'utf-8');

View File

@ -0,0 +1,37 @@
import { ObjectId } from "bson";
import http from "http";
import https from "https";
import { BaseHttpClient, BaseHttpClientOptions, defaultBaseHttpClientOptions } from "tsrpc-base-client";
import { BaseServiceType, ServiceProto } from "tsrpc-proto";
import { HttpProxy } from "./HttpProxy";
/**
* Client for TSRPC HTTP Server.
* It uses native http module of NodeJS.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
export class HttpClient<ServiceType extends BaseServiceType> extends BaseHttpClient<ServiceType> {
readonly options!: Readonly<HttpClientOptions>;
constructor(proto: ServiceProto<ServiceType>, options?: Partial<HttpClientOptions>) {
let httpProxy = new HttpProxy;
super(proto, httpProxy, {
customObjectIdClass: ObjectId,
...defaultHttpClientOptions,
...options
});
httpProxy.agent = this.options.agent;
}
}
const defaultHttpClientOptions: HttpClientOptions = {
...defaultBaseHttpClientOptions
}
export interface HttpClientOptions extends BaseHttpClientOptions {
/** NodeJS HTTP Agent */
agent?: http.Agent | https.Agent;
}

View File

@ -0,0 +1,69 @@
import http from "http";
import https from "https";
import { IHttpProxy } from "tsrpc-base-client";
import { TsrpcError } from "tsrpc-proto";
/** @internal */
export class HttpProxy implements IHttpProxy {
/** NodeJS HTTP Agent */
agent?: http.Agent | https.Agent;
fetch(options: Parameters<IHttpProxy['fetch']>[0]): ReturnType<IHttpProxy['fetch']> {
let nodeHttp = options.url.startsWith('https://') ? https : http;
let rs!: (v: { isSucc: true, res: string | Uint8Array } | { isSucc: false, err: TsrpcError }) => void;
let promise: ReturnType<IHttpProxy['fetch']>['promise'] = new Promise(_rs => {
rs = _rs;
})
let httpReq: http.ClientRequest;
httpReq = nodeHttp.request(options.url, {
method: options.method,
agent: this.agent,
timeout: options.timeout,
headers: options.headers,
}, httpRes => {
let data: Buffer[] = [];
httpRes.on('data', (v: Buffer) => {
data.push(v)
});
httpRes.on('end', () => {
let buf: Uint8Array = Buffer.concat(data);
if (options.responseType === 'text') {
rs({
isSucc: true,
res: buf.toString()
})
}
else {
rs({
isSucc: true,
res: buf
})
}
})
});
httpReq.on('error', e => {
rs({
isSucc: false,
err: new TsrpcError(e.message, {
type: TsrpcError.Type.NetworkError,
code: (e as any).code
})
});
});
let buf = options.data;
httpReq.end(typeof buf === 'string' ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength));
let abort = httpReq.abort.bind(httpReq);
return {
promise: promise,
abort: abort
}
}
}

View File

@ -0,0 +1,58 @@
import { IWebSocketProxy } from "tsrpc-base-client";
import { TsrpcError } from "tsrpc-proto";
import WebSocket from 'ws';
/**
* @internal
*/
export class WebSocketProxy implements IWebSocketProxy {
options!: IWebSocketProxy['options']
private _ws?: WebSocket;
connect(server: string, protocols?: string[]): void {
this._ws = new WebSocket(server, protocols);
this._ws.onopen = this.options.onOpen;
this._ws.onclose = e => {
this.options.onClose(e.code, e.reason);
this._ws = undefined;
}
this._ws.onerror = e => {
this.options.onError(e.error);
}
this._ws.onmessage = e => {
if (e.data instanceof ArrayBuffer) {
this.options.onMessage(new Uint8Array(e.data));
}
else if (Array.isArray(e.data)) {
this.options.onMessage(Buffer.concat(e.data));
}
else {
this.options.onMessage(e.data);
}
}
}
close(code?: number, reason?: string): void {
this._ws?.close(code, reason);
this._ws = undefined;
}
send(data: string | Uint8Array): Promise<{ err?: TsrpcError | undefined; }> {
return new Promise(rs => {
this._ws?.send(data, err => {
if (err) {
this.options.logger?.error('WebSocket Send Error:', err);
rs({
err: new TsrpcError('Network Error', {
code: 'SEND_BUF_ERR',
type: TsrpcError.Type.NetworkError,
innerErr: err
})
});
return;
}
rs({});
});
})
}
}

31
src/client/ws/WsClient.ts Normal file
View File

@ -0,0 +1,31 @@
import { ObjectId } from "bson";
import { BaseWsClient, BaseWsClientOptions, defaultBaseWsClientOptions } from "tsrpc-base-client";
import { BaseServiceType, ServiceProto } from "tsrpc-proto";
import { WebSocketProxy } from "./WebSocketProxy";
/**
* Client for TSRPC WebSocket Server.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
export class WsClient<ServiceType extends BaseServiceType> extends BaseWsClient<ServiceType> {
readonly options!: Readonly<WsClientOptions>;
constructor(proto: ServiceProto<ServiceType>, options?: Partial<WsClientOptions>) {
let wsp = new WebSocketProxy();
super(proto, wsp, {
customObjectIdClass: ObjectId,
...defaultWsClientOptions,
...options
})
}
}
const defaultWsClientOptions: WsClientOptions = {
...defaultBaseWsClientOptions,
}
export interface WsClientOptions extends BaseWsClientOptions {
}

27
src/index.ts Normal file
View File

@ -0,0 +1,27 @@
import 'k8w-extend-native';
// Common
export * from 'tsrpc-base-client';
export * from 'tsrpc-proto';
export * from './client/http/HttpClient';
export * from './client/ws/WsClient';
export * from './models/version';
export * from './server/base/ApiCall';
// Base
export * from './server/base/BaseCall';
export * from './server/base/BaseConnection';
export * from './server/base/BaseServer';
export * from './server/base/MsgCall';
export * from './server/http/ApiCallHttp';
// Http
export * from './server/http/HttpConnection';
export * from './server/http/HttpServer';
export * from './server/http/MsgCallHttp';
export * from './server/models/PrefixLogger';
export * from './server/models/TerminalColorLogger';
// WebSocket
export * from './server/ws/ApiCallWs';
export * from './server/ws/MsgCallWs';
export * from './server/ws/WsConnection';
export * from './server/ws/WsServer';

22
src/models/HttpUtil.ts Normal file
View File

@ -0,0 +1,22 @@
import * as http from "http";
export class HttpUtil {
static getClientIp(req: http.IncomingMessage) {
var ipAddress;
// The request may be forwarded from local web server.
var forwardedIpsStr = req.headers['x-forwarded-for'] as string | undefined;
if (forwardedIpsStr) {
// 'x-forwarded-for' header may return multiple IP addresses in
// the format: "client IP, proxy 1 IP, proxy 2 IP" so take the
// the first one
var forwardedIps = forwardedIpsStr.split(',');
ipAddress = forwardedIps[0];
}
if (!ipAddress) {
// If request was not forwarded
ipAddress = req.connection.remoteAddress;
}
// Remove prefix ::ffff:
return ipAddress ? ipAddress.replace(/^::ffff:/, '') : '';
};
}

35
src/models/Pool.ts Normal file
View File

@ -0,0 +1,35 @@
export class Pool<ItemClass extends PoolItem> {
private _pools: ItemClass[] = [];
private _itemClass: { new(): ItemClass };
enabled: boolean;
constructor(itemClass: { new(): ItemClass }, enabled: boolean) {
this._itemClass = itemClass;
this.enabled = enabled;
}
get() {
let item = this.enabled && this._pools.pop();
if (!item) {
item = new this._itemClass();
}
item.reuse?.();
return item;
}
put(item: ItemClass) {
if (!this.enabled || this._pools.indexOf(item) > -1) {
return;
}
item.unuse?.();
this._pools.push(item);
}
}
export interface PoolItem {
reuse: () => void;
unuse: () => void;
}

2
src/models/version.ts Normal file
View File

@ -0,0 +1,2 @@
/** Version of TSRPC */
export const TSRPC_VERSION = '__TSRPC_VERSION__';

215
src/server/base/ApiCall.ts Normal file
View File

@ -0,0 +1,215 @@
import { TSBuffer } from 'tsbuffer';
import { ApiService, TransportDataUtil } from "tsrpc-base-client";
import { ApiReturn, BaseServiceType, ServerOutputData, TsrpcError, TsrpcErrorData, TsrpcErrorType } from "tsrpc-proto";
import { PrefixLogger } from "../models/PrefixLogger";
import { BaseCall, BaseCallOptions } from "./BaseCall";
import { BaseConnection } from './BaseConnection';
export interface ApiCallOptions<Req, ServiceType extends BaseServiceType> extends BaseCallOptions<ServiceType> {
/** Which service the Call is belong to */
service: ApiService,
/** Only exists in long connection, it is used to associate request and response.
* It is created by the client, and the server would return the same value in `ApiReturn`.
*/
sn?: number,
/** Request Data */
req: Req
}
/**
* A call request by `client.callApi()`
* @typeParam Req - Type of request
* @typeParam Res - Type of response
* @typeParam ServiceType - The same `ServiceType` to server, it is used for code auto hint.
*/
export abstract class ApiCall<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends BaseCall<ServiceType> {
readonly type = 'api' as const;
/**
* Which `ApiService` the request is calling for
*/
readonly service!: ApiService;
/** Only exists in long connection, it is used to associate request and response.
* It is created by the client, and the server would return the same value in `ApiReturn`.
*/
readonly sn?: number;
/**
* Request data from the client, type of it is checked by the framework already.
*/
readonly req: Req;
constructor(options: ApiCallOptions<Req, ServiceType>, logger?: PrefixLogger) {
super(options, logger ?? new PrefixLogger({
logger: options.conn.logger,
prefixs: [`[Api:${options.service.name}]${options.sn !== undefined ? ` SN=${options.sn}` : ''}`]
}));
this.sn = options.sn;
this.req = options.req;
}
protected _return?: ApiReturn<Res>;
/**
* Response Data that sent already.
* `undefined` means no return data is sent yet. (Never `call.succ()` and `call.error()`)
*/
public get return(): ApiReturn<Res> | undefined {
return this._return;
}
protected _usedTime: number | undefined;
/** Time from received req to send return data */
public get usedTime(): number | undefined {
return this._usedTime;
}
/**
* Send a successful `ApiReturn` with response data
* @param res - Response data
* @returns Promise resolved means the buffer is sent to kernel
*/
succ(res: Res): Promise<void> {
return this._prepareReturn({
isSucc: true,
res: res
})
}
/**
* Send a error `ApiReturn` with a `TsrpcError`
* @returns Promise resolved means the buffer is sent to kernel
*/
error(message: string, info?: Partial<TsrpcErrorData>): Promise<void>;
error(err: TsrpcError): Promise<void>;
error(errOrMsg: string | TsrpcError, data?: Partial<TsrpcErrorData>): Promise<void> {
let error: TsrpcError = typeof errOrMsg === 'string' ? new TsrpcError(errOrMsg, data) : errOrMsg;
return this._prepareReturn({
isSucc: false,
err: error
})
};
protected async _prepareReturn(ret: ApiReturn<Res>): Promise<void> {
if (this._return) {
return;
}
this._return = ret;
// Pre Flow
let preFlow = await this.server.flows.preApiReturnFlow.exec({ call: this, return: ret }, this.logger);
// Stopped!
if (!preFlow) {
this.logger.debug('[preApiReturnFlow]', 'Canceled')
return;
}
ret = preFlow.return;
// record & log ret
this._usedTime = Date.now() - this.startTime;
if (ret.isSucc) {
this.logger.log('[ApiRes]', `${this.usedTime}ms`, this.server.options.logResBody ? ret.res : '');
}
else {
if (ret.err.type === TsrpcErrorType.ApiError) {
this.logger.log('[ApiErr]', `${this.usedTime}ms`, ret.err, 'req=', this.req);
}
else {
this.logger.error(`[ApiErr]`, `${this.usedTime}ms`, ret.err, 'req=', this.req)
}
}
// Do send!
this._return = ret;
let opSend = await this._sendReturn(ret);
if (!opSend.isSucc) {
if (opSend.canceledByFlow) {
this.logger.debug(`[${opSend.canceledByFlow}]`, 'Canceled');
}
else {
this.logger.error('[SendDataErr]', opSend.errMsg);
if (ret.isSucc || ret.err.type === TsrpcErrorType.ApiError) {
this._return = undefined;
this.server.onInternalServerError({ message: opSend.errMsg, name: 'SendReturnErr' }, this)
}
}
return;
}
// Post Flow
await this.server.flows.postApiReturnFlow.exec(preFlow, this.logger);
}
protected async _sendReturn(ret: ApiReturn<Res>): ReturnType<SendReturnMethod<Res>> {
// Encode
let opServerOutput = ApiCall.encodeApiReturn(this.server.tsbuffer, this.service, ret, this.conn.dataType, this.sn);;
if (!opServerOutput.isSucc) {
this.server.onInternalServerError({ message: opServerOutput.errMsg, stack: ' |- TransportDataUtil.encodeApiReturn\n |- ApiCall._sendReturn' }, this);
return opServerOutput;
}
let opSend = await this.conn.sendData(opServerOutput.output);
if (!opSend.isSucc) {
return opSend;
}
return opSend;
}
static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'text', sn?: number): EncodeApiReturnOutput<string>
static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'buffer', sn?: number): EncodeApiReturnOutput<Uint8Array>
static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'json', sn?: number): EncodeApiReturnOutput<object>
static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'text' | 'buffer' | 'json', sn?: number): EncodeApiReturnOutput<Uint8Array> | EncodeApiReturnOutput<string> | EncodeApiReturnOutput<object>;
static encodeApiReturn(tsbuffer: TSBuffer, service: ApiService, apiReturn: ApiReturn<any>, type: 'text' | 'buffer' | 'json', sn?: number): EncodeApiReturnOutput<Uint8Array> | EncodeApiReturnOutput<string> | EncodeApiReturnOutput<object> {
if (type === 'buffer') {
let serverOutputData: ServerOutputData = {
sn: sn,
serviceId: sn !== undefined ? service.id : undefined
};
if (apiReturn.isSucc) {
let op = tsbuffer.encode(apiReturn.res, service.resSchemaId);
if (!op.isSucc) {
return op;
}
serverOutputData.buffer = op.buf;
}
else {
serverOutputData.error = apiReturn.err;
}
let op = TransportDataUtil.tsbuffer.encode(serverOutputData, 'ServerOutputData');
return op.isSucc ? { isSucc: true, output: op.buf } : { isSucc: false, errMsg: op.errMsg };
}
else {
apiReturn = { ...apiReturn };
if (apiReturn.isSucc) {
let op = tsbuffer.encodeJSON(apiReturn.res, service.resSchemaId);
if (!op.isSucc) {
return op;
}
apiReturn.res = op.json;
}
else {
apiReturn.err = {
...apiReturn.err
}
}
let json = sn == undefined ? apiReturn : [service.name, apiReturn, sn];
return { isSucc: true, output: type === 'json' ? json : JSON.stringify(json) };
}
}
}
export type SendReturnMethod<Res> = (ret: ApiReturn<Res>) => ReturnType<BaseConnection['sendData']>;
export declare type EncodeApiReturnOutput<T> = {
isSucc: true;
/** Encoded binary buffer */
output: T;
errMsg?: undefined;
} | {
isSucc: false;
/** Error message */
errMsg: string;
output?: undefined;
};

View File

@ -0,0 +1,30 @@
import { ApiService, MsgService } from 'tsrpc-base-client';
import { BaseServiceType } from 'tsrpc-proto';
import { PrefixLogger } from '../models/PrefixLogger';
import { BaseConnection } from './BaseConnection';
export interface BaseCallOptions<ServiceType extends BaseServiceType> {
/** Connection */
conn: BaseConnection<ServiceType>,
/** Which service the call is belong to */
service: ApiService | MsgService
}
export abstract class BaseCall<ServiceType extends BaseServiceType> {
readonly conn: BaseConnection<ServiceType>;
readonly service: ApiService | MsgService;
/** Time that server created the call */
readonly startTime: number;
readonly logger: PrefixLogger;
constructor(options: BaseCallOptions<ServiceType>, logger: PrefixLogger) {
this.conn = options.conn;
this.service = options.service;
this.startTime = Date.now();
this.logger = logger;
}
get server(): this['conn']['server'] {
return this.conn.server;
}
}

View File

@ -0,0 +1,182 @@
import { MsgHandlerManager, ParsedServerInput, TransportDataUtil } from "tsrpc-base-client";
import { BaseServiceType } from "tsrpc-proto";
import { PrefixLogger } from "../models/PrefixLogger";
import { ApiCall } from "./ApiCall";
import { BaseServer, MsgHandler } from "./BaseServer";
import { MsgCall } from "./MsgCall";
export interface BaseConnectionOptions<ServiceType extends BaseServiceType = any> {
/** Created by server, each Call has a unique id. */
id: string;
/** Client IP address */
ip: string,
server: BaseServer<ServiceType>,
dataType: 'text' | 'buffer' | 'json'
}
export abstract class BaseConnection<ServiceType extends BaseServiceType = any> {
/** It is long connection or short connection */
abstract readonly type: 'LONG' | 'SHORT';
protected abstract readonly ApiCallClass: { new(options: any): ApiCall };
protected abstract readonly MsgCallClass: { new(options: any): MsgCall };
/** Connection unique ID */
readonly id: string;
/** Client IP address */
readonly ip: string;
readonly server: BaseServer<ServiceType>;
readonly logger: PrefixLogger;
dataType: BaseConnectionOptions['dataType'];
constructor(options: BaseConnectionOptions<ServiceType>, logger: PrefixLogger) {
this.id = options.id;
this.ip = options.ip;
this.server = options.server;
this.logger = logger;
this.dataType = options.dataType;
}
abstract get status(): ConnectionStatus;
/** Close the connection */
abstract close(reason?: string): void;
/** Send buffer (with pre-flow and post-flow) */
async sendData(data: string | Uint8Array | object, call?: ApiCall): Promise<{ isSucc: true } | { isSucc: false, errMsg: string, canceledByFlow?: string }> {
// Pre Flow
let pre = await this.server.flows.preSendDataFlow.exec({ conn: this, data: data, call: call }, call?.logger || this.logger);
if (!pre) {
return { isSucc: false, errMsg: 'Canceled by preSendDataFlow', canceledByFlow: 'preSendDataFlow' };
}
data = pre.data;
// @deprecated Pre Buffer Flow
if (data instanceof Uint8Array) {
let preBuf = await this.server.flows.preSendBufferFlow.exec({ conn: this, buf: data, call: call }, call?.logger || this.logger);
if (!preBuf) {
return { isSucc: false, errMsg: 'Canceled by preSendBufferFlow', canceledByFlow: 'preSendBufferFlow' };
}
data = preBuf.buf;
}
// debugBuf log
if (this.server.options.debugBuf) {
if (typeof data === 'string') {
(call?.logger ?? this.logger)?.debug(`[SendText] length=${data.length}`, data);
}
else if (data instanceof Uint8Array) {
(call?.logger ?? this.logger)?.debug(`[SendBuf] length=${data.length}`, data);
}
else {
(call?.logger ?? this.logger)?.debug('[SendJSON]', data);
}
}
return this.doSendData(data, call);
}
protected abstract doSendData(data: string | Uint8Array | object, call?: ApiCall): Promise<{ isSucc: true } | { isSucc: false, errMsg: string }>;
makeCall(input: ParsedServerInput): ApiCall | MsgCall {
if (input.type === 'api') {
return new this.ApiCallClass({
conn: this,
service: input.service,
req: input.req,
sn: input.sn,
})
}
else {
return new this.MsgCallClass({
conn: this,
service: input.service,
msg: input.msg
})
}
}
/**
* Send message to the client, only be available when it is long connection.
* @param msgName
* @param msg - Message body
* @returns Promise resolved when the buffer is sent to kernel, it not represents the server received it.
*/
async sendMsg<T extends keyof ServiceType['msg']>(msgName: T, msg: ServiceType['msg'][T]): ReturnType<BaseConnection['sendData']> {
if (this.type === 'SHORT') {
this.logger.warn('[SendMsgErr]', `[${msgName}]`, 'Short connection cannot sendMsg');
return { isSucc: false, errMsg: 'Short connection cannot sendMsg' }
}
let service = this.server.serviceMap.msgName2Service[msgName as string];
if (!service) {
this.logger.warn('[SendMsgErr]', `[${msgName}]`, `Invalid msg name: ${msgName}`);
return { isSucc: false, errMsg: `Invalid msg name: ${msgName}` }
}
// Pre Flow
let pre = await this.server.flows.preSendMsgFlow.exec({ conn: this, service: service, msg: msg }, this.logger);
if (!pre) {
this.logger.debug('[preSendMsgFlow]', 'Canceled');
return { isSucc: false, errMsg: 'Canceled by preSendMsgFlow', canceledByFlow: 'preSendMsgFlow' };
}
msg = pre.msg;
// Encode
let opServerOutput = TransportDataUtil.encodeServerMsg(this.server.tsbuffer, service, msg, this.dataType, this.type);
if (!opServerOutput.isSucc) {
this.logger.warn('[SendMsgErr]', `[${msgName}]`, opServerOutput.errMsg);
return opServerOutput;
}
// Do send!
this.server.options.logMsg && this.logger.log('[SendMsg]', `[${msgName}]`, msg);
let opSend = await this.sendData(opServerOutput.output);
if (!opSend.isSucc) {
return opSend;
}
// Post Flow
await this.server.flows.postSendMsgFlow.exec(pre, this.logger);
return { isSucc: true };
}
// 多个Handler将异步并行执行
private _msgHandlers?: MsgHandlerManager;
/**
* Add a message handler,
* duplicate handlers to the same `msgName` would be ignored.
* @param msgName
* @param handler
*/
listenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: MsgHandler<Call>): MsgHandler<Call> {
if (!this._msgHandlers) {
this._msgHandlers = new MsgHandlerManager();
}
this._msgHandlers.addHandler(msgName as string, handler);
return handler;
};
/**
* Remove a message handler
*/
unlistenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: Function): void {
if (!this._msgHandlers) {
this._msgHandlers = new MsgHandlerManager();
}
this._msgHandlers.removeHandler(msgName as string, handler);
};
/**
* Remove all handlers from a message
*/
unlistenMsgAll<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg): void {
if (!this._msgHandlers) {
this._msgHandlers = new MsgHandlerManager();
}
this._msgHandlers.removeAllHandlers(msgName as string);
};
}
export enum ConnectionStatus {
Opened = 'OPENED',
Closing = 'CLOSING',
Closed = 'CLOSED'
}

View File

@ -0,0 +1,937 @@
import { ObjectId } from "bson";
import chalk from "chalk";
import * as path from "path";
import { TSBuffer } from 'tsbuffer';
import { ApiService, Counter, Flow, getCustomObjectIdTypes, MsgHandlerManager, MsgService, ParsedServerInput, ServiceMap, ServiceMapUtil, TransportDataUtil } from 'tsrpc-base-client';
import { ApiReturn, ApiServiceDef, BaseServiceType, Logger, LogLevel, ServerInputData, ServiceProto, setLogLevel, TsrpcError, TsrpcErrorType } from 'tsrpc-proto';
import { ApiCallInner } from "../inner/ApiCallInner";
import { InnerConnection } from "../inner/InnerConnection";
import { TerminalColorLogger } from '../models/TerminalColorLogger';
import { ApiCall } from './ApiCall';
import { BaseConnection } from './BaseConnection';
import { MsgCall } from './MsgCall';
/**
* Abstract base class for TSRPC Server.
* Implement on a transportation protocol (like HTTP WebSocket) by extend it.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
export abstract class BaseServer<ServiceType extends BaseServiceType = BaseServiceType>{
/**
* Start the server
* @throws
*/
abstract start(): Promise<void>;
/**
* Stop server immediately, not waiting for the requests ending.
*/
abstract stop(): Promise<void>;
protected _status: ServerStatus = ServerStatus.Closed;
get status(): ServerStatus {
return this._status;
}
// 配置及其衍生项
readonly proto: ServiceProto<ServiceType>;
readonly options: BaseServerOptions<ServiceType>;
readonly tsbuffer: TSBuffer;
readonly serviceMap: ServiceMap;
readonly logger: Logger;
protected _connIdCounter = new Counter(1);
/**
* Flow is a specific concept created by TSRPC family.
* All pre-flow can interrupt latter behaviours.
* All post-flow can NOT interrupt latter behaviours.
*/
readonly flows = {
// Conn Flows
/** After the connection is created */
postConnectFlow: new Flow<BaseConnection<ServiceType>>(),
/** After the connection is disconnected */
postDisconnectFlow: new Flow<{ conn: BaseConnection<ServiceType>, reason?: string }>(),
// Buffer Flows
/**
* Before processing the received data, usually be used to encryption / decryption.
* Return `null | undefined` would ignore the buffer.
*/
preRecvDataFlow: new Flow<{ conn: BaseConnection<ServiceType>, data: string | Uint8Array | object, serviceName?: string }>(),
/**
* Before send out data to network, usually be used to encryption / decryption.
* Return `null | undefined` would not send the buffer.
*/
preSendDataFlow: new Flow<{ conn: BaseConnection<ServiceType>, data: string | Uint8Array | object, call?: ApiCall }>(),
/**
* @deprecated Use `preRecvDataFlow` instead.
*/
preRecvBufferFlow: new Flow<{ conn: BaseConnection<ServiceType>, buf: Uint8Array }>(),
/**
* @deprecated Use `preSendDataFlow` instead.
*/
preSendBufferFlow: new Flow<{ conn: BaseConnection<ServiceType>, buf: Uint8Array, call?: ApiCall }>(),
// ApiCall Flows
/**
* Before a API request is send.
* Return `null | undefined` would cancel the request.
*/
preApiCallFlow: new Flow<ApiCall>(),
/**
* Before return the `ApiReturn` to the client.
* It may be used to change the return value, or return `null | undefined` to abort the request.
*/
preApiReturnFlow: new Flow<{ call: ApiCall, return: ApiReturn<any> }>(),
/**
* After the `ApiReturn` is send.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postApiReturnFlow: new Flow<{ call: ApiCall, return: ApiReturn<any> }>(),
/**
* After the api handler is executed.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postApiCallFlow: new Flow<ApiCall>(),
// MsgCall Flows
/**
* Before handle a `MsgCall`
*/
preMsgCallFlow: new Flow<MsgCall>(),
/**
* After handlers of a `MsgCall` are executed.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postMsgCallFlow: new Flow<MsgCall>(),
/**
* Before send out a message.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
preSendMsgFlow: new Flow<{ conn: BaseConnection<ServiceType>, service: MsgService, msg: any }>(),
/**
* After send out a message.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postSendMsgFlow: new Flow<{ conn: BaseConnection<ServiceType>, service: MsgService, msg: any }>(),
} as const;
// Handlers
private _apiHandlers: { [apiName: string]: ApiHandler<any> | undefined } = {};
// 多个Handler将异步并行执行
private _msgHandlers: MsgHandlerManager = new MsgHandlerManager();
private static _isUncaughtExceptionProcessed = false;
/**
* It makes the `uncaughtException` and `unhandledRejection` not lead to the server stopping.
* @param logger
* @returns
*/
static processUncaughtException(logger: Logger) {
if (this._isUncaughtExceptionProcessed) {
return;
}
this._isUncaughtExceptionProcessed = true;
process.on('uncaughtException', e => {
logger.error('[uncaughtException]', e);
});
process.on('unhandledRejection', e => {
logger.error('[unhandledRejection]', e);
});
}
constructor(proto: ServiceProto<ServiceType>, options: BaseServerOptions<ServiceType>) {
this.proto = proto;
this.options = options;
// @deprecated jsonEnabled
if (this.options.json) {
this.options.jsonEnabled = true;
}
this.tsbuffer = new TSBuffer({
...proto.types,
// Support mongodb/ObjectId
...getCustomObjectIdTypes(ObjectId)
}, {
strictNullChecks: this.options.strictNullChecks
});
this.serviceMap = ServiceMapUtil.getServiceMap(proto);
this.logger = this.options.logger;
setLogLevel(this.logger, this.options.logLevel);
// Process uncaught exception, so that Node.js process would not exit easily
BaseServer.processUncaughtException(this.logger);
// default flows onError handler
this._setDefaultFlowOnError();
}
protected _setDefaultFlowOnError() {
// API Flow Error: return [InternalServerError]
this.flows.preApiCallFlow.onError = (e, call) => {
if (e instanceof TsrpcError) {
call.error(e)
}
else {
this.onInternalServerError(e, call)
}
};
this.flows.postApiCallFlow.onError = (e, call) => {
if (!call.return) {
if (e instanceof TsrpcError) {
call.error(e)
}
else {
this.onInternalServerError(e, call)
}
}
else {
call.logger.error('postApiCallFlow Error:', e);
}
};
this.flows.preApiReturnFlow.onError = (e, last) => {
last.call['_return'] = undefined;
if (e instanceof TsrpcError) {
last.call.error(e)
}
else {
this.onInternalServerError(e, last.call)
}
}
this.flows.postApiReturnFlow.onError = (e, last) => {
if (!last.call.return) {
if (e instanceof TsrpcError) {
last.call.error(e)
}
else {
this.onInternalServerError(e, last.call)
}
}
}
}
protected _pendingApiCallNum = 0;
// #region receive buffer process flow
/**
* Process the buffer, after the `preRecvBufferFlow`.
*/
async _onRecvData(conn: BaseConnection<ServiceType>, data: string | Uint8Array | object, serviceName?: string) {
// 非 OPENED 状态 停止接受新的请求
if (!(conn instanceof InnerConnection) && this.status !== ServerStatus.Opened) {
return;
}
// debugBuf log
if (this.options.debugBuf) {
if (typeof data === 'string') {
conn.logger?.debug(`[RecvText] length=${data.length}`, data);
}
else if (data instanceof Uint8Array) {
conn.logger?.debug(`[RecvBuf] length=${data.length}`, data);
}
else {
conn.logger?.debug('[RecvJSON]', data);
}
}
// jsonEnabled 未启用,不支持文本请求
if (typeof data === 'string' && !this.options.jsonEnabled) {
this.onInputDataError('JSON mode is not enabled, please use binary instead.', conn, data);
return;
}
let pre = await this.flows.preRecvDataFlow.exec({ conn: conn, data: data, serviceName: serviceName }, conn.logger);
if (!pre) {
conn.logger.debug('[preRecvDataFlow] Canceled');
return;
}
data = pre.data;
serviceName = pre.serviceName;
// @deprecated preRecvBuffer
if (data instanceof Uint8Array) {
let preBuf = await this.flows.preRecvBufferFlow.exec({ conn: conn, buf: data }, conn.logger);
if (!preBuf) {
conn.logger.debug('[preRecvBufferFlow] Canceled');
return;
}
data = preBuf.buf;
}
// Parse Call
let opInput = this._parseServerInput(this.tsbuffer, this.serviceMap, data, serviceName);
if (!opInput.isSucc) {
this.onInputDataError(opInput.errMsg, conn, data);
return;
}
let call = conn.makeCall(opInput.result);
if (call.type === 'api') {
await this._handleApiCall(call);
}
else {
await this._onMsgCall(call);
}
}
protected async _handleApiCall(call: ApiCall) {
++this._pendingApiCallNum;
await this._onApiCall(call);
if (--this._pendingApiCallNum === 0) {
this._gracefulStop?.rs();
}
}
protected async _onApiCall(call: ApiCall) {
let timeoutTimer = this.options.apiTimeout ? setTimeout(() => {
if (!call.return) {
call.error('Server Timeout', {
code: 'SERVER_TIMEOUT',
type: TsrpcErrorType.ServerError
})
}
timeoutTimer = undefined;
}, this.options.apiTimeout) : undefined;
// Pre Flow
let preFlow = await this.flows.preApiCallFlow.exec(call, call.logger);
if (!preFlow) {
if (timeoutTimer) {
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
}
call.logger.debug('[preApiCallFlow] Canceled');
return;
}
call = preFlow;
// exec ApiCall
call.logger.log('[ApiReq]', this.options.logReqBody ? call.req : '');
let { handler } = await this.getApiHandler(call.service, this._delayImplementApiPath, call.logger);
// exec API handler
if (handler) {
try {
await handler(call);
}
catch (e: any) {
if (e instanceof TsrpcError) {
call.error(e);
}
else {
this.onInternalServerError(e, call);
}
}
}
// 未找到ApiHandler且未进行任何输出
else {
call.error(`Unhandled API: ${call.service.name}`, { code: 'UNHANDLED_API', type: TsrpcErrorType.ServerError });
}
// Post Flow
await this.flows.postApiCallFlow.exec(call, call.logger);
if (timeoutTimer) {
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
}
// Destroy call
// if (!call.return) {
// this.onInternalServerError({ message: 'API not return anything' }, call);
// }
}
protected async _onMsgCall(call: MsgCall) {
// 收到Msg即可断开连接短连接
if (call.conn.type === 'SHORT') {
call.conn.close();
}
// Pre Flow
let preFlow = await this.flows.preMsgCallFlow.exec(call, call.logger);
if (!preFlow) {
call.logger.debug('[preMsgCallFlow]', 'Canceled')
return;
}
call = preFlow;
// MsgHandler
this.options.logMsg && call.logger.log('[RecvMsg]', call.msg);
let promises = [
// Conn Handlers
...(call.conn['_msgHandlers']?.forEachHandler(call.service.name, call.logger, call) ?? []),
// Server Handlers
this._msgHandlers.forEachHandler(call.service.name, call.logger, call)
];
if (!promises.length) {
this.logger.debug('[UNHANDLED_MSG]', call.service.name);
}
else {
await Promise.all(promises);
}
// Post Flow
await this.flows.postMsgCallFlow.exec(call, call.logger);
}
// #endregion
// #region Api/Msg handler register
/**
* Associate a `ApiHandler` to a specific `apiName`.
* So that when `ApiCall` is receiving, it can be handled correctly.
* @param apiName
* @param handler
*/
implementApi<Api extends keyof ServiceType['api'], Call extends ApiCall<ServiceType['api'][Api]['req'], ServiceType['api'][Api]['res']>>(apiName: Api, handler: ApiHandler<Call>): void {
if (this._apiHandlers[apiName as string]) {
throw new Error('Already exist handler for API: ' + apiName);
}
this._apiHandlers[apiName as string] = handler;
this.logger.log(`API implemented succ: [${apiName}]`);
};
/** 用于延迟注册 API */
protected _delayImplementApiPath?: string;
/**
* Auto call `imeplementApi` by traverse the `apiPath` and find all matched `PtlXXX` and `ApiXXX`.
* It is matched by checking whether the relative path and name of an API is consistent to the service name in `serviceProto`.
* Notice that the name prefix of protocol is `Ptl`, of API is `Api`.
* For example, `protocols/a/b/c/PtlTest` is matched to `api/a/b/c/ApiTest`.
* @param apiPath Absolute path or relative path to `process.cwd()`.
* @returns
*/
async autoImplementApi(apiPath: string, delay?: boolean): Promise<{ succ: string[], fail: string[] }> {
let apiServices = Object.values(this.serviceMap.apiName2Service) as ApiServiceDef[];
let output: { succ: string[], fail: string[] } = { succ: [], fail: [] };
if (delay) {
this._delayImplementApiPath = apiPath;
return output;
}
for (let svc of apiServices) {
//get api handler
let { handler } = await this.getApiHandler(svc, apiPath, this.logger)
if (!handler) {
output.fail.push(svc.name);
continue;
}
this.implementApi(svc.name, handler);
output.succ.push(svc.name);
}
if (output.fail.length) {
this.logger.error(chalk.red(`${output.fail.length} API implemented failed: ` + output.fail.map(v => chalk.cyan.underline(v)).join(' ')))
}
return output;
}
async getApiHandler(svc: ApiServiceDef, apiPath?: string, logger?: Logger): Promise<{ handler: ApiHandler, errMsg?: undefined } | { handler?: undefined, errMsg: string }> {
if (this._apiHandlers[svc.name]) {
return { handler: this._apiHandlers[svc.name]! };
}
if (!apiPath) {
return { errMsg: `Api not implemented: ${svc.name}` };
}
// get api last name
let match = svc.name.match(/^(.+\/)*(.+)$/);
if (!match) {
logger?.error('Invalid apiName: ' + svc.name);
return { errMsg: `Invalid api name: ${svc.name}` };
}
let handlerPath = match[1] || '';
let handlerName = match[2];
// try import
let modulePath = path.resolve(apiPath, handlerPath, 'Api' + handlerName);
try {
var handlerModule = await import(modulePath);
}
catch (e: unknown) {
this.logger.error(chalk.red(`Implement API ${chalk.cyan.underline(`${svc.name}`)} failed:`), e);
return { errMsg: (e as Error).message };
}
// 优先 default其次 ApiName 同名
let handler = handlerModule.default ?? handlerModule['Api' + handlerName];
if (handler) {
return { handler: handler };
}
else {
return { errMsg: `Missing 'export Api${handlerName}' or 'export default' in: ${modulePath}` }
}
}
/**
* Add a message handler,
* duplicate handlers to the same `msgName` would be ignored.
* @param msgName
* @param handler
*/
listenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: MsgHandler<Call>): MsgHandler<Call> {
this._msgHandlers.addHandler(msgName as string, handler);
return handler;
};
/**
* Remove a message handler
*/
unlistenMsg<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg, handler: Function): void {
this._msgHandlers.removeHandler(msgName as string, handler);
};
/**
* Remove all handlers from a message
*/
unlistenMsgAll<Msg extends keyof ServiceType['msg'], Call extends MsgCall<ServiceType['msg'][Msg]>>(msgName: Msg): void {
this._msgHandlers.removeAllHandlers(msgName as string);
};
// #endregion
/**
* Event when the server cannot parse input buffer to api/msg call.
* By default, it will return "Input Data Error" .
*/
async onInputDataError(errMsg: string, conn: BaseConnection<ServiceType>, data: string | Uint8Array | object) {
if (this.options.debugBuf) {
if (typeof data === 'string') {
conn.logger.error(`[InputDataError] ${errMsg} length = ${data.length}`, data)
}
else if (data instanceof Uint8Array) {
conn.logger.error(`[InputBufferError] ${errMsg} length = ${data.length}`, data.subarray(0, 16))
}
else {
conn.logger.error(`[InputJsonError] ${errMsg} `, data)
}
}
const message = data instanceof Uint8Array ? `Invalid request buffer, please check the version of service proto.` : errMsg;
// Short conn, send apiReturn with error
if (conn.type === 'SHORT') {
// Return API Error
let opEncode = ApiCall.encodeApiReturn(this.tsbuffer, {
type: 'api',
name: '?',
id: 0,
reqSchemaId: '?',
resSchemaId: '?'
}, {
isSucc: false,
err: new TsrpcError({
message: message,
type: TsrpcErrorType.ServerError,
code: 'INPUT_DATA_ERR'
})
}, conn.dataType)
if (opEncode.isSucc) {
let opSend = await conn.sendData(opEncode.output);
if (opSend.isSucc) {
return;
}
}
}
conn.close(message);
}
/**
* Event when a uncaught error (except `TsrpcError`) is throwed.
* By default, it will return a `TsrpcError` with message "Internal server error".
* If `returnInnerError` is `true`, the original error would be returned as `innerErr` property.
*/
onInternalServerError(err: { message: string, stack?: string, name?: string }, call: ApiCall) {
call.logger.error(err);
call.error('Internal Server Error', {
code: 'INTERNAL_ERR',
type: TsrpcErrorType.ServerError,
innerErr: call.conn.server.options.returnInnerError ? err.message : undefined
});
}
protected _gracefulStop?: {
rs: () => void
};
/**
* Stop the server gracefully.
* Wait all API requests finished and then stop the server.
* @param maxWaitTime - The max time(ms) to wait before force stop the server.
* `undefined` and `0` means unlimited time.
*/
async gracefulStop(maxWaitTime?: number) {
if (this._status !== ServerStatus.Opened) {
throw new Error(`Cannot gracefulStop when server status is '${this._status}'.`);
}
this.logger.log('[GracefulStop] Start graceful stop, waiting all ApiCall finished...')
this._status = ServerStatus.Closing;
let promiseWaitApi = new Promise<void>(rs => {
this._gracefulStop = {
rs: rs
};
});
return new Promise<void>(rs => {
let maxWaitTimer: ReturnType<typeof setTimeout> | undefined;
if (maxWaitTime) {
maxWaitTimer = setTimeout(() => {
maxWaitTimer = undefined;
if (this._gracefulStop) {
this._gracefulStop = undefined;
this.logger.log('Graceful stop timeout, stop the server directly.');
this.stop().then(() => { rs() });
}
}, maxWaitTime);
}
promiseWaitApi.then(() => {
this.logger.log('All ApiCall finished, continue stop server.');
if (maxWaitTimer) {
clearTimeout(maxWaitTimer);
maxWaitTimer = undefined;
}
if (this._gracefulStop) {
this._gracefulStop = undefined;
this.stop().then(() => { rs() });
}
})
})
}
/**
* Execute API function through the inner connection, which is useful for unit test.
*
* **NOTICE**
* The `req` and return value is native JavaScript object which is not compatible to JSON. (etc. ArrayBuffer, Date, ObjectId)
* If you are using pure JSON as transfering, you may need use `callApiByJSON`.
* @param apiName
* @param req
* @param options
*/
callApi<T extends keyof ServiceType['api']>(apiName: T, req: ServiceType['api'][T]['req']): Promise<ApiReturn<ServiceType['api'][T]['res']>> {
return new Promise(rs => {
// 确认是哪个Service
let service = this.serviceMap.apiName2Service[apiName as string];
if (!service) {
let errMsg = `Cannot find service: ${apiName}`;
this.logger.warn(`[callApi]`, errMsg);
rs({ isSucc: false, err: new TsrpcError(errMsg, { type: TsrpcErrorType.ServerError, code: 'ERR_API_NAME' }) });
return;
}
let conn = new InnerConnection({
dataType: 'json',
server: this,
id: '' + this._connIdCounter.getNext(),
ip: '',
return: {
type: 'raw',
rs: rs
}
});
let call = new ApiCallInner({
conn: conn,
req: req,
service: service
});
this._handleApiCall(call);
})
}
/**
* Like `server.callApi`, but both input and output are pure JSON object,
* which can be `JSON.stringify()` and `JSON.parse()` directly.
* Types that not compatible to JSON, would be encoded and decoded automatically.
* @param apiName - The same with `server.callApi`, may be parsed from the URL.
* @param jsonReq - Request data in pure JSON
* @returns Encoded `ApiReturn<Res>` in pure JSON
*/
/**
* Process JSON request by inner proxy, this is useful when you are porting to cloud function services.
* Both the input and output is pure JSON, ArrayBuffer/Date/ObjectId are encoded to string automatically.
* @param apiName - Parsed from URL
* @param req - Pure JSON
* @returns - Pure JSON
*/
async inputJSON(apiName: string, req: object): Promise<ApiReturn<object>> {
if (apiName.startsWith('/')) {
apiName = apiName.slice(1);
}
if (!this.serviceMap.apiName2Service[apiName]) {
return {
isSucc: false,
err: new TsrpcError(`Invalid service name: ${apiName}`, {
type: TsrpcErrorType.ServerError,
code: 'INPUT_DATA_ERR'
})
};
}
return new Promise(rs => {
let conn = new InnerConnection({
dataType: 'json',
server: this,
id: '' + this._connIdCounter.getNext(),
ip: '',
return: {
type: 'json',
rs: rs
}
});
this._onRecvData(conn, req, apiName);
})
}
/**
* Process input buffer by inner proxy, this is useful when you are porting to cloud function services.
* @param buf Input buffer (may be sent by TSRPC client)
* @returns Response buffer
*/
inputBuffer(buf: Uint8Array): Promise<Uint8Array> {
return new Promise(rs => {
let conn = new InnerConnection({
dataType: 'buffer',
server: this,
id: '' + this._connIdCounter.getNext(),
ip: '',
return: {
type: 'buffer',
rs: rs
}
});
this._onRecvData(conn, buf);
})
}
protected _parseServerInput(tsbuffer: TSBuffer, serviceMap: ServiceMap, data: string | Uint8Array | object, serviceName?: string): { isSucc: true, result: ParsedServerInput } | { isSucc: false, errMsg: string } {
if (data instanceof Uint8Array) {
let opServerInputData = TransportDataUtil.tsbuffer.decode(data, 'ServerInputData');
if (!opServerInputData.isSucc) {
return opServerInputData;
}
let serverInput = opServerInputData.value as ServerInputData;
// 确认是哪个Service
let service = serviceMap.id2Service[serverInput.serviceId];
if (!service) {
return { isSucc: false, errMsg: `Cannot find service ID: ${serverInput.serviceId}` }
}
// 解码Body
if (service.type === 'api') {
let opReq = tsbuffer.decode(serverInput.buffer, service.reqSchemaId);
return opReq.isSucc ? {
isSucc: true,
result: {
type: 'api',
service: service,
req: opReq.value,
sn: serverInput.sn
}
} : opReq
}
else {
let opMsg = tsbuffer.decode(serverInput.buffer, service.msgSchemaId);
return opMsg.isSucc ? {
isSucc: true,
result: {
type: 'msg',
service: service,
msg: opMsg.value
}
} : opMsg;
}
}
else {
let json: object;
if (typeof data === 'string') {
try {
json = JSON.parse(data);
}
catch (e: any) {
return { isSucc: false, errMsg: `Input is not a valid JSON string: ${e.message}` };
}
}
else {
json = data;
}
let body: any;
let sn: number | undefined;
// Parse serviceName / body / sn
let service: ApiService | MsgService | undefined;
const oriServiceName = serviceName;
if (serviceName == undefined) {
if (!Array.isArray(json)) {
return { isSucc: false, errMsg: `Invalid request format: unresolved service name.` };
}
serviceName = json[0] as string;
body = json[1];
sn = json[2];
}
else {
body = json;
}
// Get Service
service = serviceMap.apiName2Service[serviceName] ?? serviceMap.msgName2Service[serviceName];
if (!service) {
let errMsg = `Invalid service name: ${chalk.cyan.underline(serviceName)}`;
// 可能是 JSON 模式下jsonHostPath 未设置妥当的原因,此时给予友好提示
if (oriServiceName) {
// TODO
}
return { isSucc: false, errMsg: errMsg };
}
// Decode
if (service.type === 'api') {
let op = tsbuffer.decodeJSON(body, service.reqSchemaId);
if (!op.isSucc) {
return op;
}
return {
isSucc: true,
result: {
type: 'api',
service: service,
sn: sn,
req: op.value
}
};
}
else {
let op = tsbuffer.decodeJSON(body, service.msgSchemaId);
if (!op.isSucc) {
return op;
}
return {
isSucc: true,
result: {
type: 'msg',
service: service,
msg: op.value
}
}
}
}
}
}
export interface BaseServerOptions<ServiceType extends BaseServiceType> {
/**
* Whether to enable JSON compatible mode.
* When it is true, it can be compatible with typical HTTP JSON request (like RESTful API).
*
* @remarks
* The JSON request methods are:
*
* 1. Add `Content-type: application/json` to request header.
* 2. HTTP request is: `POST /{jsonUrlPath}/{apiName}`.
* 3. POST body is JSON string.
* 4. The response body is JSON string also.
*
* NOTICE: Buffer type are not supported due to JSON not support them.
* For security and efficient reason, we strongly recommend you use binary encoded transportation.
*
* @defaultValue `false`
*/
json: boolean,
/** @deprecated Use `json` instead. */
jsonEnabled?: boolean,
// TSBuffer相关
/**
* Whether to strictly distinguish between `null` and `undefined` when encoding, decoding, and type checking.
* @defaultValue false
*/
strictNullChecks: boolean,
/**
* Timeout for processing an `ApiCall`(ms)
* `0` and `undefined` means unlimited time
* @defaultValue 30000
*/
apiTimeout: number | undefined,
// LOG相关
/**
* Logger for processing log
* @defaultValue `new TerminalColorLogger()` (print to console with color)
*/
logger: Logger;
/**
* The minimum log level of `logger`
* @defaultValue `debug`
*/
logLevel: LogLevel;
/**
* Whethere to print API request body into log (may increase log size)
* @defaultValue `true`
*/
logReqBody: boolean;
/**
* Whethere to print API response body into log (may increase log size)
* @defaultValue `true`
*/
logResBody: boolean;
/**
* Whethere to print `[SendMsg]` and `[RecvMsg]` log into log
* @defaultValue `true`
*/
logMsg: boolean;
/**
* If `true`, all sent and received raw buffer would be print into the log.
* It may be useful when you do something for buffer encryption/decryption, and want to debug them.
*/
debugBuf?: boolean;
/**
* When uncaught error throwed,
* whether to return the original error as a property `innerErr`.
* (May include some sensitive information, suggests set to `false` in production environment.)
* @defaultValue It depends on environment variable `NODE_ENV`.
* If `NODE_ENV` equals to `production`, the default value is `false`, otherwise is `true`.
*/
returnInnerError: boolean;
}
export const defaultBaseServerOptions: BaseServerOptions<any> = {
json: false,
strictNullChecks: false,
apiTimeout: 30000,
logger: new TerminalColorLogger,
logLevel: 'debug',
logReqBody: true,
logResBody: true,
logMsg: true,
returnInnerError: process.env['NODE_ENV'] !== 'production'
}
export type ApiHandler<Call extends ApiCall = ApiCall> = (call: Call) => void | Promise<void>;
export type MsgHandler<Call extends MsgCall = MsgCall> = (call: Call) => void | Promise<void>;
export enum ServerStatus {
Opening = 'OPENING',
Opened = 'OPENED',
Closing = 'CLOSING',
Closed = 'CLOSED',
}

View File

@ -0,0 +1,30 @@
import { MsgService } from "tsrpc-base-client";
import { BaseServiceType } from "tsrpc-proto";
import { PrefixLogger } from "../models/PrefixLogger";
import { BaseCall, BaseCallOptions } from "./BaseCall";
export interface MsgCallOptions<Msg, ServiceType extends BaseServiceType> extends BaseCallOptions<ServiceType> {
service: MsgService,
msg: Msg
}
/**
* A call request by `client.sendMsg()`
* @typeParam Msg - Type of the message
* @typeParam ServiceType - The same `ServiceType` to server, it is used for code auto hint.
*/
export abstract class MsgCall<Msg = any, ServiceType extends BaseServiceType = any> extends BaseCall<ServiceType> {
readonly type = 'msg' as const;
readonly service!: MsgService;
readonly msg: Msg;
constructor(options: MsgCallOptions<Msg, ServiceType>, logger?: PrefixLogger) {
super(options, logger ?? new PrefixLogger({
logger: options.conn.logger,
prefixs: [`[Msg:${options.service.name}]`]
}));
this.msg = options.msg;
}
}

View File

@ -0,0 +1,28 @@
import { ApiReturn, BaseServiceType, TsrpcErrorType } from 'tsrpc-proto';
import { ApiCall, ApiCallOptions } from '../base/ApiCall';
import { HttpConnection } from './HttpConnection';
export interface ApiCallHttpOptions<Req, ServiceType extends BaseServiceType> extends ApiCallOptions<Req, ServiceType> {
conn: HttpConnection<ServiceType>;
}
export class ApiCallHttp<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends ApiCall<Req, Res, ServiceType> {
readonly conn!: HttpConnection<ServiceType>;
constructor(options: ApiCallHttpOptions<Req, ServiceType>) {
super(options);
}
protected async _sendReturn(ret: ApiReturn<Res>): Promise<{ isSucc: true } | { isSucc: false, errMsg: string }> {
if (this.conn.dataType === 'text') {
if (ret.isSucc) {
this.conn.httpRes.statusCode = 200;
}
else {
this.conn.httpRes.statusCode = ret.err.type === TsrpcErrorType.ApiError ? 200 : 500;
}
}
return super._sendReturn(ret);
}
}

View File

@ -0,0 +1,90 @@
import * as http from "http";
import { ParsedServerInput } from "tsrpc-base-client";
import { BaseServiceType } from "tsrpc-proto";
import { ApiCall } from "../base/ApiCall";
import { BaseConnection, BaseConnectionOptions, ConnectionStatus } from '../base/BaseConnection';
import { PrefixLogger } from "../models/PrefixLogger";
import { ApiCallHttp } from "./ApiCallHttp";
import { HttpServer } from './HttpServer';
import { MsgCallHttp } from "./MsgCallHttp";
export interface HttpConnectionOptions<ServiceType extends BaseServiceType> extends BaseConnectionOptions<ServiceType> {
server: HttpServer<ServiceType>,
httpReq: http.IncomingMessage,
httpRes: http.ServerResponse,
}
export class HttpConnection<ServiceType extends BaseServiceType = any> extends BaseConnection<ServiceType> {
readonly type = 'SHORT';
protected readonly ApiCallClass = ApiCallHttp;
protected readonly MsgCallClass = MsgCallHttp;
readonly httpReq: http.IncomingMessage;
readonly httpRes: http.ServerResponse;
readonly server!: HttpServer<ServiceType>;
/**
* Whether the transportation of the connection is JSON encoded instead of binary encoded.
*/
readonly isJSON: boolean | undefined;
/**
* In short connection, one connection correspond one call.
* It may be `undefined` when the request data is not fully received yet.
*/
call?: ApiCallHttp | MsgCallHttp;
constructor(options: HttpConnectionOptions<ServiceType>) {
super(options, new PrefixLogger({
logger: options.server.logger,
prefixs: [`${options.ip} #${options.id}`]
}));
this.httpReq = options.httpReq;
this.httpRes = options.httpRes;
}
public get status(): ConnectionStatus {
if (this.httpRes.socket?.writableFinished) {
return ConnectionStatus.Closed;
}
else if (this.httpRes.socket?.writableEnded) {
return ConnectionStatus.Closing;
}
else {
return ConnectionStatus.Opened;
}
}
protected async doSendData(data: string | Uint8Array, call?: ApiCall): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> {
if (typeof data === 'string') {
this.httpRes.setHeader('Content-Type', 'application/json; charset=utf-8');
}
this.httpRes.end(typeof data === 'string' ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength));
return { isSucc: true }
}
/**
* Close the connection, the reason would be attached to response header `X-TSRPC-Close-Reason`.
*/
close(reason?: string) {
if (this.status !== ConnectionStatus.Opened) {
return;
}
// 有Reason代表是异常关闭
if (reason) {
this.logger.warn(this.httpReq.method, this.httpReq.url, reason);
}
reason && this.httpRes.setHeader('X-TSRPC-Close-Reason', reason);
this.httpRes.end();
}
// HTTP Server 一个conn只有一个call对应关联之
makeCall(input: ParsedServerInput): ApiCallHttp | MsgCallHttp {
let call = super.makeCall(input) as ApiCallHttp | MsgCallHttp;
this.call = call;
return call;
}
}

View File

@ -0,0 +1,241 @@
import * as http from "http";
import { BaseServiceType, ServiceProto } from 'tsrpc-proto';
import { HttpUtil } from '../../models/HttpUtil';
import { TSRPC_VERSION } from "../../models/version";
import { BaseServer, BaseServerOptions, defaultBaseServerOptions, ServerStatus } from '../base/BaseServer';
import { HttpConnection } from './HttpConnection';
/**
* TSRPC Server, based on HTTP connection.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
export class HttpServer<ServiceType extends BaseServiceType = any> extends BaseServer<ServiceType>{
readonly options!: HttpServerOptions<ServiceType>;
constructor(proto: ServiceProto<ServiceType>, options?: Partial<HttpServerOptions<ServiceType>>) {
super(proto, {
...defaultHttpServerOptions,
...options
});
// 确保 jsonHostPath 以 / 开头和结尾
this.options.jsonHostPath = this.options.jsonHostPath ?
(this.options.jsonHostPath.startsWith('/') ? '' : '/') + this.options.jsonHostPath + (this.options.jsonHostPath.endsWith('/') ? '' : '/')
: '/';
}
/** Native `http.Server` of NodeJS */
httpServer?: http.Server;
/**
* {@inheritDoc BaseServer.start}
*/
start(): Promise<void> {
if (this.httpServer) {
throw new Error('Server already started');
}
return new Promise(rs => {
this._status = ServerStatus.Opening;
this.logger.log(`Starting HTTP server ...`);
this.httpServer = http.createServer((httpReq, httpRes) => {
if (this.status !== ServerStatus.Opened) {
httpRes.statusCode = 503;
httpRes.end();
return;
}
let ip = HttpUtil.getClientIp(httpReq);
httpRes.statusCode = 200;
httpRes.setHeader('X-Powered-By', `TSRPC ${TSRPC_VERSION}`);
if (this.options.cors) {
httpRes.setHeader('Access-Control-Allow-Origin', this.options.cors);
httpRes.setHeader('Access-Control-Allow-Headers', 'Content-Type,*');
if (this.options.corsMaxAge) {
httpRes.setHeader('Access-Control-Max-Age', '' + this.options.corsMaxAge);
}
if (httpReq.method === 'OPTIONS') {
httpRes.writeHead(200);
httpRes.end();
return;
}
};
let chunks: Buffer[] = [];
httpReq.on('data', data => {
chunks.push(data);
});
let conn: HttpConnection<ServiceType> | undefined;
httpReq.on('end', async () => {
let isJSON = this.options.jsonEnabled && httpReq.headers["content-type"]?.toLowerCase().includes('application/json')
&& httpReq.method === 'POST' && httpReq.url?.startsWith(this.options.jsonHostPath);
conn = new HttpConnection({
server: this,
id: '' + this._connIdCounter.getNext(),
ip: ip,
httpReq: httpReq,
httpRes: httpRes,
dataType: isJSON ? 'text' : 'buffer'
});
await this.flows.postConnectFlow.exec(conn, conn.logger);
let buf = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks);
if (conn.dataType === 'text') {
let url = conn.httpReq.url!;
let urlEndPos = url.indexOf('?');
if (urlEndPos > -1) {
url = url.slice(0, urlEndPos);
}
let serviceName = url.slice(this.options.jsonHostPath.length);
this._onRecvData(conn, buf.toString(), serviceName);
}
else {
this._onRecvData(conn, buf);
}
});
// 处理连接异常关闭的情况
httpRes.on('close', async () => {
// 客户端Abort
if (httpReq.aborted) {
if (conn) {
if (conn.call) {
conn.call.logger.log('[ReqAborted]');
}
else {
conn.logger.log('[ReqAborted]');
}
}
else {
this.logger.log('[ReqAborted]', {
url: httpReq.url,
method: httpReq.method,
ip: ip,
chunksLength: chunks.length,
chunksSize: chunks.sum(v => v.byteLength),
reqComplete: httpReq.complete,
headers: httpReq.rawHeaders
});
}
}
// 非Abort异常中断直到连接关闭Client也未endConn未生成
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)
}

View File

@ -0,0 +1,16 @@
import { BaseServiceType } from "tsrpc-proto";
import { MsgCall, MsgCallOptions } from "../base/MsgCall";
import { HttpConnection } from "./HttpConnection";
export interface MsgCallHttpOptions<Msg, ServiceType extends BaseServiceType> extends MsgCallOptions<Msg, ServiceType> {
conn: HttpConnection<ServiceType>;
}
export class MsgCallHttp<Msg = any, ServiceType extends BaseServiceType = any> extends MsgCall<Msg, ServiceType> {
readonly conn!: HttpConnection<ServiceType>;
constructor(options: MsgCallHttpOptions<Msg, ServiceType>) {
super(options);
}
}

View File

@ -0,0 +1,31 @@
import { ApiReturn, BaseServiceType } from 'tsrpc-proto';
import { ApiCall, ApiCallOptions } from '../base/ApiCall';
import { InnerConnection } from './InnerConnection';
export interface ApiCallInnerOptions<Req, ServiceType extends BaseServiceType> extends ApiCallOptions<Req, ServiceType> {
conn: InnerConnection<ServiceType>;
}
export class ApiCallInner<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends ApiCall<Req, Res, ServiceType> {
readonly conn!: InnerConnection<ServiceType>;
constructor(options: ApiCallInnerOptions<Req, ServiceType>) {
super(options);
}
protected async _sendReturn(ret: ApiReturn<Res>): Promise<{ isSucc: true } | { isSucc: false, errMsg: string }> {
if (this.conn.return.type === 'raw') {
// Validate Res
if (ret.isSucc) {
let resValidate = this.server.tsbuffer.validate(ret.res, this.service.resSchemaId);
if (!resValidate.isSucc) {
return resValidate;
}
}
return this.conn.sendData(ret);
}
return super._sendReturn(ret);
}
}

View File

@ -0,0 +1,79 @@
import { ApiReturn, TsrpcError, TsrpcErrorType } from "tsrpc-proto";
import { ApiCall, BaseConnection, BaseServiceType, PrefixLogger, TransportDataUtil } from "../..";
import { BaseConnectionOptions, ConnectionStatus } from "../base/BaseConnection";
import { ApiCallInner } from "./ApiCallInner";
export interface InnerConnectionOptions<ServiceType extends BaseServiceType> extends BaseConnectionOptions<ServiceType> {
return: {
type: 'raw' | 'json',
rs: (ret: ApiReturn<any>) => void;
} | {
type: 'buffer',
rs: (ret: Uint8Array) => void;
}
}
/**
* Server can `callApi` it self by using this inner connection
*/
export class InnerConnection<ServiceType extends BaseServiceType = any> extends BaseConnection<ServiceType> {
readonly type = 'SHORT';
protected readonly ApiCallClass = ApiCallInner;
protected readonly MsgCallClass = null as any;
return!: InnerConnectionOptions<any>['return'];
constructor(options: InnerConnectionOptions<ServiceType>) {
super(options, new PrefixLogger({
logger: options.server.logger,
prefixs: [`Inner #${options.id}`]
}));
this.return = options.return;
}
private _status: ConnectionStatus = ConnectionStatus.Opened;
get status(): ConnectionStatus {
return this._status;
}
close(reason?: string): void {
this.doSendData({
isSucc: false,
err: new TsrpcError(reason ?? 'Internal Server Error', {
type: TsrpcErrorType.ServerError,
code: 'CONN_CLOSED',
reason: reason
})
});
}
protected async doSendData(data: Uint8Array | ApiReturn<any>, call?: ApiCall): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> {
this._status = ConnectionStatus.Closed;
if (this.return.type === 'buffer') {
if (!(data instanceof Uint8Array)) {
// encode tsrpc error
if (!data.isSucc) {
let op = TransportDataUtil.tsbuffer.encode({
error: data.err
}, 'ServerOutputData');
if (op.isSucc) {
return this.doSendData(op.buf, call);
}
}
return { isSucc: false, errMsg: 'Error data type' };
}
this.return.rs(data);
return { isSucc: true }
}
else {
if (data instanceof Uint8Array) {
return { isSucc: false, errMsg: 'Error data type' };
}
this.return.rs(data);
return { isSucc: true }
}
}
}

View File

@ -0,0 +1,41 @@
import { Logger } from 'tsrpc-proto';
export interface PrefixLoggerOptions {
logger: Logger
prefixs: (string | (() => string))[];
}
/**
* Auto add prefix using existed `Logger`
*/
export class PrefixLogger implements Logger {
readonly logger: PrefixLoggerOptions['logger'];
readonly prefixs: PrefixLoggerOptions['prefixs'];
constructor(options: PrefixLoggerOptions) {
this.logger = options.logger;
this.prefixs = options.prefixs;
}
getPrefix(): string[] {
return this.prefixs.map(v => typeof v === 'string' ? v : v());
}
log(...args: any[]) {
this.logger.log(...this.getPrefix().concat(args));
}
debug(...args: any[]) {
this.logger.debug(...this.getPrefix().concat(args));
}
warn(...args: any[]) {
this.logger.warn(...this.getPrefix().concat(args));
}
error(...args: any[]) {
this.logger.error(...this.getPrefix().concat(args));
}
}

View File

@ -0,0 +1,54 @@
import chalk from "chalk";
import { Logger } from "tsrpc-proto";
export interface TerminalColorLoggerOptions {
/**
* Process ID prefix
* @defaultValue `process.pid`
*/
pid: string,
/**
* `undefined` represents not print time
* @defaultValue 'yyyy-MM-dd hh:mm:ss'
*/
timeFormat?: string
}
/**
* Print log to terminal, with color.
*/
export class TerminalColorLogger implements Logger {
options: TerminalColorLoggerOptions = {
pid: process.pid.toString(),
timeFormat: 'yyyy-MM-dd hh:mm:ss'
}
private _pid: string;
constructor(options?: Partial<TerminalColorLoggerOptions>) {
Object.assign(this.options, options);
this._pid = this.options.pid ? `<${this.options.pid}> ` : '';
}
private _time(): string {
return this.options.timeFormat ? new Date().format(this.options.timeFormat) : '';
}
debug(...args: any[]) {
console.debug.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.cyan('[DEBUG]'), ...args);
}
log(...args: any[]) {
console.log.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.green('[INFO]'), ...args);
}
warn(...args: any[]) {
console.warn.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.yellow('[WARN]'), ...args);
}
error(...args: any[]) {
console.error.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.red('[ERROR]'), ...args);
}
}

View File

@ -0,0 +1,27 @@
import { ApiReturn, BaseServiceType } from 'tsrpc-proto';
import { ApiCall, ApiCallOptions, SendReturnMethod } from '../base/ApiCall';
import { ConnectionStatus } from '../base/BaseConnection';
import { WsConnection } from './WsConnection';
export interface ApiCallWsOptions<Req, ServiceType extends BaseServiceType> extends ApiCallOptions<Req, ServiceType> {
conn: WsConnection<ServiceType>
}
export class ApiCallWs<Req = any, Res = any, ServiceType extends BaseServiceType = any> extends ApiCall<Req, Res, ServiceType> {
readonly conn!: WsConnection<ServiceType>;
constructor(options: ApiCallWsOptions<Req, ServiceType>) {
super(options);
}
protected async _prepareReturn(ret: ApiReturn<Res>): Promise<void> {
if (this.conn.status !== ConnectionStatus.Opened) {
this.logger.error('[SendReturnErr]', 'WebSocket is not opened', ret);
this._return = ret;
return;
}
return super._prepareReturn(ret);
}
}

View File

@ -0,0 +1,16 @@
import { BaseServiceType } from "tsrpc-proto";
import { MsgCall, MsgCallOptions } from "../base/MsgCall";
import { WsConnection } from "./WsConnection";
export interface MsgCallWsOptions<Msg, ServiceType extends BaseServiceType> extends MsgCallOptions<Msg, ServiceType> {
conn: WsConnection<ServiceType>;
}
export class MsgCallWs<Msg = any, ServiceType extends BaseServiceType = any> extends MsgCall<Msg, ServiceType> {
readonly conn!: WsConnection<ServiceType>;
constructor(options: MsgCallWsOptions<Msg, ServiceType>) {
super(options);
}
}

View File

@ -0,0 +1,141 @@
import * as http from "http";
import { TransportDataUtil } from "tsrpc-base-client";
import { BaseServiceType } from "tsrpc-proto";
import * as WebSocket from "ws";
import { BaseConnection, BaseConnectionOptions, ConnectionStatus } from "../base/BaseConnection";
import { PrefixLogger } from "../models/PrefixLogger";
import { ApiCallWs } from "./ApiCallWs";
import { MsgCallWs } from "./MsgCallWs";
import { WsServer } from "./WsServer";
export interface WsConnectionOptions<ServiceType extends BaseServiceType> extends BaseConnectionOptions<ServiceType> {
server: WsServer<ServiceType>,
ws: WebSocket,
httpReq: http.IncomingMessage,
onClose: (conn: WsConnection<ServiceType>, code: number, reason: string) => Promise<void>,
dataType: 'text' | 'buffer',
isDataTypeConfirmed?: boolean
}
/**
* Connected client
*/
export class WsConnection<ServiceType extends BaseServiceType = any> extends BaseConnection<ServiceType> {
readonly type = "LONG";
protected readonly ApiCallClass = ApiCallWs;
protected readonly MsgCallClass = MsgCallWs;
readonly ws: WebSocket;
readonly httpReq: http.IncomingMessage;
readonly server!: WsServer<ServiceType>;
dataType!: 'text' | 'buffer';
// 是否已经收到了客户端的第一条消息,以确认了客户端的 dataType
isDataTypeConfirmed?: boolean;
constructor(options: WsConnectionOptions<ServiceType>) {
super(options, new PrefixLogger({
logger: options.server.logger,
prefixs: [`${options.ip} Conn#${options.id}`]
}));
this.ws = options.ws;
this.httpReq = options.httpReq;
this.isDataTypeConfirmed = options.isDataTypeConfirmed;
if (this.server.options.heartbeatWaitTime) {
const timeout = this.server.options.heartbeatWaitTime;
this._heartbeatInterval = setInterval(() => {
if (Date.now() - this._lastHeartbeatTime > timeout) {
this.ws.close(3001, 'Receive heartbeat timeout');
}
}, timeout);
}
// Init WS
this.ws.onclose = async e => {
if (this._heartbeatInterval) {
clearInterval(this._heartbeatInterval);
this._heartbeatInterval = undefined;
}
await options.onClose(this, e.code, e.reason);
this._rsClose?.();
};
this.ws.onerror = e => { this.logger.warn('[ClientErr]', e.error) };
this.ws.onmessage = e => {
let data: Buffer | string;
if (e.data instanceof ArrayBuffer) {
data = Buffer.from(e.data);
}
else if (Array.isArray(e.data)) {
data = Buffer.concat(e.data)
}
else if (Buffer.isBuffer(e.data)) {
data = e.data;
}
else {
data = e.data;
}
// 心跳包,直接回复
if (data instanceof Buffer && data.equals(TransportDataUtil.HeartbeatPacket)) {
this.server.options.debugBuf && this.logger.log('[Heartbeat] Recv ping and send pong', TransportDataUtil.HeartbeatPacket);
this._lastHeartbeatTime = Date.now();
this.ws.send(TransportDataUtil.HeartbeatPacket);
return;
}
// dataType 尚未确认,自动检测
if (!this.isDataTypeConfirmed) {
if (this.server.options.jsonEnabled && typeof data === 'string') {
this.dataType = 'text';
}
else {
this.dataType = 'buffer';
}
this.isDataTypeConfirmed = true;
}
// dataType 已确认
this.server._onRecvData(this, data)
};
}
private _lastHeartbeatTime = 0;
private _heartbeatInterval?: ReturnType<typeof setInterval>;
get status(): ConnectionStatus {
if (this.ws.readyState === WebSocket.CLOSED) {
return ConnectionStatus.Closed;
}
if (this.ws.readyState === WebSocket.CLOSING) {
return ConnectionStatus.Closing;
}
return ConnectionStatus.Opened;
}
protected async doSendData(data: string | Uint8Array, call?: ApiCallWs): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> {
let opSend = await new Promise<{ isSucc: true } | { isSucc: false, errMsg: string }>((rs) => {
this.ws.send(data, e => {
e ? rs({ isSucc: false, errMsg: e.message || 'Send buffer error' }) : rs({ isSucc: true });
})
});
if (!opSend.isSucc) {
return opSend;
}
return { isSucc: true }
}
protected _rsClose?: () => void;
/** Close WebSocket connection */
close(reason?: string): Promise<void> {
// 已连接 Close之
return new Promise<void>(rs => {
this._rsClose = rs;
this.ws.close(1000, reason);
}).finally(() => {
this._rsClose = undefined
})
}
}

241
src/server/ws/WsServer.ts Normal file
View File

@ -0,0 +1,241 @@
import * as http from "http";
import { EncodeOutput, TransportDataUtil } from "tsrpc-base-client";
import { BaseServiceType, ServiceProto } from 'tsrpc-proto';
import * as WebSocket from 'ws';
import { Server as WebSocketServer } from 'ws';
import { HttpUtil } from '../../models/HttpUtil';
import { BaseServer, BaseServerOptions, defaultBaseServerOptions, ServerStatus } from '../base/BaseServer';
import { WsConnection } from './WsConnection';
/**
* TSRPC Server, based on WebSocket connection.
* It can support realtime cases.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
export class WsServer<ServiceType extends BaseServiceType = any> extends BaseServer<ServiceType> {
readonly options!: WsServerOptions<ServiceType>;
readonly connections: WsConnection<ServiceType>[] = [];
private readonly _id2Conn: { [connId: string]: WsConnection<ServiceType> | undefined } = {};
constructor(proto: ServiceProto<ServiceType>, options?: Partial<WsServerOptions<ServiceType>>) {
super(proto, {
...defaultWsServerOptions,
...options
});
}
private _wsServer?: WebSocketServer;
/**
* {@inheritDoc BaseServer.start}
*/
start(): Promise<void> {
if (this._wsServer) {
throw new Error('Server already started');
}
this._status = ServerStatus.Opening;
return new Promise((rs, rj) => {
this.logger.log('Starting WebSocket server...');
this._wsServer = new WebSocketServer({
port: this.options.port
}, () => {
this.logger.log(`Server started at ${this.options.port}...`);
this._status = ServerStatus.Opened;
rs();
});
this._wsServer.on('connection', this._onClientConnect);
this._wsServer.on('error', e => {
this.logger.error('[ServerError]', e);
rj(e);
});
})
}
/**
* {@inheritDoc BaseServer.stop}
*/
async stop(): Promise<void> {
// Closed Already
if (!this._wsServer) {
throw new Error('Server has not been started')
}
if (this._status === ServerStatus.Closed) {
throw new Error('Server is closed already');
}
this._status = ServerStatus.Closing;
return new Promise<void>(async (rs, rj) => {
await Promise.all(this.connections.map(v => v.close('Server Stop')));
this._wsServer!.close(err => { err ? rj(err) : rs() })
}).then(() => {
this.logger.log('Server stopped');
this._status = ServerStatus.Closed;
this._wsServer = undefined;
});
}
private _onClientConnect = (ws: WebSocket, httpReq: http.IncomingMessage) => {
// 停止中 不再接受新的连接
if (this._status !== ServerStatus.Opened) {
ws.close(1012);
return;
}
// 推测 dataType 和 isDataTypeConfirmed
let isDataTypeConfirmed = true;
let dataType: 'text' | 'buffer';
let protocols = httpReq.headers['sec-websocket-protocol']?.split(',').map(v => v.trim()).filter(v => !!v);
if (protocols?.includes('text')) {
dataType = 'text';
}
else if (protocols?.includes('buffer')) {
dataType = 'buffer';
}
else {
dataType = this.options.jsonEnabled ? 'text' : 'buffer';
isDataTypeConfirmed = false;
}
// Create Active Connection
let conn = new WsConnection({
id: '' + this._connIdCounter.getNext(),
ip: HttpUtil.getClientIp(httpReq),
server: this,
ws: ws,
httpReq: httpReq,
onClose: this._onClientClose,
dataType: dataType,
isDataTypeConfirmed: isDataTypeConfirmed
});
this.connections.push(conn);
this._id2Conn[conn.id] = conn;
conn.logger.log('[Connected]', `ActiveConn=${this.connections.length}`);
this.flows.postConnectFlow.exec(conn, conn.logger);
};
private _onClientClose = async (conn: WsConnection<ServiceType>, code: number, reason: string) => {
this.connections.removeOne(v => v.id === conn.id);
delete this._id2Conn[conn.id];
conn.logger.log('[Disconnected]', `Code=${code} ${reason ? `Reason=${reason} ` : ''}ActiveConn=${this.connections.length}`)
await this.flows.postDisconnectFlow.exec({ conn: conn, reason: reason }, conn.logger);
}
/**
* Send the same message to many connections.
* No matter how many target connections are, the message would be only encoded once.
* @param msgName
* @param msg - Message body
* @param connIds - `id` of target connections, `undefined` means broadcast to every connections.
* @returns Send result, `isSucc: true` means the message buffer is sent to kernel, not represents the clients received.
*/
async broadcastMsg<T extends keyof ServiceType['msg']>(msgName: T, msg: ServiceType['msg'][T], conns?: WsConnection<ServiceType>[]): Promise<{ isSucc: true; } | { isSucc: false; errMsg: string; }> {
let connStr: string;
if (!conns) {
conns = this.connections;
connStr = '*';
}
else {
connStr = conns ? conns.map(v => v.id).join(',') : '*';
}
if (!conns.length) {
return { isSucc: true };
}
if (this.status !== ServerStatus.Opened) {
this.logger.warn('[BroadcastMsgErr]', `[${msgName}]`, `[To:${connStr}]`, 'Server not open');
return { isSucc: false, errMsg: 'Server not open' };
}
// GetService
let service = this.serviceMap.msgName2Service[msgName as string];
if (!service) {
this.logger.warn('[BroadcastMsgErr]', `[${msgName}]`, `[To:${connStr}]`, 'Invalid msg name: ' + msgName);
return { isSucc: false, errMsg: 'Invalid msg name: ' + msgName };
}
// Encode group by dataType
let _opEncodeBuf: EncodeOutput<Uint8Array> | undefined;
let _opEncodeText: EncodeOutput<string> | undefined;
const getOpEncodeBuf = () => {
if (!_opEncodeBuf) {
_opEncodeBuf = TransportDataUtil.encodeServerMsg(this.tsbuffer, service!, msg, 'buffer', 'LONG');
}
return _opEncodeBuf;
}
const getOpEncodeText = () => {
if (!_opEncodeText) {
_opEncodeText = TransportDataUtil.encodeServerMsg(this.tsbuffer, service!, msg, 'text', 'LONG');
}
return _opEncodeText;
}
// 测试一下编码可以通过
let op = conns.some(v => v.dataType === 'buffer') ? getOpEncodeBuf() : getOpEncodeText();
if (!op.isSucc) {
this.logger.warn('[BroadcastMsgErr]', `[${msgName}]`, `[To:${connStr}]`, op.errMsg);
return op;
}
this.options.logMsg && this.logger.log(`[BroadcastMsg]`, `[${msgName}]`, `[To:${connStr}]`, msg);
// Batch send
let errMsgs: string[] = [];
return Promise.all(conns.map(async conn => {
// Pre Flow
let pre = await this.flows.preSendMsgFlow.exec({ conn: conn, service: service!, msg: msg }, this.logger);
if (!pre) {
conn.logger.debug('[preSendMsgFlow]', 'Canceled');
return { isSucc: false, errMsg: 'Prevented by preSendMsgFlow' };
}
msg = pre.msg;
// Do send!
let opSend = await conn.sendData((conn.dataType === 'buffer' ? getOpEncodeBuf() : getOpEncodeText())!.output!);
if (!opSend.isSucc) {
return opSend;
}
// Post Flow
this.flows.postSendMsgFlow.exec(pre, this.logger);
return { isSucc: true };
})).then(results => {
for (let i = 0; i < results.length; ++i) {
let op = results[i];
if (!op.isSucc) {
errMsgs.push(`Conn#conns[i].id: ${op.errMsg}`)
};
}
if (errMsgs.length) {
return { isSucc: false, errMsg: errMsgs.join('\n') }
}
else {
return { isSucc: true }
}
})
};
}
export interface WsServerOptions<ServiceType extends BaseServiceType> extends BaseServerOptions<ServiceType> {
/** Which port the WebSocket server is listen to */
port: number;
/**
* Close a connection if not receive heartbeat after the time (ms).
* This value should be greater than `client.heartbeat.interval`, for exmaple 2x of it.
* `undefined` or `0` represent disable this feature.
* @defaultValue `undefined`
*/
heartbeatWaitTime?: number;
};
const defaultWsServerOptions: WsServerOptions<any> = {
...defaultBaseServerOptions,
port: 3000
}

1
test/Base.ts Normal file
View File

@ -0,0 +1 @@
import 'k8w-extend-native';

10
test/api/ApiObjId.ts Normal file
View File

@ -0,0 +1,10 @@
import { ApiCall } from "../../src/server/base/ApiCall";
import { ReqObjId, ResObjId } from "../proto/PtlObjId";
export async function ApiObjId(call: ApiCall<ReqObjId, ResObjId>) {
call.succ({
id2: call.req.id1,
buf: call.req.buf,
date: call.req.date
})
}

27
test/api/ApiTest.ts Normal file
View File

@ -0,0 +1,27 @@
import { TsrpcError } from "tsrpc-proto";
import { ApiCall } from "../../src/server/base/ApiCall";
import { ReqTest, ResTest } from "../proto/PtlTest";
export async function ApiTest(call: ApiCall<ReqTest, ResTest>) {
if (call.req.name === 'InnerError') {
await new Promise(rs => { setTimeout(rs, 50) })
throw new Error('Test InnerError')
}
else if (call.req.name === 'TsrpcError') {
await new Promise(rs => { setTimeout(rs, 50) })
throw new TsrpcError('Test TsrpcError', {
code: 'CODE_TEST',
info: 'ErrInfo Test'
});
}
else if (call.req.name === 'error') {
await new Promise(rs => { setTimeout(rs, 50) })
call.error('Got an error')
}
else {
await new Promise(rs => { setTimeout(rs, 50) })
call.succ({
reply: 'Test reply: ' + call.req.name
})
}
}

21
test/api/a/b/c/ApiTest.ts Normal file
View File

@ -0,0 +1,21 @@
import { TsrpcError } from "tsrpc-proto";
export async function ApiTest(call: any) {
if (call.req.name === 'InnerError') {
throw new Error('a/b/c/Test InnerError')
}
else if (call.req.name === 'TsrpcError') {
throw new TsrpcError('a/b/c/Test TsrpcError', {
code: 'CODE_TEST',
info: 'ErrInfo a/b/c/Test'
});
}
else if (call.req.name === 'error') {
call.error('Got an error')
}
else {
call.succ({
reply: 'a/b/c/Test reply: ' + call.req.name
})
}
}

907
test/cases/http.test.ts Normal file
View File

@ -0,0 +1,907 @@
import { ObjectId } from 'bson';
import { assert } from 'chai';
import chalk from 'chalk';
import * as path from "path";
import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto';
import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src';
import { HttpClient } from '../../src/client/http/HttpClient';
import { HttpServer } from '../../src/server/http/HttpServer';
import { PrefixLogger } from '../../src/server/models/PrefixLogger';
import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest';
import { ApiTest } from '../api/ApiTest';
import { MsgChat } from '../proto/MsgChat';
import { ReqTest, ResTest } from '../proto/PtlTest';
import { serviceProto, ServiceType } from '../proto/serviceProto';
const serverLogger = new PrefixLogger({
prefixs: [chalk.bgGreen.white(' Server ')],
logger: new TerminalColorLogger({ pid: 'Server' })
});
const clientLogger = new PrefixLogger({
prefixs: [chalk.bgBlue.white(' Client ')],
logger: new TerminalColorLogger({ pid: 'Client' })
})
const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>;
async function testApi(server: HttpServer<ServiceType>, client: HttpClient<ServiceType>) {
// Succ
assert.deepStrictEqual(await client.callApi('Test', {
name: 'Req1'
}), {
isSucc: true,
res: {
reply: 'Test reply: Req1'
}
});
assert.deepStrictEqual(await client.callApi('a/b/c/Test', {
name: 'Req2'
}), {
isSucc: true,
res: {
reply: 'a/b/c/Test reply: Req2'
}
});
// Inner error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await client.callApi(v as any, {
name: 'InnerError'
});
delete ret.err!.innerErr.stack;
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
code: 'INTERNAL_ERR',
type: TsrpcErrorType.ServerError,
innerErr: `${v} InnerError`
})
});
}
// TsrpcError
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await client.callApi(v as any, {
name: 'TsrpcError'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError(`${v} TsrpcError`, {
code: 'CODE_TEST',
type: TsrpcErrorType.ApiError,
info: 'ErrInfo ' + v
})
});
}
// call.error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await client.callApi(v as any, {
name: 'error'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Got an error', {
type: TsrpcErrorType.ApiError
})
});
}
}
describe('HTTP Server & Client basic', function () {
it('implement API manually', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
await server.start();
server.implementApi('Test', ApiTest);
server.implementApi('a/b/c/Test', ApiAbcTest);
let client = new HttpClient(getProto(), {
logger: clientLogger,
debugBuf: true
})
await testApi(server, client);
await server.stop();
})
it('extend call in handler', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => {
call.value1 = 'xxx';
call.value2 = 'xxx';
});
server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => {
call.msg.content;
call.value1 = 'xxx';
call.value2 = 'xxx';
})
})
it('extend call in flow', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
type MyConn = HttpConnection<any> & {
currentUser: {
uid: string,
nickName: string
}
}
server.flows.postConnectFlow.push((conn: MyConn) => {
conn.currentUser.nickName = 'asdf';
return conn;
});
server.flows.postConnectFlow.exec(null as any as MyConn, console);
server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => {
call.value2 = 'x';
return call;
});
server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => {
call.value2 = 'f';
return call;
})
})
it('autoImplementApi', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 5000
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
logger: clientLogger
})
await testApi(server, client);
await server.stop();
});
it('sendMsg', async function () {
let server = new HttpServer(getProto(), {
port: 3001,
logger: serverLogger,
// debugBuf: true
});
await server.start();
let client = new HttpClient(getProto(), {
server: 'http://127.0.0.1:3001',
logger: clientLogger,
// debugBuf: true
});
return new Promise(rs => {
let msg: MsgChat = {
channel: 123,
userName: 'fff',
content: '666',
time: Date.now()
};
server.listenMsg('Chat', async v => {
assert.deepStrictEqual(v.msg, msg);
await server.stop();
rs();
});
client.sendMsg('Chat', msg);
})
})
it('abort', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
logger: clientLogger
})
let result: any | undefined;
let promise = client.callApi('Test', { name: 'aaaaaaaa' });
let sn = client.lastSN;
setTimeout(() => {
client.abort(sn)
}, 10);
promise.then(v => {
result = v;
});
await new Promise<void>(rs => {
setTimeout(() => {
assert.strictEqual(result, undefined);
rs();
}, 150)
})
await server.stop();
});
it('abortByKey', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
logger: clientLogger
})
let result: any | undefined;
let result1: any | undefined;
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; });
setTimeout(() => {
client.abortByKey('XXX')
}, 10);
await new Promise<void>(rs => {
setTimeout(() => {
assert.strictEqual(result, undefined);
assert.deepStrictEqual(result1, {
isSucc: true,
res: {
reply: 'Test reply: bbbbbb'
}
})
rs();
}, 150)
})
await server.stop();
})
it('abortAll', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
logger: clientLogger
})
let result: any | undefined;
let result1: any | undefined;
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; });
setTimeout(() => {
client.abortAll()
}, 10);
await new Promise<void>(rs => {
setTimeout(() => {
assert.strictEqual(result, undefined);
assert.strictEqual(result1, undefined);
rs();
}, 150)
})
await server.stop();
})
it('pendingApis', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
logger: clientLogger
})
for (let i = 0; i < 10; ++i) {
let promise = Promise.all(Array.from({ length: 10 }, () => new Promise<void>(rs => {
let name = ['Req', 'InnerError', 'TsrpcError', 'error'][Math.random() * 4 | 0];
let ret: any | undefined;
let promise = client.callApi('Test', { name: name });
let sn = client.lastSN;
let abort = Math.random() > 0.5;
if (abort) {
setTimeout(() => {
client.abort(sn)
}, 0);
}
promise.then(v => {
ret = v;
});
setTimeout(() => {
client.logger?.log('sn', sn, name, abort, ret)
if (abort) {
assert.strictEqual(ret, undefined);
}
else {
assert.notEqual(ret, undefined);
if (name === 'Req') {
assert.strictEqual(ret.isSucc, true);
}
else {
assert.strictEqual(ret.isSucc, false)
}
}
rs();
}, 300)
})));
assert.strictEqual(client['_pendingApis'].length, 10);
await promise;
assert.strictEqual(client['_pendingApis'].length, 0);
}
await server.stop();
})
it('error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.start();
let client1 = new HttpClient(getProto(), {
server: 'http://localhost:80',
logger: clientLogger
})
let ret = await client1.callApi('Test', { name: 'xx' });
console.log(ret);
assert.strictEqual(ret.isSucc, false);
assert.strictEqual(ret.err?.code, 'ECONNREFUSED');
assert.strictEqual(ret.err?.type, TsrpcErrorType.NetworkError);
await server.stop();
})
it('server timeout', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 100
});
server.implementApi('Test', call => {
return new Promise(rs => {
setTimeout(() => {
call.req && call.succ({
reply: 'Hi, ' + call.req.name
});
rs();
}, 200)
})
})
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
let ret = await client.callApi('Test', { name: 'Jack' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Server Timeout', {
code: 'SERVER_TIMEOUT',
type: TsrpcErrorType.ServerError
})
});
await server.stop();
});
it('client timeout', async function () {
let server1 = new HttpServer(getProto(), {
logger: serverLogger
});
server1.implementApi('Test', call => {
return new Promise(rs => {
setTimeout(() => {
call.succ({
reply: 'Hello, ' + call.req.name
});
rs();
}, 2000)
})
})
await server1.start();
let client = new HttpClient(getProto(), {
timeout: 100,
logger: clientLogger
});
let ret = await client.callApi('Test', { name: 'Jack123' });
// SERVER TIMEOUT的call还没执行完但是call却被放入Pool了导致这个BUG
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError({
message: 'Request Timeout',
code: 'TIMEOUT',
type: TsrpcErrorType.NetworkError
})
});
await server1.stop();
});
it('Graceful stop', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
let reqNum = 0;
server.implementApi('Test', async call => {
if (++reqNum === 10) {
server.gracefulStop();
}
await new Promise(rs => setTimeout(rs, parseInt(call.req.name)));
call.succ({ reply: 'OK' });
});
await server.start();
let isStopped = false;
let client = new HttpClient(getProto(), {
logger: clientLogger
})
let succNum = 0;
await Promise.all(Array.from({ length: 10 }, (v, i) => client.callApi('Test', { name: '' + (i * 100) }).then(v => {
if (v.res?.reply === 'OK') {
++succNum;
}
})))
assert.strictEqual(succNum, 10);
})
})
describe('HTTP Flows', function () {
it('Server conn flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
assert.strictEqual((call.conn as any).xxxx, 'asdfasdf')
assert.strictEqual(flowExecResult.postConnectFlow, true);
assert.strictEqual(flowExecResult.postDisconnectFlow, undefined);
call.succ({ reply: 'ok' });
assert.strictEqual(flowExecResult.postDisconnectFlow, undefined);
});
server.flows.postConnectFlow.push(v => {
flowExecResult.postConnectFlow = true;
(v as any).xxxx = 'asdfasdf';
return v;
});
server.flows.postDisconnectFlow.push(v => {
flowExecResult.postDisconnectFlow = true;
return v;
})
await server.start();
assert.strictEqual(flowExecResult.postConnectFlow, undefined);
assert.strictEqual(flowExecResult.postDisconnectFlow, undefined);
let client = new HttpClient(getProto(), {
logger: clientLogger
});
await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postConnectFlow, true);
assert.strictEqual(flowExecResult.postDisconnectFlow, true);
await server.stop();
})
it('Buffer enc/dec flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'Enc&Dec' });
});
server.flows.preRecvBufferFlow.push(v => {
flowExecResult.preRecvBufferFlow = true;
for (let i = 0; i < v.buf.length; ++i) {
v.buf[i] ^= 128;
}
return v;
});
server.flows.preSendBufferFlow.push(v => {
flowExecResult.preSendBufferFlow = true;
for (let i = 0; i < v.buf.length; ++i) {
v.buf[i] ^= 128;
}
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
client.flows.preSendBufferFlow.push(v => {
for (let i = 0; i < v.buf.length; ++i) {
v.buf[i] ^= 128;
}
return v;
});
client.flows.preRecvBufferFlow.push(v => {
for (let i = 0; i < v.buf.length; ++i) {
v.buf[i] ^= 128;
}
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preRecvBufferFlow, true);
assert.strictEqual(flowExecResult.preSendBufferFlow, true);
assert.deepStrictEqual(ret, {
isSucc: true,
res: {
reply: 'Enc&Dec'
}
})
await server.stop();
});
it('ApiCall flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
call.error('You need login');
return call;
});
server.flows.postApiCallFlow.push(v => {
flowExecResult.postApiCallFlow = true;
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
client.flows.preCallApiFlow.push(v => {
if (v.apiName !== 'ObjId') {
v.req.name = 'Changed'
}
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postApiCallFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
await server.stop();
});
it('ApiCall flow break', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
call.error('You need login');
return undefined;
});
server.flows.postApiCallFlow.push(v => {
flowExecResult.postApiCallFlow = true;
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
client.flows.preCallApiFlow.push(v => {
if (v.apiName !== 'ObjId') {
v.req.name = 'Changed'
}
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postApiCallFlow, undefined);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
await server.stop();
});
it('ApiCall flow error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
throw new Error('ASDFASDF')
});
server.flows.postApiCallFlow.push(v => {
flowExecResult.postApiCallFlow = true;
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
client.flows.preCallApiFlow.push(v => {
if (v.apiName !== 'ObjId') {
v.req.name = 'Changed'
}
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postApiCallFlow, undefined);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
type: TsrpcErrorType.ServerError,
innerErr: 'ASDFASDF',
code: 'INTERNAL_ERR'
})
})
await server.stop();
});
it('server ApiReturn flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
server.flows.preApiReturnFlow.push(v => {
flowExecResult.preApiReturnFlow = true;
v.return = {
isSucc: false,
err: new TsrpcError('Ret changed')
}
return v;
});
server.flows.postApiReturnFlow.push(v => {
flowExecResult.postApiReturnFlow = true;
v.call.logger.log('RETTT', v.return);
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preApiReturnFlow, true);
assert.strictEqual(flowExecResult.postApiReturnFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Ret changed')
})
await server.stop();
});
it('client ApiReturn flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof HttpClient<any>['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
client.flows.preApiReturnFlow.push(v => {
flowExecResult.preApiReturnFlow = true;
v.return = {
isSucc: false,
err: new TsrpcError('Ret changed')
}
return v;
});
client.flows.postApiReturnFlow.push(v => {
flowExecResult.postApiReturnFlow = true;
client.logger?.log('RETTT', v.return);
return v;
})
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preApiReturnFlow, true);
assert.strictEqual(flowExecResult.postApiReturnFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Ret changed')
})
await server.stop();
});
it('client SendBufferFlow prevent', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
// const flowExecResult: { [K in (keyof BaseClient<any>['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
client.flows.preSendBufferFlow.push(v => {
return undefined
});
let ret: any;
client.callApi('Test', { name: 'xxx' }).then(v => { ret = v });
await new Promise(rs => { setTimeout(rs, 200) });
assert.strictEqual(ret, undefined)
await server.stop();
});
it('onInputBufferError', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
client.flows.preSendBufferFlow.push(v => {
for (let i = 0; i < v.buf.length; ++i) {
v.buf[i] += 1;
}
return v;
});
let ret = await client.callApi('Test', { name: 'XXX' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Invalid request buffer, please check the version of service proto.', {
type: TsrpcErrorType.ServerError,
code: 'INPUT_DATA_ERR'
})
})
await server.stop();
})
it('ObjectId', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
server.autoImplementApi(path.resolve(__dirname, '../api'))
await server.start();
let client = new HttpClient(getProto(), {
logger: clientLogger
});
// ObjectId
let objId1 = new ObjectId();
let ret = await client.callApi('ObjId', {
id1: objId1
});
assert.strictEqual(ret.isSucc, true, ret.err?.message);
assert.strictEqual(objId1.toString(), ret.res!.id2.toString());
await server.stop();
})
})

997
test/cases/httpJSON.test.ts Normal file
View File

@ -0,0 +1,997 @@
import { ObjectId } from 'bson';
import { assert } from 'chai';
import chalk from 'chalk';
import * as path from "path";
import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto';
import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src';
import { HttpClient } from '../../src/client/http/HttpClient';
import { HttpProxy } from '../../src/client/http/HttpProxy';
import { HttpServer } from '../../src/server/http/HttpServer';
import { PrefixLogger } from '../../src/server/models/PrefixLogger';
import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest';
import { ApiTest } from '../api/ApiTest';
import { MsgChat } from '../proto/MsgChat';
import { ReqTest, ResTest } from '../proto/PtlTest';
import { serviceProto, ServiceType } from '../proto/serviceProto';
const serverLogger = new PrefixLogger({
prefixs: [chalk.bgGreen.white(' Server ')],
logger: new TerminalColorLogger({ pid: 'Server' })
});
const clientLogger = new PrefixLogger({
prefixs: [chalk.bgBlue.white(' Client ')],
logger: new TerminalColorLogger({ pid: 'Client' })
})
const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>;
async function testApi(server: HttpServer<ServiceType>, client: HttpClient<ServiceType>) {
// Succ
assert.deepStrictEqual(await client.callApi('Test', {
name: 'Req1'
}), {
isSucc: true,
res: {
reply: 'Test reply: Req1'
}
});
assert.deepStrictEqual(await client.callApi('a/b/c/Test', {
name: 'Req2'
}), {
isSucc: true,
res: {
reply: 'a/b/c/Test reply: Req2'
}
});
// Inner error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await client.callApi(v as any, {
name: 'InnerError'
});
delete ret.err!.innerErr.stack;
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
code: 'INTERNAL_ERR',
type: TsrpcErrorType.ServerError,
innerErr: `${v} InnerError`
})
});
}
// TsrpcError
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await client.callApi(v as any, {
name: 'TsrpcError'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError(`${v} TsrpcError`, {
code: 'CODE_TEST',
type: TsrpcErrorType.ApiError,
info: 'ErrInfo ' + v
})
});
}
// call.error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await client.callApi(v as any, {
name: 'error'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Got an error', {
type: TsrpcErrorType.ApiError
})
});
}
}
describe('HTTP Server & Client basic', function () {
it('implement API manually', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger,
debugBuf: true
});
await server.start();
server.implementApi('Test', ApiTest);
server.implementApi('a/b/c/Test', ApiAbcTest);
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger,
debugBuf: true
})
await testApi(server, client);
await server.stop();
})
it('extend call in handler', function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => {
call.value1 = 'xxx';
call.value2 = 'xxx';
});
server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => {
call.msg.content;
call.value1 = 'xxx';
call.value2 = 'xxx';
})
})
it('extend call in flow', function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
type MyConn = HttpConnection<any> & {
currentUser: {
uid: string,
nickName: string
}
}
server.flows.postConnectFlow.push((conn: MyConn) => {
conn.currentUser.nickName = 'asdf';
return conn;
});
server.flows.postConnectFlow.exec(null as any as MyConn, console);
server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => {
call.value2 = 'x';
return call;
});
server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => {
call.value2 = 'f';
return call;
})
})
it('autoImplementApi', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger,
apiTimeout: 5000
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
})
await testApi(server, client);
await server.stop();
});
it('sendMsg', async function () {
let server = new HttpServer(getProto(), {
json: true,
port: 3001,
logger: serverLogger,
// debugBuf: true
});
await server.start();
let client = new HttpClient(getProto(), {
json: true,
server: 'http://127.0.0.1:3001',
logger: clientLogger,
// debugBuf: true
});
return new Promise(rs => {
let msg: MsgChat = {
channel: 123,
userName: 'fff',
content: '666',
time: Date.now()
};
server.listenMsg('Chat', async v => {
assert.deepStrictEqual(v.msg, msg);
await server.stop();
rs();
});
client.sendMsg('Chat', msg);
})
})
it('abort', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
})
let result: any | undefined;
let promise = client.callApi('Test', { name: 'aaaaaaaa' });
let sn = client.lastSN;
setTimeout(() => {
client.abort(sn)
}, 10);
promise.then(v => {
result = v;
});
await new Promise<void>(rs => {
setTimeout(() => {
assert.strictEqual(result, undefined);
rs();
}, 150)
})
await server.stop();
});
it('abortByKey', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
})
let result: any | undefined;
let result1: any | undefined;
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; });
setTimeout(() => {
client.abortByKey('XXX')
}, 10);
await new Promise<void>(rs => {
setTimeout(() => {
assert.strictEqual(result, undefined);
assert.deepStrictEqual(result1, {
isSucc: true,
res: {
reply: 'Test reply: bbbbbb'
}
})
rs();
}, 150)
})
await server.stop();
})
it('abortAll', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
})
let result: any | undefined;
let result1: any | undefined;
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'aaaaaaaa' }, { abortKey: 'XXX' }).then(v => { result = v; });
client.callApi('Test', { name: 'bbbbbb' }).then(v => { result1 = v; });
setTimeout(() => {
client.abortAll()
}, 10);
await new Promise<void>(rs => {
setTimeout(() => {
assert.strictEqual(result, undefined);
assert.strictEqual(result1, undefined);
rs();
}, 150)
})
await server.stop();
})
it('pendingApis', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
server.autoImplementApi(path.resolve(__dirname, '../api'))
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
})
for (let i = 0; i < 10; ++i) {
let promise = Promise.all(Array.from({ length: 10 }, () => new Promise<void>(rs => {
let name = ['Req', 'InnerError', 'TsrpcError', 'error'][Math.random() * 4 | 0];
let ret: any | undefined;
let promise = client.callApi('Test', { name: name });
let sn = client.lastSN;
let abort = Math.random() > 0.5;
if (abort) {
setTimeout(() => {
client.abort(sn)
}, 0);
}
promise.then(v => {
ret = v;
});
setTimeout(() => {
client.logger?.log('sn', sn, name, abort, ret)
if (abort) {
assert.strictEqual(ret, undefined);
}
else {
assert.notEqual(ret, undefined);
if (name === 'Req') {
assert.strictEqual(ret.isSucc, true);
}
else {
assert.strictEqual(ret.isSucc, false)
}
}
rs();
}, 300)
})));
assert.strictEqual(client['_pendingApis'].length, 10);
await promise;
assert.strictEqual(client['_pendingApis'].length, 0);
}
await server.stop();
})
it('error', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
let client1 = new HttpClient(getProto(), {
json: true,
server: 'http://localhost:80',
logger: clientLogger
})
let ret = await client1.callApi('Test', { name: 'xx' });
console.log(ret);
assert.strictEqual(ret.isSucc, false);
assert.strictEqual(ret.err?.code, 'ECONNREFUSED');
assert.strictEqual(ret.err?.type, TsrpcErrorType.NetworkError);
await server.stop();
})
it('server timeout', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger,
apiTimeout: 100
});
server.implementApi('Test', call => {
return new Promise(rs => {
setTimeout(() => {
call.req && call.succ({
reply: 'Hi, ' + call.req.name
});
rs();
}, 200)
})
})
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
let ret = await client.callApi('Test', { name: 'Jack' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Server Timeout', {
code: 'SERVER_TIMEOUT',
type: TsrpcErrorType.ServerError
})
});
await server.stop();
});
it('client timeout', async function () {
let server1 = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
server1.implementApi('Test', call => {
return new Promise(rs => {
setTimeout(() => {
call.succ({
reply: 'Hello, ' + call.req.name
});
rs();
}, 2000)
})
})
await server1.start();
let client = new HttpClient(getProto(), {
json: true,
timeout: 100,
logger: clientLogger
});
let ret = await client.callApi('Test', { name: 'Jack123' });
// SERVER TIMEOUT的call还没执行完但是call却被放入Pool了导致这个BUG
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError({
message: 'Request Timeout',
code: 'TIMEOUT',
type: TsrpcErrorType.NetworkError
})
});
await server1.stop();
});
it('Graceful stop', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
let reqNum = 0;
server.implementApi('Test', async call => {
if (++reqNum === 10) {
server.gracefulStop();
}
await new Promise(rs => setTimeout(rs, parseInt(call.req.name)));
call.succ({ reply: 'OK' });
});
await server.start();
let isStopped = false;
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
})
let succNum = 0;
await Promise.all(Array.from({ length: 10 }, (v, i) => client.callApi('Test', { name: '' + (i * 100) }).then(v => {
if (v.res?.reply === 'OK') {
++succNum;
}
})))
assert.strictEqual(succNum, 10);
})
})
describe('HTTP Flows', function () {
it('Server conn flow', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
assert.strictEqual((call.conn as any).xxxx, 'asdfasdf')
assert.strictEqual(flowExecResult.postConnectFlow, true);
assert.strictEqual(flowExecResult.postDisconnectFlow, undefined);
call.succ({ reply: 'ok' });
assert.strictEqual(flowExecResult.postDisconnectFlow, undefined);
});
server.flows.postConnectFlow.push(v => {
flowExecResult.postConnectFlow = true;
(v as any).xxxx = 'asdfasdf';
return v;
});
server.flows.postDisconnectFlow.push(v => {
flowExecResult.postDisconnectFlow = true;
return v;
})
await server.start();
assert.strictEqual(flowExecResult.postConnectFlow, undefined);
assert.strictEqual(flowExecResult.postDisconnectFlow, undefined);
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postConnectFlow, true);
assert.strictEqual(flowExecResult.postDisconnectFlow, true);
await server.stop();
})
it('Buffer enc/dec flow', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'Enc&Dec' });
});
server.flows.preRecvDataFlow.push(v => {
flowExecResult.preRecvDataFlow = true;
v.data = (v.data as string).split('').reverse().join('');
return v;
});
server.flows.preSendDataFlow.push(v => {
flowExecResult.preSendDataFlow = true;
v.data = (v.data as string).split('').reverse().join('');
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
client.flows.preSendDataFlow.push(v => {
v.data = (v.data as string).split('').reverse().join('');
return v;
});
client.flows.preRecvDataFlow.push(v => {
v.data = (v.data as string).split('').reverse().join('');
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preRecvDataFlow, true);
assert.strictEqual(flowExecResult.preSendDataFlow, true);
assert.deepStrictEqual(ret, {
isSucc: true,
res: {
reply: 'Enc&Dec'
}
})
await server.stop();
});
it('ApiCall flow', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
call.error('You need login');
return call;
});
server.flows.postApiCallFlow.push(v => {
flowExecResult.postApiCallFlow = true;
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
client.flows.preCallApiFlow.push(v => {
if (v.apiName !== 'ObjId') {
v.req.name = 'Changed'
}
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postApiCallFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
await server.stop();
});
it('ApiCall flow break', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
call.error('You need login');
return undefined;
});
server.flows.postApiCallFlow.push(v => {
flowExecResult.postApiCallFlow = true;
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
client.flows.preCallApiFlow.push(v => {
if (v.apiName !== 'ObjId') {
v.req.name = 'Changed'
}
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postApiCallFlow, undefined);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
await server.stop();
});
it('ApiCall flow error', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
throw new Error('ASDFASDF')
});
server.flows.postApiCallFlow.push(v => {
flowExecResult.postApiCallFlow = true;
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
client.flows.preCallApiFlow.push(v => {
if (v.apiName !== 'ObjId') {
v.req.name = 'Changed'
}
return v;
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.postApiCallFlow, undefined);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
type: TsrpcErrorType.ServerError,
innerErr: 'ASDFASDF',
code: 'INTERNAL_ERR'
})
})
await server.stop();
});
it('server ApiReturn flow', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
server.flows.preApiReturnFlow.push(v => {
flowExecResult.preApiReturnFlow = true;
v.return = {
isSucc: false,
err: new TsrpcError('Ret changed')
}
return v;
});
server.flows.postApiReturnFlow.push(v => {
flowExecResult.postApiReturnFlow = true;
v.call.logger.log('RETTT', v.return);
return v;
})
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preApiReturnFlow, true);
assert.strictEqual(flowExecResult.postApiReturnFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Ret changed')
})
await server.stop();
});
it('client ApiReturn flow', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
const flowExecResult: { [K in (keyof HttpClient<any>['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
client.flows.preApiReturnFlow.push(v => {
flowExecResult.preApiReturnFlow = true;
v.return = {
isSucc: false,
err: new TsrpcError('Ret changed')
}
return v;
});
client.flows.postApiReturnFlow.push(v => {
flowExecResult.postApiReturnFlow = true;
client.logger?.log('RETTT', v.return);
return v;
})
let ret = await client.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preApiReturnFlow, true);
assert.strictEqual(flowExecResult.postApiReturnFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Ret changed')
})
await server.stop();
});
it('client SendBufferFlow prevent', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
// const flowExecResult: { [K in (keyof BaseClient<any>['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
client.flows.preSendDataFlow.push(v => {
return undefined
});
let ret: any;
client.callApi('Test', { name: 'xxx' }).then(v => { ret = v });
await new Promise(rs => { setTimeout(rs, 200) });
assert.strictEqual(ret, undefined)
await server.stop();
});
it('onInputBufferError', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
client.flows.preSendDataFlow.push(v => {
v.data = (v.data as string).split('').reverse().join('');
return v;
});
let ret = await client.callApi('Test', { name: 'XXX' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Input is not a valid JSON string: Unexpected token } in JSON at position 0', {
type: TsrpcErrorType.ServerError,
code: 'INPUT_DATA_ERR'
})
})
await server.stop();
})
it('throw type error in client', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
let ret = await client.callApi('Test', { name: 23456 } as any);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError({
"code": "INPUT_DATA_ERR",
"message": "Property `name`: Expected type to be `string`, actually `number`.",
"type": TsrpcErrorType.ClientError
})
})
await server.stop();
})
it('throw type error in server', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
await server.start();
let retHttp = await new HttpProxy().fetch({
url: 'http://127.0.0.1:3000/Test',
data: JSON.stringify({ name: 12345 }),
method: 'POST',
headers: {
'content-type': 'application/json; charset=utf-8'
},
transportOptions: {},
responseType: 'text'
}).promise;
assert.deepStrictEqual(JSON.parse((retHttp as any).res), {
isSucc: false,
err: {
"code": "INPUT_DATA_ERR",
"message": "Property `name`: Expected type to be `string`, actually `number`.",
"type": TsrpcErrorType.ServerError
}
})
await server.stop();
})
it('ObjectId', async function () {
let server = new HttpServer(getProto(), {
json: true,
logger: serverLogger
});
server.autoImplementApi(path.resolve(__dirname, '../api'))
await server.start();
let client = new HttpClient(getProto(), {
json: true,
logger: clientLogger
});
// ObjectId
let objId1 = new ObjectId();
let ret = await client.callApi('ObjId', {
id1: objId1
});
assert.strictEqual(ret.isSucc, true, ret.err?.message);
assert.strictEqual(objId1.toString(), ret.res!.id2.toString());
await server.stop();
})
})

396
test/cases/inner.test.ts Normal file
View File

@ -0,0 +1,396 @@
import { ObjectId } from 'bson';
import { assert } from 'chai';
import chalk from 'chalk';
import * as path from "path";
import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto';
import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src';
import { HttpServer } from '../../src/server/http/HttpServer';
import { PrefixLogger } from '../../src/server/models/PrefixLogger';
import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest';
import { ApiTest } from '../api/ApiTest';
import { MsgChat } from '../proto/MsgChat';
import { ReqTest, ResTest } from '../proto/PtlTest';
import { serviceProto, ServiceType } from '../proto/serviceProto';
const serverLogger = new PrefixLogger({
prefixs: [chalk.bgGreen.white(' Server ')],
logger: new TerminalColorLogger({ pid: 'Server' })
});
const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>;
async function testApi(server: HttpServer<ServiceType>) {
// Succ
assert.deepStrictEqual(await server.callApi('Test', {
name: 'Req1'
}), {
isSucc: true,
res: {
reply: 'Test reply: Req1'
}
});
assert.deepStrictEqual(await server.callApi('a/b/c/Test', {
name: 'Req2'
}), {
isSucc: true,
res: {
reply: 'a/b/c/Test reply: Req2'
}
});
// Inner error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await server.callApi(v as any, {
name: 'InnerError'
});
delete ret.err!.innerErr.stack;
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
code: 'INTERNAL_ERR',
type: TsrpcErrorType.ServerError,
innerErr: `${v} InnerError`
})
});
}
// TsrpcError
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await server.callApi(v as any, {
name: 'TsrpcError'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError(`${v} TsrpcError`, {
code: 'CODE_TEST',
type: TsrpcErrorType.ApiError,
info: 'ErrInfo ' + v
})
});
}
// call.error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await server.callApi(v as any, {
name: 'error'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Got an error', {
type: TsrpcErrorType.ApiError
})
});
}
}
describe('HTTP Server & Client basic', function () {
it('implement API manually', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
server.implementApi('Test', ApiTest);
server.implementApi('a/b/c/Test', ApiAbcTest);
await testApi(server);
})
it('extend call in handler', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => {
call.value1 = 'xxx';
call.value2 = 'xxx';
});
server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => {
call.msg.content;
call.value1 = 'xxx';
call.value2 = 'xxx';
})
})
it('extend call in flow', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
type MyConn = HttpConnection<any> & {
currentUser: {
uid: string,
nickName: string
}
}
server.flows.postConnectFlow.push((conn: MyConn) => {
conn.currentUser.nickName = 'asdf';
return conn;
});
server.flows.postConnectFlow.exec(null as any as MyConn, console);
server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => {
call.value2 = 'x';
return call;
});
server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => {
call.value2 = 'f';
return call;
})
})
it('autoImplementApi', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 5000
});
await server.autoImplementApi(path.resolve(__dirname, '../api'))
await testApi(server);
});
it('autoImplementApi delay', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 5000
});
server.autoImplementApi(path.resolve(__dirname, '../api'), true)
await testApi(server);
});
it('error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
let ret = await server.callApi('TesASFt' as any, { name: 'xx' } as any);
console.log(ret);
assert.strictEqual(ret.isSucc, false);
assert.strictEqual(ret.err?.code, 'ERR_API_NAME');
assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError);
})
it('server timeout', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 100
});
server.implementApi('Test', call => {
return new Promise(rs => {
setTimeout(() => {
call.req && call.succ({
reply: 'Hi, ' + call.req.name
});
rs();
}, 200)
})
})
let ret = await server.callApi('Test', { name: 'Jack' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Server Timeout', {
code: 'SERVER_TIMEOUT',
type: TsrpcErrorType.ServerError
})
});
});
it('Graceful stop', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
let reqNum = 0;
server.implementApi('Test', async call => {
if (++reqNum === 10) {
server.gracefulStop();
}
await new Promise(rs => setTimeout(rs, parseInt(call.req.name)));
call.succ({ reply: 'OK' });
});
let isStopped = false;
let succNum = 0;
await Promise.all(Array.from({ length: 10 }, (v, i) => server.callApi('Test', { name: '' + (i * 100) }).then(v => {
if (v.res?.reply === 'OK') {
++succNum;
}
})))
assert.strictEqual(succNum, 10);
})
})
describe('HTTP Flows', function () {
it('ApiCall flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
if (call.req.apiName !== 'ObjId') {
call.req.name = 'Changed'
}
return call;
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
call.error('You need login');
return call;
});
let ret = await server.callApi('Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
});
it('ApiCall flow break', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
call.error('You need login');
return undefined;
});
let ret = await server.callApi('Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
});
it('ApiCall flow error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
throw new Error('ASDFASDF')
});
let ret = await server.callApi('Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
type: TsrpcErrorType.ServerError,
innerErr: 'ASDFASDF',
code: 'INTERNAL_ERR'
})
})
});
it('server ApiReturn flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
server.flows.preApiReturnFlow.push(v => {
flowExecResult.preApiReturnFlow = true;
v.return = {
isSucc: false,
err: new TsrpcError('Ret changed')
}
return v;
});
let ret = await server.callApi('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preApiReturnFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Ret changed')
})
});
it('Extended JSON Types', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.autoImplementApi(path.resolve(__dirname, '../api'))
let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]);
let date = new Date('2021/11/17');
// ObjectId
let objId1 = new ObjectId();
let ret = await server.callApi('ObjId', {
id1: objId1,
buf: buf,
date: date
});
assert.deepStrictEqual(ret, {
isSucc: true,
res: {
id2: objId1,
buf: buf,
date: date
}
});
})
})

View File

@ -0,0 +1,414 @@
import { ObjectId } from 'bson';
import { assert } from 'chai';
import chalk from 'chalk';
import * as path from "path";
import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto';
import { ApiCall, ApiService, BaseServer, HttpConnection, MsgCall, TerminalColorLogger, TransportDataUtil } from '../../src';
import { HttpServer } from '../../src/server/http/HttpServer';
import { PrefixLogger } from '../../src/server/models/PrefixLogger';
import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest';
import { ApiTest } from '../api/ApiTest';
import { MsgChat } from '../proto/MsgChat';
import { ReqTest, ResTest } from '../proto/PtlTest';
import { serviceProto, ServiceType } from '../proto/serviceProto';
const serverLogger = new PrefixLogger({
prefixs: [chalk.bgGreen.white(' Server ')],
logger: new TerminalColorLogger({ pid: 'Server' })
});
async function inputBuffer(server: BaseServer<any>, apiName: string, req: any) {
let apiSvc: ApiService | undefined = server.serviceMap.apiName2Service[apiName];
let inputBuf = apiSvc ? (await TransportDataUtil.encodeApiReq(server.tsbuffer, apiSvc, req, 'buffer')).output : new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]);
if (!inputBuf) {
throw new Error('failed to encode inputBuf')
}
let retBuf = await server.inputBuffer(inputBuf);
assert.ok(retBuf instanceof Uint8Array)
let opDecode = await TransportDataUtil.parseServerOutout(server.tsbuffer, server.serviceMap, retBuf, apiSvc?.id ?? 0);
if (!opDecode.isSucc) {
throw new Error('decode buf failed')
}
if (opDecode.result.type !== 'api') {
throw new Error('decode result is not api')
}
return opDecode.result.ret;
}
const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>;
async function testApi(server: HttpServer<ServiceType>) {
// Succ
assert.deepStrictEqual(await inputBuffer(server, 'Test', {
name: 'Req1'
}), {
isSucc: true,
res: {
reply: 'Test reply: Req1'
}
});
assert.deepStrictEqual(await inputBuffer(server, 'a/b/c/Test', {
name: 'Req2'
}), {
isSucc: true,
res: {
reply: 'a/b/c/Test reply: Req2'
}
});
// Inner error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await inputBuffer(server, v as any, {
name: 'InnerError'
});
delete ret.err!.innerErr.stack;
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
code: 'INTERNAL_ERR',
type: TsrpcErrorType.ServerError,
innerErr: `${v} InnerError`
})
});
}
// TsrpcError
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await inputBuffer(server, v as any, {
name: 'TsrpcError'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError(`${v} TsrpcError`, {
code: 'CODE_TEST',
type: TsrpcErrorType.ApiError,
info: 'ErrInfo ' + v
})
});
}
// call.error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await inputBuffer(server, v as any, {
name: 'error'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Got an error', {
type: TsrpcErrorType.ApiError
})
});
}
}
describe('HTTP Server & Client basic', function () {
it('implement API manually', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
server.implementApi('Test', ApiTest);
server.implementApi('a/b/c/Test', ApiAbcTest);
await testApi(server);
})
it('extend call in handler', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => {
call.value1 = 'xxx';
call.value2 = 'xxx';
});
server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => {
call.msg.content;
call.value1 = 'xxx';
call.value2 = 'xxx';
})
})
it('extend call in flow', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
type MyConn = HttpConnection<any> & {
currentUser: {
uid: string,
nickName: string
}
}
server.flows.postConnectFlow.push((conn: MyConn) => {
conn.currentUser.nickName = 'asdf';
return conn;
});
server.flows.postConnectFlow.exec(null as any as MyConn, console);
server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => {
call.value2 = 'x';
return call;
});
server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => {
call.value2 = 'f';
return call;
})
})
it('autoImplementApi', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 5000
});
await server.autoImplementApi(path.resolve(__dirname, '../api'))
await testApi(server);
});
it('autoImplementApi delay', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 5000
});
server.autoImplementApi(path.resolve(__dirname, '../api'), true)
await testApi(server);
});
it('error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
let ret = await inputBuffer(server, 'TesASFt' as any, { name: 'xx' } as any);
console.log(ret);
assert.strictEqual(ret.isSucc, false);
assert.strictEqual(ret.err?.code, 'INPUT_DATA_ERR');
assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError);
})
it('server timeout', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 100
});
server.implementApi('Test', call => {
return new Promise(rs => {
setTimeout(() => {
call.req && call.succ({
reply: 'Hi, ' + call.req.name
});
rs();
}, 200)
})
})
let ret = await inputBuffer(server, 'Test', { name: 'Jack' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Server Timeout', {
code: 'SERVER_TIMEOUT',
type: TsrpcErrorType.ServerError
})
});
});
it('Graceful stop', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
let reqNum = 0;
server.implementApi('Test', async call => {
if (++reqNum === 10) {
server.gracefulStop();
}
await new Promise(rs => setTimeout(rs, parseInt(call.req.name)));
call.succ({ reply: 'OK' });
});
let isStopped = false;
let succNum = 0;
await Promise.all(Array.from({ length: 10 }, (v, i) => inputBuffer(server, 'Test', { name: '' + (i * 100) }).then((v: any) => {
if (v.res?.reply === 'OK') {
++succNum;
}
})))
assert.strictEqual(succNum, 10);
})
})
describe('HTTP Flows', function () {
it('ApiCall flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
if (call.req.apiName !== 'ObjId') {
call.req.name = 'Changed'
}
return call;
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
call.error('You need login');
return call;
});
let ret = await inputBuffer(server, 'Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
});
it('ApiCall flow break', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
call.error('You need login');
return undefined;
});
let ret = await inputBuffer(server, 'Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('You need login')
})
});
it('ApiCall flow error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
throw new Error('ASDFASDF')
});
let ret = await inputBuffer(server, 'Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Internal Server Error', {
type: TsrpcErrorType.ServerError,
innerErr: 'ASDFASDF',
code: 'INTERNAL_ERR'
})
})
});
it('server ApiReturn flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
server.flows.preApiReturnFlow.push(v => {
flowExecResult.preApiReturnFlow = true;
v.return = {
isSucc: false,
err: new TsrpcError('Ret changed')
}
return v;
});
let ret = await inputBuffer(server, 'Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preApiReturnFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Ret changed')
})
});
it('Extended JSON Types', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.autoImplementApi(path.resolve(__dirname, '../api'))
let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]);
let date = new Date('2021/11/17');
// ObjectId
let objId1 = new ObjectId();
let ret = await inputBuffer(server, 'ObjId', {
id1: objId1,
buf: buf,
date: date
});
assert.deepStrictEqual(ret, {
isSucc: true,
res: {
id2: objId1,
buf: buf,
date: date
}
});
})
})

View File

@ -0,0 +1,410 @@
import { ObjectId } from 'bson';
import { assert } from 'chai';
import chalk from 'chalk';
import * as path from "path";
import { Base64Util } from 'tsbuffer';
import { ServiceProto, TsrpcError, TsrpcErrorType } from 'tsrpc-proto';
import { ApiCall, BaseServer, HttpConnection, MsgCall, TerminalColorLogger } from '../../src';
import { HttpServer } from '../../src/server/http/HttpServer';
import { PrefixLogger } from '../../src/server/models/PrefixLogger';
import { ApiTest as ApiAbcTest } from '../api/a/b/c/ApiTest';
import { ApiTest } from '../api/ApiTest';
import { MsgChat } from '../proto/MsgChat';
import { ReqTest, ResTest } from '../proto/PtlTest';
import { serviceProto, ServiceType } from '../proto/serviceProto';
const serverLogger = new PrefixLogger({
prefixs: [chalk.bgGreen.white(' Server ')],
logger: new TerminalColorLogger({ pid: 'Server' })
});
const getProto = () => Object.merge({}, serviceProto) as ServiceProto<ServiceType>;
async function testApi(server: HttpServer<ServiceType>) {
// Succ
assert.deepStrictEqual(await server.inputJSON('Test', {
name: 'Req1'
}), {
isSucc: true,
res: {
reply: 'Test reply: Req1'
}
});
assert.deepStrictEqual(await server.inputJSON('/a/b/c/Test', {
name: 'Req2'
}), {
isSucc: true,
res: {
reply: 'a/b/c/Test reply: Req2'
}
});
// Inner error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await server.inputJSON(v as any, {
name: 'InnerError'
});
delete ret.err!.innerErr.stack;
assert.deepStrictEqual(ret, {
isSucc: false,
err: {
...new TsrpcError('Internal Server Error', {
code: 'INTERNAL_ERR',
type: TsrpcErrorType.ServerError,
innerErr: `${v} InnerError`
})
}
});
}
// TsrpcError
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await server.inputJSON(v as any, {
name: 'TsrpcError'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: {
...new TsrpcError(`${v} TsrpcError`, {
code: 'CODE_TEST',
type: TsrpcErrorType.ApiError,
info: 'ErrInfo ' + v
})
}
});
}
// call.error
for (let v of ['Test', 'a/b/c/Test']) {
let ret = await server.inputJSON(v as any, {
name: 'error'
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: {
...new TsrpcError('Got an error', {
type: TsrpcErrorType.ApiError
})
}
});
}
}
describe('HTTP Server & Client basic', function () {
it('implement API manually', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
server.implementApi('Test', ApiTest);
server.implementApi('a/b/c/Test', ApiAbcTest);
await testApi(server);
})
it('extend call in handler', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
server.implementApi('Test', (call: MyApiCall<ReqTest, ResTest>) => {
call.value1 = 'xxx';
call.value2 = 'xxx';
});
server.listenMsg('Chat', (call: MyMsgCall<MsgChat>) => {
call.msg.content;
call.value1 = 'xxx';
call.value2 = 'xxx';
})
})
it('extend call in flow', function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
debugBuf: true
});
type MyApiCall<Req, Res> = ApiCall<Req, Res> & {
value1?: string;
value2: string;
}
type MyMsgCall<Msg> = MsgCall<Msg> & {
value1?: string;
value2: string;
}
type MyConn = HttpConnection<any> & {
currentUser: {
uid: string,
nickName: string
}
}
server.flows.postConnectFlow.push((conn: MyConn) => {
conn.currentUser.nickName = 'asdf';
return conn;
});
server.flows.postConnectFlow.exec(null as any as MyConn, console);
server.flows.preApiCallFlow.push((call: MyApiCall<any, any>) => {
call.value2 = 'x';
return call;
});
server.flows.preSendMsgFlow.push((call: MyMsgCall<any>) => {
call.value2 = 'f';
return call;
})
})
it('autoImplementApi', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 5000
});
await server.autoImplementApi(path.resolve(__dirname, '../api'))
await testApi(server);
});
it('autoImplementApi delay', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 5000
});
server.autoImplementApi(path.resolve(__dirname, '../api'), true)
await testApi(server);
});
it('error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
let ret = await server.inputJSON('TesASFt' as any, { name: 'xx' } as any);
console.log(ret);
assert.strictEqual(ret.isSucc, false);
assert.strictEqual(ret.err?.message, 'Invalid service name: TesASFt');
assert.strictEqual(ret.err?.code, 'INPUT_DATA_ERR');
assert.strictEqual(ret.err?.type, TsrpcErrorType.ServerError);
})
it('server timeout', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger,
apiTimeout: 100
});
server.implementApi('Test', call => {
return new Promise(rs => {
setTimeout(() => {
call.req && call.succ({
reply: 'Hi, ' + call.req.name
});
rs();
}, 200)
})
})
let ret = await server.inputJSON('Test', { name: 'Jack' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: {
...new TsrpcError('Server Timeout', {
code: 'SERVER_TIMEOUT',
type: TsrpcErrorType.ServerError
})
}
});
});
it('Graceful stop', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
let reqNum = 0;
server.implementApi('Test', async call => {
if (++reqNum === 10) {
server.gracefulStop();
}
await new Promise(rs => setTimeout(rs, parseInt(call.req.name)));
call.succ({ reply: 'OK' });
});
let isStopped = false;
let succNum = 0;
await Promise.all(Array.from({ length: 10 }, (v, i) => server.inputJSON('Test', { name: '' + (i * 100) }).then((v: any) => {
if (v.res?.reply === 'OK') {
++succNum;
}
})))
assert.strictEqual(succNum, 10);
})
})
describe('HTTP Flows', function () {
it('ApiCall flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
if (call.req.apiName !== 'ObjId') {
call.req.name = 'Changed'
}
return call;
});
server.flows.preApiCallFlow.push(call => {
assert.strictEqual(call.req.name, 'Changed')
call.error('You need login');
return call;
});
let ret = await server.inputJSON('Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: {
...new TsrpcError('You need login')
}
})
});
it('ApiCall flow break', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
call.error('You need login');
return undefined;
});
let ret = await server.inputJSON('Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: {
...new TsrpcError('You need login')
}
})
});
it('ApiCall flow error', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'asdgasdgasdgasdg' });
});
server.flows.preApiCallFlow.push(call => {
throw new Error('ASDFASDF')
});
let ret = await server.inputJSON('Test', { name: 'xxx' });
assert.deepStrictEqual(ret, {
isSucc: false,
err: {
...new TsrpcError('Internal Server Error', {
type: TsrpcErrorType.ServerError,
innerErr: 'ASDFASDF',
code: 'INTERNAL_ERR'
})
}
})
});
it('server ApiReturn flow', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
const flowExecResult: { [K in (keyof BaseServer['flows'])]?: boolean } = {};
server.implementApi('Test', async call => {
call.succ({ reply: 'xxxxxxxxxxxxxxxxxxxx' });
});
server.flows.preApiReturnFlow.push(v => {
flowExecResult.preApiReturnFlow = true;
v.return = {
isSucc: false,
err: { ...new TsrpcError('Ret changed') }
}
return v;
});
let ret = await server.inputJSON('Test', { name: 'xxx' });
assert.strictEqual(flowExecResult.preApiReturnFlow, true);
assert.deepStrictEqual(ret, {
isSucc: false,
err: { ...new TsrpcError('Ret changed') }
})
});
it('Extended JSON Types', async function () {
let server = new HttpServer(getProto(), {
logger: serverLogger
});
await server.autoImplementApi(path.resolve(__dirname, '../api'))
let buf = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]);
let date = new Date('2021/11/17');
// ObjectId
let objId1 = new ObjectId();
let ret = await server.inputJSON('ObjId', {
id1: objId1,
buf: buf,
date: date
});
assert.deepStrictEqual(ret, {
isSucc: true,
res: {
id2: objId1.toHexString(),
buf: Base64Util.bufferToBase64(buf),
date: date.toJSON()
}
});
})
})

1051
test/cases/ws.test.ts Normal file

File diff suppressed because it is too large Load Diff

1278
test/cases/wsJSON.test.ts Normal file

File diff suppressed because it is too large Load Diff

6
test/proto/MsgChat.ts Normal file
View File

@ -0,0 +1,6 @@
export interface MsgChat {
channel: number,
userName: string,
content: string,
time: number
}

14
test/proto/PtlObjId.ts Normal file
View File

@ -0,0 +1,14 @@
// @ts-ignore
import { ObjectId } from "mongodb";
export interface ReqObjId {
id1: ObjectId;
buf?: Uint8Array,
date?: Date
}
export interface ResObjId {
id2: ObjectId;
buf?: Uint8Array,
date?: Date
}

7
test/proto/PtlTest.ts Normal file
View File

@ -0,0 +1,7 @@
export interface ReqTest {
name: string
};
export type ResTest = {
reply: string
};

View File

@ -0,0 +1,10 @@
import { MsgChat } from '../../../MsgChat';
export interface ReqTest {
name: string
};
export type ResTest = {
reply: string,
chat?: MsgChat
};

203
test/proto/serviceProto.ts Normal file
View File

@ -0,0 +1,203 @@
import { ServiceProto } from 'tsrpc-proto';
import { ReqTest, ResTest } from './a/b/c/PtlTest';
import { MsgChat } from './MsgChat';
import { ReqObjId, ResObjId } from './PtlObjId';
import { ReqTest as ReqTest_1, ResTest as ResTest_1 } from './PtlTest';
export interface ServiceType {
api: {
"a/b/c/Test": {
req: ReqTest,
res: ResTest
},
"ObjId": {
req: ReqObjId,
res: ResObjId
},
"Test": {
req: ReqTest_1,
res: ResTest_1
}
},
msg: {
"Chat": MsgChat
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"version": 1,
"services": [
{
"id": 0,
"name": "a/b/c/Test",
"type": "api"
},
{
"id": 1,
"name": "Chat",
"type": "msg"
},
{
"id": 2,
"name": "ObjId",
"type": "api"
},
{
"id": 3,
"name": "Test",
"type": "api"
}
],
"types": {
"a/b/c/PtlTest/ReqTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "name",
"type": {
"type": "String"
}
}
]
},
"a/b/c/PtlTest/ResTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "reply",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "chat",
"type": {
"type": "Reference",
"target": "MsgChat/MsgChat"
},
"optional": true
}
]
},
"MsgChat/MsgChat": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "channel",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "userName",
"type": {
"type": "String"
}
},
{
"id": 2,
"name": "content",
"type": {
"type": "String"
}
},
{
"id": 3,
"name": "time",
"type": {
"type": "Number"
}
}
]
},
"PtlObjId/ReqObjId": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id1",
"type": {
"type": "Reference",
"target": "?mongodb/ObjectId"
}
},
{
"id": 1,
"name": "buf",
"type": {
"type": "Buffer",
"arrayType": "Uint8Array"
},
"optional": true
},
{
"id": 2,
"name": "date",
"type": {
"type": "Date"
},
"optional": true
}
]
},
"PtlObjId/ResObjId": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id2",
"type": {
"type": "Reference",
"target": "?mongodb/ObjectId"
}
},
{
"id": 1,
"name": "buf",
"type": {
"type": "Buffer",
"arrayType": "Uint8Array"
},
"optional": true
},
{
"id": 2,
"name": "date",
"type": {
"type": "Date"
},
"optional": true
}
]
},
"PtlTest/ReqTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "name",
"type": {
"type": "String"
}
}
]
},
"PtlTest/ResTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "reply",
"type": {
"type": "String"
}
}
]
}
}
};

18
test/test.ts Normal file
View File

@ -0,0 +1,18 @@
import { HttpServer } from '../src/server/http/HttpServer';
import { serviceProto } from './proto/serviceProto';
let server = new HttpServer(serviceProto, {
jsonEnabled: true,
jsonRootPath: 'api'
});
server.implementApi('a/b/c/Test', call => {
call.logger.log('xxx', call.req);
call.succ({
reply: 'xxxxxxxxxxx',
aasdg: 'd',
b: 'asdg'
} as any)
});
server.start();

22
test/try/client/http.ts Normal file
View File

@ -0,0 +1,22 @@
import { HttpClient } from '../../src/client/http/HttpClient';
import { serviceProto, ServiceType } from '../proto/serviceProto';
let client = new HttpClient<ServiceType>({
server: 'http://localhost:3000',
proto: serviceProto
});
async function main() {
const P = 50, N = 1000;
let max = 0;
console.time(`test ${P}/${N}`);
for (let i = 0, len = N / P; i < len; ++i) {
let res = await Promise.all(Array.from({ length: P }, () => {
let start = Date.now();
return client.callApi('a/b/c/Test', { name: '123' }).then(() => Date.now() - start);
}));
max = Math.max(res.max(), max)
}
console.timeEnd(`test ${P}/${N}`);
console.log('max', max)
}
main();

82
test/try/client/ws.ts Normal file
View File

@ -0,0 +1,82 @@
import { serviceProto, ServiceType } from '../proto/serviceProto';
import { WsClient } from '../../src/client/ws/WsClient';
import SuperPromise from 'k8w-super-promise';
import { Func } from 'mocha';
async function main() {
let client = new WsClient({
server: 'ws://127.0.0.1:3000',
proto: serviceProto,
onStatusChange: v => {
console.log('StatusChange', v);
},
// onLostConnection: () => {
// console.log('连接断开2秒后重连');
// setTimeout(() => {
// client.connect().catch(() => { });
// }, 2000)
// }
});
await client.connect();
let cancel = client.callApi('Test', { name: 'XXXXXXXXXXXXX' }).catch(e => e);
cancel.cancel();
let res = await client.callApi('Test', { name: '小明同学' }).catch(e => e);
console.log('Test Res', res);
res = await client.callApi('a/b/c/Test', { name: '小明同学' }).catch(e => e);
console.log('Test1 Res', res);
// setInterval(async () => {
// try {
// let res = await client.callApi('Test', { name: '小明同学' });
// console.log('收到回复', res);
// }
// catch (e) {
// if (e.info === 'NETWORK_ERR') {
// return;
// }
// console.log('API错误', e)
// }
// }, 1000);
// client.listenMsg('Chat', msg => {
// console.log('收到MSG', msg);
// });
// setInterval(() => {
// try {
// client.sendMsg('Chat', {
// channel: 123,
// userName: '王小明',
// content: '你好',
// time: Date.now()
// }).catch(e => {
// console.log('SendMsg Failed', e.message)
// })
// }
// catch{ }
// }, 1000)
// #region Benchmark
// let maxTime = 0;
// let done = 0;
// let startTime = Date.now();
// setTimeout(() => {
// console.log('done', maxTime, done);
// process.exit();
// }, 3000);
// for (let i = 0; i < 10000; ++i) {
// client.callApi('Test', { name: '小明同学' }).then(() => {
// ++done;
// maxTime = Math.max(maxTime, Date.now() - startTime)
// })
// }
// #endregion
}
main();

27
test/try/massive.ts Normal file
View File

@ -0,0 +1,27 @@
import { TSRPCClient } from "..";
import { serviceProto, ServiceType } from './proto/serviceProto';
async function main() {
setInterval(() => {
for (let i = 0; i < 100; ++i) {
let client = new TSRPCClient<ServiceType>({
server: 'ws://127.0.0.1:3000',
proto: serviceProto
});
client.connect().then(() => {
client.callApi('a/b/c/Test1', { name: '小明同学' }).then(v => {
// console.log('成功', v)
}).catch(e => {
console.error('错误', e.message)
}).then(() => {
client.disconnect();
});
}).catch(e => {
console.error('连接错误', e)
})
}
}, 1000)
}
main();

View File

@ -0,0 +1,12 @@
import { TsrpcClient } from "../../..";
import { serviceProto } from '../server/protocols/proto';
let client = new TsrpcClient({
proto: serviceProto
});
client.callApi('Test', { name: 'ssss' }).then(v => {
console.log('then', v)
}).catch(e => {
console.log('catch', e)
})

View File

@ -0,0 +1,10 @@
import { TsrpcServer } from '../../../index';
import { serviceProto } from './protocols/proto';
let server = new TsrpcServer({
proto: serviceProto,
});
server.autoImplementApi('src/api');
server.start();

View File

@ -0,0 +1,7 @@
export interface ReqTest {
name: string
}
export interface ResTest {
reply: string
}

View File

@ -0,0 +1,52 @@
import { ServiceProto } from 'tsrpc-proto';
import { ReqTest, ResTest } from './PtlTest'
export interface ServiceType {
req: {
"Test": ReqTest
},
res: {
"Test": ResTest
},
msg: {
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"services": [
{
"id": 0,
"name": "Test",
"type": "api",
"req": "PtlTest/ReqTest",
"res": "PtlTest/ResTest"
}
],
"types": {
"PtlTest/ReqTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "name",
"type": {
"type": "String"
}
}
]
},
"PtlTest/ResTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "reply",
"type": {
"type": "String"
}
}
]
}
}
};

View File

@ -0,0 +1,18 @@
import { ReqTest, ResTest } from "../../protocols/PtlTest";
import { ApiCall } from "../../../../..";
export async function ApiTest(call: ApiCall<ReqTest, ResTest>) {
await new Promise(rs => {
let i = 5;
call.logger.log(i);
let interval = setInterval(() => {
call.logger.log(--i);
if (i === 0) {
clearInterval(interval);
rs();
}
}, 1000);
});
call.error('asdfasdf', { a: 1, b: 2 })
}

12
test/try/package-lock.json generated Normal file
View File

@ -0,0 +1,12 @@
{
"name": "try",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "1.0.0",
"license": "ISC"
}
}
}

11
test/try/package.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "try",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

@ -0,0 +1,6 @@
export interface MsgChat {
channel: number,
userName: string,
content: string,
time: number
}

View File

@ -0,0 +1,7 @@
export interface ReqTest {
name: string
};
export type ResTest = {
reply: string
};

View File

@ -0,0 +1,10 @@
import { MsgChat } from '../../../MsgChat';
export interface ReqTest {
name: string
};
export type ResTest = {
reply: string,
chat?: MsgChat
};

View File

@ -0,0 +1,135 @@
import { ServiceProto } from 'tsrpc-proto';
import { ReqTest, ResTest } from './a/b/c/PtlTest'
import { MsgChat } from './MsgChat'
import { ReqTest as ReqTest_1, ResTest as ResTest_1 } from './PtlTest'
export interface ServiceType {
req: {
"a/b/c/Test": ReqTest,
"Test": ReqTest_1
},
res: {
"a/b/c/Test": ResTest,
"Test": ResTest_1
},
msg: {
"Chat": MsgChat
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"services": [
{
"id": 0,
"name": "a/b/c/Test",
"type": "api",
"req": "a/b/c/PtlTest/ReqTest",
"res": "a/b/c/PtlTest/ResTest"
},
{
"id": 1,
"name": "Chat",
"type": "msg",
"msg": "MsgChat/MsgChat"
},
{
"id": 2,
"name": "Test",
"type": "api",
"req": "PtlTest/ReqTest",
"res": "PtlTest/ResTest"
}
],
"types": {
"a/b/c/PtlTest/ReqTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "name",
"type": {
"type": "String"
}
}
]
},
"a/b/c/PtlTest/ResTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "reply",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "chat",
"type": {
"type": "Reference",
"target": "MsgChat/MsgChat"
},
"optional": true
}
]
},
"MsgChat/MsgChat": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "channel",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "userName",
"type": {
"type": "String"
}
},
{
"id": 2,
"name": "content",
"type": {
"type": "String"
}
},
{
"id": 3,
"name": "time",
"type": {
"type": "Number"
}
}
]
},
"PtlTest/ReqTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "name",
"type": {
"type": "String"
}
}
]
},
"PtlTest/ResTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "reply",
"type": {
"type": "String"
}
}
]
}
}
};

View File

@ -0,0 +1,59 @@
{
"MsgChat/MsgChat": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "channel",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "userName",
"type": {
"type": "String"
}
},
{
"id": 2,
"name": "content",
"type": {
"type": "String"
}
},
{
"id": 3,
"name": "time",
"type": {
"type": "Number"
}
}
]
},
"PtlTest/ReqTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "name",
"type": {
"type": "String"
}
}
]
},
"PtlTest/ResTest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "reply",
"type": {
"type": "String"
}
}
]
}
}

View File

@ -0,0 +1,21 @@
import { TsrpcError } from "tsrpc-proto";
import { ApiCallHttp } from '../../../src/server/http/HttpCall';
import { ReqTest } from "../../proto/PtlTest";
import { ResTest } from '../../proto/a/b/c/PtlTest';
export async function ApiTest(call: ApiCallHttp<ReqTest, ResTest>) {
if (Math.random() > 0.75) {
call.succ({
reply: 'Hello, ' + call.req.name
})
}
else if (Math.random() > 0.5) {
call.error('What the fuck??', { msg: '哈哈哈哈' })
}
else if (Math.random() > 0.25) {
throw new Error('这应该是InternalERROR')
}
else {
throw new TsrpcError('返回到前台的错误', 'ErrInfo');
}
}

View File

@ -0,0 +1,5 @@
export async function ApiTest(call: any) {
call.succ({
reply: 'Api Test1 Succ'
})
}

22
test/try/server/http.ts Normal file
View File

@ -0,0 +1,22 @@
import { serviceProto } from '../proto/serviceProto';
import * as path from "path";
import { TsrpcServer } from '../../index';
let server = new TsrpcServer({
proto: serviceProto
});
server.dataFlow.push((data, conn) => {
let httpReq = conn.options.httpReq;
if (httpReq.method === 'GET') {
conn.logger.log('url', httpReq.url);
conn.logger.log('host', httpReq.headers.host);
conn.options.httpRes.end('Hello~~~');
return false;
}
return true;
})
server.autoImplementApi(path.resolve(__dirname, 'api'));
server.start();

18
test/try/server/ws.ts Normal file
View File

@ -0,0 +1,18 @@
import { serviceProto, ServiceType } from '../proto/serviceProto';
import * as path from "path";
import { TsrpcServerWs } from '../../index';
let server = new TsrpcServerWs<ServiceType>({
proto: serviceProto
});
server.start();
server.autoImplementApi(path.resolve(__dirname, 'api'));
server.listenMsg('Chat', v => {
v.conn.sendMsg('Chat', {
channel: v.msg.channel,
userName: 'SYSTEM',
content: '收到',
time: Date.now()
})
})

59
test/try/tsconfig.json Normal file
View File

@ -0,0 +1,59 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"useUnknownInCatchVariables": false
}
}

66
tsconfig.json Normal file
View File

@ -0,0 +1,66 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [
// "es5",
// "dom"
// ], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "incremental": true, /* Enable incremental compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"useUnknownInCatchVariables": false
},
"include": [
"src"
// "test"
// "benchmark"
]
}