@nixjs23n6/grpc-socket-core
Version:
gRPC for Web Clients.
389 lines • 17 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WSClient = void 0;
const tslib_1 = require("tslib");
const backoff_typescript_1 = require("@nixjs23n6/backoff-typescript");
const objectify_1 = require("@nixjs23n6/objectify");
const debug_1 = tslib_1.__importDefault(require("debug"));
const proto_1 = require("./proto");
const constants_1 = require("./constants");
const enums_1 = require("./enums");
const logger_1 = require("./utils/logger");
class WSClient {
constructor(baseURL, protocols, path, backOff, protoConfigParameters, executeAnyFunc, logger, maxRetries, // The maximum number of retries
autoReconnect, autoDestroyListeners // Auto remove listeners when the socket closed
) {
this._feedLiveIntervalId = null;
this._lastPing = 0;
this._onceListeners = [];
this._longListeners = [];
this._stateListeners = [];
this._closedByUser = false;
this._autoReconnect = false; // Auto reconnect when the socket closed
this._initState = false;
this._retry = false;
this._handleOpenEvent = (ev) => this._handleEvent(enums_1.WSEnums.States.ON_OPENED, enums_1.WSEnums.WebsocketEvents.open, ev);
this._handleCloseEvent = (ev) => this._handleEvent(enums_1.WSEnums.States.ON_CLOSE, enums_1.WSEnums.WebsocketEvents.close, ev);
this._handleErrorEvent = (ev) => this._handleEvent(enums_1.WSEnums.States.ON_ERROR, enums_1.WSEnums.WebsocketEvents.error, ev);
this._handleMessageEvent = (ev) => this._handleEvent(enums_1.WSEnums.States.ON_CONNECTED, enums_1.WSEnums.WebsocketEvents.message, ev);
this._mergeProtoConfig = ({ nestedRoot, protoFile, protoJSONFallback, executeEncoderDecoderMap, }) => objectify_1.Objectify.merge(this._protoConfigParameters, {
nestedRoot,
protoFile,
protoJSONFallback,
executeEncoderDecoderMap,
});
this._baseURL = baseURL;
this._path = path || "";
this.URL = this._baseURL + this._path;
this._protocols = protocols;
this._backOff = backOff || new backoff_typescript_1.ConstantBackOff(1000);
this._retries = 0;
this._autoDestroyListeners = autoDestroyListeners || false;
this._autoReconnect = autoReconnect || false;
this._protoConfigParameters = protoConfigParameters || {
nestedRoot: "",
protoFile: "",
executeEncoderDecoderMap: () => null,
};
this._maxRetries = maxRetries || 9;
this.setInitialState();
this._executeAnyFunc = executeAnyFunc;
this.log = (0, debug_1.default)("");
this._initLogger(logger);
}
set connected(v) {
this.connected = v;
}
get connected() {
return !!(this.websocket && this.websocket.readyState === WebSocket.OPEN);
}
get closed() {
return !!(this.websocket && this.websocket.readyState === WebSocket.CLOSED);
}
set protocols(v) {
this._protocols = v;
}
get protocols() {
return this._protocols;
}
get protoRoot() {
return this._protoRoot;
}
get protoMsg() {
return this._Msg;
}
get protoMsgType() {
return this._MsgType;
}
setInitialState() {
if (this._feedLiveIntervalId)
clearInterval(this._feedLiveIntervalId);
if (this._onceListeners) {
const numOnceListener = this._onceListeners.length;
for (let i = 0; i < numOnceListener; i += 1) {
const listener = this._onceListeners[i];
listener.reject({ status: enums_1.WSEnums.Statuses.TIMEOUT });
}
}
this._onceListeners = [];
if (this._longListeners.length > 0) {
this._longListeners = [];
}
this.websocket = undefined;
this._feedLiveIntervalId = null;
this._lastPing = 0;
// state
this._initState = false;
if (this._stateListeners.length > 0)
this._stateListeners = [];
}
initProto(nestedRoot = "ws", protoFile, protoJSONFallback, executeEncoderDecoderMap) {
return new Promise((resolve, reject) => {
if (proto_1.proto.protoRoot) {
return resolve(enums_1.WSEnums.ProtoState.READY);
}
this._protoConfigParameters = this._mergeProtoConfig({
nestedRoot,
protoFile,
protoJSONFallback: protoJSONFallback || {},
executeEncoderDecoderMap: executeEncoderDecoderMap,
});
proto_1.proto
.init(this._protoConfigParameters.nestedRoot, this._protoConfigParameters.protoFile, this._protoConfigParameters.protoJSONFallback, this._protoConfigParameters.executeEncoderDecoderMap)
.then((res) => {
return resolve(res);
})
.catch((error) => reject(error));
});
}
connect(path, protocols, protoConfigParameters, executeAnyFunc) {
return new Promise((resolve, reject) => {
var _a, _b, _c;
if (this.connected) {
return resolve(enums_1.WSEnums.States.ON_CONNECTED);
}
if (!this._retry) {
// The first time, all parameters will set data
// Do not set data when retry to connect socket.
if (path) {
this._path = path;
this.URL = this._baseURL + path;
}
if (protocols)
this._protocols = protocols;
if (protoConfigParameters) {
this._protoConfigParameters = this._mergeProtoConfig(protoConfigParameters);
}
if (executeAnyFunc)
this._executeAnyFunc = executeAnyFunc;
}
else {
this._retry = false;
}
if (typeof this._executeAnyFunc === "function")
this._executeAnyFunc(this);
if (proto_1.proto.protoRoot) {
this._storeProtoInfo();
resolve(enums_1.WSEnums.States.ON_PROTO_INIT);
this._logger("Notify", "Init proto");
return this._createInstance();
}
this.initProto((_a = this._protoConfigParameters) === null || _a === void 0 ? void 0 : _a.nestedRoot, (_b = this._protoConfigParameters) === null || _b === void 0 ? void 0 : _b.protoFile, (_c = this._protoConfigParameters) === null || _c === void 0 ? void 0 : _c.protoJSONFallback, this._protoConfigParameters.executeEncoderDecoderMap)
.then((res) => {
if (res === enums_1.WSEnums.ProtoState.READY) {
resolve(enums_1.WSEnums.States.ON_PROTO_INIT);
this._logger("Notify", "Init proto");
this._storeProtoInfo();
this._createInstance();
}
else {
reject(enums_1.WSEnums.States.ON_ERROR);
}
})
.catch((error) => reject(error));
});
}
feedLive() {
const now = Date.now();
if (this.websocket &&
this.connected &&
now - this._lastPing > constants_1.WSConstant.WS_CONFIG.PING_TIME_IN_MS) {
this._lastPing = now;
const pingMsg = proto_1.proto.createPingMessage();
if (pingMsg) {
this.websocket.send(pingMsg);
}
else {
this._logger("Error", "Cannot create ping message");
}
}
const numOnceListener = this._onceListeners.length;
for (let i = 0; i < numOnceListener; i++) {
const listener = this._onceListeners[i];
if (now - listener.addedTime >
constants_1.WSConstant.WS_CONFIG.MESSAGE_TIMEOUT_IN_MS) {
listener.reject({ status: enums_1.WSEnums.Statuses.TIMEOUT });
}
}
}
send(msgType, msgData) {
if (this._closedByUser)
new Promise(() => null);
const msgId = proto_1.proto.generateId();
const promise = this.addToListener(msgType, msgId, msgData, enums_1.WSEnums.ListenerTypes.ONCE, () => null);
const data = proto_1.proto.buildMsg(msgType, msgData, msgId);
if (this.websocket && promise != null && data)
this.websocket.send(data);
return promise;
}
subscribe(msgType, callback) {
return this.addToListener(msgType, null, null, enums_1.WSEnums.ListenerTypes.LONG, callback);
}
unsubscribe(subscriptionId) {
const numLongListener = this._longListeners.length;
const numStateListener = this._stateListeners.length;
for (let i = 0; i < numLongListener; i += 1) {
if (this._longListeners[i].subscriptionId === subscriptionId) {
return this._longListeners.splice(i, 1);
}
}
for (let i = 0; i < numStateListener; i += 1) {
if (this._stateListeners[i].subscriptionId === subscriptionId) {
return this._stateListeners.splice(i, 1);
}
}
return -1;
}
addToListener(msgType, msgId, msgData, listenerType, callback) {
const subscriptionId = proto_1.proto.generateId();
let listener = null;
switch (listenerType) {
case enums_1.WSEnums.ListenerTypes.ONCE:
listener = this._onceListeners;
break;
case enums_1.WSEnums.ListenerTypes.LONG:
listener = this._longListeners;
break;
case enums_1.WSEnums.ListenerTypes.STATE:
listener = this._stateListeners;
break;
default:
break;
}
if (listener != null) {
const promise = new Promise((resolve, reject) => {
listener.push({
subscriptionId,
msgType,
msgId,
msgData,
addedTime: Date.now(),
resolve,
reject,
callback,
});
});
return promise;
}
return subscriptionId;
}
broadcastState(state, type, code, reason) {
this._logger("Notify", `Broadcasting state: ${state}. Metadata: ${JSON.stringify(type)}. URL: ${this.URL}`);
for (const listener of this._stateListeners) {
listener.callback({ state, type, code, reason });
}
}
subscribeState(callback, code, reason) {
return this.addToListener(null, null, { code, reason }, enums_1.WSEnums.ListenerTypes.STATE, callback);
}
broadcastMessage(msg) {
let foundOnce = false;
const numOnceListener = this._onceListeners.length;
const numLongListener = this._longListeners.length;
this._logger("Notify", `Broadcasting message: ${JSON.stringify(msg)}. Protocols: ${this._protocols}`);
for (let i = 0; i < numOnceListener; i += 1) {
const listener = this._onceListeners[i];
if ((listener === null || listener === void 0 ? void 0 : listener.msgId) === msg.id) {
foundOnce = true;
if (msg.success)
listener.resolve(msg.data);
else
listener.reject(msg);
this._onceListeners.splice(i, 1);
break;
}
}
for (let i = 0; i < numLongListener; i += 1) {
const listener = this._longListeners[i];
if ((listener === null || listener === void 0 ? void 0 : listener.msgType) === msg.type) {
foundOnce = true;
listener.callback(msg === null || msg === void 0 ? void 0 : msg.data);
}
}
if (!foundOnce && msg.type !== proto_1.proto.MsgType.PONG) {
this._logger("Error", `Unhandled msg: ${JSON.stringify(msg)}`);
}
}
close(code, reason) {
return new Promise((resolve) => {
if (!this.websocket)
return resolve(enums_1.WSEnums.States.ON_CLOSE);
this._closedByUser = true; // logout
if (this.websocket.readyState === WebSocket.OPEN) {
this._logger("Notify", `${code}: ${reason || "Client is closing web socket"}.`);
this._initState = true;
this.websocket.close(code, reason);
this.subscribeState(resolve, code, reason);
return;
}
return resolve(enums_1.WSEnums.States.ON_CLOSE);
});
}
reconnect() {
return new Promise((resolve, reject) => {
if (!this._backOff)
return;
const backOff = this._backOff.next();
if (this._retries >= this._maxRetries) {
this._retry = false;
this._backOff.reset();
return reject(enums_1.WSEnums.States.ON_ERROR);
}
this._retry = true;
setTimeout(() => {
// retry connection after waiting out the backOff-interval
this._logger("Notify", "Auto-reconnecting to server");
this._retries += 1;
this.connect().then(resolve).catch(reject);
}, backOff);
});
}
_handleEvent(protoState, type, ev) {
var _a;
// eslint-disable-next-line default-case
switch (type) {
case enums_1.WSEnums.WebsocketEvents.close:
if (this._initState && this._autoDestroyListeners) {
this.setInitialState();
this._logger("Notify", "Clear all onceListeners, longListeners and broadcastStates");
}
if (!this._closedByUser && this._autoReconnect) {
// failed to connect or connection lost, try to reconnect
this.reconnect();
}
break;
case enums_1.WSEnums.WebsocketEvents.open:
this._retries = 0;
this._feedLiveIntervalId = setInterval(() => this.feedLive(), 1000);
(_a = this._backOff) === null || _a === void 0 ? void 0 : _a.reset(); // reset backOff
break;
}
this._dispatchEvent(protoState, type, ev); // forward to all listeners
}
_dispatchEvent(protoState, type, ev) {
if (type === enums_1.WSEnums.WebsocketEvents.message) {
this.broadcastMessage(proto_1.proto.decodeMsg(ev.data));
}
else {
let _code, _reason;
if (ev.type === enums_1.WSEnums.WebsocketEvents.close) {
const { code, reason } = ev;
_code = code;
_reason = reason;
}
this.broadcastState(protoState, type, _code, _reason);
}
}
_createInstance() {
if (this.websocket !== undefined) {
// remove all event-listeners from broken socket
this.websocket.removeEventListener(enums_1.WSEnums.WebsocketEvents.open, this._handleOpenEvent);
this.websocket.removeEventListener(enums_1.WSEnums.WebsocketEvents.close, this._handleCloseEvent);
this.websocket.removeEventListener(enums_1.WSEnums.WebsocketEvents.error, this._handleErrorEvent);
this.websocket.removeEventListener(enums_1.WSEnums.WebsocketEvents.message, this._handleMessageEvent);
this.websocket.close(enums_1.WSEnums.ReasonCode.CLOSE, "Close socket instance before create the new instance");
}
this.websocket = new WebSocket(this.URL, this._protocols);
this.websocket.binaryType = "arraybuffer";
this._logger("Success", "The Socket instance created");
this.websocket.addEventListener(enums_1.WSEnums.WebsocketEvents.open, this._handleOpenEvent);
this.websocket.addEventListener(enums_1.WSEnums.WebsocketEvents.close, this._handleCloseEvent);
this.websocket.addEventListener(enums_1.WSEnums.WebsocketEvents.error, this._handleErrorEvent);
this.websocket.addEventListener(enums_1.WSEnums.WebsocketEvents.message, this._handleMessageEvent);
}
_storeProtoInfo() {
this._protoRoot = proto_1.proto.protoRoot;
this._Msg = proto_1.proto.Msg;
this._MsgType = proto_1.proto.MsgType;
}
_initLogger(logger) {
this.log.enabled = (logger && logger.debug) || false;
this.log.namespace = (logger && logger.namespace) || "[Socket]";
this.log.color = (logger && logger.color) || "#D3DEDC";
}
_logger(type, message) {
return (0, logger_1.showLogger)(this.log, type, message);
}
}
exports.WSClient = WSClient;
//# sourceMappingURL=WSClient.js.map