UNPKG

mobility-toolbox-js

Version:

Toolbox for JavaScript applications in the domains of mobility and logistics.

372 lines (371 loc) 13.8 kB
/** * Class used to facilitate connection to a WebSocketAPI and * also to manage properly messages send to the WebSocketAPI. * This class must not contain any specific implementation. * @private */ class WebSocketAPI { constructor() { this.defineProperties(); } /** * Get the websocket request string. * * @param {string} method Request mehtod {GET, SUB}. * @param {WebSocketParameters} params Request parameters. * @param {string} params.channel Channel name * @param {string} [params.args] Request arguments * @param {Number|string} [params.id] Request identifier * @return {string} request string * @private */ static getRequestString(method, params = {}) { let reqStr = `${method} ${params.channel}`; reqStr += params.args ? ` ${params.args}` : ''; reqStr += params.id ? ` ${params.id}` : ''; return reqStr.trim(); } addEvents(onMessage, onError) { if (this.websocket) { this.websocket.addEventListener('message', onMessage); if (onError) { this.websocket.addEventListener('error', onError); this.websocket.addEventListener('close', onError); } } } /** * Close the websocket definitively. * * @private */ close() { if (this.websocket && (this.open || this.connecting)) { this.websocket.onclose = () => { }; this.websocket.close(); this.messagesOnOpen = []; } } /** * (Re)connect the websocket. * * @param {string} url Websocket url. * @param {function} onOpen Callback called when the websocket connection is opened and before subscriptions of previous subscriptions. * @private */ connect(url, onOpen = () => { }) { var _a; // if no url specify, close the current websocket and do nothing. if (!url) { (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.close(); return; } // Behavior when a websocket already exists. if (this.websocket) { // If the current websocket has the same url and is open or is connecting, do nothing. if (this.websocket.url === url && (this.open || this.connecting)) { return; } // If the current websocket has not the same url and is open or is connecting, close it. if (this.websocket.url !== url && (this.open || this.connecting)) { this.websocket.close(); } } this.websocket = new WebSocket(url); if (!this.open) { this.websocket.addEventListener('open', () => { onOpen(); this.subscribePreviousSubscriptions(); }); } else { onOpen(); this.subscribePreviousSubscriptions(); } } defineProperties() { Object.defineProperties(this, { closed: { get: () => { return !!(!this.websocket || this.websocket.readyState === this.websocket.CLOSED); }, }, closing: { get: () => { return !!(this.websocket && this.websocket.readyState === this.websocket.CLOSING); }, }, connecting: { get: () => { return !!(this.websocket && this.websocket.readyState === this.websocket.CONNECTING); }, }, /** * Array of message to send on open. * @type {Array<string>} * @private */ messagesOnOpen: { value: [], writable: true, }, open: { get: () => { return !!(this.websocket && this.websocket.readyState === this.websocket.OPEN); }, }, /** * List of channels subscribed. * @type {WebSocketSubscribed} * @private */ subscribed: { value: {}, writable: true, }, /** * Array of subscriptions. * @type {Array<WebSocketSubscription>} * @private */ subscriptions: { value: [], writable: true, }, }); } /** * Sends a get request to the websocket. * The callback is called only once, when the response is received or when the call returns an error. * * @param {Object} params Parameters for the websocket get request * @param {function} cb callback on message event * @param {function} errorCb Callback on error and close event * @private */ get(params, cb, errorCb) { const requestString = WebSocketAPI.getRequestString('GET', params); this.send(requestString); // We wrap the callbacks to make sure they are called only once. const once = (callback) => { return (...args) => { // @ts-expect-error - We know that args is an array callback(...args); const index = this.requests.findIndex((request) => { return requestString === request.requestString && cb === request.cb; }); const { onErrorCb, onMessageCb } = this.requests[index]; this.removeEvents(onMessageCb, onErrorCb); this.requests.splice(index, 1); }; }; const { onErrorCb, onMessageCb } = this.listen(params, once(cb), errorCb && once(errorCb)); // Store requests and callbacks to be able to remove them. if (!this.requests) { this.requests = []; } const index = this.requests.findIndex((request) => { return requestString === request.requestString && cb === request.cb; }); const newReq = { cb, errorCb, onErrorCb, onMessageCb, params, requestString, }; if (index > -1) { // @ts-expect-error - We know that the requests is an array of WebSocketAPIRequest this.requests[index] = newReq; } else { // @ts-expect-error - We know that the requests is an array of WebSocketAPIRequest this.requests.push(newReq); } } /** * Listen to websocket messages. * * @param {WebSocketParameters} params Parameters for the websocket get request * @param {function} cb callback on listen * @param {function} errorCb Callback on error * @return {{onMessage: function, errorCb: function}} Object with onMessage and error callbacks * @private */ listen(params, cb, errorCb) { // Remove the previous identical callback this.unlisten(params, cb); // We wrap the message callback to be sure we only propagate the message if it is for the right channel. const onMessage = (evt) => { let data; try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data = JSON.parse(evt.data); } catch (err) { // eslint-disable-next-line no-console console.error('WebSocket: unable to parse JSON data', err, evt.data); return; } let source = params.channel; source += params.args ? ` ${params.args}` : ''; // Buffer channel message return a list of other channels to propagate to proper callbacks. let contents; if (data.source === 'buffer') { // @ts-expect-error - We know that the data is a WebSocketAPIBufferMessageEventData contents = data .content; } else { contents = [data]; } contents.forEach((content) => { // Because of backend optimization, the last content is null. if ((content === null || content === void 0 ? void 0 : content.source) === source && (!params.id || params.id === data.client_reference)) { cb(content); } }); }; this.addEvents(onMessage, errorCb); return { onErrorCb: errorCb, onMessageCb: onMessage }; } removeEvents(onMessage, onError) { if (this.websocket) { this.websocket.removeEventListener('message', onMessage); if (onError) { this.websocket.removeEventListener('error', onError); this.websocket.removeEventListener('close', onError); } } } /** * Sends a message to the websocket. * * @param {message} message Message to send. * @private */ send(message) { if (!this.websocket || this.closed || this.closing) { return; } const send = () => { var _a; (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.send(message); }; if (!this.open) { // This 'if' avoid sending 2 identical BBOX message on open, if (!this.messagesOnOpen.includes(message)) { this.messagesOnOpen.push(message); this.websocket.addEventListener('open', () => { this.messagesOnOpen = []; send(); }); this.websocket.addEventListener('close', () => { this.messagesOnOpen = []; }); } } else if (!this.messagesOnOpen.includes(message)) { send(); } } /** * Subscribe to a given channel. * * @param {Object} params Parameters for the websocket get request * @param {function} cb callback on listen * @param {function} errorCb Callback on error * @param {boolean} quiet if false, no GET or SUB requests are send, only the callback is registered. * @private */ subscribe(params, cb, errorCb, quiet = false) { const { onErrorCb, onMessageCb } = this.listen(params, cb, errorCb); const reqStr = WebSocketAPI.getRequestString('', params); const index = this.subscriptions.findIndex((subcr) => { return params.channel === subcr.params.channel && cb === subcr.cb; }); const newSubscr = { cb, errorCb, onErrorCb, onMessageCb, params, quiet }; if (index > -1) { // @ts-expect-error - We know that the subscriptions is an array of WebSocketAPISubscription this.subscriptions[index] = newSubscr; } else { // @ts-expect-error - We know that the subscriptions is an array of WebSocketAPISubscription this.subscriptions.push(newSubscr); } if (!this.subscribed[reqStr]) { if (!newSubscr.quiet) { this.send(`GET ${reqStr}`); this.send(`SUB ${reqStr}`); } this.subscribed[reqStr] = true; } } /** * After an auto reconnection we need to re-subscribe to the channels. */ subscribePreviousSubscriptions() { // Before to subscribe previous subscriptions we make sure they // are all defined as unsubscribed, because this code is asynchrone // and a subscription could have been added in between. Object.keys(this.subscribed).forEach((key) => { this.subscribed[key] = false; }); // Subscribe all previous subscriptions. [...this.subscriptions].forEach((s) => { this.subscribe(s.params, s.cb, s.errorCb, s.quiet); }); } /** * Unlisten websocket messages. * * @param {Object} params Parameters for the websocket get request. * @param {function} cb Callback used when listen. * @private */ unlisten(params, cb) { [...(this.subscriptions || []), ...(this.requests || [])] .filter((s) => { return s.params.channel === params.channel && (!cb || s.cb === cb); }) .forEach(({ onErrorCb, onMessageCb }) => { this.removeEvents(onMessageCb, onErrorCb); }); } /** * Unsubscribe from a channel. * @param {string} source source to unsubscribe from * @param {function} cb Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @private */ unsubscribe(source, cb) { const toRemove = this.subscriptions.filter((s) => { return s.params.channel === source && (!cb || s.cb === cb); }); toRemove.forEach(({ onErrorCb, onMessageCb }) => { this.removeEvents(onMessageCb, onErrorCb); }); this.subscriptions = this.subscriptions.filter((s) => { return s.params.channel !== source || (cb && s.cb !== cb); }); // If there is no more subscriptions to this channel, and the removed subscriptions didn't register quietly, // we DEL it. if (source && this.subscribed[source] && !this.subscriptions.find((s) => { return s.params.channel === source; }) && toRemove.find((subscr) => { return !subscr.quiet; })) { this.send(`DEL ${source}`); this.subscribed[source] = false; } } } export default WebSocketAPI;