padlocal-client-ts
Version:
Padlocal ts client
406 lines • 18.4 kB
JavaScript
"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