浏览代码

Merge pull request #498 from afrokick/refactoring/classes

Refactoring/classes
afrokick 6 年之前
父节点
当前提交
66f24c3f8b
共有 14 个文件被更改,包括 1837 次插入1469 次删除
  1. 0 9
      README.md
  2. 175 173
      index.d.ts
  3. 123 0
      lib/api.ts
  4. 35 0
      lib/baseconnection.ts
  5. 261 244
      lib/dataconnection.ts
  6. 49 0
      lib/enums.ts
  7. 2 2
      lib/exports.ts
  8. 79 78
      lib/mediaconnection.ts
  9. 351 293
      lib/negotiator.ts
  10. 488 494
      lib/peer.ts
  11. 5 0
      lib/servermessage.ts
  12. 267 174
      lib/socket.ts
  13. 1 1
      lib/util.ts
  14. 1 1
      package.json

+ 0 - 9
README.md

@@ -1,12 +1,3 @@
-# Collaborators wanted!!
-
-I'm too busy to improve PeerJS, but I don't want it to fall in dust so if anyone wants to become a maintainer, anyone is welcome!! I'll give support with any problem when coding and will be reviewing PRs. I'll also be in charge of the server and sysadmin things.
-
-it is a good opportunity to gain experience too, if you think it's too big for you, don't worry! I'll be mentoring you (sometimes :) )
-
-If you want to become a maintainer, you can write me to kidandcat@gmail.com
-
-
 # PeerJS: Simple peer-to-peer with WebRTC #
 
 [![Backers on Open Collective](https://opencollective.com/peer/backers/badge.svg)](#backers)

+ 175 - 173
index.d.ts

@@ -5,190 +5,192 @@
 export = Peer;
 
 declare class Peer {
-    prototype: RTCIceServer;
+  prototype: RTCIceServer;
 
-    /**
-     * A peer can connect to other peers and listen for connections.
-     * @param id Other peers can connect to this peer using the provided ID.
-     *     If no ID is given, one will be generated by the brokering server.
-     * @param options for specifying details about PeerServer
-     */
-    constructor(id: string, options?: Peer.PeerJSOption);
+  /**
+   * A peer can connect to other peers and listen for connections.
+   * @param id Other peers can connect to this peer using the provided ID.
+   *     If no ID is given, one will be generated by the brokering server.
+   * @param options for specifying details about PeerServer
+   */
+  constructor(id: string, options?: Peer.PeerJSOption);
 
-    /**
-     * A peer can connect to other peers and listen for connections.
-     * @param options for specifying details about PeerServer
-     */
-    constructor(options: Peer.PeerJSOption);
+  /**
+   * A peer can connect to other peers and listen for connections.
+   * @param options for specifying details about PeerServer
+   */
+  constructor(options: Peer.PeerJSOption);
 
-    /**
-     *
-     * @param id The brokering ID of the remote peer (their peer.id).
-     * @param options for specifying details about Peer Connection
-     */
-    connect(id: string, options?: Peer.PeerConnectOption): Peer.DataConnection;
-    /**
-     * Connects to the remote peer specified by id and returns a data connection.
-     * @param id The brokering ID of the remote peer (their peer.id).
-     * @param stream The caller's media stream
-     * @param options Metadata associated with the connection, passed in by whoever initiated the connection.
-     */
-    call(id: string, stream: any, options?: any): Peer.MediaConnection;
-    /**
-     * Calls the remote peer specified by id and returns a media connection.
-     * @param event Event name
-     * @param cb Callback Function
-     */
-    on(event: string, cb: ()=>void): void;
-    /**
-     * Emitted when a connection to the PeerServer is established.
-     * @param event Event name
-     * @param cb id is the brokering ID of the peer
-     */
-    on(event: 'open', cb: (id: string)=>void): void;
-    /**
-     * Emitted when a new data connection is established from a remote peer.
-     * @param event Event name
-     * @param cb Callback Function
-     */
-    on(event: 'connection', cb: (dataConnection: Peer.DataConnection)=>void): void;
-    /**
-     * Emitted when a remote peer attempts to call you.
-     * @param event Event name
-     * @param cb Callback Function
-     */
-    on(event: 'call', cb: (mediaConnection: Peer.MediaConnection)=>void): void;
-    /**
-     * Emitted when the peer is destroyed and can no longer accept or create any new connections.
-     * @param event Event name
-     * @param cb Callback Function
-     */
-    on(event: 'close', cb: ()=>void): void;
-    /**
-     * Emitted when the peer is disconnected from the signalling server
-     * @param event Event name
-     * @param cb Callback Function
-     */
-    on(event: 'disconnected', cb: ()=>void): void;
-    /**
-     * Errors on the peer are almost always fatal and will destroy the peer.
-     * @param event Event name
-     * @param cb Callback Function
-     */
-    on(event: 'error', cb: (err: any)=>void): void;
-    /**
-     * Remove event listeners.(EventEmitter3)
-     * @param {String} event The event we want to remove.
-     * @param {Function} fn The listener that we need to find.
-     * @param {Boolean} once Only remove once listeners.
-     */
-    off(event: string, fn: Function, once?: boolean): void;
-    /**
-     * Close the connection to the server, leaving all existing data and media connections intact.
-     */
-    disconnect(): void;
-    /**
-     * Attempt to reconnect to the server with the peer's old ID
-     */
-    reconnect(): void;
-    /**
-     * Close the connection to the server and terminate all existing connections.
-     */
-    destroy(): void;
+  /**
+   *
+   * @param id The brokering ID of the remote peer (their peer.id).
+   * @param options for specifying details about Peer Connection
+   */
+  connect(id: string, options?: Peer.PeerConnectOption): Peer.DataConnection;
+  /**
+   * Connects to the remote peer specified by id and returns a data connection.
+   * @param id The brokering ID of the remote peer (their peer.id).
+   * @param stream The caller's media stream
+   * @param options Metadata associated with the connection, passed in by whoever initiated the connection.
+   */
+  call(id: string, stream: MediaStream, options?: any): Peer.MediaConnection;
+  /**
+   * Calls the remote peer specified by id and returns a media connection.
+   * @param event Event name
+   * @param cb Callback Function
+   */
+  on(event: string, cb: () => void): void;
+  /**
+   * Emitted when a connection to the PeerServer is established.
+   * @param event Event name
+   * @param cb id is the brokering ID of the peer
+   */
+  on(event: "open", cb: (id: string) => void): void;
+  /**
+   * Emitted when a new data connection is established from a remote peer.
+   * @param event Event name
+   * @param cb Callback Function
+   */
+  on(
+    event: "connection",
+    cb: (dataConnection: Peer.DataConnection) => void
+  ): void;
+  /**
+   * Emitted when a remote peer attempts to call you.
+   * @param event Event name
+   * @param cb Callback Function
+   */
+  on(event: "call", cb: (mediaConnection: Peer.MediaConnection) => void): void;
+  /**
+   * Emitted when the peer is destroyed and can no longer accept or create any new connections.
+   * @param event Event name
+   * @param cb Callback Function
+   */
+  on(event: "close", cb: () => void): void;
+  /**
+   * Emitted when the peer is disconnected from the signalling server
+   * @param event Event name
+   * @param cb Callback Function
+   */
+  on(event: "disconnected", cb: () => void): void;
+  /**
+   * Errors on the peer are almost always fatal and will destroy the peer.
+   * @param event Event name
+   * @param cb Callback Function
+   */
+  on(event: "error", cb: (err: any) => void): void;
+  /**
+   * Remove event listeners.(EventEmitter3)
+   * @param {String} event The event we want to remove.
+   * @param {Function} fn The listener that we need to find.
+   * @param {Boolean} once Only remove once listeners.
+   */
+  off(event: string, fn: Function, once?: boolean): void;
+  /**
+   * Close the connection to the server, leaving all existing data and media connections intact.
+   */
+  disconnect(): void;
+  /**
+   * Attempt to reconnect to the server with the peer's old ID
+   */
+  reconnect(): void;
+  /**
+   * Close the connection to the server and terminate all existing connections.
+   */
+  destroy(): void;
 
-    /**
-     * Retrieve a data/media connection for this peer.
-     * @param peer
-     * @param id
-     */
-    getConnection(peer: Peer, id: string): any;
+  /**
+   * Retrieve a data/media connection for this peer.
+   * @param peer
+   * @param id
+   */
+  getConnection(peer: Peer, id: string): any;
 
-    /**
-     * Get a list of available peer IDs
-     * @param callback
-     */
-    listAllPeers(callback: (peerIds: Array<string>)=>void): void;
-    /**
-     * The brokering ID of this peer
-     */
-    id: string;
-    /**
-     * A hash of all connections associated with this peer, keyed by the remote peer's ID.
-     */
-    connections: any;
-    /**
-     * false if there is an active connection to the PeerServer.
-     */
-    disconnected: boolean;
-    /**
-     * true if this peer and all of its connections can no longer be used.
-     */
-    destroyed: boolean;
+  /**
+   * Get a list of available peer IDs
+   * @param callback
+   */
+  listAllPeers(callback: (peerIds: Array<string>) => void): void;
+  /**
+   * The brokering ID of this peer
+   */
+  id: string;
+  /**
+   * A hash of all connections associated with this peer, keyed by the remote peer's ID.
+   */
+  connections: any;
+  /**
+   * false if there is an active connection to the PeerServer.
+   */
+  disconnected: boolean;
+  /**
+   * true if this peer and all of its connections can no longer be used.
+   */
+  destroyed: boolean;
 }
 
 declare namespace Peer {
-    interface PeerJSOption{
-        key?: string;
-        host?: string;
-        port?: number;
-        path?: string;
-        secure?: boolean;
-        config?: RTCConfiguration;
-        debug?: number;
-    }
-
-    interface PeerConnectOption{
-        label?: string;
-        metadata?: any;
-        serialization?: string;
-        reliable?: boolean;
+  interface PeerJSOption {
+    key?: string;
+    host?: string;
+    port?: number;
+    path?: string;
+    secure?: boolean;
+    config?: RTCConfiguration;
+    debug?: number;
+  }
 
-    }
+  interface PeerConnectOption {
+    label?: string;
+    metadata?: any;
+    serialization?: string;
+    reliable?: boolean;
+  }
 
-    interface DataConnection{
-        send(data: any): void;
-        close(): void;
-        on(event: string, cb: ()=>void): void;
-        on(event: 'data', cb: (data: any)=>void): void;
-        on(event: 'open', cb: ()=>void): void;
-        on(event: 'close', cb: ()=>void): void;
-        on(event: 'error', cb: (err: any)=>void): void;
-        off(event: string, fn: Function, once?: boolean): void;
-        dataChannel: RTCDataChannel;
-        label: string;
-        metadata: any;
-        open: boolean;
-        peerConnection: any;
-        peer: string;
-        reliable: boolean;
-        serialization: string;
-        type: string;
-        buffSize: number;
-    }
+  interface DataConnection {
+    send(data: any): void;
+    close(): void;
+    on(event: string, cb: () => void): void;
+    on(event: "data", cb: (data: any) => void): void;
+    on(event: "open", cb: () => void): void;
+    on(event: "close", cb: () => void): void;
+    on(event: "error", cb: (err: any) => void): void;
+    off(event: string, fn: Function, once?: boolean): void;
+    dataChannel: RTCDataChannel;
+    label: string;
+    metadata: any;
+    open: boolean;
+    peerConnection: any;
+    peer: string;
+    reliable: boolean;
+    serialization: string;
+    type: string;
+    buffSize: number;
+  }
 
-    interface MediaConnection{
-        answer(stream?: any): void;
-        close(): void;
-        on(event: string, cb: ()=>void): void;
-        on(event: 'stream', cb: (stream: any)=>void): void;
-        on(event: 'close', cb: ()=>void): void;
-        on(event: 'error', cb: (err: any)=>void): void;
-        off(event: string, fn: Function, once?: boolean): void;
-        open: boolean;
-        metadata: any;
-        peer: string;
-        type: string;
-    }
+  interface MediaConnection {
+    answer(stream?: any): void;
+    close(): void;
+    on(event: string, cb: () => void): void;
+    on(event: "stream", cb: (stream: any) => void): void;
+    on(event: "close", cb: () => void): void;
+    on(event: "error", cb: (err: any) => void): void;
+    off(event: string, fn: Function, once?: boolean): void;
+    open: boolean;
+    metadata: any;
+    peer: string;
+    type: string;
+  }
 
-    interface utilSupportsObj {
-        audioVideo: boolean;
-        data: boolean;
-        binary: boolean;
-        reliable: boolean;
-    }
+  interface utilSupportsObj {
+    audioVideo: boolean;
+    data: boolean;
+    binary: boolean;
+    reliable: boolean;
+  }
 
-    interface util{
-        browser: string;
-        supports: utilSupportsObj;
-    }
+  interface util {
+    browser: string;
+    supports: utilSupportsObj;
+  }
 }

+ 123 - 0
lib/api.ts

@@ -0,0 +1,123 @@
+import { util } from "./util";
+import { PeerErrorType, PeerEventType } from "./enums";
+
+export class ApiError {
+  type: PeerErrorType;
+  message: string = "";
+}
+export class API {
+  constructor(private readonly _options: any) {}
+
+  private _buildUrl(method: string): string {
+    const protocol = this._options.secure ? "https://" : "http://";
+    let url =
+      protocol +
+      this._options.host +
+      ":" +
+      this._options.port +
+      this._options.path +
+      this._options.key +
+      "/" +
+      method;
+    const queryString = "?ts=" + new Date().getTime() + "" + Math.random();
+    url += queryString;
+
+    return url;
+  }
+
+  /** Get a unique ID from the server via XHR and initialize with it. */
+  retrieveId(cb = (error: ApiError, id?: string) => {}): void {
+    const http = new XMLHttpRequest();
+    const url = this._buildUrl("id");
+    // If there's no ID we need to wait for one before trying to init socket.
+    http.open("get", url, true);
+
+    const self = this;
+
+    http.onerror = function(e) {
+      util.error("Error retrieving ID", e);
+      let pathError = "";
+
+      if (
+        self._options.path === "/" &&
+        self._options.host !== util.CLOUD_HOST
+      ) {
+        pathError =
+          " If you passed in a `path` to your self-hosted PeerServer, " +
+          "you'll also need to pass in that same path when creating a new " +
+          "Peer.";
+      }
+
+      cb({
+        type: PeerErrorType.ServerError,
+        message: "Could not get an ID from the server." + pathError
+      });
+    };
+
+    http.onreadystatechange = function() {
+      if (http.readyState !== 4 || http.status === 0) {
+        return;
+      }
+
+      if (http.status !== 200) {
+        http.onerror(new ProgressEvent(`status === ${http.status}`));
+        return;
+      }
+
+      cb(null, http.responseText);
+    };
+
+    http.send(null);
+  }
+
+  listAllPeers(cb = (error: ApiError, peers?: any[]) => {}): void {
+    const http = new XMLHttpRequest();
+    let url = this._buildUrl("peers");
+
+    // If there's no ID we need to wait for one before trying to init socket.
+    http.open("get", url, true);
+
+    const self = this;
+
+    http.onerror = function(e) {
+      util.error("Error retrieving list of peers", e);
+
+      cb({
+        type: PeerErrorType.ServerError,
+        message: "Could not get peers from the server."
+      });
+    };
+
+    http.onreadystatechange = function() {
+      if (http.readyState !== 4) {
+        return;
+      }
+
+      if (http.status === 401) {
+        let helpfulError = "";
+        if (self._options.host !== util.CLOUD_HOST) {
+          helpfulError =
+            "It looks like you're using the cloud server. You can email " +
+            "team@peerjs.com to enable peer listing for your API key.";
+        } else {
+          helpfulError =
+            "You need to enable `allow_discovery` on your self-hosted " +
+            "PeerServer to use this feature.";
+        }
+
+        cb({
+          type: PeerErrorType.ServerError,
+          message:
+            "It doesn't look like you have permission to list peers IDs. " +
+            helpfulError
+        });
+      } else if (http.status !== 200) {
+        cb(null, []);
+      } else {
+        cb(JSON.parse(http.responseText));
+      }
+    };
+
+    http.send(null);
+  }
+}

+ 35 - 0
lib/baseconnection.ts

@@ -0,0 +1,35 @@
+import { EventEmitter } from "eventemitter3";
+import { Peer } from "./peer";
+import { RTCPeerConnection } from "./adapter";
+import { ServerMessage } from "./servermessage";
+import { ConnectionType } from "./enums";
+
+export abstract class BaseConnection extends EventEmitter {
+  protected _open = false;
+
+  readonly metadata: any;
+  connectionId: string;
+
+  peerConnection: RTCPeerConnection;
+
+  abstract get type(): ConnectionType;
+
+  get open() {
+    return this._open;
+  }
+
+  constructor(
+    readonly peer: string,
+    readonly provider: Peer,
+    readonly options: any
+  ) {
+    super();
+
+    this.metadata = options.metadata;
+    this.connectionId = options.connectionId;
+  }
+
+  abstract close(): void;
+
+  abstract handleMessage(message: ServerMessage): void;
+}

+ 261 - 244
lib/dataconnection.ts

@@ -1,289 +1,306 @@
 import { util } from "./util";
-import { EventEmitter } from "eventemitter3";
-import { Negotiator } from "./negotiator";
+import Negotiator from "./negotiator";
 import { Reliable } from "reliable";
+import {
+  ConnectionType,
+  ConnectionEventType,
+  SerializationType
+} from "./enums";
+import { Peer } from "./peer";
+import { BaseConnection } from "./baseconnection";
+import { ServerMessage } from "./servermessage";
 
 /**
  * Wraps a DataChannel between two Peers.
  */
-export function DataConnection(peer, provider, options) {
-  if (!(this instanceof DataConnection))
-    return new DataConnection(peer, provider, options);
-  EventEmitter.call(this);
-
-  this.options = util.extend(
-    {
-      serialization: "binary",
-      reliable: false
-    },
-    options
-  );
-
-  // Connection is not open yet.
-  this.open = false;
-  this.type = "data";
-  this.peer = peer;
-  this.provider = provider;
-
-  this.id =
-    this.options.connectionId || DataConnection._idPrefix + util.randomToken();
-
-  this.label = this.options.label || this.id;
-  this.metadata = this.options.metadata;
-  this.serialization = this.options.serialization;
-  this.reliable = this.options.reliable;
-
-  // Data channel buffering.
-  this._buffer = [];
-  this._buffering = false;
-  this.bufferSize = 0;
-
-  // For storing large data.
-  this._chunkedData = {};
-
-  if (this.options._payload) {
-    this._peerBrowser = this.options._payload.browser;
+export class DataConnection extends BaseConnection {
+  private static readonly ID_PREFIX = "dc_";
+
+  readonly label: string;
+  readonly serialization: SerializationType;
+  readonly reliable: boolean;
+
+  get type() {
+    return ConnectionType.Data;
   }
 
-  Negotiator.startConnection(
-    this,
-    this.options._payload || {
-      originator: true
-    }
-  );
-}
+  private _buffer: any[] = [];
+  private _bufferSize = 0;
+  private _buffering = false;
+  private _chunkedData = {};
+
+  private _peerBrowser: any;
+  private _dc: RTCDataChannel;
+  private _reliable: Reliable;
+
+  get dataChannel() {
+    return this._dc;
+  }
 
-util.inherits(DataConnection, EventEmitter);
+  constructor(peerId: string, provider: Peer, options: any) {
+    super(peerId, provider, options);
 
-DataConnection._idPrefix = "dc_";
+    this.connectionId =
+      options.connectionId || DataConnection.ID_PREFIX + util.randomToken();
 
-/** Called by the Negotiator when the DataChannel is ready. */
-DataConnection.prototype.initialize = function(dc) {
-  this._dc = this.dataChannel = dc;
-  this._configureDataChannel();
-};
+    this.label = options.label || this.connectionId;
+    this.serialization = options.serialization;
+    this.reliable = options.reliable;
 
-DataConnection.prototype._configureDataChannel = function() {
-  var self = this;
-  if (util.supports.sctp) {
-    this._dc.binaryType = "arraybuffer";
+    if (options._payload) {
+      this._peerBrowser = options._payload.browser;
+    }
+
+    Negotiator.startConnection(
+      this,
+      options._payload || {
+        originator: true
+      }
+    );
   }
-  this._dc.onopen = function() {
-    util.log("Data channel connection success");
-    self.open = true;
-    self.emit("open");
-  };
-
-  // Use the Reliable shim for non Firefox browsers
-  if (!util.supports.sctp && this.reliable) {
-    this._reliable = new Reliable(this._dc, util.debug);
+
+  /** Called by the Negotiator when the DataChannel is ready. */
+  initialize(dc: RTCDataChannel): void {
+    this._dc = dc;
+    this._configureDataChannel();
   }
 
-  if (this._reliable) {
-    this._reliable.onmessage = function(msg) {
-      self.emit("data", msg);
+  private _configureDataChannel(): void {
+    if (util.supports.sctp) {
+      this.dataChannel.binaryType = "arraybuffer";
+    }
+
+    const self = this;
+
+    this.dataChannel.onopen = function() {
+      util.log("Data channel connection success");
+      self._open = true;
+      self.emit(ConnectionEventType.Open);
     };
-  } else {
-    this._dc.onmessage = function(e) {
-      self._handleDataMessage(e);
+
+    // Use the Reliable shim for non Firefox browsers
+    if (!util.supports.sctp && this.reliable) {
+      this._reliable = new Reliable(this.dataChannel, util.debug);
+    }
+
+    if (this._reliable) {
+      this._reliable.onmessage = function(msg) {
+        self.emit(ConnectionEventType.Data, msg);
+      };
+    } else {
+      this.dataChannel.onmessage = function(e) {
+        self._handleDataMessage(e);
+      };
+    }
+    this.dataChannel.onclose = function(e) {
+      util.log("DataChannel closed for:", self.peer);
+      self.close();
     };
   }
-  this._dc.onclose = function(e) {
-    util.log("DataChannel closed for:", self.peer);
-    self.close();
-  };
-};
-
-// Handles a DataChannel message.
-DataConnection.prototype._handleDataMessage = function(e) {
-  var self = this;
-  var data = e.data;
-  var datatype = data.constructor;
-  if (this.serialization === "binary" || this.serialization === "binary-utf8") {
-    if (datatype === Blob) {
-      // Datatype should never be blob
-      util.blobToArrayBuffer(data, function(ab) {
+
+  // Handles a DataChannel message.
+  private _handleDataMessage(e): void {
+    let data = e.data;
+    const datatype = data.constructor;
+
+    if (
+      this.serialization === SerializationType.Binary ||
+      this.serialization === SerializationType.BinaryUTF8
+    ) {
+      if (datatype === Blob) {
+        const self = this;
+
+        // Datatype should never be blob
+        util.blobToArrayBuffer(data, function(ab) {
+          data = util.unpack(ab);
+          self.emit(ConnectionEventType.Data, data);
+        });
+        return;
+      } else if (datatype === ArrayBuffer) {
+        data = util.unpack(data);
+      } else if (datatype === String) {
+        // String fallback for binary data for browsers that don't support binary yet
+        const ab = util.binaryStringToArrayBuffer(data);
         data = util.unpack(ab);
-        self.emit("data", data);
-      });
-      return;
-    } else if (datatype === ArrayBuffer) {
-      data = util.unpack(data);
-    } else if (datatype === String) {
-      // String fallback for binary data for browsers that don't support binary yet
-      var ab = util.binaryStringToArrayBuffer(data);
-      data = util.unpack(ab);
+      }
+    } else if (this.serialization === SerializationType.JSON) {
+      data = JSON.parse(data);
     }
-  } else if (this.serialization === "json") {
-    data = JSON.parse(data);
-  }
 
-  // Check if we've chunked--if so, piece things back together.
-  // We're guaranteed that this isn't 0.
-  if (data.__peerData) {
-    var id = data.__peerData;
-    var chunkInfo = this._chunkedData[id] || {
-      data: [],
-      count: 0,
-      total: data.total
-    };
+    // Check if we've chunked--if so, piece things back together.
+    // We're guaranteed that this isn't 0.
+    if (data.__peerData) {
+      const id = data.__peerData;
+      const chunkInfo = this._chunkedData[id] || {
+        data: [],
+        count: 0,
+        total: data.total
+      };
+
+      chunkInfo.data[data.n] = data.data;
+      chunkInfo.count++;
+
+      if (chunkInfo.total === chunkInfo.count) {
+        // Clean up before making the recursive call to `_handleDataMessage`.
+        delete this._chunkedData[id];
+
+        // We've received all the chunks--time to construct the complete data.
+        data = new Blob(chunkInfo.data);
+        this._handleDataMessage({ data: data });
+      }
+
+      this._chunkedData[id] = chunkInfo;
+      return;
+    }
 
-    chunkInfo.data[data.n] = data.data;
-    chunkInfo.count += 1;
+    super.emit(ConnectionEventType.Data, data);
+  }
 
-    if (chunkInfo.total === chunkInfo.count) {
-      // Clean up before making the recursive call to `_handleDataMessage`.
-      delete this._chunkedData[id];
+  /**
+   * Exposed functionality for users.
+   */
 
-      // We've received all the chunks--time to construct the complete data.
-      data = new Blob(chunkInfo.data);
-      this._handleDataMessage({ data: data });
+  /** Allows user to close connection. */
+  close(): void {
+    if (!this.open) {
+      return;
     }
 
-    this._chunkedData[id] = chunkInfo;
-    return;
+    this._open = false;
+    Negotiator.cleanup(this);
+    super.emit(ConnectionEventType.Close);
   }
 
-  this.emit("data", data);
-};
-
-/**
- * Exposed functionality for users.
- */
+  /** Allows user to send data. */
+  send(data: any, chunked: boolean): void {
+    if (!this.open) {
+      super.emit(
+        ConnectionEventType.Error,
+        new Error(
+          "Connection is not open. You should listen for the `open` event before sending messages."
+        )
+      );
+      return;
+    }
 
-/** Allows user to close connection. */
-DataConnection.prototype.close = function() {
-  if (!this.open) {
-    return;
-  }
-  this.open = false;
-  Negotiator.cleanup(this);
-  this.emit("close");
-};
-
-/** Allows user to send data. */
-DataConnection.prototype.send = function(data, chunked) {
-  if (!this.open) {
-    this.emit(
-      "error",
-      new Error(
-        "Connection is not open. You should listen for the `open` event before sending messages."
-      )
-    );
-    return;
-  }
-  if (this._reliable) {
-    // Note: reliable shim sending will make it so that you cannot customize
-    // serialization.
-    this._reliable.send(data);
-    return;
-  }
-  var self = this;
-  if (this.serialization === "json") {
-    this._bufferedSend(JSON.stringify(data));
-  } else if (
-    this.serialization === "binary" ||
-    this.serialization === "binary-utf8"
-  ) {
-    var blob = util.pack(data);
-
-    // For Chrome-Firefox interoperability, we need to make Firefox "chunk"
-    // the data it sends out.
-    var needsChunking =
-      util.chunkedBrowsers[this._peerBrowser] ||
-      util.chunkedBrowsers[util.browser];
-    if (needsChunking && !chunked && blob.size > util.chunkedMTU) {
-      this._sendChunks(blob);
+    if (this._reliable) {
+      // Note: reliable shim sending will make it so that you cannot customize
+      // serialization.
+      this._reliable.send(data);
       return;
     }
 
-    // DataChannel currently only supports strings.
-    if (!util.supports.sctp) {
-      util.blobToBinaryString(blob, function(str) {
-        self._bufferedSend(str);
-      });
-    } else if (!util.supports.binaryBlob) {
-      // We only do this if we really need to (e.g. blobs are not supported),
-      // because this conversion is costly.
-      util.blobToArrayBuffer(blob, function(ab) {
-        self._bufferedSend(ab);
-      });
+    if (this.serialization === SerializationType.JSON) {
+      this._bufferedSend(JSON.stringify(data));
+    } else if (
+      this.serialization === SerializationType.Binary ||
+      this.serialization === SerializationType.BinaryUTF8
+    ) {
+      const blob = util.pack(data);
+
+      // For Chrome-Firefox interoperability, we need to make Firefox "chunk"
+      // the data it sends out.
+      const needsChunking =
+        util.chunkedBrowsers[this._peerBrowser] ||
+        util.chunkedBrowsers[util.browser];
+
+      if (needsChunking && !chunked && blob.size > util.chunkedMTU) {
+        this._sendChunks(blob);
+        return;
+      }
+
+      const self = this;
+
+      // DataChannel currently only supports strings.
+      if (!util.supports.sctp) {
+        util.blobToBinaryString(blob, function(str) {
+          self._bufferedSend(str);
+        });
+      } else if (!util.supports.binaryBlob) {
+        // We only do this if we really need to (e.g. blobs are not supported),
+        // because this conversion is costly.
+        util.blobToArrayBuffer(blob, function(ab) {
+          self._bufferedSend(ab);
+        });
+      } else {
+        this._bufferedSend(blob);
+      }
     } else {
-      this._bufferedSend(blob);
+      this._bufferedSend(data);
     }
-  } else {
-    this._bufferedSend(data);
   }
-};
 
-DataConnection.prototype._bufferedSend = function(msg) {
-  if (this._buffering || !this._trySend(msg)) {
-    this._buffer.push(msg);
-    this.bufferSize = this._buffer.length;
-  }
-};
-
-// Returns true if the send succeeds.
-DataConnection.prototype._trySend = function(msg) {
-  try {
-    this._dc.send(msg);
-  } catch (e) {
-    this._buffering = true;
-
-    var self = this;
-    setTimeout(function() {
-      // Try again.
-      self._buffering = false;
-      self._tryBuffer();
-    }, 100);
-    return false;
+  private _bufferedSend(msg): void {
+    if (this._buffering || !this._trySend(msg)) {
+      this._buffer.push(msg);
+      this._bufferSize = this._buffer.length;
+    }
   }
-  return true;
-};
 
-// Try to send the first message in the buffer.
-DataConnection.prototype._tryBuffer = function() {
-  if (this._buffer.length === 0) {
-    return;
+  // Returns true if the send succeeds.
+  private _trySend(msg): boolean {
+    try {
+      this.dataChannel.send(msg);
+    } catch (e) {
+      this._buffering = true;
+
+      const self = this;
+
+      setTimeout(function() {
+        // Try again.
+        self._buffering = false;
+        self._tryBuffer();
+      }, 100);
+      return false;
+    }
+
+    return true;
   }
 
-  var msg = this._buffer[0];
+  // Try to send the first message in the buffer.
+  private _tryBuffer(): void {
+    if (this._buffer.length === 0) {
+      return;
+    }
+
+    const msg = this._buffer[0];
 
-  if (this._trySend(msg)) {
-    this._buffer.shift();
-    this.bufferSize = this._buffer.length;
-    this._tryBuffer();
+    if (this._trySend(msg)) {
+      this._buffer.shift();
+      this._bufferSize = this._buffer.length;
+      this._tryBuffer();
+    }
   }
-};
 
-DataConnection.prototype._sendChunks = function(blob) {
-  var blobs = util.chunk(blob);
-  for (var i = 0, ii = blobs.length; i < ii; i += 1) {
-    var blob = blobs[i];
-    this.send(blob, true);
+  private _sendChunks(blob): void {
+    const blobs = util.chunk(blob);
+
+    for (let blob of blobs) {
+      this.send(blob, true);
+    }
   }
-};
-
-DataConnection.prototype.handleMessage = function(message) {
-  var payload = message.payload;
-
-  switch (message.type) {
-    case "ANSWER":
-      this._peerBrowser = payload.browser;
-
-      // Forward to negotiator
-      Negotiator.handleSDP(message.type, this, payload.sdp);
-      break;
-    case "CANDIDATE":
-      Negotiator.handleCandidate(this, payload.candidate);
-      break;
-    default:
-      util.warn(
-        "Unrecognized message type:",
-        message.type,
-        "from peer:",
-        this.peer
-      );
-      break;
+
+  handleMessage(message: ServerMessage): void {
+    const payload = message.payload;
+
+    switch (message.type) {
+      case "ANSWER":
+        this._peerBrowser = payload.browser;
+
+        // Forward to negotiator
+        Negotiator.handleSDP(message.type, this, payload.sdp);
+        break;
+      case "CANDIDATE":
+        Negotiator.handleCandidate(this, payload.candidate);
+        break;
+      default:
+        util.warn(
+          "Unrecognized message type:",
+          message.type,
+          "from peer:",
+          this.peer
+        );
+        break;
+    }
   }
-};
+}

+ 49 - 0
lib/enums.ts

@@ -0,0 +1,49 @@
+export enum ConnectionEventType {
+  Open = "open",
+  Stream = "stream",
+  Data = "data",
+  Close = "close",
+  Error = "error"
+}
+
+export enum ConnectionType {
+  Data = "data",
+  Media = "media"
+}
+
+export enum PeerEventType {
+  Open = "open",
+  Close = "close",
+  Connection = "connection",
+  Call = "call",
+  Disconnected = "disconnected",
+  Error = "error"
+}
+
+export enum PeerErrorType {
+  BrowserIncompatible = "browser-incompatible",
+  Disconnected = "disconnected",
+  InvalidID = "invalid-id",
+  InvalidKey = "invalid-key",
+  Network = "network",
+  PeerUnavailable = "peer-unavailable",
+  SslUnavailable = "ssl-unavailable",
+  ServerError = "server-error",
+  SocketError = "socket-error",
+  SocketClosed = "socket-closed",
+  UnavailableID = "unavailable-id",
+  WebRTC = "webrtc"
+}
+
+export enum SerializationType {
+  Binary = "binary",
+  BinaryUTF8 = "binary-utf8",
+  JSON = "json"
+}
+
+export enum SocketEventType {
+  Message = "message",
+  Disconnected = "disconnected",
+  Error = "error",
+  Close = "close"
+}

+ 2 - 2
lib/exports.ts

@@ -8,7 +8,7 @@ import { Socket } from "./socket";
 import { MediaConnection } from "./mediaconnection";
 import { DataConnection } from "./dataconnection";
 import { Peer } from "./peer";
-import { Negotiator } from "./negotiator";
+import Negotiator from "./negotiator";
 import jsBinarypack from "js-binarypack";
 
 window.Socket = Socket;
@@ -22,4 +22,4 @@ window.Negotiator = Negotiator;
 window.util = util;
 window.BinaryPack = jsBinarypack;
 
-export default Peer
+export default Peer;

+ 79 - 78
lib/mediaconnection.ts

@@ -1,98 +1,99 @@
 import { util } from "./util";
-import { EventEmitter } from "eventemitter3";
-import { Negotiator } from "./negotiator";
+import Negotiator from "./negotiator";
+import { ConnectionType, ConnectionEventType } from "./enums";
+import { Peer } from "./peer";
+import { BaseConnection } from "./baseconnection";
+import { ServerMessage } from "./servermessage";
 
 /**
  * Wraps the streaming interface between two Peers.
  */
-export function MediaConnection(peer, provider, options) {
-  if (!(this instanceof MediaConnection))
-    return new MediaConnection(peer, provider, options);
-  EventEmitter.call(this);
-
-  this.options = util.extend({}, options);
-
-  this.open = false;
-  this.type = "media";
-  this.peer = peer;
-  this.provider = provider;
-  this.metadata = this.options.metadata;
-  this.localStream = this.options._stream;
-
-  this.id =
-    this.options.connectionId || MediaConnection._idPrefix + util.randomToken();
-  if (this.localStream) {
-    Negotiator.startConnection(this, {
-      _stream: this.localStream,
-      originator: true
-    });
+export class MediaConnection extends BaseConnection {
+  private static readonly ID_PREFIX = "mc_";
+
+  private localStream: MediaStream;
+  private remoteStream: MediaStream;
+
+  get type() {
+    return ConnectionType.Media;
   }
-}
 
-util.inherits(MediaConnection, EventEmitter);
+  constructor(peerId: string, provider: Peer, options: any) {
+    super(peerId, provider, options);
+
+    this.localStream = this.options._stream;
+    this.connectionId =
+      this.options.connectionId ||
+      MediaConnection.ID_PREFIX + util.randomToken();
 
-MediaConnection._idPrefix = "mc_";
+    if (this.localStream) {
+      Negotiator.startConnection(this, {
+        _stream: this.localStream,
+        originator: true
+      });
+    }
+  }
+
+  addStream(remoteStream) {
+    util.log("Receiving stream", remoteStream);
 
-MediaConnection.prototype.addStream = function(remoteStream) {
-  util.log("Receiving stream", remoteStream);
+    this.remoteStream = remoteStream;
+    super.emit(ConnectionEventType.Stream, remoteStream); // Should we call this `open`?
+  }
 
-  this.remoteStream = remoteStream;
-  this.emit("stream", remoteStream); // Should we call this `open`?
-};
+  handleMessage(message: ServerMessage): void {
+    const type = message.type;
+    const payload = message.payload;
 
-MediaConnection.prototype.handleMessage = function(message) {
-  var payload = message.payload;
+    switch (message.type) {
+      case "ANSWER":
+        // Forward to negotiator
+        Negotiator.handleSDP(type, this, payload.sdp);
+        this._open = true;
+        break;
+      case "CANDIDATE":
+        Negotiator.handleCandidate(this, payload.candidate);
+        break;
+      default:
+        util.warn(`Unrecognized message type:${type} from peer:${this.peer}`);
+        break;
+    }
+  }
 
-  switch (message.type) {
-    case "ANSWER":
-      // Forward to negotiator
-      Negotiator.handleSDP(message.type, this, payload.sdp);
-      this.open = true;
-      break;
-    case "CANDIDATE":
-      Negotiator.handleCandidate(this, payload.candidate);
-      break;
-    default:
+  answer(stream: MediaStream): void {
+    if (this.localStream) {
       util.warn(
-        "Unrecognized message type:",
-        message.type,
-        "from peer:",
-        this.peer
+        "Local stream already exists on this MediaConnection. Are you answering a call twice?"
       );
-      break;
-  }
-};
-
-MediaConnection.prototype.answer = function(stream) {
-  if (this.localStream) {
-    util.warn(
-      "Local stream already exists on this MediaConnection. Are you answering a call twice?"
-    );
-    return;
-  }
+      return;
+    }
+
+    this.options._payload._stream = stream;
+
+    this.localStream = stream;
+    Negotiator.startConnection(this, this.options._payload);
+    // Retrieve lost messages stored because PeerConnection not set up.
+    const messages = this.provider._getMessages(this.connectionId);
 
-  this.options._payload._stream = stream;
+    for (let message of messages) {
+      this.handleMessage(message);
+    }
 
-  this.localStream = stream;
-  Negotiator.startConnection(this, this.options._payload);
-  // Retrieve lost messages stored because PeerConnection not set up.
-  var messages = this.provider._getMessages(this.id);
-  for (var i = 0, ii = messages.length; i < ii; i += 1) {
-    this.handleMessage(messages[i]);
+    this._open = true;
   }
-  this.open = true;
-};
 
-/**
- * Exposed functionality for users.
- */
+  /**
+   * Exposed functionality for users.
+   */
 
-/** Allows user to close connection. */
-MediaConnection.prototype.close = function() {
-  if (!this.open) {
-    return;
+  /** Allows user to close connection. */
+  close(): void {
+    if (!this.open) {
+      return;
+    }
+
+    this._open = false;
+    Negotiator.cleanup(this);
+    super.emit(ConnectionEventType.Close);
   }
-  this.open = false;
-  Negotiator.cleanup(this);
-  this.emit("close");
-};
+}

+ 351 - 293
lib/negotiator.ts

@@ -4,349 +4,407 @@ import {
   RTCSessionDescription,
   RTCIceCandidate
 } from "./adapter";
+import * as Reliable from "reliable";
+import { MediaConnection } from "./mediaconnection";
+import { DataConnection } from "./dataconnection";
+import { ConnectionType, PeerErrorType, ConnectionEventType } from "./enums";
+import { BaseConnection } from "./baseconnection";
+import { utils } from "mocha";
 
 /**
  * Manages all negotiations between Peers.
  */
-export const Negotiator = {
-  pcs: {
+class Negotiator {
+  readonly pcs = {
     data: {},
     media: {}
-  }, // type => {peerId: {pc_id: pc}}.
-  //providers: {}, // provider's id => providers (there may be multiple providers/client.
-  queue: [] // connections that are delayed due to a PC being in use.
-};
+  };
 
-Negotiator._idPrefix = "pc_";
+  queue: any[] = []; // connections that are delayed due to a PC being in use.
 
-/** Returns a PeerConnection object set up correctly (for data, media). */
-Negotiator.startConnection = function(connection, options) {
-  var pc = Negotiator._getPeerConnection(connection, options);
+  private readonly _idPrefix = "pc_";
 
-  // Set the connection's PC.
-  connection.pc = connection.peerConnection = pc;
+  /** Returns a PeerConnection object set up correctly (for data, media). */
+  startConnection(connection: BaseConnection, options: any) {
+    const peerConnection = this._getPeerConnection(connection, options);
 
-  if (connection.type === "media" && options._stream) {
-    addStreamToConnection(options._stream, pc);
-  }
+    // Set the connection's PC.
+    connection.peerConnection = peerConnection;
 
-  // What do we need to do now?
-  if (options.originator) {
-    if (connection.type === "data") {
-      // Create the datachannel.
-      var config = {};
-      // Dropping reliable:false support, since it seems to be crashing
-      // Chrome.
-      /*if (util.supports.sctp && !options.reliable) {
-        // If we have canonical reliable support...
-        config = {maxRetransmits: 0};
-      }*/
-      // Fallback to ensure older browsers don't crash.
-      if (!util.supports.sctp) {
-        config = { reliable: options.reliable };
-      }
-      var dc = pc.createDataChannel(connection.label, config);
-      connection.initialize(dc);
+    if (connection.type === ConnectionType.Media && options._stream) {
+      this._addTracksToConnection(options._stream, peerConnection);
     }
 
-    Negotiator._makeOffer(connection);
-  } else {
-    Negotiator.handleSDP("OFFER", connection, options.sdp);
-  }
-};
+    // What do we need to do now?
+    if (options.originator) {
+      if (connection.type === ConnectionType.Data) {
+        const dataConnection = <DataConnection>connection;
 
-Negotiator._getPeerConnection = function(connection, options) {
-  if (!Negotiator.pcs[connection.type]) {
-    util.error(
-      connection.type +
-        " is not a valid connection type. Maybe you overrode the `type` property somewhere."
-    );
-  }
+        let config = {};
 
-  if (!Negotiator.pcs[connection.type][connection.peer]) {
-    Negotiator.pcs[connection.type][connection.peer] = {};
-  }
-  var peerConnections = Negotiator.pcs[connection.type][connection.peer];
-
-  var pc;
-  // Not multiplexing while FF and Chrome have not-great support for it.
-  /*if (options.multiplex) {
-    ids = Object.keys(peerConnections);
-    for (var i = 0, ii = ids.length; i < ii; i += 1) {
-      pc = peerConnections[ids[i]];
-      if (pc.signalingState === 'stable') {
-        break; // We can go ahead and use this PC.
+        if (!util.supports.sctp) {
+          config = { reliable: options.reliable };
+        }
+
+        const dataChannel = peerConnection.createDataChannel(
+          dataConnection.label,
+          config
+        );
+        dataConnection.initialize(dataChannel);
       }
+
+      this._makeOffer(connection);
+    } else {
+      this.handleSDP("OFFER", connection, options.sdp);
     }
-  } else */
-  if (options.pc) {
-    // Simplest case: PC id already provided for us.
-    pc = Negotiator.pcs[connection.type][connection.peer][options.pc];
   }
 
-  if (!pc || pc.signalingState !== "stable") {
-    pc = Negotiator._startPeerConnection(connection);
-  }
-  return pc;
-};
-
-/*
-Negotiator._addProvider = function(provider) {
-  if ((!provider.id && !provider.disconnected) || !provider.socket.open) {
-    // Wait for provider to obtain an ID.
-    provider.on('open', function(id) {
-      Negotiator._addProvider(provider);
-    });
-  } else {
-    Negotiator.providers[provider.id] = provider;
-  }
-}*/
+  private _getPeerConnection(
+    connection: BaseConnection,
+    options: any
+  ): RTCPeerConnection {
+    if (!this.pcs[connection.type]) {
+      util.error(
+        connection.type +
+          " is not a valid connection type. Maybe you overrode the `type` property somewhere."
+      );
+    }
 
-/** Start a PC. */
-Negotiator._startPeerConnection = function(connection) {
-  util.log("Creating RTCPeerConnection.");
+    if (!this.pcs[connection.type][connection.peer]) {
+      this.pcs[connection.type][connection.peer] = {};
+    }
 
-  var id = Negotiator._idPrefix + util.randomToken();
-  var optional = {};
+    const peerConnections = this.pcs[connection.type][connection.peer];
 
-  if (connection.type === "data" && !util.supports.sctp) {
-    optional = { optional: [{ RtpDataChannels: true }] };
-  } else if (connection.type === "media") {
-    // Interop req for chrome.
-    optional = { optional: [{ DtlsSrtpKeyAgreement: true }] };
-  }
+    let pc;
 
-  var pc = new RTCPeerConnection(connection.provider.options.config, optional);
-  Negotiator.pcs[connection.type][connection.peer][id] = pc;
-
-  Negotiator._setupListeners(connection, pc, id);
-
-  return pc;
-};
-
-/** Set up various WebRTC listeners. */
-Negotiator._setupListeners = function(connection, pc, pc_id) {
-  var peerId = connection.peer;
-  var connectionId = connection.id;
-  var provider = connection.provider;
-
-  // ICE CANDIDATES.
-  util.log("Listening for ICE candidates.");
-  pc.onicecandidate = function(evt) {
-    if (evt.candidate) {
-      util.log("Received ICE candidates for:", connection.peer);
-      provider.socket.send({
-        type: "CANDIDATE",
-        payload: {
-          candidate: evt.candidate,
-          type: connection.type,
-          connectionId: connection.id
-        },
-        dst: peerId
-      });
+    if (options.pc) {
+      // Simplest case: PC id already provided for us.
+      pc = peerConnections[options.pc];
     }
-  };
 
-  pc.oniceconnectionstatechange = function() {
-    switch (pc.iceConnectionState) {
-      case "failed":
-        util.log(
-          "iceConnectionState is disconnected, closing connections to " + peerId
-        );
-        connection.emit(
-          "error",
-          new Error("Negotiation of connection to " + peerId + " failed.")
-        );
-        connection.close();
-        break;
-      case "disconnected":
-        util.log(
-          "iceConnectionState is disconnected, closing connections to " + peerId
-        );
-        break;
-      case "completed":
-        pc.onicecandidate = util.noop;
-        break;
+    if (!pc || pc.signalingState !== "stable") {
+      pc = this._startPeerConnection(connection);
     }
-  };
 
-  // Fallback for older Chrome impls.
-  pc.onicechange = pc.oniceconnectionstatechange;
-
-  // DATACONNECTION.
-  util.log("Listening for data channel");
-  // Fired between offer and answer, so options should already be saved
-  // in the options hash.
-  pc.ondatachannel = function(evt) {
-    util.log("Received data channel");
-    var dc = evt.channel;
-    var connection = provider.getConnection(peerId, connectionId);
-    connection.initialize(dc);
-  };
+    return pc;
+  }
+
+  /** Start a PC. */
+  private _startPeerConnection(connection: BaseConnection): RTCPeerConnection {
+    util.log("Creating RTCPeerConnection.");
+
+    const id = this._idPrefix + util.randomToken();
+    let optional = {};
 
-  // MEDIACONNECTION.
-  util.log("Listening for remote stream");
-  pc.ontrack = function(evt) {
-    util.log("Received remote stream");
-    var stream = evt.streams[0];
-    var connection = provider.getConnection(peerId, connectionId);
-    if (connection.type === "media") {
-      addStreamToConnection(stream, connection);
+    if (connection.type === ConnectionType.Data && !util.supports.sctp) {
+      optional = { optional: [{ RtpDataChannels: true }] };
+    } else if (connection.type === ConnectionType.Media) {
+      // Interop req for chrome.
+      optional = { optional: [{ DtlsSrtpKeyAgreement: true }] };
     }
-  };
-};
 
-Negotiator.cleanup = function(connection) {
-  util.log("Cleaning up PeerConnection to " + connection.peer);
+    const peerConnection = new RTCPeerConnection(
+      connection.provider.options.config,
+      optional
+    );
+
+    this.pcs[connection.type][connection.peer][id] = peerConnection;
 
-  var pc = connection.pc;
+    this._setupListeners(connection, peerConnection);
 
-  if (
-    !!pc &&
-    ((pc.readyState && pc.readyState !== "closed") ||
-      pc.signalingState !== "closed")
+    return peerConnection;
+  }
+
+  /** Set up various WebRTC listeners. */
+  private _setupListeners(
+    connection: BaseConnection,
+    peerConnection: RTCPeerConnection
   ) {
-    pc.close();
-    connection.pc = null;
+    const peerId = connection.peer;
+    const connectionId = connection.connectionId;
+    const connectionType = connection.type;
+    const provider = connection.provider;
+
+    // ICE CANDIDATES.
+    util.log("Listening for ICE candidates.");
+
+    peerConnection.onicecandidate = function(evt) {
+      if (evt.candidate) {
+        util.log("Received ICE candidates for:", peerId);
+        provider.socket.send({
+          type: "CANDIDATE",
+          payload: {
+            candidate: evt.candidate,
+            type: connectionType,
+            connectionId: connectionId
+          },
+          dst: peerId
+        });
+      }
+    };
+
+    peerConnection.oniceconnectionstatechange = function() {
+      switch (peerConnection.iceConnectionState) {
+        case "failed":
+          util.log(
+            "iceConnectionState is disconnected, closing connections to " +
+              peerId
+          );
+          connection.emit(
+            ConnectionEventType.Error,
+            new Error("Negotiation of connection to " + peerId + " failed.")
+          );
+          connection.close();
+          break;
+        case "disconnected":
+          util.log(
+            "iceConnectionState is disconnected, closing connections to " +
+              peerId
+          );
+          break;
+        case "completed":
+          peerConnection.onicecandidate = util.noop;
+          break;
+      }
+    };
+
+    // Fallback for older Chrome impls.
+    //@ts-ignore
+    peerConnection.onicechange = peerConnection.oniceconnectionstatechange;
+
+    // DATACONNECTION.
+    util.log("Listening for data channel");
+    // Fired between offer and answer, so options should already be saved
+    // in the options hash.
+    peerConnection.ondatachannel = function(evt) {
+      util.log("Received data channel");
+
+      const dataChannel = evt.channel;
+      const connection = <DataConnection>(
+        provider.getConnection(peerId, connectionId)
+      );
+
+      connection.initialize(dataChannel);
+    };
+
+    // MEDIACONNECTION.
+    util.log("Listening for remote stream");
+    const self = this;
+    peerConnection.ontrack = function(evt) {
+      util.log("Received remote stream");
+
+      const stream = evt.streams[0];
+      const connection = provider.getConnection(peerId, connectionId);
+
+      if (connection.type === ConnectionType.Media) {
+        const mediaConnection = <MediaConnection>connection;
+
+        self._addStreamToMediaConnection(stream, mediaConnection);
+      }
+    };
   }
-};
-
-Negotiator._makeOffer = function(connection) {
-  var pc: RTCPeerConnection = connection.pc;
-  const callback = function(offer) {
-    util.log("Created offer.");
-
-    if (
-      !util.supports.sctp &&
-      connection.type === "data" &&
-      connection.reliable
-    ) {
-      offer.sdp = Reliable.higherBandwidthSDP(offer.sdp);
+
+  cleanup(connection: BaseConnection): void {
+    util.log("Cleaning up PeerConnection to " + connection.peer);
+
+    const peerConnection = connection.peerConnection;
+
+    if (!peerConnection) {
+      return;
     }
-    const descCallback = function() {
-      util.log("Set localDescription: offer", "for:", connection.peer);
-      connection.provider.socket.send({
-        type: "OFFER",
-        payload: {
+
+    const peerConnectionNotClosed = peerConnection.signalingState !== "closed";
+    let dataChannelNotClosed = false;
+
+    if (connection.type === ConnectionType.Data) {
+      const dataConnection = <DataConnection>connection;
+      const dataChannel = dataConnection.dataChannel;
+
+      dataChannelNotClosed =
+        dataChannel.readyState && dataChannel.readyState !== "closed";
+    }
+
+    if (peerConnectionNotClosed || dataChannelNotClosed) {
+      peerConnection.close();
+      connection.peerConnection = null;
+    }
+  }
+
+  private async _makeOffer(connection: BaseConnection): Promise<void> {
+    const peerConnection = connection.peerConnection;
+
+    try {
+      const offer = await peerConnection.createOffer(
+        connection.options.constraints
+      );
+
+      util.log("Created offer.");
+
+      if (!util.supports.sctp && connection.type === ConnectionType.Data) {
+        const dataConnection = <DataConnection>connection;
+        if (dataConnection.reliable) {
+          offer.sdp = Reliable.higherBandwidthSDP(offer.sdp);
+        }
+      }
+
+      try {
+        await peerConnection.setLocalDescription(offer);
+
+        util.log("Set localDescription:", offer, `for:${connection.peer}`);
+
+        let payload: any = {
           sdp: offer,
           type: connection.type,
-          label: connection.label,
-          connectionId: connection.id,
-          reliable: connection.reliable,
-          serialization: connection.serialization,
+          connectionId: connection.connectionId,
           metadata: connection.metadata,
           browser: util.browser
-        },
-        dst: connection.peer
-      });
+        };
+
+        if (connection.type === ConnectionType.Data) {
+          const dataConnection = <DataConnection>connection;
+
+          payload = {
+            ...payload,
+            label: dataConnection.label,
+            reliable: dataConnection.reliable,
+            serialization: dataConnection.serialization
+          };
+        }
+
+        connection.provider.socket.send({
+          type: "OFFER",
+          payload,
+          dst: connection.peer
+        });
+      } catch (err) {
+        // TODO: investigate why _makeOffer is being called from the answer
+        if (
+          err !=
+          "OperationError: Failed to set local offer sdp: Called in wrong state: kHaveRemoteOffer"
+        ) {
+          connection.provider.emitError(PeerErrorType.WebRTC, err);
+          util.log("Failed to setLocalDescription, ", err);
+        }
+      }
+    } catch (err_1) {
+      connection.provider.emitError(PeerErrorType.WebRTC, err_1);
+      util.log("Failed to createOffer, ", err_1);
     }
-    const descError = function(err) {
-      // TODO: investigate why _makeOffer is being called from the answer
-      if (
-        err !=
-        "OperationError: Failed to set local offer sdp: Called in wrong state: kHaveRemoteOffer"
-      ) {
-        connection.provider.emitError("webrtc", err);
+  }
+
+  private async _makeAnswer(connection: BaseConnection): Promise<void> {
+    const peerConnection = connection.peerConnection;
+
+    try {
+      const answer = await peerConnection.createAnswer();
+      util.log("Created answer.");
+
+      if (!util.supports.sctp && connection.type === ConnectionType.Data) {
+        const dataConnection = <DataConnection>connection;
+        if (dataConnection.reliable) {
+          answer.sdp = Reliable.higherBandwidthSDP(answer.sdp);
+        }
+      }
+
+      try {
+        await peerConnection.setLocalDescription(answer);
+
+        util.log(`Set localDescription:`, answer, `for:${connection.peer}`);
+
+        connection.provider.socket.send({
+          type: "ANSWER",
+          payload: {
+            sdp: answer,
+            type: connection.type,
+            connectionId: connection.connectionId,
+            browser: util.browser
+          },
+          dst: connection.peer
+        });
+      } catch (err) {
+        connection.provider.emitError(PeerErrorType.WebRTC, err);
         util.log("Failed to setLocalDescription, ", err);
       }
+    } catch (err_1) {
+      connection.provider.emitError(PeerErrorType.WebRTC, err_1);
+      util.log("Failed to create answer, ", err_1);
     }
-    pc.setLocalDescription(offer)
-    .then(() => descCallback())
-    .catch(err => descError(err));
-  }
-  const errorHandler = function(err) {
-    connection.provider.emitError("webrtc", err);
-    util.log("Failed to createOffer, ", err);
   }
-  pc.createOffer(connection.options.constraints)
-    .then(offer => callback(offer))
-    .catch(err => errorHandler(err));
-};
-
-Negotiator._makeAnswer = function(connection) {
-  var pc: RTCPeerConnection = connection.pc;
-  const callback = function(answer) {
-    util.log("Created answer.");
-
-    if (
-      !util.supports.sctp &&
-      connection.type === "data" &&
-      connection.reliable
-    ) {
-      answer.sdp = Reliable.higherBandwidthSDP(answer.sdp);
-    }
 
-    const descCallback = function() {
-      util.log("Set localDescription: answer", "for:", connection.peer);
-      connection.provider.socket.send({
-        type: "ANSWER",
-        payload: {
-          sdp: answer,
-          type: connection.type,
-          connectionId: connection.id,
-          browser: util.browser
-        },
-        dst: connection.peer
-      });
+  /** Handle an SDP. */
+  async handleSDP(
+    type: string,
+    connection: BaseConnection,
+    sdp: any
+  ): Promise<void> {
+    sdp = new RTCSessionDescription(sdp);
+    const peerConnection = connection.peerConnection;
+
+    util.log("Setting remote description", sdp);
+
+    const self = this;
+
+    try {
+      await peerConnection.setRemoteDescription(sdp);
+      util.log(`Set remoteDescription:${type} for:${connection.peer}`);
+      if (type === "OFFER") {
+        await self._makeAnswer(connection);
+      }
+    } catch (err) {
+      connection.provider.emitError(PeerErrorType.WebRTC, err);
+      util.log("Failed to setRemoteDescription, ", err);
     }
-    pc.setLocalDescription(answer)
-    .then(() => descCallback())
-    .catch(err => {
-        connection.provider.emitError("webrtc", err);
-        util.log("Failed to setLocalDescription, ", err);
-      });
-  };
-  pc.createAnswer()
-  .then(answer => callback(answer))
-  .catch(err => {
-    connection.provider.emitError("webrtc", err);
-    util.log("Failed to create answer, ", err);
-  });
-};
-
-/** Handle an SDP. */
-Negotiator.handleSDP = function(type, connection, sdp) {
-  sdp = new RTCSessionDescription(sdp);
-  const pc: RTCPeerConnection = connection.pc;
-
-  util.log("Setting remote description", sdp);
-
-  const callback = function() {
-    util.log("Set remoteDescription:", type, "for:", connection.peer);
-
-    if (type === "OFFER") {
-      Negotiator._makeAnswer(connection);
+  }
+
+  /** Handle a candidate. */
+  async handleCandidate(connection: BaseConnection, ice: any): Promise<void> {
+    const candidate = ice.candidate;
+    const sdpMLineIndex = ice.sdpMLineIndex;
+
+    try {
+      await connection.peerConnection.addIceCandidate(
+        new RTCIceCandidate({
+          sdpMLineIndex: sdpMLineIndex,
+          candidate: candidate
+        })
+      );
+      util.log(`Added ICE candidate for:${connection.peer}`);
+    } catch (err) {
+      connection.provider.emitError(PeerErrorType.WebRTC, err);
+      util.log("Failed to handleCandidate, ", err);
     }
-  };
+  }
 
-  pc.setRemoteDescription(sdp)
-  .then(() => callback())
-  .catch(err => {
-      connection.provider.emitError("webrtc", err);
-      util.log("Failed to setRemoteDescription, ", err);
+  private _addTracksToConnection(
+    stream: MediaStream,
+    peerConnection: RTCPeerConnection
+  ): void {
+    util.log(`add tracks from stream ${stream.id} to peer connection`);
+
+    if (!peerConnection.addTrack) {
+      return util.error(
+        `Your browser does't support RTCPeerConnection#addTrack. Ignored.`
+      );
     }
-  );
-};
-
-/** Handle a candidate. */
-Negotiator.handleCandidate = function(connection, ice) {
-  var candidate = ice.candidate;
-  var sdpMLineIndex = ice.sdpMLineIndex;
-  connection.pc.addIceCandidate(
-    new RTCIceCandidate({
-      sdpMLineIndex: sdpMLineIndex,
-      candidate: candidate
-    })
-  );
-  util.log("Added ICE candidate for:", connection.peer);
-};
-
-function addStreamToConnection(stream: MediaStream, connection: RTCPeerConnection) {
-  if ('addTrack' in connection) {
+
     stream.getTracks().forEach(track => {
-      connection.addTrack(track, stream);
+      peerConnection.addTrack(track, stream);
     });
-  } else if ('addStream' in connection) {
-    (<any>connection).addStream(stream);
+  }
+
+  private _addStreamToMediaConnection(
+    stream: MediaStream,
+    mediaConnection: MediaConnection
+  ): void {
+    util.log(
+      `add stream ${stream.id} to media connection ${
+        mediaConnection.connectionId
+      }`
+    );
+
+    mediaConnection.addStream(stream);
   }
 }
+
+export default new Negotiator();

+ 488 - 494
lib/peer.ts

@@ -3,559 +3,553 @@ import { EventEmitter } from "eventemitter3";
 import { Socket } from "./socket";
 import { MediaConnection } from "./mediaconnection";
 import { DataConnection } from "./dataconnection";
+import {
+  ConnectionType,
+  PeerErrorType,
+  PeerEventType,
+  SocketEventType,
+  SerializationType
+} from "./enums";
+import { BaseConnection } from "./baseconnection";
+import { ServerMessage } from "./servermessage";
+import { PeerConnectOption, PeerJSOption } from "..";
+import { API } from "./api";
+
+class PeerOptions implements PeerJSOption {
+  debug: number; // 1: Errors, 2: Warnings, 3: All logs
+  host: string;
+  port: number;
+  wsport: number;
+  path: string;
+  key: string;
+  token: string;
+  config: any;
+  secure: boolean;
+  logFunction: any;
+}
 
 /**
  * A peer who can initiate connections with other peers.
  */
-export function Peer(id, options): void {
-  if (!(this instanceof Peer)) return new Peer(id, options);
-  EventEmitter.call(this);
-
-  // Deal with overloading
-  if (id && id.constructor == Object) {
-    options = id;
-    id = undefined;
-  } else if (id) {
-    // Ensure id is a string
-    id = id.toString();
-  }
-  //
+export class Peer extends EventEmitter {
+  private static readonly DEFAULT_KEY = "peerjs";
 
-  // Configurize options
-  options = util.extend(
-    {
-      debug: 0, // 1: Errors, 2: Warnings, 3: All logs
-      host: util.CLOUD_HOST,
-      port: util.CLOUD_PORT,
-      path: "/",
-      key: "peerjs",
-      token: util.randomToken(),
-      config: util.defaultConfig
-    },
-    options
-  );
-  this.options = options;
-  // Detect relative URL host.
-  if (options.host === "/") {
-    options.host = window.location.hostname;
+  private readonly _options: PeerOptions;
+  private _id: string;
+  private _lastServerId: string;
+  private _api: API;
+
+  // States.
+  private _destroyed = false; // Connections have been killed
+  private _disconnected = false; // Connection to PeerServer killed but P2P connections still active
+  private _open = false; // Sockets and such are not yet open.
+  private readonly _connections: Map<string, BaseConnection[]> = new Map(); // DataConnections for this peer.
+  private readonly _lostMessages: Map<string, ServerMessage[]> = new Map(); // src => [list of messages]
+
+  private _socket: Socket;
+
+  get id() {
+    return this._id;
   }
-  // Set path correctly.
-  if (options.path[0] !== "/") {
-    options.path = "/" + options.path;
+
+  get options() {
+    return this._options;
   }
-  if (options.path[options.path.length - 1] !== "/") {
-    options.path += "/";
+
+  get open() {
+    return this._open;
   }
 
-  // Set whether we use SSL to same as current host
-  if (options.secure === undefined && options.host !== util.CLOUD_HOST) {
-    options.secure = util.isSecure();
-  } else if (options.host == util.CLOUD_HOST) {
-    options.secure = true;
+  get socket() {
+    return this._socket;
   }
-  // Set a custom log function if present
-  if (options.logFunction) {
-    util.setLogFunction(options.logFunction);
+
+  get connections() {
+    return this._connections;
   }
-  util.setLogLevel(options.debug);
-  //
-
-  // Sanity checks
-  // Ensure WebRTC supported
-  if (!util.supports.audioVideo && !util.supports.data) {
-    this._delayedAbort(
-      "browser-incompatible",
-      "The current browser does not support WebRTC"
-    );
-    return;
+
+  get destroyed() {
+    return this._destroyed;
   }
-  // Ensure alphanumeric id
-  if (!util.validateId(id)) {
-    this._delayedAbort("invalid-id", 'ID "' + id + '" is invalid');
-    return;
+  get disconnected() {
+    return this._disconnected;
   }
-  // Ensure valid key
-  // if (!util.validateKey(options.key)) {
-  //   this._delayedAbort(
-  //     "invalid-key",
-  //     'API KEY "' + options.key + '" is invalid'
-  //   );
-  //   return;
-  // }
-  // Ensure not using unsecure cloud server on SSL page
-  // if (options.secure && options.host === "0.peerjs.com") {
-  //   this._delayedAbort(
-  //     "ssl-unavailable",
-  //     "The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS."
-  //   );
-  //   return;
-  // }
-  //
 
-  // States.
-  this.destroyed = false; // Connections have been killed
-  this.disconnected = false; // Connection to PeerServer killed but P2P connections still active
-  this.open = false; // Sockets and such are not yet open.
-  //
-
-  // References
-  this.connections = {}; // DataConnections for this peer.
-  this._lostMessages = {}; // src => [list of messages]
-  //
-
-  // Start the server connection
-  this._initializeServerConnection();
-  if (id) {
-    this._initialize(id);
-  } else {
-    this._retrieveId();
-  }
-  //
-}
+  constructor(id: any, options?: PeerOptions) {
+    super();
 
-util.inherits(Peer, EventEmitter);
-
-// Initialize the 'socket' (which is actually a mix of XHR streaming and
-// websockets.)
-Peer.prototype._initializeServerConnection = function() {
-  var self = this;
-  this.socket = new Socket(
-    this.options.secure,
-    this.options.host,
-    this.options.port,
-    this.options.path,
-    this.options.key,
-    this.options.wsport
-  );
-  this.socket.on("message", function(data) {
-    self._handleMessage(data);
-  });
-  this.socket.on("error", function(error) {
-    self._abort("socket-error", error);
-  });
-  this.socket.on("disconnected", function() {
-    // If we haven't explicitly disconnected, emit error and disconnect.
-    if (!self.disconnected) {
-      self.emitError("network", "Lost connection to server.");
-      self.disconnect();
+    // Deal with overloading
+    if (id && id.constructor == Object) {
+      options = id;
+      id = undefined;
+    } else if (id) {
+      // Ensure id is a string
+      id = id.toString();
     }
-  });
-  this.socket.on("close", function() {
-    // If we haven't explicitly disconnected, emit error.
-    if (!self.disconnected) {
-      self._abort("socket-closed", "Underlying socket is already closed.");
+    //
+
+    // Configurize options
+    options = {
+      debug: 0, // 1: Errors, 2: Warnings, 3: All logs
+      host: util.CLOUD_HOST,
+      port: util.CLOUD_PORT,
+      path: "/",
+      key: Peer.DEFAULT_KEY,
+      token: util.randomToken(),
+      config: util.defaultConfig,
+      ...options
+    };
+    this._options = options;
+
+    // Detect relative URL host.
+    if (options.host === "/") {
+      options.host = window.location.hostname;
     }
-  });
-};
-
-/** Get a unique ID from the server via XHR. */
-Peer.prototype._retrieveId = function(cb) {
-  var self = this;
-  var http = new XMLHttpRequest();
-  var protocol = this.options.secure ? "https://" : "http://";
-  var url =
-    protocol +
-    this.options.host +
-    ":" +
-    this.options.port +
-    this.options.path +
-    this.options.key +
-    "/id";
-  var queryString = "?ts=" + new Date().getTime() + "" + Math.random();
-  url += queryString;
-
-  // If there's no ID we need to wait for one before trying to init socket.
-  http.open("get", url, true);
-  http.onerror = function(e) {
-    util.error("Error retrieving ID", e);
-    var pathError = "";
-    if (self.options.path === "/" && self.options.host !== util.CLOUD_HOST) {
-      pathError =
-        " If you passed in a `path` to your self-hosted PeerServer, " +
-        "you'll also need to pass in that same path when creating a new " +
-        "Peer.";
+    // Set path correctly.
+    if (options.path[0] !== "/") {
+      options.path = "/" + options.path;
     }
-    self._abort(
-      "server-error",
-      "Could not get an ID from the server." + pathError
-    );
-  };
-  http.onreadystatechange = function() {
-    if (http.readyState !== 4) {
+    if (options.path[options.path.length - 1] !== "/") {
+      options.path += "/";
+    }
+
+    // Set whether we use SSL to same as current host
+    if (options.secure === undefined && options.host !== util.CLOUD_HOST) {
+      options.secure = util.isSecure();
+    } else if (options.host == util.CLOUD_HOST) {
+      options.secure = true;
+    }
+    // Set a custom log function if present
+    if (options.logFunction) {
+      util.setLogFunction(options.logFunction);
+    }
+    util.setLogLevel(String(options.debug));
+
+    // Sanity checks
+    // Ensure WebRTC supported
+    if (!util.supports.audioVideo && !util.supports.data) {
+      this._delayedAbort(
+        PeerErrorType.BrowserIncompatible,
+        "The current browser does not support WebRTC"
+      );
       return;
     }
-    if (http.status !== 200) {
-      http.onerror();
+    // Ensure alphanumeric id
+    if (!util.validateId(id)) {
+      this._delayedAbort(PeerErrorType.InvalidID, `ID "${id}" is invalid`);
       return;
     }
-    self._initialize(http.responseText);
-  };
-  http.send(null);
-};
-
-/** Initialize a connection with the server. */
-Peer.prototype._initialize = function(id) {
-  this.id = id;
-  this.socket.start(this.id, this.options.token);
-};
-
-/** Handles messages from the server. */
-Peer.prototype._handleMessage = function(message) {
-  var type = message.type;
-  var payload = message.payload;
-  var peer = message.src;
-  var connection;
-
-  switch (type) {
-    case "OPEN": // The connection to the server is open.
-      this.emit("open", this.id);
-      this.open = true;
-      break;
-    case "ERROR": // Server error.
-      this._abort("server-error", payload.msg);
-      break;
-    case "ID-TAKEN": // The selected ID is taken.
-      this._abort("unavailable-id", "ID `" + this.id + "` is taken");
-      break;
-    case "INVALID-KEY": // The given API key cannot be found.
-      this._abort(
-        "invalid-key",
-        'API KEY "' + this.options.key + '" is invalid'
-      );
-      break;
 
-    //
-    case "LEAVE": // Another peer has closed its connection to this peer.
-      util.log("Received leave message from", peer);
-      this._cleanupPeer(peer);
-      break;
-
-    case "EXPIRE": // The offer sent to a peer has expired without response.
-      this.emitError("peer-unavailable", "Could not connect to peer " + peer);
-      break;
-    case "OFFER": // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
-      var connectionId = payload.connectionId;
-      connection = this.getConnection(peer, connectionId);
-
-      if (connection) {
-        connection.close();
-        util.warn("Offer received for existing Connection ID:", connectionId);
-      }
+    this._api = new API(options);
+
+    // Start the server connection
+    this._initializeServerConnection();
+
+    if (id) {
+      this._initialize(id);
+    } else {
+      this._api.retrieveId((error, id) => {
+        if (error) {
+          return this._abort(error.type, error.message);
+        }
 
-      // Create a new connection.
-      if (payload.type === "media") {
-        connection = new MediaConnection(peer, this, {
-          connectionId: connectionId,
-          _payload: payload,
-          metadata: payload.metadata
-        });
-        this._addConnection(peer, connection);
-        this.emit("call", connection);
-      } else if (payload.type === "data") {
-        connection = new DataConnection(peer, this, {
-          connectionId: connectionId,
-          _payload: payload,
-          metadata: payload.metadata,
-          label: payload.label,
-          serialization: payload.serialization,
-          reliable: payload.reliable
-        });
-        this._addConnection(peer, connection);
-        this.emit("connection", connection);
-      } else {
-        util.warn("Received malformed connection type:", payload.type);
-        return;
+        this._initialize(id);
+      });
+    }
+  }
+
+  // Initialize the 'socket' (which is actually a mix of XHR streaming and
+  // websockets.)
+  private _initializeServerConnection(): void {
+    this._socket = new Socket(
+      this._options.secure,
+      this._options.host,
+      this._options.port,
+      this._options.path,
+      this._options.key,
+      this._options.wsport
+    );
+
+    const self = this;
+
+    this.socket.on(SocketEventType.Message, data => {
+      self._handleMessage(data);
+    });
+
+    this.socket.on(SocketEventType.Error, error => {
+      self._abort(PeerErrorType.SocketError, error);
+    });
+
+    this.socket.on(SocketEventType.Disconnected, () => {
+      // If we haven't explicitly disconnected, emit error and disconnect.
+      if (!self.disconnected) {
+        self.emitError(PeerErrorType.Network, "Lost connection to server.");
+        self.disconnect();
       }
-      // Find messages.
-      var messages = this._getMessages(connectionId);
-      for (var i = 0, ii = messages.length; i < ii; i += 1) {
-        connection.handleMessage(messages[i]);
+    });
+
+    this.socket.on(SocketEventType.Close, function() {
+      // If we haven't explicitly disconnected, emit error.
+      if (!self.disconnected) {
+        self._abort(
+          PeerErrorType.SocketClosed,
+          "Underlying socket is already closed."
+        );
       }
+    });
+  }
+
+  /** Initialize a connection with the server. */
+  private _initialize(id: string): void {
+    this._id = id;
+    this.socket.start(this.id, this._options.token);
+  }
 
-      break;
-    default:
-      if (!payload) {
-        util.warn(
-          "You received a malformed message from " + peer + " of type " + type
+  /** Handles messages from the server. */
+  private _handleMessage(message: ServerMessage): void {
+    const type = message.type;
+    const payload = message.payload;
+    const peerId = message.src;
+
+    switch (type) {
+      case "OPEN": // The connection to the server is open.
+        this.emit(PeerEventType.Open, this.id);
+        this._open = true;
+        break;
+      case "ERROR": // Server error.
+        this._abort(PeerErrorType.ServerError, payload.msg);
+        break;
+      case "ID-TAKEN": // The selected ID is taken.
+        this._abort(PeerErrorType.UnavailableID, `ID "${this.id}" is taken`);
+        break;
+      case "INVALID-KEY": // The given API key cannot be found.
+        this._abort(
+          PeerErrorType.InvalidKey,
+          `API KEY "${this._options.key}" is invalid`
         );
-        return;
+        break;
+
+      //
+      case "LEAVE": // Another peer has closed its connection to this peer.
+        util.log("Received leave message from", peerId);
+        this._cleanupPeer(peerId);
+        break;
+
+      case "EXPIRE": // The offer sent to a peer has expired without response.
+        this.emitError(
+          PeerErrorType.PeerUnavailable,
+          "Could not connect to peer " + peerId
+        );
+        break;
+      case "OFFER": {
+        // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
+        const connectionId = payload.connectionId;
+        let connection = this.getConnection(peerId, connectionId);
+
+        if (connection) {
+          connection.close();
+          util.warn("Offer received for existing Connection ID:", connectionId);
+        }
+
+        // Create a new connection.
+        if (payload.type === ConnectionType.Media) {
+          connection = new MediaConnection(peerId, this, {
+            connectionId: connectionId,
+            _payload: payload,
+            metadata: payload.metadata
+          });
+          this._addConnection(peerId, connection);
+          this.emit(PeerEventType.Call, connection);
+        } else if (payload.type === ConnectionType.Data) {
+          connection = new DataConnection(peerId, this, {
+            connectionId: connectionId,
+            _payload: payload,
+            metadata: payload.metadata,
+            label: payload.label,
+            serialization: payload.serialization,
+            reliable: payload.reliable
+          });
+          this._addConnection(peerId, connection);
+          this.emit(PeerEventType.Connection, connection);
+        } else {
+          util.warn("Received malformed connection type:", payload.type);
+          return;
+        }
+
+        // Find messages.
+        const messages = this._getMessages(connectionId);
+        for (let message of messages) {
+          connection.handleMessage(message);
+        }
+
+        break;
       }
-
-      var id = payload.connectionId;
-      connection = this.getConnection(peer, id);
-
-      if (connection && connection.pc) {
-        // Pass it on.
-        connection.handleMessage(message);
-      } else if (id) {
-        // Store for possible later use
-        this._storeMessage(id, message);
-      } else {
-        util.warn("You received an unrecognized message:", message);
+      default: {
+        if (!payload) {
+          util.warn(
+            `You received a malformed message from ${peerId} of type ${type}`
+          );
+          return;
+        }
+
+        const connectionId = payload.connectionId;
+        const connection = this.getConnection(peerId, connectionId);
+
+        if (connection && connection.peerConnection) {
+          // Pass it on.
+          connection.handleMessage(message);
+        } else if (connectionId) {
+          // Store for possible later use
+          this._storeMessage(connectionId, message);
+        } else {
+          util.warn("You received an unrecognized message:", message);
+        }
+        break;
       }
-      break;
+    }
   }
-};
 
-/** Stores messages without a set up connection, to be claimed later. */
-Peer.prototype._storeMessage = function(connectionId, message) {
-  if (!this._lostMessages[connectionId]) {
-    this._lostMessages[connectionId] = [];
+  /** Stores messages without a set up connection, to be claimed later. */
+  private _storeMessage(connectionId: string, message: ServerMessage): void {
+    if (!this._lostMessages.has(connectionId)) {
+      this._lostMessages.set(connectionId, []);
+    }
+
+    this._lostMessages.get(connectionId).push(message);
   }
-  this._lostMessages[connectionId].push(message);
-};
-
-/** Retrieve messages from lost message store */
-Peer.prototype._getMessages = function(connectionId) {
-  var messages = this._lostMessages[connectionId];
-  if (messages) {
-    delete this._lostMessages[connectionId];
-    return messages;
-  } else {
+
+  /** Retrieve messages from lost message store */
+  private _getMessages(connectionId: string): ServerMessage[] {
+    const messages = this._lostMessages.get(connectionId);
+
+    if (messages) {
+      this._lostMessages.delete(connectionId);
+      return messages;
+    }
+
     return [];
   }
-};
 
-/**
- * Returns a DataConnection to the specified peer. See documentation for a
- * complete list of options.
- */
-Peer.prototype.connect = function(peer, options) {
-  if (this.disconnected) {
-    util.warn(
-      "You cannot connect to a new Peer because you called " +
-        ".disconnect() on this Peer and ended your connection with the " +
-        "server. You can create a new Peer to reconnect, or call reconnect " +
-        "on this peer if you believe its ID to still be available."
-    );
-    this.emitError(
-      "disconnected",
-      "Cannot connect to new Peer after disconnecting from server."
-    );
-    return;
+  /**
+   * Returns a DataConnection to the specified peer. See documentation for a
+   * complete list of options.
+   */
+  connect(peer: string, options?: PeerConnectOption): DataConnection {
+    if (this.disconnected) {
+      util.warn(
+        "You cannot connect to a new Peer because you called " +
+          ".disconnect() on this Peer and ended your connection with the " +
+          "server. You can create a new Peer to reconnect, or call reconnect " +
+          "on this peer if you believe its ID to still be available."
+      );
+      this.emitError(
+        PeerErrorType.Disconnected,
+        "Cannot connect to new Peer after disconnecting from server."
+      );
+      return;
+    }
+
+    const connection = new DataConnection(peer, this, options);
+    this._addConnection(peer, connection);
+    return connection;
   }
-  var connection = new DataConnection(peer, this, options);
-  this._addConnection(peer, connection);
-  return connection;
-};
 
-/**
- * Returns a MediaConnection to the specified peer. See documentation for a
- * complete list of options.
- */
-Peer.prototype.call = function(peer, stream, options) {
-  if (this.disconnected) {
-    util.warn(
-      "You cannot connect to a new Peer because you called " +
-        ".disconnect() on this Peer and ended your connection with the " +
-        "server. You can create a new Peer to reconnect."
-    );
-    this.emitError(
-      "disconnected",
-      "Cannot connect to new Peer after disconnecting from server."
-    );
-    return;
+  /**
+   * Returns a MediaConnection to the specified peer. See documentation for a
+   * complete list of options.
+   */
+  call(peer: string, stream: MediaStream, options: any = {}): MediaConnection {
+    if (this.disconnected) {
+      util.warn(
+        "You cannot connect to a new Peer because you called " +
+          ".disconnect() on this Peer and ended your connection with the " +
+          "server. You can create a new Peer to reconnect."
+      );
+      this.emitError(
+        PeerErrorType.Disconnected,
+        "Cannot connect to new Peer after disconnecting from server."
+      );
+      return;
+    }
+
+    if (!stream) {
+      util.error(
+        "To call a peer, you must provide a stream from your browser's `getUserMedia`."
+      );
+      return;
+    }
+
+    options._stream = stream;
+
+    const call = new MediaConnection(peer, this, options);
+    this._addConnection(peer, call);
+    return call;
   }
-  if (!stream) {
-    util.error(
-      "To call a peer, you must provide a stream from your browser's `getUserMedia`."
+
+  /** Add a data/media connection to this peer. */
+  private _addConnection(peerId: string, connection: BaseConnection): void {
+    util.log(
+      `add connection ${connection.type}:${connection.connectionId}
+       to peerId:${peerId}`
     );
-    return;
-  }
-  options = options || {};
-  options._stream = stream;
-  var call = new MediaConnection(peer, this, options);
-  this._addConnection(peer, call);
-  return call;
-};
-
-/** Add a data/media connection to this peer. */
-Peer.prototype._addConnection = function(peer, connection) {
-  if (!this.connections[peer]) {
-    this.connections[peer] = [];
-  }
-  this.connections[peer].push(connection);
-};
 
-/** Retrieve a data/media connection for this peer. */
-Peer.prototype.getConnection = function(peer, id) {
-  var connections = this.connections[peer];
-  if (!connections) {
-    return null;
-  }
-  for (var i = 0, ii = connections.length; i < ii; i++) {
-    if (connections[i].id === id) {
-      return connections[i];
+    if (!this.connections.has(peerId)) {
+      this.connections.set(peerId, []);
     }
+    this.connections.get(peerId).push(connection);
   }
-  return null;
-};
 
-Peer.prototype._delayedAbort = function(type, message) {
-  var self = this;
-  util.setZeroTimeout(function() {
-    self._abort(type, message);
-  });
-};
+  /** Retrieve a data/media connection for this peer. */
+  getConnection(peerId: string, connectionId: string): null | BaseConnection {
+    const connections = this.connections.get(peerId);
+    if (!connections) {
+      return null;
+    }
 
-/**
- * Destroys the Peer and emits an error message.
- * The Peer is not destroyed if it's in a disconnected state, in which case
- * it retains its disconnected state and its existing connections.
- */
-Peer.prototype._abort = function(type, message) {
-  util.error("Aborting!");
-  if (!this._lastServerId) {
-    this.destroy();
-  } else {
-    this.disconnect();
+    for (let connection of connections) {
+      if (connection.connectionId === connectionId) {
+        return connection;
+      }
+    }
+
+    return null;
   }
-  this.emitError(type, message);
-};
-
-/** Emits a typed error message. */
-Peer.prototype.emitError = function(type, err) {
-  util.error("Error:", err);
-  if (typeof err === "string") {
-    err = new Error(err);
+
+  private _delayedAbort(type: PeerErrorType, message): void {
+    const self = this;
+    util.setZeroTimeout(function() {
+      self._abort(type, message);
+    });
   }
-  err.type = type;
-  this.emit("error", err);
-};
 
-/**
- * Destroys the Peer: closes all active connections as well as the connection
- *  to the server.
- * Warning: The peer can no longer create or accept connections after being
- *  destroyed.
- */
-Peer.prototype.destroy = function() {
-  if (!this.destroyed) {
-    this._cleanup();
-    this.disconnect();
-    this.destroyed = true;
+  /**
+   * Destroys the Peer and emits an error message.
+   * The Peer is not destroyed if it's in a disconnected state, in which case
+   * it retains its disconnected state and its existing connections.
+   */
+  private _abort(type: PeerErrorType, message): void {
+    util.error("Aborting!");
+
+    if (!this._lastServerId) {
+      this.destroy();
+    } else {
+      this.disconnect();
+    }
+
+    this.emitError(type, message);
   }
-};
-
-/** Disconnects every connection on this peer. */
-Peer.prototype._cleanup = function() {
-  if (this.connections) {
-    var peers = Object.keys(this.connections);
-    for (var i = 0, ii = peers.length; i < ii; i++) {
-      this._cleanupPeer(peers[i]);
+
+  /** Emits a typed error message. */
+  emitError(type: PeerErrorType, err): void {
+    util.error("Error:", err);
+
+    if (typeof err === "string") {
+      err = new Error(err);
     }
+
+    err.type = type;
+
+    this.emit(PeerEventType.Error, err);
   }
-  this.emit("close");
-};
-
-/** Closes all connections to this peer. */
-Peer.prototype._cleanupPeer = function(peer) {
-  var connections = this.connections[peer];
-  for (var j = 0, jj = connections.length; j < jj; j += 1) {
-    connections[j].close();
+
+  /**
+   * Destroys the Peer: closes all active connections as well as the connection
+   *  to the server.
+   * Warning: The peer can no longer create or accept connections after being
+   *  destroyed.
+   */
+  destroy(): void {
+    if (!this.destroyed) {
+      this._cleanup();
+      this.disconnect();
+      this._destroyed = true;
+    }
   }
-};
 
-/**
- * Disconnects the Peer's connection to the PeerServer. Does not close any
- *  active connections.
- * Warning: The peer can no longer create or accept connections after being
- *  disconnected. It also cannot reconnect to the server.
- */
-Peer.prototype.disconnect = function() {
-  var self = this;
-  util.setZeroTimeout(function() {
-    if (!self.disconnected) {
-      self.disconnected = true;
-      self.open = false;
-      if (self.socket) {
-        self.socket.close();
-      }
-      self.emit("disconnected", self.id);
-      self._lastServerId = self.id;
-      self.id = null;
+  /** Disconnects every connection on this peer. */
+  private _cleanup(): void {
+    for (let peerId of this.connections.keys()) {
+      this._cleanupPeer(peerId);
+      this.connections.delete(peerId);
     }
-  });
-};
-
-/** Attempts to reconnect with the same ID. */
-Peer.prototype.reconnect = function() {
-  if (this.disconnected && !this.destroyed) {
-    util.log("Attempting reconnection to server with ID " + this._lastServerId);
-    this.disconnected = false;
-    this._initializeServerConnection();
-    this._initialize(this._lastServerId);
-  } else if (this.destroyed) {
-    throw new Error(
-      "This peer cannot reconnect to the server. It has already been destroyed."
-    );
-  } else if (!this.disconnected && !this.open) {
-    // Do nothing. We're still connecting the first time.
-    util.error(
-      "In a hurry? We're still trying to make the initial connection!"
-    );
-  } else {
-    throw new Error(
-      "Peer " +
-        this.id +
-        " cannot reconnect because it is not disconnected from the server!"
-    );
+
+    this.emit(PeerEventType.Close);
   }
-};
 
-/**
- * Get a list of available peer IDs. If you're running your own server, you'll
- * want to set allow_discovery: true in the PeerServer options. If you're using
- * the cloud server, email team@peerjs.com to get the functionality enabled for
- * your key.
- */
-Peer.prototype.listAllPeers = function(cb) {
-  cb = cb || function() {};
-  var self = this;
-  var http = new XMLHttpRequest();
-  var protocol = this.options.secure ? "https://" : "http://";
-  var url =
-    protocol +
-    this.options.host +
-    ":" +
-    this.options.port +
-    this.options.path +
-    this.options.key +
-    "/peers";
-  var queryString = "?ts=" + new Date().getTime() + "" + Math.random();
-  url += queryString;
-
-  // If there's no ID we need to wait for one before trying to init socket.
-  http.open("get", url, true);
-  http.onerror = function(e) {
-    self._abort("server-error", "Could not get peers from the server.");
-    cb([]);
-  };
-  http.onreadystatechange = function() {
-    if (http.readyState !== 4) {
-      return;
+  /** Closes all connections to this peer. */
+  private _cleanupPeer(peerId: string): void {
+    const connections = this.connections.get(peerId);
+
+    if (!connections) return;
+
+    for (let connection of connections) {
+      connection.close();
     }
-    if (http.status === 401) {
-      var helpfulError = "";
-      if (self.options.host !== util.CLOUD_HOST) {
-        helpfulError =
-          "It looks like you're using the cloud server. You can email " +
-          "team@peerjs.com to enable peer listing for your API key.";
-      } else {
-        helpfulError =
-          "You need to enable `allow_discovery` on your self-hosted " +
-          "PeerServer to use this feature.";
+  }
+
+  /**
+   * Disconnects the Peer's connection to the PeerServer. Does not close any
+   *  active connections.
+   * Warning: The peer can no longer create or accept connections after being
+   *  disconnected. It also cannot reconnect to the server.
+   */
+  disconnect(): void {
+    const self = this;
+    util.setZeroTimeout(function() {
+      if (!self.disconnected) {
+        self._disconnected = true;
+        self._open = false;
+        if (self.socket) {
+          self.socket.close();
+        }
+
+        self.emit(PeerEventType.Disconnected, self.id);
+        self._lastServerId = self.id;
+        self._id = null;
       }
-      cb([]);
+    });
+  }
+
+  /** Attempts to reconnect with the same ID. */
+  reconnect(): void {
+    if (this.disconnected && !this.destroyed) {
+      util.log(
+        "Attempting reconnection to server with ID " + this._lastServerId
+      );
+      this._disconnected = false;
+      this._initializeServerConnection();
+      this._initialize(this._lastServerId);
+    } else if (this.destroyed) {
       throw new Error(
-        "It doesn't look like you have permission to list peers IDs. " +
-          helpfulError
+        "This peer cannot reconnect to the server. It has already been destroyed."
+      );
+    } else if (!this.disconnected && !this.open) {
+      // Do nothing. We're still connecting the first time.
+      util.error(
+        "In a hurry? We're still trying to make the initial connection!"
       );
-    } else if (http.status !== 200) {
-      cb([]);
     } else {
-      cb(JSON.parse(http.responseText));
+      throw new Error(
+        "Peer " +
+          this.id +
+          " cannot reconnect because it is not disconnected from the server!"
+      );
     }
-  };
-  http.send(null);
-};
+  }
+
+  /**
+   * Get a list of available peer IDs. If you're running your own server, you'll
+   * want to set allow_discovery: true in the PeerServer options. If you're using
+   * the cloud server, email team@peerjs.com to get the functionality enabled for
+   * your key.
+   */
+  listAllPeers(cb = (arg: any[]) => {}): void {
+    this._api.listAllPeers((error, peers) => {
+      if (error) {
+        return this._abort(error.type, error.message);
+      }
+
+      cb(peers);
+    });
+  }
+}

+ 5 - 0
lib/servermessage.ts

@@ -0,0 +1,5 @@
+export class ServerMessage {
+  type: string;
+  payload: any;
+  src: string;
+}

+ 267 - 174
lib/socket.ts

@@ -1,220 +1,313 @@
 import { util } from "./util";
 import { EventEmitter } from "eventemitter3";
+import { SocketEventType } from "./enums";
+
+class HttpRequest {
+  index = 1;
+  readonly buffer: number[] = [];
+  previousRequest: HttpRequest;
+  private _xmlHttpRequest = new XMLHttpRequest();
+
+  private _onError = sender => {};
+  private _onSuccess = sender => {};
+
+  set onError(handler) {
+    this._onError = handler;
+  }
+
+  set onSuccess(handler) {
+    this._onSuccess = handler;
+  }
+
+  constructor(readonly streamIndex: number, private readonly _httpUrl: string) {
+    const self = this;
+
+    this._xmlHttpRequest.onerror = () => {
+      self._onError(self);
+    };
+
+    this._xmlHttpRequest.onreadystatechange = () => {
+      if (self.needsClearPreviousRequest()) {
+        self.clearPreviousRequest();
+      } else if (self.isSuccess()) {
+        self._onSuccess(self);
+      }
+    };
+  }
+
+  send(): void {
+    this._xmlHttpRequest.open(
+      "post",
+      this._httpUrl + "/id?i=" + this.streamIndex,
+      true
+    );
+    this._xmlHttpRequest.send(null);
+  }
+
+  abort(): void {
+    this._xmlHttpRequest.abort();
+    this._xmlHttpRequest = null;
+  }
+
+  isSuccess(): boolean {
+    return (
+      this._xmlHttpRequest.readyState > 2 &&
+      this._xmlHttpRequest.status === 200 &&
+      !!this._xmlHttpRequest.responseText
+    );
+  }
+
+  needsClearPreviousRequest(): boolean {
+    return this._xmlHttpRequest.readyState === 2 && !!this.previousRequest;
+  }
+
+  clearPreviousRequest(): void {
+    if (!this.previousRequest) return;
+
+    this.previousRequest.abort();
+    this.previousRequest = null;
+  }
+
+  getMessages(): string[] {
+    return this._xmlHttpRequest.responseText.split("\n");
+  }
+
+  hasBufferedIndices(): boolean {
+    return this.buffer.length > 0;
+  }
+
+  popBufferedIndex(): number {
+    return this.buffer.shift();
+  }
+
+  pushIndexToBuffer(index: number) {
+    this.buffer.push(index);
+  }
+}
 
 /**
  * An abstraction on top of WebSockets and XHR streaming to provide fastest
  * possible connection for peers.
  */
-export function Socket(secure, host, port, path, key, wsport) {
-  if (!(this instanceof Socket))
-    return new Socket(secure, host, port, path, key, wsport);
+export class Socket extends EventEmitter {
+  private readonly HTTP_TIMEOUT = 25000;
 
-  wsport = wsport || port;
+  private _disconnected = false;
+  private _id: string;
+  private _messagesQueue: Array<any> = [];
+  private _httpUrl: string;
+  private _wsUrl: string;
+  private _socket: WebSocket;
+  private _httpRequest: HttpRequest;
+  private _timeout: any;
 
-  EventEmitter.call(this);
+  constructor(
+    secure: any,
+    host: string,
+    port: number,
+    path: string,
+    key: string,
+    wsport = port
+  ) {
+    super();
 
-  // Disconnected manually.
-  this.disconnected = false;
-  this._queue = [];
+    const httpProtocol = secure ? "https://" : "http://";
+    const wsProtocol = secure ? "wss://" : "ws://";
 
-  var httpProtocol = secure ? "https://" : "http://";
-  var wsProtocol = secure ? "wss://" : "ws://";
-  this._httpUrl = httpProtocol + host + ":" + port + path + key;
-  this._wsUrl = wsProtocol + host + ":" + wsport + path + "peerjs?key=" + key;
-}
+    this._httpUrl = httpProtocol + host + ":" + port + path + key;
+    this._wsUrl = wsProtocol + host + ":" + wsport + path + "peerjs?key=" + key;
+  }
 
-util.inherits(Socket, EventEmitter);
+  /** Check in with ID or get one from server. */
+  start(id: string, token: string): void {
+    this._id = id;
 
-/** Check in with ID or get one from server. */
-Socket.prototype.start = function(id, token) {
-  this.id = id;
+    this._httpUrl += "/" + id + "/" + token;
+    this._wsUrl += "&id=" + id + "&token=" + token;
 
-  this._httpUrl += "/" + id + "/" + token;
-  this._wsUrl += "&id=" + id + "&token=" + token;
+    this._startXhrStream();
+    this._startWebSocket();
+  }
+
+  /** Start up websocket communications. */
+  private _startWebSocket(): void {
+    if (this._socket) {
+      return;
+    }
+
+    this._socket = new WebSocket(this._wsUrl);
+
+    const self = this;
+
+    this._socket.onmessage = function(event) {
+      let data;
+
+      try {
+        data = JSON.parse(event.data);
+      } catch (e) {
+        util.log("Invalid server message", event.data);
+        return;
+      }
+      self.emit(SocketEventType.Message, data);
+    };
 
-  this._startXhrStream();
-  this._startWebSocket();
-};
+    this._socket.onclose = function(event) {
+      util.log("Socket closed.");
+      self._disconnected = true;
+      self.emit(SocketEventType.Disconnected);
+    };
 
-/** Start up websocket communications. */
-Socket.prototype._startWebSocket = function(id) {
-  var self = this;
+    // Take care of the queue of connections if necessary and make sure Peer knows
+    // socket is open.
+    this._socket.onopen = function() {
+      if (self._timeout) {
+        clearTimeout(self._timeout);
+        setTimeout(function() {
+          self._httpRequest.abort();
+          self._httpRequest = null;
+        }, 5000);
+      }
 
-  if (this._socket) {
-    return;
+      self._sendQueuedMessages();
+      util.log("Socket open");
+    };
   }
 
-  this._socket = new WebSocket(this._wsUrl);
+  /** Start XHR streaming. */
+  private _startXhrStream(streamIndex: number = 0) {
+    const newRequest = new HttpRequest(streamIndex, this._httpUrl);
+    this._httpRequest = newRequest;
 
-  this._socket.onmessage = function(event) {
-    try {
-      var data = JSON.parse(event.data);
-    } catch (e) {
-      util.log("Invalid server message", event.data);
-      return;
-    }
-    self.emit("message", data);
-  };
-
-  this._socket.onclose = function(event) {
-    util.log("Socket closed.");
-    self.disconnected = true;
-    self.emit("disconnected");
-  };
-
-  // Take care of the queue of connections if necessary and make sure Peer knows
-  // socket is open.
-  this._socket.onopen = function() {
-    if (self._timeout) {
-      clearTimeout(self._timeout);
-      setTimeout(function() {
-        self._http.abort();
-        self._http = null;
-      }, 5000);
-    }
-    self._sendQueuedMessages();
-    util.log("Socket open");
-  };
-};
-
-/** Start XHR streaming. */
-Socket.prototype._startXhrStream = function(n) {
-  try {
-    var self = this;
-    this._http = new XMLHttpRequest();
-    this._http._index = 1;
-    this._http._streamIndex = n || 0;
-    this._http.open(
-      "post",
-      this._httpUrl + "/id?i=" + this._http._streamIndex,
-      true
-    );
-    this._http.onerror = function() {
+    newRequest.onError = () => {
       // If we get an error, likely something went wrong.
       // Stop streaming.
-      clearTimeout(self._timeout);
-      self.emit("disconnected");
+      clearTimeout(this._timeout);
+      this.emit(SocketEventType.Disconnected);
     };
-    this._http.onreadystatechange = function() {
-      if (this.readyState == 2 && this.old) {
-        this.old.abort();
-        delete this.old;
-      } else if (
-        this.readyState > 2 &&
-        this.status === 200 &&
-        this.responseText
-      ) {
-        self._handleStream(this);
-      }
+
+    newRequest.onSuccess = () => {
+      this._handleStream(newRequest);
     };
-    this._http.send(null);
-    this._setHTTPTimeout();
-  } catch (e) {
-    util.log("XMLHttpRequest not available; defaulting to WebSockets");
-  }
-};
-
-/** Handles onreadystatechange response as a stream. */
-Socket.prototype._handleStream = function(http) {
-  // 3 and 4 are loading/done state. All others are not relevant.
-  var messages = http.responseText.split("\n");
-
-  // Check to see if anything needs to be processed on buffer.
-  if (http._buffer) {
-    while (http._buffer.length > 0) {
-      var index = http._buffer.shift();
-      var bufferedMessage = messages[index];
+
+    try {
+      newRequest.send();
+      this._setHTTPTimeout();
+    } catch (e) {
+      util.log("XMLHttpRequest not available; defaulting to WebSockets");
+    }
+  }
+
+  /** Handles onreadystatechange response as a stream. */
+  private _handleStream(httpRequest: HttpRequest) {
+    // 3 and 4 are loading/done state. All others are not relevant.
+    const messages = httpRequest.getMessages();
+
+    // Check to see if anything needs to be processed on buffer.
+
+    while (httpRequest.hasBufferedIndices()) {
+      const index = httpRequest.popBufferedIndex();
+      let bufferedMessage = messages[index];
+
       try {
         bufferedMessage = JSON.parse(bufferedMessage);
       } catch (e) {
-        http._buffer.shift(index);
+        //TODO should we need to put it back?
+        httpRequest.buffer.unshift(index);
         break;
       }
-      this.emit("message", bufferedMessage);
+
+      this.emit(SocketEventType.Message, bufferedMessage);
     }
-  }
 
-  var message = messages[http._index];
-  if (message) {
-    http._index += 1;
-    // Buffering--this message is incomplete and we'll get to it next time.
-    // This checks if the httpResponse ended in a `\n`, in which case the last
-    // element of messages should be the empty string.
-    if (http._index === messages.length) {
-      if (!http._buffer) {
-        http._buffer = [];
-      }
-      http._buffer.push(http._index - 1);
-    } else {
-      try {
-        message = JSON.parse(message);
-      } catch (e) {
-        util.log("Invalid server message", message);
-        return;
+    let message = messages[httpRequest.index];
+
+    if (message) {
+      httpRequest.index += 1;
+      // Buffering--this message is incomplete and we'll get to it next time.
+      // This checks if the httpResponse ended in a `\n`, in which case the last
+      // element of messages should be the empty string.
+      if (httpRequest.index === messages.length) {
+        httpRequest.pushIndexToBuffer(httpRequest.index - 1);
+      } else {
+        try {
+          message = JSON.parse(message);
+        } catch (e) {
+          util.log("Invalid server message", message);
+          return;
+        }
+        this.emit(SocketEventType.Message, message);
       }
-      this.emit("message", message);
     }
   }
-};
 
-Socket.prototype._setHTTPTimeout = function() {
-  var self = this;
-  this._timeout = setTimeout(function() {
-    var old = self._http;
-    if (!self._wsOpen()) {
-      self._startXhrStream(old._streamIndex + 1);
-      self._http.old = old;
-    } else {
-      old.abort();
-    }
-  }, 25000);
-};
+  private _setHTTPTimeout() {
+    const self = this;
 
-/** Is the websocket currently open? */
-Socket.prototype._wsOpen = function() {
-  return this._socket && this._socket.readyState == 1;
-};
-
-/** Send queued messages. */
-Socket.prototype._sendQueuedMessages = function() {
-  for (var i = 0, ii = this._queue.length; i < ii; i += 1) {
-    this.send(this._queue[i]);
+    this._timeout = setTimeout(function() {
+      if (!self._wsOpen()) {
+        const oldHttp = self._httpRequest;
+        self._startXhrStream(oldHttp.streamIndex + 1);
+        self._httpRequest.previousRequest = oldHttp;
+      } else {
+        self._httpRequest.abort();
+        self._httpRequest = null;
+      }
+    }, this.HTTP_TIMEOUT);
   }
-};
 
-/** Exposed send for DC & Peer. */
-Socket.prototype.send = function(data) {
-  if (this.disconnected) {
-    return;
+  /** Is the websocket currently open? */
+  private _wsOpen(): boolean {
+    return this._socket && this._socket.readyState == 1;
   }
 
-  // If we didn't get an ID yet, we can't yet send anything so we should queue
-  // up these messages.
-  if (!this.id) {
-    this._queue.push(data);
-    return;
-  }
+  /** Send queued messages. */
+  private _sendQueuedMessages(): void {
+    //TODO is it ok?
+    //Create copy of queue and clear it,
+    //because send method push the message back to queue if smth will go wrong
+    const copiedQueue = [...this._messagesQueue];
+    this._messagesQueue = [];
 
-  if (!data.type) {
-    this.emit("error", "Invalid message");
-    return;
+    for (const message of copiedQueue) {
+      this.send(message);
+    }
   }
 
-  var message = JSON.stringify(data);
-  if (this._wsOpen()) {
-    this._socket.send(message);
-  } else {
-    var http = new XMLHttpRequest();
-    var url = this._httpUrl + "/" + data.type.toLowerCase();
-    http.open("post", url, true);
-    http.setRequestHeader("Content-Type", "application/json");
-    http.send(message);
+  /** Exposed send for DC & Peer. */
+  send(data: any): void {
+    if (this._disconnected) {
+      return;
+    }
+
+    // If we didn't get an ID yet, we can't yet send anything so we should queue
+    // up these messages.
+    if (!this._id) {
+      this._messagesQueue.push(data);
+      return;
+    }
+
+    if (!data.type) {
+      this.emit(SocketEventType.Error, "Invalid message");
+      return;
+    }
+
+    const message = JSON.stringify(data);
+
+    if (this._wsOpen()) {
+      this._socket.send(message);
+    } else {
+      const http = new XMLHttpRequest();
+      const url = this._httpUrl + "/" + data.type.toLowerCase();
+      http.open("post", url, true);
+      http.setRequestHeader("Content-Type", "application/json");
+      http.send(message);
+    }
   }
-};
 
-Socket.prototype.close = function() {
-  if (!this.disconnected && this._wsOpen()) {
-    this._socket.close();
-    this.disconnected = true;
+  close(): void {
+    if (!this._disconnected && this._wsOpen()) {
+      this._socket.close();
+      this._disconnected = true;
+    }
   }
-};
+}

+ 1 - 1
lib/util.ts

@@ -12,7 +12,7 @@ Prints log messages depending on the debug level passed in. Defaults to 0.
 2  Prints errors and warnings.
 3  Prints all logs.
 */
-const enum DebugLevel {
+export enum DebugLevel {
   Disabled,
   Errors,
   Warnings,

+ 1 - 1
package.json

@@ -48,7 +48,7 @@
   "dependencies": {
     "@types/node": "^10.12.0",
     "@types/webrtc": "0.0.24",
-    "eventemitter3": "^0.1.6",
+    "eventemitter3": "^3.1.0",
     "js-binarypack": "0.0.9",
     "opencollective": "^1.0.3",
     "opencollective-postinstall": "^2.0.0",