tsrpc
Version:
A TypeScript RPC Framework, with runtime type checking and built-in serialization, support both HTTP and WebSocket.
1,380 lines (1,361 loc) • 78.5 kB
JavaScript
/*!
* TSRPC v3.4.19
* -----------------------------------------
* Copyright (c) King Wang.
* MIT License
* https://github.com/k8w/tsrpc
*/
import 'k8w-extend-native';
import { defaultBaseHttpClientOptions, BaseHttpClient, defaultBaseWsClientOptions, BaseWsClient, TransportDataUtil, MsgHandlerManager, Counter, Flow, getCustomObjectIdTypes, ServiceMapUtil } from 'tsrpc-base-client';
export * from 'tsrpc-base-client';
import { TsrpcError, TsrpcErrorType, setLogLevel } from 'tsrpc-proto';
export * from 'tsrpc-proto';
import * as http from 'http';
import http__default from 'http';
import https from 'https';
import * as WebSocket from 'ws';
import WebSocket__default, { Server } from 'ws';
import chalk from 'chalk';
import * as path from 'path';
import { TSBuffer } from 'tsbuffer';
function getClassObjectId() {
let classObjId;
try {
classObjId = require("mongodb").ObjectId;
}
catch (_a) { }
if (!classObjId) {
try {
classObjId = require("bson").ObjectId;
}
catch (_b) { }
}
if (!classObjId) {
classObjId = String;
}
return classObjId;
}
/** @internal */
class HttpProxy {
fetch(options) {
let nodeHttp = options.url.startsWith("https://") ? https : http__default;
let rs;
let promise = new Promise(_rs => {
rs = _rs;
});
let httpReq;
httpReq = nodeHttp.request(options.url, {
method: options.method,
agent: this.agent,
timeout: options.timeout,
headers: options.headers,
}, httpRes => {
let data = [];
httpRes.on("data", (v) => {
data.push(v);
});
httpRes.on("end", () => {
let buf = 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.code,
}),
});
});
// Timeout
httpReq.on("timeout", () => {
rs({
isSucc: false,
err: new TsrpcError("Request timeout", {
type: TsrpcError.Type.NetworkError,
code: "ECONNABORTED",
}),
});
});
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,
};
}
}
/**
* Client for TSRPC HTTP Server.
* It uses native http module of NodeJS.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
class HttpClient extends BaseHttpClient {
constructor(proto, options) {
let httpProxy = new HttpProxy();
super(proto, httpProxy, {
customObjectIdClass: getClassObjectId(),
...defaultHttpClientOptions,
...options,
});
httpProxy.agent = this.options.agent;
}
}
const defaultHttpClientOptions = {
...defaultBaseHttpClientOptions,
};
/**
* @internal
*/
class WebSocketProxy {
connect(server, protocols) {
this._ws = new WebSocket__default(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, reason) {
var _a;
(_a = this._ws) === null || _a === void 0 ? void 0 : _a.close(code, reason);
this._ws = undefined;
}
send(data) {
return new Promise(rs => {
var _a;
(_a = this._ws) === null || _a === void 0 ? void 0 : _a.send(data, err => {
var _a;
if (err) {
(_a = this.options.logger) === null || _a === void 0 ? void 0 : _a.error("WebSocket Send Error:", err);
rs({
err: new TsrpcError("Network Error", {
code: "SEND_BUF_ERR",
type: TsrpcError.Type.NetworkError,
innerErr: err,
}),
});
return;
}
rs({});
});
});
}
}
/**
* Client for TSRPC WebSocket Server.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
class WsClient extends BaseWsClient {
constructor(proto, options) {
let wsp = new WebSocketProxy();
super(proto, wsp, {
customObjectIdClass: getClassObjectId(),
...defaultWsClientOptions,
...options,
});
}
}
const defaultWsClientOptions = {
...defaultBaseWsClientOptions,
};
/** Version of TSRPC */
const TSRPC_VERSION = "3.4.19";
/**
* Auto add prefix using existed `Logger`
*/
class PrefixLogger {
constructor(options) {
this.logger = options.logger;
this.prefixs = options.prefixs;
}
getPrefix() {
return this.prefixs.map(v => (typeof v === "string" ? v : v()));
}
log(...args) {
this.logger.log(...this.getPrefix().concat(args));
}
debug(...args) {
this.logger.debug(...this.getPrefix().concat(args));
}
warn(...args) {
this.logger.warn(...this.getPrefix().concat(args));
}
error(...args) {
this.logger.error(...this.getPrefix().concat(args));
}
}
class BaseCall {
constructor(options, logger) {
this.conn = options.conn;
this.service = options.service;
this.startTime = Date.now();
this.logger = logger;
}
get server() {
return this.conn.server;
}
}
/**
* 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.
*/
class ApiCall extends BaseCall {
constructor(options, logger) {
super(options, logger !== null && logger !== void 0 ? logger : new PrefixLogger({
logger: options.conn.logger,
prefixs: [
`${chalk.cyan.underline(`[Api:${options.service.name}]`)}${options.sn !== undefined ? chalk.gray(` SN=${options.sn}`) : ""}`,
],
}));
this.type = "api";
this.sn = options.sn;
this.req = options.req;
}
/**
* Response Data that sent already.
* `undefined` means no return data is sent yet. (Never `call.succ()` and `call.error()`)
*/
get return() {
return this._return;
}
/** Time from received req to send return data */
get usedTime() {
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) {
return this._prepareReturn({
isSucc: true,
res: res,
});
}
error(errOrMsg, data) {
let error = typeof errOrMsg === "string" ? new TsrpcError(errOrMsg, data) : errOrMsg;
return this._prepareReturn({
isSucc: false,
err: error,
});
}
async _prepareReturn(ret) {
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(chalk.green("[ApiRes]"), `${this.usedTime}ms`, this.server.options.logResBody ? ret.res : "");
}
else {
if (ret.err.type === TsrpcErrorType.ApiError) {
this.logger.log(chalk.red("[ApiErr]"), `${this.usedTime}ms`, ret.err, "req=", this.req);
}
else {
this.logger.error(chalk.red("[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);
}
async _sendReturn(ret) {
// 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, service, apiReturn, type, sn) {
if (type === "buffer") {
let 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) };
}
}
}
class BaseConnection {
constructor(options, logger) {
this.id = options.id;
this.ip = options.ip;
this.server = options.server;
this.logger = logger;
this.dataType = options.dataType;
}
/** Send buffer (with pre-flow and post-flow) */
async sendData(data, call) {
var _a, _b, _c, _d, _e, _f;
// Pre Flow
let pre = await this.server.flows.preSendDataFlow.exec({ conn: this, data: data, call: call }, (call === null || call === void 0 ? void 0 : 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 === null || call === void 0 ? void 0 : 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") {
(_b = ((_a = call === null || call === void 0 ? void 0 : call.logger) !== null && _a !== void 0 ? _a : this.logger)) === null || _b === void 0 ? void 0 : _b.debug(`[SendText] length=${data.length}`, data);
}
else if (data instanceof Uint8Array) {
(_d = ((_c = call === null || call === void 0 ? void 0 : call.logger) !== null && _c !== void 0 ? _c : this.logger)) === null || _d === void 0 ? void 0 : _d.debug(`[SendBuf] length=${data.length}`, data);
}
else {
(_f = ((_e = call === null || call === void 0 ? void 0 : call.logger) !== null && _e !== void 0 ? _e : this.logger)) === null || _f === void 0 ? void 0 : _f.debug("[SendJSON]", data);
}
}
return this.doSendData(data, call);
}
makeCall(input) {
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(msgName, msg) {
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];
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(chalk.cyan.underline(`[Msg:${msgName}]`), chalk.green("[SendMsg]"), 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 };
}
/**
* Add a message handler,
* duplicate handlers to the same `msgName` would be ignored.
* @param msgName
* @param handler
*/
listenMsg(msgName, handler) {
if (!this._msgHandlers) {
this._msgHandlers = new MsgHandlerManager();
}
this._msgHandlers.addHandler(msgName, handler);
return handler;
}
/**
* Remove a message handler
*/
unlistenMsg(msgName, handler) {
if (!this._msgHandlers) {
this._msgHandlers = new MsgHandlerManager();
}
this._msgHandlers.removeHandler(msgName, handler);
}
/**
* Remove all handlers from a message
*/
unlistenMsgAll(msgName) {
if (!this._msgHandlers) {
this._msgHandlers = new MsgHandlerManager();
}
this._msgHandlers.removeAllHandlers(msgName);
}
}
var ConnectionStatus;
(function (ConnectionStatus) {
ConnectionStatus["Opened"] = "OPENED";
ConnectionStatus["Closing"] = "CLOSING";
ConnectionStatus["Closed"] = "CLOSED";
})(ConnectionStatus || (ConnectionStatus = {}));
class ApiCallHttp extends ApiCall {
constructor(options) {
super(options);
}
async _sendReturn(ret) {
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);
}
}
/**
* 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.
*/
class MsgCall extends BaseCall {
constructor(options, logger) {
super(options, logger !== null && logger !== void 0 ? logger : new PrefixLogger({
logger: options.conn.logger,
prefixs: [chalk.cyan.underline(`[Msg:${options.service.name}]`)],
}));
this.type = "msg";
this.msg = options.msg;
}
}
class MsgCallHttp extends MsgCall {
constructor(options) {
super(options);
}
}
class HttpConnection extends BaseConnection {
constructor(options) {
super(options, new PrefixLogger({
logger: options.server.logger,
prefixs: [chalk.gray(`${options.ip} #${options.id}`)],
}));
this.type = "SHORT";
this.ApiCallClass = ApiCallHttp;
this.MsgCallClass = MsgCallHttp;
this.httpReq = options.httpReq;
this.httpRes = options.httpRes;
}
get status() {
var _a, _b;
if ((_a = this.httpRes.socket) === null || _a === void 0 ? void 0 : _a.writableFinished) {
return ConnectionStatus.Closed;
}
else if ((_b = this.httpRes.socket) === null || _b === void 0 ? void 0 : _b.writableEnded) {
return ConnectionStatus.Closing;
}
else {
return ConnectionStatus.Opened;
}
}
async doSendData(data, call) {
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) {
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) {
let call = super.makeCall(input);
this.call = call;
return call;
}
}
class ApiCallInner extends ApiCall {
constructor(options) {
super(options);
}
async _sendReturn(ret) {
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);
}
}
/**
* Server can `callApi` it self by using this inner connection
*/
class InnerConnection extends BaseConnection {
constructor(options, logger) {
super(options, logger !== null && logger !== void 0 ? logger : new PrefixLogger({
logger: options.server.logger,
prefixs: [chalk.gray(`Inner #${options.id}`)],
}));
this.type = "SHORT";
this.ApiCallClass = ApiCallInner;
this.MsgCallClass = null;
this._status = ConnectionStatus.Opened;
this.return = options.return;
}
get status() {
return this._status;
}
close(reason) {
this.doSendData({
isSucc: false,
err: new TsrpcError(reason !== null && reason !== void 0 ? reason : "Internal Server Error", {
type: TsrpcErrorType.ServerError,
code: "CONN_CLOSED",
reason: reason,
}),
});
}
async doSendData(data, call) {
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 };
}
}
}
/**
* Print log to terminal, with color.
*/
class TerminalColorLogger {
constructor(options) {
this.options = {
pid: process.pid.toString(),
timeFormat: "yyyy-MM-dd hh:mm:ss",
};
Object.assign(this.options, options);
this._pid = this.options.pid ? `<${this.options.pid}> ` : "";
}
_time() {
return this.options.timeFormat ? new Date().format(this.options.timeFormat) : "";
}
debug(...args) {
console.debug.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.cyan("[DEBUG]"), ...args);
}
log(...args) {
console.log.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.green("[INFO]"), ...args);
}
warn(...args) {
console.warn.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.yellow("[WARN]"), ...args);
}
error(...args) {
console.error.call(console, chalk.gray(`${this._pid}${this._time()}`), chalk.red("[ERROR]"), ...args);
}
}
/**
* Abstract base class for TSRPC Server.
* Implement on a transportation protocol (like HTTP WebSocket) by extend it.
* @typeParam ServiceType - `ServiceType` from generated `proto.ts`
*/
class BaseServer {
get status() {
return this._status;
}
/**
* It makes the `uncaughtException` and `unhandledRejection` not lead to the server stopping.
* @param logger
* @returns
*/
static processUncaughtException(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, options) {
this._status = ServerStatus.Closed;
this._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.
*/
this.flows = {
// Conn Flows
/** After the connection is created */
postConnectFlow: new Flow(),
/** After the connection is disconnected */
postDisconnectFlow: new Flow(),
// Buffer Flows
/**
* Before processing the received data, usually be used to encryption / decryption.
* Return `null | undefined` would ignore the buffer.
*/
preRecvDataFlow: new Flow(),
/**
* Before send out data to network, usually be used to encryption / decryption.
* Return `null | undefined` would not send the buffer.
*/
preSendDataFlow: new Flow(),
/**
* @deprecated Use `preRecvDataFlow` instead.
*/
preRecvBufferFlow: new Flow(),
/**
* @deprecated Use `preSendDataFlow` instead.
*/
preSendBufferFlow: new Flow(),
// ApiCall Flows
/**
* Before a API request is send.
* Return `null | undefined` would cancel the request.
*/
preApiCallFlow: new Flow(),
/**
* 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(),
/**
* After the `ApiReturn` is send.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postApiReturnFlow: new Flow(),
/**
* After the api handler is executed.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postApiCallFlow: new Flow(),
// MsgCall Flows
/**
* Before handle a `MsgCall`
*/
preMsgCallFlow: new Flow(),
/**
* After handlers of a `MsgCall` are executed.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postMsgCallFlow: new Flow(),
/**
* Before send out a message.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
preSendMsgFlow: new Flow(),
/**
* After send out a message.
* return `null | undefined` would NOT interrupt latter behaviours.
*/
postSendMsgFlow: new Flow(),
};
// Handlers
this._apiHandlers = {};
// 多个Handler将异步并行执行
this._msgHandlers = new MsgHandlerManager();
this._pendingApiCallNum = 0;
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(getClassObjectId()),
}, {
strictNullChecks: this.options.strictNullChecks,
customTypes: this.options.customTypes,
});
this.serviceMap = ServiceMapUtil.getServiceMap(proto);
this.logger = this.options.logger;
if (this.logger) {
this.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();
}
_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);
}
}
};
}
// #region receive buffer process flow
/**
* Process the buffer, after the `preRecvBufferFlow`.
*/
async _onRecvData(conn, data, serviceId) {
var _a, _b, _c, _d;
// 非 OPENED 状态 停止接受新的请求
if (!(conn instanceof InnerConnection) && this.status !== ServerStatus.Opened) {
return;
}
// debugBuf log
if (this.options.debugBuf) {
if (typeof data === "string") {
(_a = conn.logger) === null || _a === void 0 ? void 0 : _a.debug(`[RecvText] length=${data.length}`, data);
}
else if (data instanceof Uint8Array) {
(_b = conn.logger) === null || _b === void 0 ? void 0 : _b.debug(`[RecvBuf] length=${data.length}`, data);
}
else {
(_c = conn.logger) === null || _c === void 0 ? void 0 : _c.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;
}
// Pre flow
const preServiceName = serviceId ? this.serviceMap.id2Service[serviceId].name : undefined;
let pre = await this.flows.preRecvDataFlow.exec({
conn: conn,
data: data,
serviceId: serviceId,
serviceName: preServiceName,
}, conn.logger);
if (!pre) {
conn.logger.debug("[preRecvDataFlow] Canceled");
return;
}
data = pre.data;
serviceId = pre.serviceId;
// @deprecated 兼容
if (pre.serviceName && pre.serviceName !== preServiceName) {
serviceId = (_d = this.serviceMap.apiName2Service[pre.serviceName]) === null || _d === void 0 ? void 0 : _d.id;
}
// @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;
}
if (serviceId === undefined && conn instanceof HttpConnection && typeof data === "string") {
this.onInputDataError(`Invalid URL path: ${conn.httpReq.url}`, conn, data);
return;
}
// Parse Call
let opInput = this._parseServerInput(this.tsbuffer, this.serviceMap, data, serviceId);
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);
}
}
async _handleApiCall(call) {
var _a;
++this._pendingApiCallNum;
await this._onApiCall(call);
if (--this._pendingApiCallNum === 0) {
(_a = this._gracefulStop) === null || _a === void 0 ? void 0 : _a.rs();
}
}
async _onApiCall(call) {
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(chalk.green("[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) {
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);
// }
}
async _onMsgCall(call) {
var _a, _b;
// 收到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(chalk.green("[RecvMsg]"), call.msg);
let promises = [
// Conn Handlers
...((_b = (_a = call.conn["_msgHandlers"]) === null || _a === void 0 ? void 0 : _a.forEachHandler(call.service.name, call.logger, call)) !== null && _b !== void 0 ? _b : []),
// 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(apiName, handler) {
if (this._apiHandlers[apiName]) {
throw new Error("Already exist handler for API: " + apiName);
}
this._apiHandlers[apiName] = handler;
this.logger.log(`API implemented succ: [${apiName}]`);
}
/**
* 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, delay) {
let apiServices = Object.values(this.serviceMap.apiName2Service);
let output = { 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, apiPath, logger) {
var _a;
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 === null || logger === void 0 ? void 0 : 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) {
this.logger.error(chalk.red(`Implement API ${chalk.cyan.underline(`${svc.name}`)} failed:`), e);
return { errMsg: e.message };
}
// 优先 default,其次 ApiName 同名
let handler = (_a = handlerModule.default) !== null && _a !== void 0 ? _a : 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
* @returns
*/
listenMsg(msgName, handler) {
if (msgName instanceof RegExp) {
Object.keys(this.serviceMap.msgName2Service)
.filter(k => msgName.test(k))
.forEach(k => {
this._msgHandlers.addHandler(k, handler);
});
}
else {
this._msgHandlers.addHandler(msgName, handler);
}
return handler;
}
/**
* Remove a message handler
*/
unlistenMsg(msgName, handler) {
if (msgName instanceof RegExp) {
Object.keys(this.serviceMap.msgName2Service)
.filter(k => msgName.test(k))
.forEach(k => {
this._msgHandlers.removeHandler(k, handler);
});
}
else {
this._msgHandlers.removeHandler(msgName, handler);
}
}
/**
* Remove all handlers from a message
*/
unlistenMsgAll(msgName) {
if (msgName instanceof RegExp) {
Object.keys(this.serviceMap.msgName2Service)
.filter(k => msgName.test(k))
.forEach(k => {
this._msgHandlers.removeAllHandlers(k);
});
}
else {
this._msgHandlers.removeAllHandlers(msgName);
}
}
// #endregion
/**
* Event when the server cannot parse input buffer to api/msg call.
* By default, it will return "Input Data Error" .
*/
async onInputDataError(errMsg, conn, data) {
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, call) {
call.logger.error(err);
call.error("Internal Server Error", {
code: "INTERNAL_ERR",
type: TsrpcErrorType.ServerError,
innerErr: call.conn.server.options.returnInnerError ? err.message : undefined,
});
}
/**
* 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) {
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(rs => {
this._gracefulStop = {
rs: rs,
};
});
return new Promise(rs => {
var _a;
let maxWaitTimer;
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);
}
const stopResult = 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();
});
}
});
if (this._pendingApiCallNum === 0) {
(_a = this._gracefulStop) === null || _a === void 0 ? void 0 : _a.rs();
}
return stopResult;
});
}
/**
* 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(apiName, req) {
return new Promise(rs => {
// 确认是哪个Service
let service = this.serviceMap.apiName2Service[apiName];
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
* @param logger - Custom logger
* @returns - Pure JSON
*/
async inputJSON(apiName, req, logger) {
if (apiName.startsWith("/")) {
apiName = apiName.slice(1);
}
const service = this.serviceMap.apiName2Service[apiName];
if (!service) {
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",