UNPKG

padlocal-client-ts

Version:
406 lines 18.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IOError = exports.Status = exports.WeChatLongLinkProxy = void 0; const RetryStrategy_1 = require("../utils/RetryStrategy"); const PromiseUtils_1 = require("../utils/PromiseUtils"); const events_1 = require("events"); const net_1 = require("net"); const ByteUtils_1 = require("../utils/ByteUtils"); const padlocal_pb_1 = require("../proto/padlocal_pb"); const verror_1 = __importDefault(require("verror")); const SerialExecutor_1 = require("../utils/SerialExecutor"); const Utils_1 = require("../utils/Utils"); const Host_1 = require("../utils/Host"); const Log_1 = __importDefault(require("../utils/Log")); const LOGPRE = "[LongLink]"; class WeChatLongLinkProxy extends events_1.EventEmitter { constructor(client) { super(); this._status = Status.IDLE; this._reconnectStrategy = RetryStrategy_1.RetryStrategy.getStrategy(RetryStrategy_1.RetryStrategyRule.FAST, Number.MAX_SAFE_INTEGER); // reconnect infinitely this._requestCallbackMap = new Map(); this._client = client; this._serialExecutor = new SerialExecutor_1.SerialExecutor(); this._instanceId = Utils_1.genUUID(); } emit(event, ...args) { return super.emit(event, ...args); } updateHostList(hostList) { if (!hostList || hostList.length === 0) { return false; } this._hostList = hostList; Log_1.default.silly(LOGPRE, `update longlink host: ${JSON.stringify(hostList)}`); return true; } reconnect() { return __awaiter(this, void 0, void 0, function* () { this.shutdown(); yield this.makeSureConnected(); }); } /** * disconnect long link */ shutdown(clearHost = false) { if (this._status === Status.STOP) { return; } Log_1.default.silly(LOGPRE, `[${this._currentHost()}] longlink shutdown`); this._clearReconnectTimer(); this._reconnectStrategy.reset(); // call before _destroyLongLink, forbid future socket close or error event to trigger reconnect this._updateStatus(Status.STOP); this._destroyLongLink(); if (clearHost) { this._hostList = undefined; } } isConnected() { return this._status === Status.CONNECTED; } isIdle() { return (this._status !== Status.CONNECTING && this._status !== Status.CONNECTED && this._status !== Status.HALF_CONNECTED); } getId() { return this._id; } makeSureConnected() { return __awaiter(this, void 0, void 0, function* () { if (this.isConnected()) { return; } // trigger connect immediately try { yield this._connect(); } catch (e) { throw new IOError(e, `longlink fail to connect`); } }); } sendStreamData(data, streamCallback) { if (streamCallback) { this._streamCallback = streamCallback; } Log_1.default.silly(LOGPRE, `[${this._currentHost()}] socket send:${ByteUtils_1.bytesToHexString(data, ByteUtils_1.MAX_LOG_BYTES_LEN)}`); this._socket.write(data); } sendRequest(longLinkRequest) { return __awaiter(this, void 0, void 0, function* () { if (longLinkRequest.getLonglinkid() && longLinkRequest.getLonglinkid() !== this._id) { throw new Error(`request must be sent by longlink with id:${longLinkRequest.getLonglinkid()}, but current id is: ${this._id}`); } // during init phase, long link status is not connected, makeSureConnected will be blocked. if (!longLinkRequest.getInitphase()) { yield this.makeSureConnected(); } const messageId = longLinkRequest.getMessageid(); let packResponse; try { packResponse = yield this._serialExecutor.execute(() => __awaiter(this, void 0, void 0, function* () { return this._client.request(new padlocal_pb_1.LongLinkPackRequest().setLonglinkid(this._id).setMessageid(messageId)); })); } catch (e) { this._onSocketError(new IOError(e, "Exception while packing long link data")); throw e; } const buffer = Buffer.from(packResponse.getPayload()); return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { Log_1.default.silly(LOGPRE, `[${this._currentHost()}] socket send:${ByteUtils_1.bytesToHexString(buffer, ByteUtils_1.MAX_LOG_BYTES_LEN)}`); this._socket.write(buffer, (error) => { if (!error) { this._resetHeartBeatTimer(true); const timeoutId = setTimeout(() => { this._notifyRequestCallback(messageId, new IOError("long link send request timeout")); }, WeChatLongLinkProxy.REQUEST_TIMEOUT); this._requestCallbackMap.set(messageId, new PromiseUtils_1.PromiseCallback(resolve, reject, timeoutId)); } else { reject(error); } }); })); }); } _updateStatus(newStatus) { const oldStatus = this._status; if (oldStatus === newStatus) { return; } this._status = newStatus; this.emit("status", { newStatus, oldStatus, }); } _resetHeartBeatTimer(start, delay) { if (this._heartBeatTimer) { clearTimeout(this._heartBeatTimer); this._heartBeatTimer = undefined; } if (!start) { return; } this._heartBeatTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () { try { yield this._client.request(new padlocal_pb_1.LongLinkHeartBeatRequest().setLonglinkid(this._id)); this._resetHeartBeatTimer(true); } catch (e) { this._onSocketError(new IOError(e, "send heart beat failed")); } }), delay || WeChatLongLinkProxy.HEART_BEAT_INTERVAL); } _startHeartbeat() { if (this._heartBeatTimer) { return; } Log_1.default.silly(LOGPRE, `[${this._currentHost()}] longlink startHeartbeat`); this._resetHeartBeatTimer(true); } _stopHeartbeat() { if (!this._heartBeatTimer) { return; } Log_1.default.silly(LOGPRE, `[${this._currentHost()}] longlink stopHeartbeat`); this._resetHeartBeatTimer(false); } _clearReconnectTimer() { if (!this._reconnectDelayTimer) { return; } clearInterval(this._reconnectDelayTimer); this._reconnectDelayTimer = undefined; } _notifyRequestCallback(messageId, error) { const promiseCallback = this._requestCallbackMap.get(messageId); this._requestCallbackMap.delete(messageId); if (!promiseCallback) { return; } if (!error) { promiseCallback.resolve(); } else { promiseCallback.reject(error); } } _notifyAllRequestCallbackWithError(error) { for (const [, promiseCallback] of this._requestCallbackMap.entries()) { promiseCallback.reject(error); } this._requestCallbackMap.clear(); } _destroyLongLink(error) { if (error) { this._notifyAllRequestCallbackWithError(error); } else { this._notifyAllRequestCallbackWithError(new Error("longlink reset")); } this._stopHeartbeat(); if (this._socketConnectTimeout) { clearTimeout(this._socketConnectTimeout); this._socketConnectTimeout = undefined; } if (this._socket) { this._socket.removeAllListeners(); this._socket.destroy(); this._socket = undefined; } this._id = undefined; this._socketPromise = undefined; this._serialExecutor.clear(); this._streamCallback = undefined; } _onSocketError(error) { const preStatus = this._status; if (preStatus === Status.STOP || preStatus === Status.ERROR) { return; } Log_1.default.warn(LOGPRE, `[${this._currentHost()}] close connection on error`, error); this._destroyLongLink(error); this._updateStatus(Status.ERROR); this._tryReconnect(); } _tryReconnect() { // already reconnecting if (this._reconnectDelayTimer) { Log_1.default.silly(LOGPRE, "dup reconnect, skip"); return; } if (!this._reconnectStrategy.canRetry()) { Log_1.default.warn(LOGPRE, "reconnect policy failed"); // 重试失败,关闭 this.shutdown(); return; } const delay = this._reconnectStrategy.nextRetryDelay(); Log_1.default.warn(LOGPRE, `longlink reconnect [${this._reconnectStrategy.retryCount}] after delay:${delay}ms`); this._reconnectDelayTimer = setTimeout(() => { this._clearReconnectTimer(); this._connect().then(); }, delay); } _connect() { return __awaiter(this, void 0, void 0, function* () { const host = Host_1.HostResolver.selectBestHostFromList(this._hostList); if (!host) { throw new Error("longlink host port is not configured yet"); } if (!this._socketPromise) { this._socketPromise = new Promise((resolve, reject) => { const startDate = new Date(); Log_1.default.silly(LOGPRE, `longlink start connect: ${host.host}:${host.port}`); const socket = new net_1.Socket(); socket.setTimeout(WeChatLongLinkProxy.SOCKET_TIMEOUT); this._socket = socket; this._id = `${this._instanceId.substr(0, 2)}:${Utils_1.genUUID().substr(0, 4)}`; // node socket doesn't support connect timeout natively, so implement our own version. this._socketConnectTimeout = setTimeout(() => { Log_1.default.warn(LOGPRE, `[${this._currentHost()}] longlink socket connect timeout`); this._adjustHostQuality(host, false); const error = new IOError("longlink socket connect timeout"); this._onSocketError(error); reject(error); }, WeChatLongLinkProxy.CONNECT_TIMEOUT); this._updateStatus(Status.CONNECTING); socket.connect({ host: host.host, port: host.port, }, () => __awaiter(this, void 0, void 0, function* () { const endDate = new Date(); Log_1.default.silly(LOGPRE, `[${this._currentHost()}] longlink connect success, cost ${endDate.getTime() - startDate.getTime()}ms`); this._adjustHostQuality(host, true); clearTimeout(this._socketConnectTimeout); this._socketConnectTimeout = undefined; this._updateStatus(Status.HALF_CONNECTED); try { const startDate = new Date(); Log_1.default.silly(LOGPRE, `[${this._currentHost()}] longlink start init`); yield this._client.request(new padlocal_pb_1.LongLinkInitRequest().setLonglinkid(this._id)); Log_1.default.silly(LOGPRE, `[${this._currentHost()}] longlink init done, cost ${new Date().getTime() - startDate.getTime()}ms`); this._reconnectStrategy.reset(); this._updateStatus(Status.CONNECTED); resolve(); this._startHeartbeat(); } catch (e) { this._onSocketError(new IOError(e, "long link init failed")); } })); socket.on("data", (data) => { Log_1.default.silly(LOGPRE, `[${this._currentHost()}] socket recv:${ByteUtils_1.bytesToHexString(data, ByteUtils_1.MAX_LOG_BYTES_LEN)}`); // stream mode if (this._streamCallback) { this._serialExecutor.execute(() => __awaiter(this, void 0, void 0, function* () { const eof = yield this._streamCallback.onStreamData(data); if (eof) { // end stream mode, switch to normal this._streamCallback = undefined; } })); } else { this._onSocketData(data); } }); socket.on("close", () => { this._serialExecutor.execute(() => __awaiter(this, void 0, void 0, function* () { yield this._onSocketError(new IOError(`[${host.host}:${host.port}] longlink socket is closed`)); })); }); socket.on("timeout", () => { this._serialExecutor.execute(() => __awaiter(this, void 0, void 0, function* () { yield this._onSocketError(new IOError(`[${host.host}:${host.port}] longlink socket is read-write timeout`)); })); }); socket.on("error", (error) => { this._serialExecutor.execute(() => __awaiter(this, void 0, void 0, function* () { yield this._onSocketError(new IOError(error, `[${host.host}:${host.port}]`)); })); }); }); } else { Log_1.default.silly(LOGPRE, "longlink duplicated connect"); } return this._socketPromise; }); } _onSocketData(data) { return __awaiter(this, void 0, void 0, function* () { let unpackResponse; try { unpackResponse = yield this._serialExecutor.execute(() => __awaiter(this, void 0, void 0, function* () { return this._client.request(new padlocal_pb_1.LongLinkUnpackRequest().setLonglinkid(this._id).setPayload(data)); })); } catch (e) { // if longlink unpack failed, notify all longlink pending callback with error, // bcz we don't know which request corresponding to current unpackResponse exactly. this._onSocketError(new IOError(e, "Exception while unpacking long link data")); throw e; } const pushMessageList = []; const messageList = unpackResponse.getMessageList(); for (const message of messageList) { if (message.getType() === padlocal_pb_1.LongLinkMessageType.NORMAL_MESSAGE) { this._notifyRequestCallback(message.getMessageid()); } else if (message.getType() === padlocal_pb_1.LongLinkMessageType.PUSH_MESSAGE) { const pushMessage = message.getPush(); if (pushMessage.getType() === padlocal_pb_1.LongLinkMessagePushType.NEW_MESSAGE) { this.emit("message-push"); } else { pushMessageList.push(message); } } } if (pushMessageList.length) { this.emit("push", pushMessageList); } }); } _adjustHostQuality(host, connectSuccess) { Host_1.HostResolver.adjustHostQuality(host, connectSuccess); Log_1.default.silly(LOGPRE, `adjust host quality:${JSON.stringify(host)}, connect success:${connectSuccess}, host list:${JSON.stringify(this._hostList)}`); } _currentHost() { return this._socket && this._socket.remoteAddress ? `${this._socket.remoteAddress}:${this._socket.remotePort}` : ""; } } exports.WeChatLongLinkProxy = WeChatLongLinkProxy; WeChatLongLinkProxy.REQUEST_TIMEOUT = 10 * 1000; WeChatLongLinkProxy.CONNECT_TIMEOUT = 10 * 1000; WeChatLongLinkProxy.SOCKET_TIMEOUT = 180 * 1000; WeChatLongLinkProxy.HEART_BEAT_INTERVAL = 160 * 1000; var Status; (function (Status) { Status[Status["IDLE"] = 0] = "IDLE"; Status[Status["CONNECTING"] = 1] = "CONNECTING"; Status[Status["HALF_CONNECTED"] = 2] = "HALF_CONNECTED"; Status[Status["CONNECTED"] = 3] = "CONNECTED"; Status[Status["STOP"] = 4] = "STOP"; Status[Status["ERROR"] = 5] = "ERROR"; })(Status = exports.Status || (exports.Status = {})); class IOError extends verror_1.default { } exports.IOError = IOError; //# sourceMappingURL=WeChatLongLinkProxy.js.map