Home Manual Reference Source Test Repository

src/Connection.js

/**
 * Does the signaling for the DataChannel
 * @protected
 */
export default class Connection {
  /**
   * Instantiates a new Connection object
   * @protected
   * @param {string} signalingUrl - The relative or absolute URL of the signaling.php file
   * @param {DataChannel} dataChannel - The DataChannel to operate on
   * @param {ChannelFactory} channelFactory - Instance factory for the opened channel
   */
  constructor(signalingUrl, dataChannel, channelFactory) {
    /**
     * The DataChannel to operate on
     * @private
     * @type {DataChannel}
     */
    this.dataChannel = dataChannel;
    /**
     * The relative or absolute URL of the signaling.php file
     * @private
     * @type {string}
     */
    this.signalingUrl = signalingUrl;
    /**
     * Instance factory for the opened channel
     * @private
     * @type {ChannelFactory}
     */
    this.channelFactory = channelFactory;
    /**
     * Signaling message callbacks per DataChannel, key = DataChannel id
     * @private
     * @type {Map<string, function>}
     */
    this.onMessageCallbacks = new Map();
    /**
     * List of received message ids
     * @private
     * @type {Set<number>}
     */
    this.messagesReceived = new Set();
  }

  /**
   * Creates a new channel with a random ID
   * @protected
   * @return {Channel} the opened channel
   */
  open() {
    this.setupChannel();
    const randomValueBuffer = new Uint32Array(1);
    window.crypto.getRandomValues(randomValueBuffer);
    const channelId = randomValueBuffer[0].toString(36);
    this.dataChannel.open(channelId);

    return this.channelFactory.instance(this.dataChannel);
  }

  /**
   * Connects to an open dataChannel
   * @protected
   * @param {string} channelId - the channel id
   * @return {Channel} the opened channel
   */
  connect(channelId) {
    this.setupChannel();
    this.dataChannel.connect(channelId);

    return this.channelFactory.instance(this.dataChannel);
  }

  /**
   * Setups and starts the signaling
   * @private
   */
  setupChannel() {
    this.repeatedlyCheck();
    this.dataChannel.openSignalingChannel = (config) => {
      const channelId = config.channel || this.dataChannel.channel;
      this.onMessageCallbacks.set(channelId, config.onmessage);
      if (config.onopen) {
        setTimeout(config.onopen, 1);
      }
      return {
        send: this.sendMessage.bind(this, channelId),
        channel: channelId,
      };
    };
  }

  /**
   * Repeatedly checks the server for new signaling messages
   * @private
   */
  repeatedlyCheck() {
    this.sendXHR().then(data => {
      if (!data || !data.length || data.every(entity => this.messagesReceived.has(entity.id))) {
        setTimeout(this.repeatedlyCheck.bind(this), 400);
        return;
      }

      data.forEach(this.handleSignalingData.bind(this));
      setTimeout(this.repeatedlyCheck.bind(this), 1);
    });
  }

  /**
   * Handles received signaling objects
   * @private
   * @param {Object} entity - A received signaling object
   */
  handleSignalingData(entity) {
    if (this.messagesReceived.has(entity.id)) {
      return;
    }

    this.messagesReceived.add(entity.id);
    const message = JSON.parse(entity.message);
    if (message.sender !== this.dataChannel.userid
        && this.onMessageCallbacks.has(message.channelId)) {
      this.onMessageCallbacks.get(message.channelId)(message.message);
    }
  }

  /**
   * Sends a signaling message over XHR to the signaling url
   * @private
   * @param {string} channelId - The channel id
   * @param {Object} message - The message object
   */
  sendMessage(channelId, message) {
    this.sendXHR({
      channelId,
      message,
      sender: this.dataChannel.userid,
    });
  }

  /**
   * Sends an XMLHttpRequest to the signaling url
   * @private
   * @param {Object} [data] - Data to send
   * @return {Promise<Object>} The response
   */
  sendXHR(data) {
    return new Promise((resolve) => {
      const request = new XMLHttpRequest();
      request.onreadystatechange = () => {
        if (request.readyState === 4 && request.status === 200) {
          resolve(JSON.parse(request.responseText));
        }
      };
      request.open('POST', this.signalingUrl);
      const formData = new FormData();
      if (data) {
        formData.append('message', JSON.stringify(data));
      }
      request.send(formData);
    });
  }
}