Browse Source

Merge pull request #217 from peers/michelle-reconnect

Reconnection
Michelle Bu 11 years ago
parent
commit
e34b89fee9
8 changed files with 224 additions and 90 deletions
  1. 95 44
      dist/peer.js
  2. 0 0
      dist/peer.min.js
  3. 39 10
      docs/api.json
  4. 0 0
      docs/index.html
  5. 1 1
      examples/chat.html
  6. 5 5
      lib/negotiator.js
  7. 69 24
      lib/peer.js
  8. 15 6
      lib/socket.js

+ 95 - 44
dist/peer.js

@@ -1052,9 +1052,9 @@ Reliable.higherBandwidthSDP = function(sdp) {
 Reliable.prototype.onmessage = function(msg) {};
 Reliable.prototype.onmessage = function(msg) {};
 
 
 exports.Reliable = Reliable;
 exports.Reliable = Reliable;
-exports.RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
-exports.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.RTCPeerConnection;
-exports.RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
+exports.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
+exports.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
+exports.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
 var defaultConfig = {'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }]};
 var defaultConfig = {'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }]};
 var dataCount = 1;
 var dataCount = 1;
 
 
@@ -1390,6 +1390,7 @@ function Peer(id, options) {
     port: util.CLOUD_PORT,
     port: util.CLOUD_PORT,
     key: 'peerjs',
     key: 'peerjs',
     path: '/',
     path: '/',
+    token: util.randomToken(),
     config: util.defaultConfig
     config: util.defaultConfig
   }, options);
   }, options);
   this.options = options;
   this.options = options;
@@ -1442,7 +1443,7 @@ function Peer(id, options) {
 
 
   // States.
   // States.
   this.destroyed = false; // Connections have been killed
   this.destroyed = false; // Connections have been killed
-  this.disconnected = false; // Connection to PeerServer killed manually but P2P connections still active
+  this.disconnected = false; // Connection to PeerServer killed but P2P connections still active
   this.open = false; // Sockets and such are not yet open.
   this.open = false; // Sockets and such are not yet open.
   //
   //
 
 
@@ -1451,8 +1452,21 @@ function Peer(id, options) {
   this._lostMessages = {}; // src => [list of messages]
   this._lostMessages = {}; // src => [list of messages]
   //
   //
 
 
-  // Initialize the 'socket' (which is actually a mix of XHR streaming and
-  // websockets.)
+  // Start the server connection
+  this._initializeServerConnection();
+  if (id) {
+    this._initialize(id);
+  } else {
+    this._retrieveId();
+  }
+  //
+};
+
+util.inherits(Peer, EventEmitter);
+
+// Initialize the 'socket' (which is actually a mix of XHR streaming and
+// websockets.)
+Peer.prototype._initializeServerConnection = function() {
   var self = this;
   var self = this;
   this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key);
   this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key);
   this.socket.on('message', function(data) {
   this.socket.on('message', function(data) {
@@ -1461,24 +1475,21 @@ function Peer(id, options) {
   this.socket.on('error', function(error) {
   this.socket.on('error', function(error) {
     self._abort('socket-error', 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();
+    }
+  });
   this.socket.on('close', function() {
   this.socket.on('close', function() {
-    if (!self.disconnected) { // If we haven't explicitly disconnected, emit error.
+    // If we haven't explicitly disconnected, emit error.
+    if (!self.disconnected) {
       self._abort('socket-closed', 'Underlying socket is already closed.');
       self._abort('socket-closed', 'Underlying socket is already closed.');
     }
     }
   });
   });
-  //
-
-  // Start the connections
-  if (id) {
-    this._initialize(id);
-  } else {
-    this._retrieveId();
-  }
-  //
 };
 };
 
 
-util.inherits(Peer, EventEmitter);
-
 /** Get a unique ID from the server via XHR. */
 /** Get a unique ID from the server via XHR. */
 Peer.prototype._retrieveId = function(cb) {
 Peer.prototype._retrieveId = function(cb) {
   var self = this;
   var self = this;
@@ -1516,9 +1527,8 @@ Peer.prototype._retrieveId = function(cb) {
 
 
 /** Initialize a connection with the server. */
 /** Initialize a connection with the server. */
 Peer.prototype._initialize = function(id) {
 Peer.prototype._initialize = function(id) {
-  var self = this;
   this.id = id;
   this.id = id;
-  this.socket.start(this.id);
+  this.socket.start(this.id, this.options.token);
 }
 }
 
 
 /** Handles messages from the server. */
 /** Handles messages from the server. */
@@ -1549,7 +1559,7 @@ Peer.prototype._handleMessage = function(message) {
       break;
       break;
 
 
     case 'EXPIRE': // The offer sent to a peer has expired without response.
     case 'EXPIRE': // The offer sent to a peer has expired without response.
-      this.emit('error', new Error('Could not connect to peer ' + peer));
+      this.emitError('peer-unavailable', 'Could not connect to peer ' + peer);
       break;
       break;
     case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
     case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
       var connectionId = payload.connectionId;
       var connectionId = payload.connectionId;
@@ -1639,8 +1649,9 @@ Peer.prototype.connect = function(peer, options) {
   if (this.disconnected) {
   if (this.disconnected) {
     util.warn('You cannot connect to a new Peer because you called '
     util.warn('You cannot connect to a new Peer because you called '
         + '.disconnect() on this Peer and ended your connection with the'
         + '.disconnect() on this Peer and ended your connection with the'
-        + ' server. You can create a new Peer to reconnect.');
-    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
+        + ' 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;
     return;
   }
   }
   var connection = new DataConnection(peer, this, options);
   var connection = new DataConnection(peer, this, options);
@@ -1657,7 +1668,7 @@ Peer.prototype.call = function(peer, stream, options) {
     util.warn('You cannot connect to a new Peer because you called '
     util.warn('You cannot connect to a new Peer because you called '
         + '.disconnect() on this Peer and ended your connection with the'
         + '.disconnect() on this Peer and ended your connection with the'
         + ' server. You can create a new Peer to reconnect.');
         + ' server. You can create a new Peer to reconnect.');
-    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
+    this.emitError('disconnected', 'Cannot connect to new Peer after disconnecting from server.');
     return;
     return;
   }
   }
   if (!stream) {
   if (!stream) {
@@ -1700,12 +1711,26 @@ Peer.prototype._delayedAbort = function(type, message) {
   });
   });
 }
 }
 
 
-/** Destroys the Peer and emits an error message. */
+/**
+ * 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) {
 Peer.prototype._abort = function(type, message) {
-  util.error('Aborting. Error:', message);
-  var err = new Error(message);
+  util.error('Aborting!');
+  if (!this.disconnected) {
+    this.destroy();
+  }
+  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);
+  }
   err.type = type;
   err.type = type;
-  this.destroy();
   this.emit('error', err);
   this.emit('error', err);
 };
 };
 
 
@@ -1758,11 +1783,29 @@ Peer.prototype.disconnect = function() {
       if (self.socket) {
       if (self.socket) {
         self.socket.close();
         self.socket.close();
       }
       }
+      self.emit('disconnected', self.id);
+      self._lastServerId = self.id;
       self.id = null;
       self.id = null;
     }
     }
   });
   });
 }
 }
 
 
+/** 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._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!');
+  }
+};
+
 /**
 /**
  * Get a list of available peer IDs. If you're running your own server, you'll
  * 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
  * want to set allow_discovery: true in the PeerServer options. If you're using
@@ -1930,12 +1973,12 @@ DataConnection.prototype._handleDataMessage = function(e) {
     chunkInfo.count += 1;
     chunkInfo.count += 1;
 
 
     if (chunkInfo.total === 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.
       // We've received all the chunks--time to construct the complete data.
       data = new Blob(chunkInfo.data);
       data = new Blob(chunkInfo.data);
       this._handleDataMessage({data: data});
       this._handleDataMessage({data: data});
-
-      // We can also just delete the chunks now.
-      delete this._chunkedData[id];
     }
     }
 
 
     this._chunkedData[id] = chunkInfo;
     this._chunkedData[id] = chunkInfo;
@@ -2384,11 +2427,11 @@ Negotiator._makeOffer = function(connection) {
         dst: connection.peer
         dst: connection.peer
       });
       });
     }, function(err) {
     }, function(err) {
-      connection.provider.emit('error', err);
+      connection.provider.emitError('webrtc', err);
       util.log('Failed to setLocalDescription, ', err);
       util.log('Failed to setLocalDescription, ', err);
     });
     });
   }, function(err) {
   }, function(err) {
-    connection.provider.emit('error', err);
+    connection.provider.emitError('webrtc', err);
     util.log('Failed to createOffer, ', err);
     util.log('Failed to createOffer, ', err);
   }, connection.options.constraints);
   }, connection.options.constraints);
 }
 }
@@ -2416,11 +2459,11 @@ Negotiator._makeAnswer = function(connection) {
         dst: connection.peer
         dst: connection.peer
       });
       });
     }, function(err) {
     }, function(err) {
-      connection.provider.emit('error', err);
+      connection.provider.emitError('webrtc', err);
       util.log('Failed to setLocalDescription, ', err);
       util.log('Failed to setLocalDescription, ', err);
     });
     });
   }, function(err) {
   }, function(err) {
-    connection.provider.emit('error', err);
+    connection.provider.emitError('webrtc', err);
     util.log('Failed to create answer, ', err);
     util.log('Failed to create answer, ', err);
   });
   });
 }
 }
@@ -2438,7 +2481,7 @@ Negotiator.handleSDP = function(type, connection, sdp) {
       Negotiator._makeAnswer(connection);
       Negotiator._makeAnswer(connection);
     }
     }
   }, function(err) {
   }, function(err) {
-    connection.provider.emit('error', err);
+    connection.provider.emitError('webrtc', err);
     util.log('Failed to setRemoteDescription, ', err);
     util.log('Failed to setRemoteDescription, ', err);
   });
   });
 }
 }
@@ -2476,12 +2519,11 @@ util.inherits(Socket, EventEmitter);
 
 
 
 
 /** Check in with ID or get one from server. */
 /** Check in with ID or get one from server. */
-Socket.prototype.start = function(id) {  
+Socket.prototype.start = function(id, token) {
   this.id = id;
   this.id = id;
 
 
-  var token = util.randomToken();
   this._httpUrl += '/' + id + '/' + token;
   this._httpUrl += '/' + id + '/' + token;
-  this._wsUrl += '&id='+id+'&token='+token;
+  this._wsUrl += '&id=' + id + '&token=' + token;
 
 
   this._startXhrStream();
   this._startXhrStream();
   this._startWebSocket();
   this._startWebSocket();
@@ -2499,14 +2541,19 @@ Socket.prototype._startWebSocket = function(id) {
   this._socket = new WebSocket(this._wsUrl);
   this._socket = new WebSocket(this._wsUrl);
 
 
   this._socket.onmessage = function(event) {
   this._socket.onmessage = function(event) {
-    var data;
     try {
     try {
-      data = JSON.parse(event.data);
+      var data = JSON.parse(event.data);
+      self.emit('message', data);
     } catch(e) {
     } catch(e) {
       util.log('Invalid server message', event.data);
       util.log('Invalid server message', event.data);
       return;
       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
   // Take care of the queue of connections if necessary and make sure Peer knows
@@ -2536,9 +2583,13 @@ Socket.prototype._startXhrStream = function(n) {
       if (this.readyState == 2 && this.old) {
       if (this.readyState == 2 && this.old) {
         this.old.abort();
         this.old.abort();
         delete this.old;
         delete this.old;
-      }
-      if (this.readyState > 2 && this.status == 200 && this.responseText) {
+      } else if (this.readyState > 2 && this.status === 200 && this.responseText) {
         self._handleStream(this);
         self._handleStream(this);
+      } else if (this.status !== 200) {
+        // If we get a different status code, likely something went wrong.
+        // Stop streaming.
+        clearTimeout(self._timeout);
+        self.emit('disconnected');
       }
       }
     };
     };
     this._http.send(null);
     this._http.send(null);

File diff suppressed because it is too large
+ 0 - 0
dist/peer.min.js


+ 39 - 10
docs/api.json

@@ -176,7 +176,13 @@
         "name": "'close'",
         "name": "'close'",
         "type": "event",
         "type": "event",
         "snippet": "peer.on('close', function() { ... });",
         "snippet": "peer.on('close', function() { ... });",
-        "description": "Emitted when the peer is <a href='#peerdestroy'>destroyed</a>.<span class='tip'>To be extra certain that peers clean up correctly, we recommend calling <code>peer.destroy()</code> on a peer when it is no longer needed.</span>"
+        "description": "Emitted when the peer is <a href='#peerdestroy'>destroyed</a> and can no longer accept or create any new connections. At this time, the peer's connections will all be closed. <span class='tip'>To be extra certain that peers clean up correctly, we recommend calling <code>peer.destroy()</code> on a peer when it is no longer needed.</span>"
+      },
+      {
+        "name": "'disconnected'",
+        "type": "event",
+        "snippet": "peer.on('disconnected', function() { ... });",
+        "description": "Emitted when the peer is disconnected from the signalling server, either <a href='#peerdisconnect'>manually</a> or because the connection to the signalling server was lost. When a peer is disconnected, its existing connections will stay alive, but the peer cannot accept or create any new connections. You can reconnect to the server by calling <a href='#peerreconnect'><code>peer.reconnect()</code></a>."
       },
       },
       {
       {
         "name": "'error'",
         "name": "'error'",
@@ -190,6 +196,11 @@
             "tags": ["fatal"],
             "tags": ["fatal"],
             "description": "The client's browser does not support some or all WebRTC features that you are trying to use."
             "description": "The client's browser does not support some or all WebRTC features that you are trying to use."
           },
           },
+          {
+            "name": "'disconnected'",
+            "type": "Error",
+            "description": "You've already disconnected this peer from the server and can no longer make any new connections on it."
+          },
           {
           {
             "name": "'invalid-id'",
             "name": "'invalid-id'",
             "type": "Error",
             "type": "Error",
@@ -203,21 +214,20 @@
             "description": "The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only)."
             "description": "The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only)."
           },
           },
           {
           {
-            "name": "'unavailable-id'",
+            "name": "'network'",
             "type": "Error",
             "type": "Error",
-            "tags": ["fatal"],
-            "description": "The ID passed into the Peer constructor is already taken."
+            "description": "Lost or cannot establish a connection to the signalling server."
           },
           },
           {
           {
-            "name": "'ssl-unavailable'",
+            "name": "'peer-unavailable'",
             "type": "Error",
             "type": "Error",
-            "tags": ["fatal"],
-            "description": "PeerJS is being used securely, but the cloud server does not support SSL. Use a custom PeerServer."
+            "description": "The peer you're trying to connect to does not exist."
           },
           },
           {
           {
-            "name": "'server-disconnected'",
+            "name": "'ssl-unavailable'",
             "type": "Error",
             "type": "Error",
-            "description": "You've already disconnected this peer and can no longer make any new connections on it."
+            "tags": ["fatal"],
+            "description": "PeerJS is being used securely, but the cloud server does not support SSL. Use a custom PeerServer."
           },
           },
           {
           {
             "name": "'server-error'",
             "name": "'server-error'",
@@ -236,6 +246,17 @@
             "type": "Error",
             "type": "Error",
             "tags": ["fatal"],
             "tags": ["fatal"],
             "description": "The underlying socket closed unexpectedly."
             "description": "The underlying socket closed unexpectedly."
+          },
+          {
+            "name": "'unavailable-id'",
+            "type": "Error",
+            "tags": ["sometimes fatal"],
+            "description": "The ID passed into the Peer constructor is already taken.<span class='warn'>This error is not fatal if your peer has open peer-to-peer connections. This can happen if you attempt to <a href='#peerreconnect'>reconnect</a> a peer that has been <a href='#peerdisconnect'>disconnected from the server</a>, but its old ID has now been taken.</span>"
+          },
+          {
+            "name": "'webrtc'",
+            "type": "Error",
+            "description": "Native WebRTC errors."
           }
           }
         ]
         ]
       }
       }
@@ -246,9 +267,17 @@
     "name": "peer.disconnect",
     "name": "peer.disconnect",
     "type": "method",
     "type": "method",
     "snippet": "peer.disconnect();",
     "snippet": "peer.disconnect();",
-    "description": "Close the connection to the server, leaving all existing data and media connections intact. <a href='#peerdisconnected'><code>peer.disconnected</code></a> will be set to <code>true</code>.<span class='warn'>This cannot be undone; the respective peer object will no longer be able to create or receive any connections and its ID will be forfeited on the (cloud) server.</span>"
+    "description": "Close the connection to the server, leaving all existing data and media connections intact. <a href='#peerdisconnected'><code>peer.disconnected</code></a> will be set to <code>true</code> and the <a href='#peeron-disconnected'><code>disconnected</code></a> event will fire.<span class='warn'>This cannot be undone; the respective peer object will no longer be able to create or receive any connections and its ID will be forfeited on the (cloud) server.</span>"
+  },
+
+  {
+    "name": "peer.reconnect",
+    "type": "method",
+    "snippet": "peer.reconnect();",
+    "description": "Attempt to reconnect to the server with the peer's old ID. Only <a href='#peerdisconnect'>disconnected peers</a> can be reconnected. Destroyed peers cannot be reconnected. If the connection fails (as an example, if the peer's old ID is now taken), the peer's existing connections will not close, but any associated errors events will fire."
   },
   },
 
 
+
   {
   {
     "name": "peer.destroy",
     "name": "peer.destroy",
     "type": "method",
     "type": "method",

File diff suppressed because it is too large
+ 0 - 0
docs/index.html


+ 1 - 1
examples/chat.html

@@ -8,7 +8,7 @@
 <link href="fancy.css" rel="stylesheet" type="text/css">
 <link href="fancy.css" rel="stylesheet" type="text/css">
 
 
 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
-<script type="text/javascript" src="../dist/peer.min.js"></script>
+<script type="text/javascript" src="../dist/peer.js"></script>
 <script>
 <script>
 // Connect to PeerJS, have server assign an ID instead of providing one
 // Connect to PeerJS, have server assign an ID instead of providing one
 // Showing off some of the configs available with PeerJS :).
 // Showing off some of the configs available with PeerJS :).

+ 5 - 5
lib/negotiator.js

@@ -223,11 +223,11 @@ Negotiator._makeOffer = function(connection) {
         dst: connection.peer
         dst: connection.peer
       });
       });
     }, function(err) {
     }, function(err) {
-      connection.provider.emit('error', err);
+      connection.provider.emitError('webrtc', err);
       util.log('Failed to setLocalDescription, ', err);
       util.log('Failed to setLocalDescription, ', err);
     });
     });
   }, function(err) {
   }, function(err) {
-    connection.provider.emit('error', err);
+    connection.provider.emitError('webrtc', err);
     util.log('Failed to createOffer, ', err);
     util.log('Failed to createOffer, ', err);
   }, connection.options.constraints);
   }, connection.options.constraints);
 }
 }
@@ -255,11 +255,11 @@ Negotiator._makeAnswer = function(connection) {
         dst: connection.peer
         dst: connection.peer
       });
       });
     }, function(err) {
     }, function(err) {
-      connection.provider.emit('error', err);
+      connection.provider.emitError('webrtc', err);
       util.log('Failed to setLocalDescription, ', err);
       util.log('Failed to setLocalDescription, ', err);
     });
     });
   }, function(err) {
   }, function(err) {
-    connection.provider.emit('error', err);
+    connection.provider.emitError('webrtc', err);
     util.log('Failed to create answer, ', err);
     util.log('Failed to create answer, ', err);
   });
   });
 }
 }
@@ -277,7 +277,7 @@ Negotiator.handleSDP = function(type, connection, sdp) {
       Negotiator._makeAnswer(connection);
       Negotiator._makeAnswer(connection);
     }
     }
   }, function(err) {
   }, function(err) {
-    connection.provider.emit('error', err);
+    connection.provider.emitError('webrtc', err);
     util.log('Failed to setRemoteDescription, ', err);
     util.log('Failed to setRemoteDescription, ', err);
   });
   });
 }
 }

+ 69 - 24
lib/peer.js

@@ -75,7 +75,7 @@ function Peer(id, options) {
 
 
   // States.
   // States.
   this.destroyed = false; // Connections have been killed
   this.destroyed = false; // Connections have been killed
-  this.disconnected = false; // Connection to PeerServer killed manually but P2P connections still active
+  this.disconnected = false; // Connection to PeerServer killed but P2P connections still active
   this.open = false; // Sockets and such are not yet open.
   this.open = false; // Sockets and such are not yet open.
   //
   //
 
 
@@ -84,8 +84,21 @@ function Peer(id, options) {
   this._lostMessages = {}; // src => [list of messages]
   this._lostMessages = {}; // src => [list of messages]
   //
   //
 
 
-  // Initialize the 'socket' (which is actually a mix of XHR streaming and
-  // websockets.)
+  // Start the server connection
+  this._initializeServerConnection();
+  if (id) {
+    this._initialize(id);
+  } else {
+    this._retrieveId();
+  }
+  //
+};
+
+util.inherits(Peer, EventEmitter);
+
+// Initialize the 'socket' (which is actually a mix of XHR streaming and
+// websockets.)
+Peer.prototype._initializeServerConnection = function() {
   var self = this;
   var self = this;
   this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key);
   this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key);
   this.socket.on('message', function(data) {
   this.socket.on('message', function(data) {
@@ -94,24 +107,21 @@ function Peer(id, options) {
   this.socket.on('error', function(error) {
   this.socket.on('error', function(error) {
     self._abort('socket-error', 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();
+    }
+  });
   this.socket.on('close', function() {
   this.socket.on('close', function() {
-    if (!self.disconnected) { // If we haven't explicitly disconnected, emit error.
+    // If we haven't explicitly disconnected, emit error.
+    if (!self.disconnected) {
       self._abort('socket-closed', 'Underlying socket is already closed.');
       self._abort('socket-closed', 'Underlying socket is already closed.');
     }
     }
   });
   });
-  //
-
-  // Start the connections
-  if (id) {
-    this._initialize(id);
-  } else {
-    this._retrieveId();
-  }
-  //
 };
 };
 
 
-util.inherits(Peer, EventEmitter);
-
 /** Get a unique ID from the server via XHR. */
 /** Get a unique ID from the server via XHR. */
 Peer.prototype._retrieveId = function(cb) {
 Peer.prototype._retrieveId = function(cb) {
   var self = this;
   var self = this;
@@ -149,7 +159,6 @@ Peer.prototype._retrieveId = function(cb) {
 
 
 /** Initialize a connection with the server. */
 /** Initialize a connection with the server. */
 Peer.prototype._initialize = function(id) {
 Peer.prototype._initialize = function(id) {
-  var self = this;
   this.id = id;
   this.id = id;
   this.socket.start(this.id, this.options.token);
   this.socket.start(this.id, this.options.token);
 }
 }
@@ -182,7 +191,7 @@ Peer.prototype._handleMessage = function(message) {
       break;
       break;
 
 
     case 'EXPIRE': // The offer sent to a peer has expired without response.
     case 'EXPIRE': // The offer sent to a peer has expired without response.
-      this.emit('error', new Error('Could not connect to peer ' + peer));
+      this.emitError('peer-unavailable', 'Could not connect to peer ' + peer);
       break;
       break;
     case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
     case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
       var connectionId = payload.connectionId;
       var connectionId = payload.connectionId;
@@ -272,8 +281,9 @@ Peer.prototype.connect = function(peer, options) {
   if (this.disconnected) {
   if (this.disconnected) {
     util.warn('You cannot connect to a new Peer because you called '
     util.warn('You cannot connect to a new Peer because you called '
         + '.disconnect() on this Peer and ended your connection with the'
         + '.disconnect() on this Peer and ended your connection with the'
-        + ' server. You can create a new Peer to reconnect.');
-    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
+        + ' 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;
     return;
   }
   }
   var connection = new DataConnection(peer, this, options);
   var connection = new DataConnection(peer, this, options);
@@ -290,7 +300,7 @@ Peer.prototype.call = function(peer, stream, options) {
     util.warn('You cannot connect to a new Peer because you called '
     util.warn('You cannot connect to a new Peer because you called '
         + '.disconnect() on this Peer and ended your connection with the'
         + '.disconnect() on this Peer and ended your connection with the'
         + ' server. You can create a new Peer to reconnect.');
         + ' server. You can create a new Peer to reconnect.');
-    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
+    this.emitError('disconnected', 'Cannot connect to new Peer after disconnecting from server.');
     return;
     return;
   }
   }
   if (!stream) {
   if (!stream) {
@@ -333,12 +343,28 @@ Peer.prototype._delayedAbort = function(type, message) {
   });
   });
 }
 }
 
 
-/** Destroys the Peer and emits an error message. */
+/**
+ * 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) {
 Peer.prototype._abort = function(type, message) {
-  util.error('Aborting. Error:', message);
-  var err = new Error(message);
+  util.error('Aborting!');
+  if (!this._lastServerId) {
+    this.destroy();
+  } else {
+    this.disconnect();
+  }
+  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);
+  }
   err.type = type;
   err.type = type;
-  this.destroy();
   this.emit('error', err);
   this.emit('error', err);
 };
 };
 
 
@@ -391,11 +417,30 @@ Peer.prototype.disconnect = function() {
       if (self.socket) {
       if (self.socket) {
         self.socket.close();
         self.socket.close();
       }
       }
+      self.emit('disconnected', self.id);
+      self._lastServerId = self.id;
       self.id = null;
       self.id = null;
     }
     }
   });
   });
 }
 }
 
 
+/** 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!');
+  }
+};
+
 /**
 /**
  * Get a list of available peer IDs. If you're running your own server, you'll
  * 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
  * want to set allow_discovery: true in the PeerServer options. If you're using

+ 15 - 6
lib/socket.js

@@ -25,7 +25,7 @@ Socket.prototype.start = function(id, token) {
   this.id = id;
   this.id = id;
 
 
   this._httpUrl += '/' + id + '/' + token;
   this._httpUrl += '/' + id + '/' + token;
-  this._wsUrl += '&id='+id+'&token='+token;
+  this._wsUrl += '&id=' + id + '&token=' + token;
 
 
   this._startXhrStream();
   this._startXhrStream();
   this._startWebSocket();
   this._startWebSocket();
@@ -43,14 +43,19 @@ Socket.prototype._startWebSocket = function(id) {
   this._socket = new WebSocket(this._wsUrl);
   this._socket = new WebSocket(this._wsUrl);
 
 
   this._socket.onmessage = function(event) {
   this._socket.onmessage = function(event) {
-    var data;
     try {
     try {
-      data = JSON.parse(event.data);
+      var data = JSON.parse(event.data);
+      self.emit('message', data);
     } catch(e) {
     } catch(e) {
       util.log('Invalid server message', event.data);
       util.log('Invalid server message', event.data);
       return;
       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
   // Take care of the queue of connections if necessary and make sure Peer knows
@@ -80,9 +85,13 @@ Socket.prototype._startXhrStream = function(n) {
       if (this.readyState == 2 && this.old) {
       if (this.readyState == 2 && this.old) {
         this.old.abort();
         this.old.abort();
         delete this.old;
         delete this.old;
-      }
-      if (this.readyState > 2 && this.status == 200 && this.responseText) {
+      } else if (this.readyState > 2 && this.status === 200 && this.responseText) {
         self._handleStream(this);
         self._handleStream(this);
+      } else if (this.status !== 200) {
+        // If we get a different status code, likely something went wrong.
+        // Stop streaming.
+        clearTimeout(self._timeout);
+        self.emit('disconnected');
       }
       }
     };
     };
     this._http.send(null);
     this._http.send(null);

Some files were not shown because too many files changed in this diff