Source: realtime/connector/WebSocketConnector.js

'use strict';

const CommunicationError = require('../../lib/error/CommunicationError');
const WebSocket = require('./websocket').WebSocket;
const lib = require('../../lib/baqend');

/**
 * @alias connector.WebSocketConnector
 */
class WebSocketConnector {
  /**
   * @param {connector.Connector} connector a connector
   * @param {String=} url The websocket connect script url
   * @return {connector.WebSocketConnector} a websocket connection
   */
  static create(url) {
    let websocket = this.websockets[url];
    if (!websocket) {
      websocket = new WebSocketConnector(url);
      this.websockets[url] = websocket;
    }
    return websocket;
  }

  /**
   * @param {String} url
   */
  constructor(url) {
    this.observers = {};
    this.socket = null;
    this.url = url;
  }

  open() {
    if (!this.socket) {
      const socket = new WebSocket(this.url);
      let socketPromise;

      const handleSocketCompletion = (error) => {
        // observable error calls can throw an exception therefore cleanup beforehand
        let isError = false;
        if (this.socket === socketPromise) {
          isError = socket.readyState !== 3;
          this.socket = null;
        }

        let firstErr;
        Object.keys(this.observers).forEach((id) => {
          const observer = this.observers[id];
          delete this.observers[id]; // unsubscribe to allow resubscriptions
          if (!observer) {
            return;
          }
          try {
            if (isError) {
              observer.error(new CommunicationError(null, error));
            } else {
              observer.complete();
            }
          } catch (e) {
            if (!firstErr) {
              firstErr = e;
            }
          }
        });

        if (firstErr) { throw firstErr; }
      };

      socket.onerror = handleSocketCompletion;
      socket.onclose = handleSocketCompletion;
      socket.onmessage = (event) => {
        const message = JSON.parse(event.data);
        message.date = new Date(message.date);

        const id = message.id;
        if (!id) {
          if (message.type === 'error') { handleSocketCompletion(message); }
          return;
        }

        const observer = this.observers[id];
        if (observer) {
          if (message.type === 'error') {
            observer.error(new CommunicationError(null, message));
          } else {
            observer.next(message);
          }
        }
      };

      socketPromise = new Promise((resolve) => {
        socket.onopen = resolve;
      }).then(() => socket);

      this.socket = socketPromise;
    }

    return this.socket;
  }

  close() {
    if (this.socket) {
      this.socket.then((socket) => {
        socket.close();
      });
      this.socket = null;
    }
  }

  /**
   * @param {util.TokenStorage} tokenStorage
   * @param {string} id subscription ID
   * @return {connector.ObservableStream} The channel for sending and receiving messages
   */
  openStream(tokenStorage, id) {
    const stream = new lib.Observable((observer) => {
      if (this.observers[id]) { throw new Error('Only one subscription per stream is allowed.'); }

      this.observers[id] = observer;
      return () => {
        // cleanup only our subscription and handle resubscription on the same stream id correctly
        if (this.observers[id] === observer) { delete this.observers[id]; }
      };
    });

    stream.send = (message) => {
      this.open().then((socket) => {
        message.id = id;
        if (tokenStorage.token) { message.token = tokenStorage.token; }
        const jsonMessage = JSON.stringify(message);
        socket.send(jsonMessage);
      });
    };

    return stream;
  }
}

Object.assign(WebSocketConnector, /** @lends connector.WebSocketConnector */ {
  /**
   * Map of all available connectors to their respective websocket connections
   * @type connector.Connector[]
   */
  websockets: {},
});

module.exports = WebSocketConnector;