Source: lib/socket.js

import EventEmitter from 'eventemitter3';
import EJSON from 'ejson';
import './mongo-id'; //  Register mongo object ids */

const events = ['open', 'close', 'message:in', 'message:out'];

/**
 * Wrapper-class for whatever native Websocket implementation
 * we use.
 * Standardizes messaging, so it's compatible with the Meteor backend.
 * @class
 */
export default class Socket extends EventEmitter {
  /**
   * Instantiate a new Socket. Pass the actual Socket implementation as constructor.
   * @example
   * const socket = Socket(Websocket, 'ws://localhost:3000/websocket');
   * socket.on('open', () => {
   *   // When the socket opens, send the `connect` message
   *   // to establish the DDP connection
   *   socket.send({
   *     msg: 'connect',
   *     version: '1.0',
   *     support: ['1.0'],
   *   });
   * });
   * socket.open();
   * @constructor
   * @param SocketConstructor {function} constructor function (Es5) or class (es6+) passed. Don't pass the instance!
   * @param endpoint {string} the websocket endpoint, usually (but not necessarily)
   *  starts with ws:// or wss:// and ends with /websocket
   */
  constructor(SocketConstructor, endpoint) {
    super();
    this.SocketConstructor = SocketConstructor;
    this.endpoint = endpoint;
    this.rawSocket = null;
  }

  /**
   * Sends a message out using the underlying
   * Websocket implementation.
   * @param object {object}
   */
  send(object) {
    if (!this.closing) {
      const message = EJSON.stringify(object);
      this.rawSocket.send(message);
      // Emit a copy of the object, as the listener might mutate it.
      this.emit('message:out', EJSON.parse(message));
    }
  }

  /**
   * Makes `open` a no-op if there's already a `rawSocket`.
   * This avoids memory / socket leaks if `open` is called twice (e.g. by a user
   * calling `ddp.connect` twice) without properly disposing of the
   * socket connection.
   *
   * `rawSocket` gets automatically set to `null` only when it goes into a
   * closed or error state.
   *
   * This way `rawSocket` is disposed of correctly: the socket connection is closed,
   * and the object can be garbage collected.
   *
   * @emits 'open' event
   * @emits 'error' event
   */
  open() {
    if (this.rawSocket) {
      return;
    }
    this.closing = false;
    this.rawSocket = new this.SocketConstructor(this.endpoint);

    /*
     *   Calls to `onopen` and `onclose` directly trigger the `open` and
     *   `close` events on the `Socket` instance.
     */
    this.rawSocket.onopen = () => this.emit('open');
    this.rawSocket.onclose = () => {
      this.rawSocket = null;
      this.emit('close');
      // TODO check in tests if this is still true when on close callbacks
      this.closing = false;
    };
    /*
     *   Calls to `onmessage` trigger a `message:in` event on the `Socket`
     *   instance only once the message (first parameter to `onmessage`) has
     *   been successfully parsed into a javascript object.
     */
    this.rawSocket.onmessage = (message) => {
      var object;
      try {
        object = EJSON.parse(message.data);
      } catch (ignore) {
        // Simply ignore the malformed message and return
        return;
      }
      // Outside the try-catch block as it must only catch JSON parsing
      // errors, not errors that may occur inside a "message:in" event
      // handler
      this.emit('message:in', object);
    };

    /**
     * Delegate the catched error one level up
     * @param event {Event} a generic Event that contains the error
     */
    this.rawSocket.onerror = (event) => {
      event.isRaw = true;
      this.emit('error', event);
    };
  }

  /**
   * Attempts to close the socket.
   * Leads to emitting the 'close' event.
   * @emits 'close' event
   */
  close() {
    /*
     *   Avoid throwing an error if `rawSocket === null`
     */
    if (this.rawSocket) {
      this.closing = true;
      this.rawSocket.close();
    }
  }
}