UNPKG

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
/*! * TSRPC v3.4.21 * ----------------------------------------- * 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.21"; /** * 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",