@nixjs23n6/grpc-socket-core
Version:
gRPC for Web Clients.
384 lines • 16.3 kB
JavaScript
import { ConstantBackOff } from "@nixjs23n6/backoff-typescript";
import { Objectify } from "@nixjs23n6/objectify";
import debug from "debug";
import { proto } from "./proto";
import { WSConstant } from "./constants";
import { WSEnums } from "./enums";
import { showLogger } from "./utils/logger";
export 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(WSEnums.States.ON_OPENED, WSEnums.WebsocketEvents.open, ev);
this._handleCloseEvent = (ev) => this._handleEvent(WSEnums.States.ON_CLOSE, WSEnums.WebsocketEvents.close, ev);
this._handleErrorEvent = (ev) => this._handleEvent(WSEnums.States.ON_ERROR, WSEnums.WebsocketEvents.error, ev);
this._handleMessageEvent = (ev) => this._handleEvent(WSEnums.States.ON_CONNECTED, WSEnums.WebsocketEvents.message, ev);
this._mergeProtoConfig = ({ nestedRoot, protoFile, protoJSONFallback, executeEncoderDecoderMap, }) => 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 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 = debug("");
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: 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.protoRoot) {
return resolve(WSEnums.ProtoState.READY);
}
this._protoConfigParameters = this._mergeProtoConfig({
nestedRoot,
protoFile,
protoJSONFallback: protoJSONFallback || {},
executeEncoderDecoderMap: executeEncoderDecoderMap,
});
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(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.protoRoot) {
this._storeProtoInfo();
resolve(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 === WSEnums.ProtoState.READY) {
resolve(WSEnums.States.ON_PROTO_INIT);
this._logger("Notify", "Init proto");
this._storeProtoInfo();
this._createInstance();
}
else {
reject(WSEnums.States.ON_ERROR);
}
})
.catch((error) => reject(error));
});
}
feedLive() {
const now = Date.now();
if (this.websocket &&
this.connected &&
now - this._lastPing > WSConstant.WS_CONFIG.PING_TIME_IN_MS) {
this._lastPing = now;
const pingMsg = 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 >
WSConstant.WS_CONFIG.MESSAGE_TIMEOUT_IN_MS) {
listener.reject({ status: WSEnums.Statuses.TIMEOUT });
}
}
}
send(msgType, msgData) {
if (this._closedByUser)
new Promise(() => null);
const msgId = proto.generateId();
const promise = this.addToListener(msgType, msgId, msgData, WSEnums.ListenerTypes.ONCE, () => null);
const data = 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, 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.generateId();
let listener = null;
switch (listenerType) {
case WSEnums.ListenerTypes.ONCE:
listener = this._onceListeners;
break;
case WSEnums.ListenerTypes.LONG:
listener = this._longListeners;
break;
case 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 }, 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.MsgType.PONG) {
this._logger("Error", `Unhandled msg: ${JSON.stringify(msg)}`);
}
}
close(code, reason) {
return new Promise((resolve) => {
if (!this.websocket)
return resolve(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(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(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 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 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 === WSEnums.WebsocketEvents.message) {
this.broadcastMessage(proto.decodeMsg(ev.data));
}
else {
let _code, _reason;
if (ev.type === 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(WSEnums.WebsocketEvents.open, this._handleOpenEvent);
this.websocket.removeEventListener(WSEnums.WebsocketEvents.close, this._handleCloseEvent);
this.websocket.removeEventListener(WSEnums.WebsocketEvents.error, this._handleErrorEvent);
this.websocket.removeEventListener(WSEnums.WebsocketEvents.message, this._handleMessageEvent);
this.websocket.close(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(WSEnums.WebsocketEvents.open, this._handleOpenEvent);
this.websocket.addEventListener(WSEnums.WebsocketEvents.close, this._handleCloseEvent);
this.websocket.addEventListener(WSEnums.WebsocketEvents.error, this._handleErrorEvent);
this.websocket.addEventListener(WSEnums.WebsocketEvents.message, this._handleMessageEvent);
}
_storeProtoInfo() {
this._protoRoot = proto.protoRoot;
this._Msg = proto.Msg;
this._MsgType = 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 showLogger(this.log, type, message);
}
}
//# sourceMappingURL=WSClient.js.map