UNPKG

@nixjs23n6/grpc-socket-core

Version:

gRPC for Web Clients.

384 lines 16.3 kB
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