/*! * TSRPC Base Client v2.1.15 * ----------------------------------------- * Copyright (c) Kingworks Corporation. * MIT License * https://github.com/k8w/tsrpc-base-client */ import 'k8w-extend-native'; import { __awaiter, __generator, __assign, __extends } from 'tslib'; import { TSBuffer } from 'tsbuffer'; import { TsrpcError, TransportDataProto, TsrpcErrorType, setLogLevel } from 'tsrpc-proto'; import { SchemaType } from 'tsbuffer-schema'; /** * An auto-increment counter */ var Counter = /** @class */ (function () { function Counter(min, max) { if (min === void 0) { min = 1; } if (max === void 0) { max = Number.MAX_SAFE_INTEGER; } this._min = min; this._max = max; this._last = max; } /** * Reset the counter, makes `getNext()` restart from `0` */ Counter.prototype.reset = function () { this._last = this._max; }; /** * Get next counter value, and auto increment `1` * @param notInc - Just get the next possible value, not actually increasing the sequence */ Counter.prototype.getNext = function (notInc) { return this._last >= this._max ? (this._last = this._min) : (notInc ? this._last : ++this._last); }; Object.defineProperty(Counter.prototype, "last", { /** * Last return of `getNext()` */ get: function () { return this._last; }, enumerable: false, configurable: true }); return Counter; }()); /** * A `Flow` is consists of many `FlowNode`, which is function with the same input and output (like pipeline). * * @remarks * `Flow` is like a hook or event, executed at a specific time. * The difference to event is it can be used to **interrupt** an action, by return `undefined` or `null` in a node. */ var Flow = /** @class */ (function () { function Flow() { /** * All node functions, if you want to adjust the sort you can modify this. */ this.nodes = []; /** * Event when error throwed from a `FlowNode` function. * By default, it does nothing except print a `Uncaught FlowError` error log. * @param e * @param last * @param input * @param logger */ this.onError = function (e, last, input, logger) { logger === null || logger === void 0 ? void 0 : logger.error('Uncaught FlowError:', e); }; } /** * Execute all node function one by one, the previous output is the next input, * until the last output would be return to the caller. * * @remarks * If any node function return `null | undefined`, or throws an error, * the latter node functions would not be executed. * And it would return `null | undefined` immediately to the caller, * which tell the caller it means a interruption, * to let the caller stop latter behaviours. * * @param input The input of the first `FlowNode` * @param logger Logger to print log, `undefined` means to hide all log. * @returns */ Flow.prototype.exec = function (input, logger) { return __awaiter(this, void 0, void 0, function () { var res, i, e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: res = input; i = 0; _a.label = 1; case 1: if (!(i < this.nodes.length)) return [3 /*break*/, 7]; _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, this.nodes[i](res)]; case 3: res = _a.sent(); return [3 /*break*/, 5]; case 4: e_1 = _a.sent(); this.onError(e_1, res, input, logger); return [2 /*return*/, undefined]; case 5: // Return 非true 表示不继续后续流程 立即中止 if (res === null || res === undefined) { return [2 /*return*/, res]; } _a.label = 6; case 6: ++i; return [3 /*break*/, 1]; case 7: return [2 /*return*/, res]; } }); }); }; /** * Append a node function to the last * @param node * @returns */ Flow.prototype.push = function (node) { this.nodes.push(node); return node; }; /** * Remove a node function * @param node * @returns */ Flow.prototype.remove = function (node) { return this.nodes.remove(function (v) { return v === node; }); }; return Flow; }()); function getCustomObjectIdTypes(classObjectId) { var output = {}; // string if (classObjectId === String) { output['?mongodb/ObjectId'] = { type: SchemaType.Custom, validate: function (value) { if (typeof value !== 'string') { return { isSucc: false, errMsg: "Expected type to be `string`, actually `".concat(typeof value, "`.") }; } if (!/^[0-9a-fA-F]{24}$/.test(value)) { return { isSucc: false, errMsg: 'ObjectId must be a string of 24 hex characters' }; } return { isSucc: true }; }, encode: function (value) { return new Uint8Array(Array.from({ length: 12 }, function (_, i) { return Number.parseInt('0x' + value.substr(i * 2, 2)); })); }, decode: function (buf) { return Array.from(buf, function (v) { var str = v.toString(16); if (str.length === 1) { str = '0' + str; } return str; }).join(''); } }; } // ObjectId else { output['?mongodb/ObjectId'] = { type: SchemaType.Custom, validate: function (value) { return (value instanceof classObjectId) ? { isSucc: true } : { isSucc: false, errMsg: "Expected to be instance of `ObjectId`, actually not." }; }, encode: function (value) { return new Uint8Array(value.id); }, decode: function (buf) { return new classObjectId(buf); }, decodeJSON: function (json) { return new classObjectId(json); } }; } output['?mongodb/ObjectID'] = output['?mongodb/ObjectId']; output['?bson/ObjectId'] = output['?mongodb/ObjectId']; output['?bson/ObjectID'] = output['?mongodb/ObjectId']; return output; } /** * A manager for TSRPC receiving messages */ var MsgHandlerManager = /** @class */ (function () { function MsgHandlerManager() { this._handlers = {}; } /** * Execute all handlers parallelly * @returns handlers count */ MsgHandlerManager.prototype.forEachHandler = function (msgName, logger) { var args = []; for (var _i = 2; _i < arguments.length; _i++) { args[_i - 2] = arguments[_i]; } var handlers = this._handlers[msgName]; if (!handlers) { return []; } var output = []; for (var _a = 0, handlers_1 = handlers; _a < handlers_1.length; _a++) { var handler = handlers_1[_a]; try { output.push(handler.apply(void 0, args)); } catch (e) { logger === null || logger === void 0 ? void 0 : logger.error('[MsgHandlerError]', e); } } return output; }; /** * Add message handler, duplicate handlers to the same `msgName` would be ignored. * @param msgName * @param handler * @returns */ MsgHandlerManager.prototype.addHandler = function (msgName, handler) { var handlers = this._handlers[msgName]; // 初始化Handlers if (!handlers) { handlers = this._handlers[msgName] = []; } // 防止重复监听 else if (handlers.some(function (v) { return v === handler; })) { return; } handlers.push(handler); }; /** * Remove handler from the specific `msgName` * @param msgName * @param handler * @returns */ MsgHandlerManager.prototype.removeHandler = function (msgName, handler) { var handlers = this._handlers[msgName]; if (!handlers) { return; } handlers.removeOne(function (v) { return v === handler; }); }; /** * Remove all handlers for the specific `msgName` * @param msgName */ MsgHandlerManager.prototype.removeAllHandlers = function (msgName) { this._handlers[msgName] = undefined; }; return MsgHandlerManager; }()); /** A utility for generate `ServiceMap` */ var ServiceMapUtil = /** @class */ (function () { function ServiceMapUtil() { } ServiceMapUtil.getServiceMap = function (proto) { var map = { id2Service: {}, apiName2Service: {}, msgName2Service: {} }; for (var _i = 0, _a = proto.services; _i < _a.length; _i++) { var v = _a[_i]; var match = v.name.match(/(.+\/)?([^\/]+)$/); var path = match[1] || ''; var name_1 = match[2]; if (v.type === 'api') { var svc = __assign(__assign({}, v), { reqSchemaId: "".concat(path, "Ptl").concat(name_1, "/Req").concat(name_1), resSchemaId: "".concat(path, "Ptl").concat(name_1, "/Res").concat(name_1) }); map.apiName2Service[v.name] = svc; map.id2Service[v.id] = svc; } else { var svc = __assign(__assign({}, v), { msgSchemaId: "".concat(path, "Msg").concat(name_1, "/Msg").concat(name_1) }); map.msgName2Service[v.name] = svc; map.id2Service[v.id] = svc; } } return map; }; return ServiceMapUtil; }()); var TransportDataUtil = /** @class */ (function () { function TransportDataUtil() { } Object.defineProperty(TransportDataUtil, "tsbuffer", { get: function () { if (!this._tsbuffer) { this._tsbuffer = new TSBuffer(TransportDataProto); } return this._tsbuffer; }, enumerable: false, configurable: true }); TransportDataUtil.encodeClientMsg = function (tsbuffer, service, msg, type, connType) { if (type === 'buffer') { var op = tsbuffer.encode(msg, service.msgSchemaId); if (!op.isSucc) { return op; } var serverInputData = { serviceId: service.id, buffer: op.buf }; var opOut = this.tsbuffer.encode(serverInputData, 'ServerInputData'); return opOut.isSucc ? { isSucc: true, output: opOut.buf } : { isSucc: false, errMsg: opOut.errMsg }; } else { var op = tsbuffer.encodeJSON(msg, service.msgSchemaId); if (!op.isSucc) { return op; } var json = connType === 'SHORT' ? op.json : [service.name, op.json]; return { isSucc: true, output: type === 'json' ? json : JSON.stringify(json) }; } }; TransportDataUtil.encodeApiReq = function (tsbuffer, service, req, type, sn) { if (type === 'buffer') { var op = tsbuffer.encode(req, service.reqSchemaId); if (!op.isSucc) { return op; } var serverInputData = { serviceId: service.id, buffer: op.buf, sn: sn }; var opOut = this.tsbuffer.encode(serverInputData, 'ServerInputData'); return opOut.isSucc ? { isSucc: true, output: opOut.buf } : { isSucc: false, errMsg: opOut.errMsg }; } else { var op = tsbuffer.encodeJSON(req, service.reqSchemaId); if (!op.isSucc) { return op; } var json = sn === undefined ? op.json : [service.name, op.json, sn]; return { isSucc: true, output: type === 'json' ? json : JSON.stringify(json) }; } }; TransportDataUtil.encodeServerMsg = function (tsbuffer, service, msg, type, connType) { if (type === 'buffer') { var op = tsbuffer.encode(msg, service.msgSchemaId); if (!op.isSucc) { return op; } var serverOutputData = { serviceId: service.id, buffer: op.buf }; var opOut = this.tsbuffer.encode(serverOutputData, 'ServerOutputData'); return opOut.isSucc ? { isSucc: true, output: opOut.buf } : { isSucc: false, errMsg: opOut.errMsg }; } else { var op = tsbuffer.encodeJSON(msg, service.msgSchemaId); if (!op.isSucc) { return op; } var json = connType === 'SHORT' ? op.json : [service.name, op.json]; return { isSucc: true, output: type === 'json' ? json : JSON.stringify(json) }; } }; TransportDataUtil.parseServerOutout = function (tsbuffer, serviceMap, data, serviceId) { if (data instanceof Uint8Array) { var opServerOutputData = this.tsbuffer.decode(data, 'ServerOutputData'); if (!opServerOutputData.isSucc) { return opServerOutputData; } var serverOutputData = opServerOutputData.value; serviceId = serviceId !== null && serviceId !== void 0 ? serviceId : serverOutputData.serviceId; if (serviceId === undefined) { return { isSucc: false, errMsg: "Missing 'serviceId' in ServerOutput" }; } var service = serviceMap.id2Service[serviceId]; if (!service) { return { isSucc: false, errMsg: "Invalid service ID: ".concat(serviceId, " (from ServerOutput)") }; } if (service.type === 'msg') { if (!serverOutputData.buffer) { return { isSucc: false, errMsg: 'Empty msg buffer (from ServerOutput)' }; } var opMsg = tsbuffer.decode(serverOutputData.buffer, service.msgSchemaId); if (!opMsg.isSucc) { return opMsg; } return { isSucc: true, result: { type: 'msg', service: service, msg: opMsg.value } }; } else { if (serverOutputData.error) { return { isSucc: true, result: { type: 'api', service: service, sn: serverOutputData.sn, ret: { isSucc: false, err: new TsrpcError(serverOutputData.error) } } }; } else { if (!serverOutputData.buffer) { return { isSucc: false, errMsg: 'Empty API res buffer (from ServerOutput)' }; } var opRes = tsbuffer.decode(serverOutputData.buffer, service.resSchemaId); if (!opRes.isSucc) { return opRes; } return { isSucc: true, result: { type: 'api', service: service, sn: serverOutputData.sn, ret: { isSucc: true, res: opRes.value, } } }; } } } else { var json = void 0; if (typeof data === 'string') { try { json = JSON.parse(data); } catch (e) { return { isSucc: false, errMsg: "Invalid input JSON: ".concat(e.message) }; } } else { json = data; } var body = void 0; var sn = void 0; var service = void 0; if (serviceId == undefined) { if (!Array.isArray(json)) { return { isSucc: false, errMsg: "Invalid server output format" }; } var serviceName = json[0]; body = json[1]; sn = json[2]; // 有 SN 是 Api,没 SN 是 Msg service = sn ? serviceMap.apiName2Service[serviceName] : serviceMap.msgName2Service[serviceName]; if (!service) { return { isSucc: false, errMsg: "Invalid service name: ".concat(serviceName, " (from ServerOutputData)") }; } } else { service = serviceMap.id2Service[serviceId]; if (!service) { return { isSucc: false, errMsg: "Invalid service ID: ".concat(serviceId) }; } body = json; } if (service.type === 'api') { if (body.isSucc && 'res' in body) { var op = tsbuffer.decodeJSON(body.res, service.resSchemaId); if (!op.isSucc) { return op; } body.res = op.value; } else if (body.err) { body.err = new TsrpcError(body.err); } else { return { isSucc: false, errMsg: "Invalid server output format" }; } return { isSucc: true, result: { type: 'api', service: service, sn: sn, ret: body } }; } else { var op = tsbuffer.decodeJSON(body, service.msgSchemaId); if (!op.isSucc) { return op; } return { isSucc: true, result: { type: 'msg', service: service, msg: op.value } }; } } }; // 心跳包(Ping & Pong),所有开头为 0 的 Buffer,均为控制指令 TransportDataUtil.HeartbeatPacket = new Uint8Array([0]); return TransportDataUtil; }()); /** * An abstract base class for TSRPC Client, * which includes some common buffer process flows. * * @remarks * You can implement a client on a specific transportation protocol (like HTTP, WebSocket, QUIP) by extend this. * * @typeParam ServiceType - `ServiceType` from generated `proto.ts` * * @see * {@link https://github.com/k8w/tsrpc} * {@link https://github.com/k8w/tsrpc-browser} * {@link https://github.com/k8w/tsrpc-miniapp} */ var BaseClient = /** @class */ (function () { function BaseClient(proto, options) { this._msgHandlers = new MsgHandlerManager(); /** * {@link Flow} to process `callApi`, `sendMsg`, buffer input/output, etc... */ this.flows = { // callApi preCallApiFlow: new Flow(), preApiReturnFlow: new Flow(), postApiReturnFlow: new Flow(), // sendMsg preSendMsgFlow: new Flow(), postSendMsgFlow: new Flow(), preRecvMsgFlow: new Flow(), postRecvMsgFlow: new Flow(), // buffer preSendDataFlow: new Flow(), preRecvDataFlow: new Flow(), /** * @deprecated Please use `preSendDataFlow` instead */ preSendBufferFlow: new Flow(), /** * @deprecated Please use `preRecvDataFlow` instead */ preRecvBufferFlow: new Flow(), // Connection Flows (Only for WebSocket) /** Before connect to WebSocket server */ preConnectFlow: new Flow(), /** After WebSocket connect successfully */ postConnectFlow: new Flow(), /** After WebSocket disconnected (from connected status) */ postDisconnectFlow: new Flow(), }; this._apiSnCounter = new Counter(1); /** * Pending API Requests */ this._pendingApis = []; /** @deprecated Please use `_onRecvData` instead */ this._onRecvBuf = this._onRecvData; this.options = options; this.serviceMap = ServiceMapUtil.getServiceMap(proto); this.dataType = this.options.json ? 'text' : 'buffer'; var types = __assign({}, proto.types); // Custom ObjectId handler if (options.customObjectIdClass) { types = __assign(__assign({}, types), getCustomObjectIdTypes(options.customObjectIdClass)); } this.tsbuffer = new TSBuffer(types); this.logger = this.options.logger; if (this.logger) { this.logger = setLogLevel(this.logger, this.options.logLevel); } } Object.defineProperty(BaseClient.prototype, "lastSN", { /** * The `SN` number of the last `callApi()`, * which can be passed to `abort()` to abort an API request. * @example * ```ts * client.callApi('xxx', { value: 'xxx' }) * .then(ret=>{ console.log('succ', ret) }); * let lastSN = client.lastSN; * client.abort(lastSN); * ``` */ get: function () { return this._apiSnCounter.last; }, enumerable: false, configurable: true }); Object.defineProperty(BaseClient.prototype, "nextSN", { /** * The `SN` number of the next `callApi()`, * which can be passed to `abort()` to abort an API request. * @example * ```ts * let nextSN = client.nextSN; * client.callApi('xxx', { value: 'xxx' }) * ``` */ get: function () { return this._apiSnCounter.getNext(true); }, enumerable: false, configurable: true }); /** * Send request and wait for the return * @param apiName * @param req - Request body * @param options - Transport options * @returns return a `ApiReturn`, all error (network error, business error, code exception...) is unified as `TsrpcError`. * The promise is never rejected, so you just need to process all error in one place. */ BaseClient.prototype.callApi = function (apiName, req, options) { if (options === void 0) { options = {}; } return __awaiter(this, void 0, void 0, function () { var sn, pendingItem, promise; var _this = this; return __generator(this, function (_a) { sn = this._apiSnCounter.getNext(); pendingItem = { sn: sn, abortKey: options.abortKey, service: this.serviceMap.apiName2Service[apiName] }; this._pendingApis.push(pendingItem); promise = new Promise(function (rs) { return __awaiter(_this, void 0, void 0, function () { var pre, ret, preReturn; var _a, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: return [4 /*yield*/, this.flows.preCallApiFlow.exec({ apiName: apiName, req: req, options: options }, this.logger)]; case 1: pre = _c.sent(); if (!pre || pendingItem.isAborted) { this.abort(pendingItem.sn); return [2 /*return*/]; } if (!pre.return) return [3 /*break*/, 2]; ret = pre.return; return [3 /*break*/, 4]; case 2: return [4 /*yield*/, this._doCallApi(pre.apiName, pre.req, pre.options, pendingItem)]; case 3: // do call means it will send buffer via network ret = _c.sent(); _c.label = 4; case 4: if (pendingItem.isAborted) { return [2 /*return*/]; } // Log Original Return if (ret.isSucc) { this.options.logApi && ((_a = this.logger) === null || _a === void 0 ? void 0 : _a.log("[ApiRes] #".concat(pendingItem.sn, " ").concat(apiName), ret.res)); } else { this.options.logApi && ((_b = this.logger) === null || _b === void 0 ? void 0 : _b[ret.err.type === TsrpcError.Type.ApiError ? 'log' : 'error']("[ApiErr] #".concat(pendingItem.sn, " ").concat(apiName), ret.err)); } return [4 /*yield*/, this.flows.preApiReturnFlow.exec(__assign(__assign({}, pre), { return: ret }), this.logger)]; case 5: preReturn = _c.sent(); if (!preReturn) { this.abort(pendingItem.sn); return [2 /*return*/]; } rs(preReturn.return); // Post Flow this.flows.postApiReturnFlow.exec(preReturn, this.logger); return [2 /*return*/]; } }); }); }); // Finally clear pendings promise.catch().then(function () { _this._pendingApis.removeOne(function (v) { return v.sn === pendingItem.sn; }); }); return [2 /*return*/, promise]; }); }); }; BaseClient.prototype._doCallApi = function (apiName, req, options, pendingItem) { var _a; if (options === void 0) { options = {}; } return __awaiter(this, void 0, void 0, function () { var promise; var _this = this; return __generator(this, function (_b) { this.options.logApi && ((_a = this.logger) === null || _a === void 0 ? void 0 : _a.log("[ApiReq] #".concat(pendingItem.sn), apiName, req)); promise = new Promise(function (rs) { return __awaiter(_this, void 0, void 0, function () { var service, opEncode, promiseReturn, promiseSend, opSend, ret; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: service = this.serviceMap.apiName2Service[apiName]; if (!service) { rs({ isSucc: false, err: new TsrpcError('Invalid api name: ' + apiName, { code: 'INVALID_API_NAME', type: TsrpcErrorType.ClientError }) }); return [2 /*return*/]; } pendingItem.service = service; opEncode = TransportDataUtil.encodeApiReq(this.tsbuffer, service, req, this.dataType, this.type === 'LONG' ? pendingItem.sn : undefined); if (!opEncode.isSucc) { rs({ isSucc: false, err: new TsrpcError(opEncode.errMsg, { type: TsrpcErrorType.ClientError, code: 'INPUT_DATA_ERR' }) }); return [2 /*return*/]; } promiseReturn = this._waitApiReturn(pendingItem, (_a = options.timeout) !== null && _a !== void 0 ? _a : this.options.timeout); promiseSend = this.sendData(opEncode.output, options, service.id, pendingItem); return [4 /*yield*/, promiseSend]; case 1: opSend = _b.sent(); if (opSend.err) { rs({ isSucc: false, err: opSend.err }); return [2 /*return*/]; } return [4 /*yield*/, promiseReturn]; case 2: ret = _b.sent(); if (pendingItem.isAborted) { return [2 /*return*/]; } rs(ret); return [2 /*return*/]; } }); }); }); return [2 /*return*/, promise]; }); }); }; /** * Send message, without response, not ensuring the server is received and processed correctly. * @param msgName * @param msg - Message body * @param options - Transport options * @returns If the promise is resolved, it means the request is sent to system kernel successfully. * Notice that not means the server received and processed the message correctly. */ BaseClient.prototype.sendMsg = function (msgName, msg, options) { var _this = this; if (options === void 0) { options = {}; } var promise = new Promise(function (rs) { return __awaiter(_this, void 0, void 0, function () { var pre, service, opEncode, promiseSend, opSend; var _a, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: return [4 /*yield*/, this.flows.preSendMsgFlow.exec({ msgName: msgName, msg: msg, options: options }, this.logger)]; case 1: pre = _c.sent(); if (!pre) { return [2 /*return*/]; } // The msg is not prevented by pre flow this.options.logMsg && ((_a = this.logger) === null || _a === void 0 ? void 0 : _a.log("[SendMsg]", msgName, msg)); service = this.serviceMap.msgName2Service[msgName]; if (!service) { (_b = this.logger) === null || _b === void 0 ? void 0 : _b.error('Invalid msg name: ' + msgName); rs({ isSucc: false, err: new TsrpcError('Invalid msg name: ' + msgName, { code: 'INVALID_MSG_NAME', type: TsrpcErrorType.ClientError }) }); return [2 /*return*/]; } opEncode = TransportDataUtil.encodeClientMsg(this.tsbuffer, service, msg, this.dataType, this.type); if (!opEncode.isSucc) { rs({ isSucc: false, err: new TsrpcError(opEncode.errMsg, { type: TsrpcErrorType.ClientError, code: 'ENCODE_MSG_ERR' }) }); return [2 /*return*/]; } promiseSend = this.sendData(opEncode.output, options, service.id); return [4 /*yield*/, promiseSend]; case 2: opSend = _c.sent(); if (opSend.err) { rs({ isSucc: false, err: opSend.err }); return [2 /*return*/]; } rs({ isSucc: true }); // Post Flow this.flows.postSendMsgFlow.exec(pre, this.logger); return [2 /*return*/]; } }); }); }); promise.then(function (v) { var _a; if (!v.isSucc) { ((_a = _this.logger) !== null && _a !== void 0 ? _a : console).error('[SendMsgErr]', v.err); } }); return promise; }; /** * Add a message handler, * duplicate handlers to the same `msgName` would be ignored. * @param msgName * @param handler * @returns */ // listenMsg(msgName: T, handler: ClientMsgHandler): ClientMsgHandler; // listenMsg(msgName: RegExp, handler: ClientMsgHandler): ClientMsgHandler; // listenMsg(msgName: string | RegExp, handler: ClientMsgHandler): ClientMsgHandler { BaseClient.prototype.listenMsg = function (msgName, handler) { var _this = this; if (msgName instanceof RegExp) { Object.keys(this.serviceMap.msgName2Service).filter(function (k) { return msgName.test(k); }).forEach(function (k) { _this._msgHandlers.addHandler(k, handler); }); } else { this._msgHandlers.addHandler(msgName, handler); } return handler; }; /** * Remove a message handler */ BaseClient.prototype.unlistenMsg = function (msgName, handler) { var _this = this; if (msgName instanceof RegExp) { Object.keys(this.serviceMap.msgName2Service).filter(function (k) { return msgName.test(k); }).forEach(function (k) { _this._msgHandlers.removeHandler(k, handler); }); } else { this._msgHandlers.removeHandler(msgName, handler); } }; /** * Remove all handlers from a message */ BaseClient.prototype.unlistenMsgAll = function (msgName) { var _this = this; if (msgName instanceof RegExp) { Object.keys(this.serviceMap.msgName2Service).filter(function (k) { return msgName.test(k); }).forEach(function (k) { _this._msgHandlers.removeAllHandlers(k); }); } else { this._msgHandlers.removeAllHandlers(msgName); } }; /** * Abort a pending API request, it makes the promise returned by `callApi()` neither resolved nor rejected forever. * @param sn - Every api request has a unique `sn` number, you can get it by `this.lastSN` */ BaseClient.prototype.abort = function (sn) { var _a, _b; // Find var index = this._pendingApis.findIndex(function (v) { return v.sn === sn; }); if (index === -1) { return; } var pendingItem = this._pendingApis[index]; // Clear this._pendingApis.splice(index, 1); pendingItem.onReturn = undefined; pendingItem.isAborted = true; // Log (_a = this.logger) === null || _a === void 0 ? void 0 : _a.log("[ApiAbort] #".concat(pendingItem.sn, " ").concat(pendingItem.service.name)); // onAbort (_b = pendingItem.onAbort) === null || _b === void 0 ? void 0 : _b.call(pendingItem); }; /** * Abort all API requests that has the `abortKey`. * It makes the promise returned by `callApi` neither resolved nor rejected forever. * @param abortKey - The `abortKey` of options when `callApi()`, see {@link TransportOptions.abortKey}. * @example * ```ts * // Send API request many times * client.callApi('SendData', { data: 'AAA' }, { abortKey: 'Session#123' }); * client.callApi('SendData', { data: 'BBB' }, { abortKey: 'Session#123' }); * client.callApi('SendData', { data: 'CCC' }, { abortKey: 'Session#123' }); * * // And abort the at once * client.abortByKey('Session#123'); * ``` */ BaseClient.prototype.abortByKey = function (abortKey) { var _this = this; this._pendingApis.filter(function (v) { return v.abortKey === abortKey; }).forEach(function (v) { _this.abort(v.sn); }); }; /** * Abort all pending API requests. * It makes the promise returned by `callApi` neither resolved nor rejected forever. */ BaseClient.prototype.abortAll = function () { var _this = this; this._pendingApis.slice().forEach(function (v) { return _this.abort(v.sn); }); }; /** * Send data (binary or text) * @remarks * Long connection: wait res by listenning `conn.onmessage` * Short connection: wait res by waitting response * @param data * @param options * @param sn */ BaseClient.prototype.sendData = function (data, options, serviceId, pendingApiItem) { var _a, _b, _c; return __awaiter(this, void 0, void 0, function () { var pre, preBuf; return __generator(this, function (_d) { switch (_d.label) { case 0: return [4 /*yield*/, this.flows.preSendDataFlow.exec({ data: data, sn: pendingApiItem === null || pendingApiItem === void 0 ? void 0 : pendingApiItem.sn }, this.logger)]; case 1: pre = _d.sent(); if (!pre) { return [2 /*return*/, new Promise(function (rs) { })]; } data = pre.data; if (!(data instanceof Uint8Array)) return [3 /*break*/, 3]; return [4 /*yield*/, this.flows.preSendBufferFlow.exec({ buf: data, sn: pendingApiItem === null || pendingApiItem === void 0 ? void 0 : pendingApiItem.sn }, this.logger)]; case 2: preBuf = _d.sent(); if (!preBuf) { return [2 /*return*/, new Promise(function (rs) { })]; } data = preBuf.buf; _d.label = 3; case 3: // debugBuf log if (this.options.debugBuf) { if (typeof data === 'string') { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('[SendText]' + (pendingApiItem ? (' #' + pendingApiItem.sn) : '') + " length=".concat(data.length), data); } else if (data instanceof Uint8Array) { (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug('[SendBuf]' + (pendingApiItem ? (' #' + pendingApiItem.sn) : '') + " length=".concat(data.length), data); } else { (_c = this.logger) === null || _c === void 0 ? void 0 : _c.debug('[SendJSON]' + (pendingApiItem ? (' #' + pendingApiItem.sn) : ''), data); } } return [2 /*return*/, this._sendData(data, options, serviceId, pendingApiItem)]; } }); }); }; // 信道可传输二进制或字符串 BaseClient.prototype._onRecvData = function (data, pendingApiItem) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; return __awaiter(this, void 0, void 0, function () { var sn, pre, pre_1, opParsed, parsed, pre_2; return __generator(this, function (_k) { switch (_k.label) { case 0: sn = pendingApiItem === null || pendingApiItem === void 0 ? void 0 : pendingApiItem.sn; return [4 /*yield*/, this.flows.preRecvDataFlow.exec({ data: data, sn: sn }, this.logger)]; case 1: pre = _k.sent(); if (!pre) { return [2 /*return*/]; } data = pre.data; if (!(typeof data === 'string')) return [3 /*break*/, 2]; this.options.debugBuf && ((_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('[RecvText]' + (sn ? (' #' + sn) : ''), data)); return [3 /*break*/, 5]; case 2: if (!(data instanceof Uint8Array)) return [3 /*break*/, 4]; this.options.debugBuf && ((_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug('[RecvBuf]' + (sn ? (' #' + sn) : ''), 'length=' + data.length, data)); return [4 /*yield*/, this.flows.preRecvBufferFlow.exec({ buf: data, sn: sn }, this.logger)]; case 3: pre_1 = _k.sent(); if (!pre_1) { return [2 /*return*/]; } data = pre_1.buf; return [3 /*break*/, 5]; case 4: this.options.debugBuf && ((_c = this.logger) === null || _c === void 0 ? void 0 : _c.debug('[RecvJSON]' + (sn ? (' #' + sn) : ''), data)); _k.label = 5; case 5: opParsed = TransportDataUtil.parseServerOutout(this.tsbuffer, this.serviceMap, data, pendingApiItem === null || pendingApiItem === void 0 ? void 0 : pendingApiItem.service.id); if (!opParsed.isSucc) { (_d = this.logger) === null || _d === void 0 ? void 0 : _d.error('ParseServerOutputError: ' + opParsed.errMsg); if (data instanceof Uint8Array) { (_e = this.logger) === null || _e === void 0 ? void 0 : _e.error('Please check the version of serviceProto between server and client'); } if (pendingApiItem) { (_f = pendingApiItem.onReturn) === null || _f === void 0 ? void 0 : _f.call(pendingApiItem, { isSucc: false, err: new TsrpcError('Parse server output error', { type: TsrpcErrorType.ServerError }) }); } return [2 /*return*/]; } parsed = opParsed.result; if (!(parsed.type === 'api')) return [3 /*break*/, 6]; sn = sn !== null && sn !== void 0 ? sn : parsed.sn; // call ApiReturn listeners (_h = (_g = this._pendingApis.find(function (v) { return v.sn === sn; })) === null || _g === void 0 ? void 0 : _g.onReturn) === null || _h === void 0 ? void 0 : _h.call(_g, parsed.ret); return [3 /*break*/, 9]; case 6: if (!(parsed.type === 'msg')) return [3 /*break*/, 9]; this.options.logMsg && ((_j = this.logger) === null || _j === void 0 ? void 0 : _j.log("[RecvMsg] ".concat(parsed.service.name), parsed.msg)); return [4 /*yield*/, this.flows.preRecvMsgFlow.exec({ msgName: parsed.service.name, msg: parsed.msg }, this.logger)]; case 7: pre_2 = _k.sent(); if (!pre_2) { return [2 /*return*/]; } this._msgHandlers.forEachHandler(pre_2.msgName, this.logger, pre_2.msg, pre_2.msgName); // Post Flow return [4 /*yield*/, this.flows.postRecvMsgFlow.exec(pre_2, this.logger)]; case 8: // Post Flow _k.sent(); _k.label = 9; case 9: return [2 /*return*/]; } }); }); }; /** * @param sn * @param timeout * @returns `undefined` 代表 canceled */ BaseClient.prototype._waitApiReturn = function (pendingItem, timeout) { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (rs) { // Timeout var timer; if (timeout) { timer = setTimeout(function () { timer = undefined; _this._pendingApis.removeOne(function (v) { return v.sn === pendingItem.sn; }); rs({ isSucc: false, err: new TsrpcError('Request Timeout', { type: TsrpcErrorType.NetworkError, code: 'TIMEOUT' }) }); }, timeout); } // Listener (trigger by `this._onRecvBuf`) pendingItem.onReturn = function (ret) { if (timer) { clearTimeout(timer); timer = undefined; } _this._pendingApis.removeOne(function (v) { return v.sn === pendingItem.sn; }); rs(ret); }; })]; }); }); }; return BaseClient; }()); var defaultBaseClientOptions = { logLevel: 'debug', logApi: true, logMsg: true, json: false, timeout: 15000, debugBuf: false }; /** * Base HTTP Client */ var BaseHttpClient = /** @class */ (function (_super) { __extends(BaseHttpClient, _super); function BaseHttpClient(proto, http, options) { var _this = this; var _a; _this = _super.call(this, proto, __assign(__assign({}, defaultBaseHttpClientOptions), options)) || this; _this.type = 'SHORT'; _this._http = http; _this._jsonServer = _this.options.server + (_this.options.server.endsWith('/') ? '' : '/'); (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.log('TSRPC HTTP Client :', _this.options.server); return _this; } BaseHttpClient.prototype._sendData = function (data, options, serviceId, pendingApiItem) { return __awaiter(this, void 0, void 0, function () { var promise; var _this = this; return __generator(this, function (_a) { promise = (function () { return __awaiter(_this, void 0, void 0, function () { var service, urlSearch, url, _a, fetchPromise, abort, fetchRes; return __generator(this, function (_b) { switch (_b.label) { case 0: service = this.serviceMap.id2Service[serviceId]; urlSearch = service.type === 'msg' ? '?type=msg' : ''; url = typeof data === 'string' ? (this._jsonServer + service.name + urlSearch) : this.options.server; _a = this._http.fetch({ url: url, data: data, method: 'POST', timeout: options.timeout || this.options.timeout, headers: { 'Content-Type': typeof data === 'string' ? 'application/json' : 'application/octet-stream' }, transportOptions: options, responseType: typeof data === 'string' ? 'text' : 'arraybuffer', }), fetchPromise = _a.promise, abort = _a.abort; if (pendingApiItem) { pendingApiItem.onAbort = function () { abort(); }; } // Aborted if (pendingApiItem === null || pendingApiItem === void 0 ? void 0 : pendingApiItem.isAborted) { return [2 /*return*/, new Promise(function (rs) { })]; } return [4 /*yield*/, fetchPromise]; case 1: fetchRes = _b.sent(); if (!fetchRes.isSucc) { return [2 /*return*/, { err: fetchRes.err }]; } return [2 /*return*/, { res: fetchRes.res }]; } }); }); })(); promise.then(function (v) { // Msg 不需要 onRecvData if (pendingApiItem && v.res) { _this._onRecvData(v.res, pendingApiItem); } }); // Finally promise.catch(function (e) { }).then(function () { if (pendingApiItem) { pendingApiItem.onAbort = undefined; } }); return [2 /*return*/, promise]; }); }); }; return BaseHttpClient; }(BaseClient)); var defaultBaseHttpClientOptions = __assign(__assign({}, defaultBaseClientOptions), { server: 'http://localhost:3000', // logger: new TerminalColorLogger(), jsonPrune: true }); /** * WebSocket Client for TSRPC. * It uses native `WebSocket` of browser. * @typeParam ServiceType - `ServiceType` from generated `proto.ts` */ var BaseWsClient = /** @class */ (function (_super) { __extends(BaseWsClient, _super); function BaseWsClient(proto, wsp, options) { var _this = this; var _a; _this = _super.call(this, proto, __assign(__assign({}, defaultBaseWsClientOptions), options)) || this; _this.type = 'LONG'; _this._onWsOpen = function () { var _a; if (!_this._connecting) { return; } _this._status = WsClientStatus.Opened; _this._connecting.rs({ isSucc: true }); _this._connecting = undefined; (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.log('WebSocket connection to server successful'); _this.flows.postConnectFlow.exec({}, _this.logger); // First heartbeat if (_this.options.heartbeat) { _this._heartbeat(); } }; _this._onWsClose = function (code, reason) { var _a, _b, _c; // 防止重复执行 if (_this._status === WsClientStatus.Closed) { return; } var isManual = !!_this._rsDisconnecting; var isConnectedBefore = _this.isConnected || isManual; _this._status = WsClientStatus.Closed; // 连接中,返回连接失败 if (_this._connecting) { _this._connecting.rs({ isSucc: false, errMsg: "Failed to connect to WebSocket server: ".concat(_this.options.server) }); _this._connecting = undefined; (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.error("Failed to connect to WebSocket server: ".concat(_this.options.server)); } // Clear heartbeat if (_this._pendingHeartbeat) { clearTimeout(_this._pendingHeartbeat.timeoutTimer); _this._pendingHeartbeat = undefined; } if (_this._nextHeartbeatTimer) { clearTimeout(_this._nextHeartbeatTimer); } // disconnect中,返回成功 if (_this._rsDisconnecting) { _this._rsDisconnecting(); _this._rsDisconnecting = undefined; (_b = _this.logger) === null || _b === void 0 ? void 0 : _b.log('Disconnected succ', "code=".concat(code, " reason=").concat(reason)); } // 非 disconnect 中,从连接中意外断开 else if (isConnectedBefore) { (_c = _this.logger) === null || _c === void 0 ? void 0 : _c.log("Lost connection to ".concat(_this.options.server), "code=".concat(code, " reason=").concat(reason)); } // postDisconnectFlow,仅从连接状态断开时触发 if (isConnectedBefore) { _this.flows.postDisconnectFlow.exec({ reason: reason, isManual: isManual }, _this.logger); } // 对所有请求中的 API 报错 _this._pendingApis.slice().forEach(function (v) { var _a; (_a = v.onReturn) === null || _a === void 0 ? void 0 : _a.call(v, { isSucc: false, err: new TsrpcError(reason || 'Lost connection to server', { type: TsrpcErrorType.NetworkError, code: 'LOST_CONN' }) }); }); }; _this._onWsError = function (e) { var _a, _b; (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.error('[WebSocket Error]', e); // 连接中,返回连接失败 if (_this._connecting) { _this._connecting.rs({ isSucc: false, errMsg: "Failed to connect to WebSocket server: ".concat(_this.options.server) }); _this._connecting = undefined; (_b = _this.logger) === null || _b === void 0 ? void 0 : _b.error("Failed to connect to WebSocket server: ".concat(_this.options.server)); } }; _this._onWsMessage = function (data) { if (_this._status !== WsClientStatus.Opened) { return; } // 心跳包回包 if (data instanceof Uint8Array && data.length === TransportDataUtil.HeartbeatPacket.length && data.every(function (v, i) { return v === TransportDataUtil.HeartbeatPacket[i]; })) { _this._onHeartbeatAnswer(data); return; } _this._onRecvData(data); }; // #region Heartbeat /** * Last latency time (ms) of heartbeat test */ _this.lastHeartbeatLatency = 0; // #endregion _this._status = WsClientStatus.Closed; _this._wsp = wsp; wsp.options = { onOpen: _this._onWsOpen, onClose: _this._onWsClose, onError: _this._onWsError, onMessage: _this._onWsMessage, logger: _this.logger }; (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.log('TSRPC WebSocket Client :', _this.options.server); return _this; } BaseWsClient.prototype._sendData = function (data) { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (rs) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (!this.isConnected) { rs({ err: new TsrpcError('WebSocket is not connected', { code: 'WS_NOT_OPEN', type: TsrpcError.Type.ClientError }) }); return [2 /*return*/]; } // Do Send rs(this._wsp.send(data)); return [2 /*return*/]; }); }); })]; }); }); }; /** * Send a heartbeat packet */ BaseWsClient.prototype._heartbeat = function () { var _this = this; var _a; if (this._pendingHeartbeat || this._status !== WsClientStatus.Opened || !this.options.heartbeat) { return; } this._pendingHeartbeat = { startTime: Date.now(), timeoutTimer: setTimeout(function () { var _a; _this._pendingHeartbeat = undefined; // heartbeat timeout, disconnect if still connected (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.error('[Heartbeat] Heartbeat timeout, the connection disconnected automatically.'); _this._wsClose(3000, 'Heartbeat timeout'); _this._wsp.options.onClose(3000, 'Heartbeat timeout'); }, this.options.heartbeat.timeout) }; this.options.debugBuf && ((_a = this.logger) === null || _a === void 0 ? void 0 : _a.log('[Heartbeat] Send ping', TransportDataUtil.HeartbeatPacket)); this._sendData(TransportDataUtil.HeartbeatPacket); }; BaseWsClient.prototype._onHeartbeatAnswer = function (data) { var _this = this; var _a; if (!this._pendingHeartbeat || this._status !== WsClientStatus.Opened || !this.options.heartbeat) { return; } // heartbeat succ this.lastHeartbeatLatency = Date.now() - this._pendingHeartbeat.startTime; this.options.debugBuf && ((_a = this.logger) === null || _a === void 0 ? void 0 : _a.log("[Heartbeat] Recv pong, latency=".concat(this.lastHeartbeatLatency, "ms"), data)); clearTimeout(this._pendingHeartbeat.timeoutTimer); this._pendingHeartbeat = undefined; // next heartbeat timer this._nextHeartbeatTimer = setTimeout(function () { _this._heartbeat(); }, this.options.heartbeat.interval); }; Object.defineProperty(BaseWsClient.prototype, "status", { get: function () { return this._status; }, enumerable: false, configurable: true }); Object.defineProperty(BaseWsClient.prototype, "isConnected", { get: function () { return this._status === WsClientStatus.Opened; }, enumerable: false, configurable: true }); /** * Start connecting, you must connect first before `callApi()` and `sendMsg()`. * @throws never */ BaseWsClient.prototype.connect = function () { var _a, _b; return __awaiter(this, void 0, void 0, function () { var pre, promiseConnect; var _this = this; return __generator(this, function (_c) { switch (_c.label) { case 0: // 已连接成功 if (this.isConnected) { return [2 /*return*/, { isSucc: true }]; } // 已连接中 if (this._connecting) { return [2 /*return*/, this._connecting.promise]; } return [4 /*yield*/, this.flows.preConnectFlow.exec({}, this.logger)]; case 1: pre = _c.sent(); // Pre return if (pre === null || pre === void 0 ? void 0 : pre.return) { return [2 /*return*/, pre.return]; } // Canceled if (!pre) { return [2 /*return*/, new Promise(function (rs) { })]; } try { this._wsp.connect(this.options.server, [this.options.json ? 'text' : 'buffer']); } catch (e) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(e); return [2 /*return*/, { isSucc: false, errMsg: e.message }]; } this._status = WsClientStatus.Opening; (_b = this.logger) === null || _b === void 0 ? void 0 : _b.log("Start connecting ".concat(this.options.server, "...")); this._connecting = {}; promiseConnect = new Promise(function (rs) { _this._connecting.rs = rs; }); this._connecting.promise = promiseConnect; return [2 /*return*/, promiseConnect]; } }); }); }; /** * Disconnect immediately * @throws never */ BaseWsClient.prototype.disconnect = function (code, reason) { var _a; return __awaiter(this, void 0, void 0, function () { var isClosed; var _this = this; return __generator(this, function (_b) { if (this._status === WsClientStatus.Closed) { return [2 /*return*/]; } this._status = WsClientStatus.Closing; (_a = this.logger) === null || _a === void 0 ? void 0 : _a.log('Start disconnecting...'); isClosed = false; return [2 /*return*/, Promise.race([ // 正常等待 onClose 关闭 new Promise(function (rs) { _this._rsDisconnecting = function () { if (isClosed) { return; } isClosed = true; rs(); }; _this._wsClose(code !== null && code !== void 0 ? code : 1000, reason !== null && reason !== void 0 ? reason : ''); }), // 超时保护,1 秒未收到关闭请求的,直接 onClose 掉 new Promise(function (rs) { setTimeout(function () { if (isClosed) { return; } isClosed = true; _this._onWsClose(1005, 'Connection closed, but not received ws.onClose event.'); }, 1000); }) ])]; }); }); }; BaseWsClient.prototype._wsClose = function (code, reason) { var _a; try { this._wsp.close(code !== null && code !== void 0 ? code : 1000, reason !== null && reason !== void 0 ? reason : ''); } catch (e) { (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error('[WsCloseError]', e); } }; return BaseWsClient; }(BaseClient)); var defaultBaseWsClientOptions = __assign(__assign({}, defaultBaseClientOptions), { server: 'ws://localhost:3000' }); var WsClientStatus; (function (WsClientStatus) { WsClientStatus["Opening"] = "OPENING"; WsClientStatus["Opened"] = "OPENED"; WsClientStatus["Closing"] = "CLOSING"; WsClientStatus["Closed"] = "CLOSED"; })(WsClientStatus || (WsClientStatus = {})); export { BaseClient, BaseHttpClient, BaseWsClient, Counter, Flow, MsgHandlerManager, ServiceMapUtil, TransportDataUtil, WsClientStatus, defaultBaseClientOptions, defaultBaseHttpClientOptions, defaultBaseWsClientOptions, getCustomObjectIdTypes };