Source: libs/P2PRouter.js

/**
* @file Peer-to-peer message routing interface.
*
* @version 0.4.1
*/
/**
* @class Transparently routes messages to/from available peer-to-peer interfaces.
* @extends EventDispatcher
* @see {@link WSSClient}
* @see {@link WSSTunnel}
* @see {@link WebRTCClient}
*/
class P2PRouter extends EventDispatcher {

   /**
   * A peer-to-peer message has been received on one of the routed transports
   * being handled by this instance. Both "direct" and "message" types emit
   * the same event (examine the <code>data</code> property to differentiate).
   *
   * @event P2PRouter#message
   * @type {Event}
   * @property {Object} data A native JSON-RPC 2.0 result or notification object.
   * @property {String} transportType The transport type through which the message
   * was received. This should match one of the <code>PeerConnectionObject.options</code>.
   * @property {Object} transport A reference to the transport interface that initially
   * handled the receipt of the message.
   */
   /**
   * A server-originating "update" message has been received on one of the routed transports
   * being handled by this instance.
   *
   * @event P2PRouter#update
   * @type {Event}
   * @property {Object} data A native JSON-RPC 2.0 result or notification object.
   * @property {String} transportType The transport type through which the message
   * was received. This should match one of the <code>PeerConnectionObject.options</code>.
   * @property {Object} transport A reference to the transport interface that initially
   * handled the receipt of the message.
   */
   /**
   * A new peer-to-peer connection has been established.
   *
   * @event P2PRouter#peerconnect
   * @type {Event}
   * @property {Object} data A native JSON-RPC 2.0 result or notification object.
   * @property {String} transportType The transport type on which the new connection was
   * established. This should match one of the <code>PeerConnectionObject.options</code>.
   * @property {Object} transport A reference to the transport interface on which the
   * new connection was established.
   */
   /**
   * A connected peer has changed private IDs.
   *
   * @event P2PRouter#peerpid
   * @type {Event}
   * @property {String} oldPrivateID The old / previous private ID for the peer.
   * @property {String} newPrivateID The new / changed private ID for the peer.
   */
   /**
   * A  peer-to-peer connection has closed.
   *
   * @event P2PRouter#peerdisconnect
   * @type {Event}
   * @property {Object} data A native JSON-RPC 2.0 result or notification object.
   * @property {String} transportType The transport type on which the connection was
   * previously active established. This should match one of the <code>PeerConnectionObject.options</code>.
   * @property {Object} transport A reference to the transport interface on which the
   * connection was previously active
   */

   /**
   * An object containing information and references to connectivity
   * options for an individual peer.
   *
   * @typedef {Object} PeerConnectionObject
   * @property {Object} options Contains the transport options (supported connectivity)
   * for the peer.
   * @property {Boolean} options.wss=false Does the peer support WebSocket Sessions connecivity?
   * @property {Boolean} options.webrtc=false Does the peer support WebRTC connecivity?
   * @property {Boolean} options.ortc=false Does the peer support ORTC connecivity?
   * @property {Object} status Contains the connection status of each transport <code>option</code>
   * @property {String} status.wss="closed" The WebSocket Sessions transport may either be <code>"closed"</code>,
   * the connection may be <code>"pending"</code>, it may be <code>"open"</code> for bi-directional
   * communication, or a connection attempt may have <code>"failed"</code>.
   * @property {String} status.webrtc="closed" The WebRTC transport may either be <code>"closed"</code>,
   * the connection may be <code>"pending"</code>, it may be <code>"open"</code> for bi-directional
   * communication, or a connection attempt may have <code>"failed"</code>.
   * @property {String} status.ortc="closed" The ORTC transport may either be <code>"closed"</code>,
   * the connection may be <code>"pending"</code>, it may be <code>"open"</code> for bi-directional
   * communication, or a connection attempt may have <code>"failed"</code>.
   * @property {Object} transport Contains references to any transports defined in
   * the conectivity <code>options</code>.
   * @property {Object} transport.wss=null A reference to the WebSocket Sessions / Tunnel transport
   * with which to communicate with the peer.
   * @property {Object} transport.webrtc=null] A reference to the WebRTC transport
   * with which to communicate with the peer.
   * @property {Object} transport.ortc=null A reference to the ORTC transport
   * with which to communicate with the peer.
   * @property {Object} connectTimeout References to <code>Timeout</code> objects
   * used when attempting to establish a connection to the peer.
   * @property {Object} connectTimeout.wss=null The <code>Timeout</code> object
   * used when attempting to establish a WebSocket Sessions connection.
   * @property {Object} connectTimeout.webrtc=null The <code>Timeout</code> object
   * used when attempting to establish a WebRTC connection.
   * @property {Object} connectTimeout.ortc=null The <code>Timeout</code> object
   * used when attempting to establish a ORTC connection.
   * @property {Object} connectPromise References to <code>Promise</code> function
   * references that are resolved or rejected when the associated transport is attempting
   * a connection.
   * @property {Object} connectPromise.wss The <code>Promise</code> functions that resolve
   * or reject when the WebSocket Sessions / Tunnel transport connects or fails to connect.
   * @property {Function} connectPromise.wss.resolve=null The <code>Promise.resolve</code>
   * function invoked on a successfull WebSocket Sessions connection.
   * @property {Function} connectPromise.wss.reject=null The <code>Promise.reject</code>
   * function invoked on a failed WebSocket Sessions connection.
   * @property {Object} connectPromise.webrtc The <code>Promise</code> functions that resolve
   * or reject when the WebRTC transport connects or fails to connect.
   * @property {Function} connectPromise.webrtc.resolve=null The <code>Promise.resolve</code>
   * function invoked on a successfull WebRTC connection.
   * @property {Function} connectPromise.webrtc.reject=null The <code>Promise.reject</code>
   * function invoked on a failed WebRTC connection.
   * @property {Object} connectPromise.ortc The <code>Promise</code> functions that resolve
   * or reject when the ObjectRTC transport connects or fails to connect.
   * @property {Function} connectPromise.ortc.resolve=null The <code>Promise.resolve</code>
   * function invoked on a successfull ObjectRTC connection.
   * @property {Function} connectPromise.ortc.reject=null The <code>Promise.reject</code>
   * function invoked on a failed ObjectRTC connection.
   */

   /**
   * Creates an instance of P2PRouter.
   *
   * @param {Object} [configData=null] Application configuration data, usually as loaded at
   * startup.
   */
   constructor(configData=null) {
      super();
      this._config = configData;
      try {
         P2PRouter.supportedTransportspreferred = this.config.p2p.transports.preferred;
      } catch (err) {
         //this is not a fatal error and doesn't requite a warning
         console.log ("Preferred peer-to-peer transport(s) not specified in application configuration. Using default.");
      }
   }

   /**
   * @property {Object} config Application configuration data, usually loaded
   * at application startup (<code>settings.json</code> file).
   * @readonly
   */
   get config() {
      return (this._config);
   }

   /**
   * Contains information on all the transports currently supported or recognized by P2PRouter.
   * @property {Array} [preferred=["webrtc","wss","ortc"]] Indexed list of transports in the order of default preference
   * (i.e. index 0 is most preferred). The first <i>available</i> preferred transport will be used
   * when communicating with peers unless another order is specified when sending. If defined in the
   * application [config]{@link P2Prouter#config}, the <code>p2p.transports.preferred</code> array
   * will be used instead of the internal default.
   * @property {Object} options Name-value pairs of transports
   * and their availabilities.
   * @property {Boolean} options.webrtc WebRTC
   * connectivity is available.
   * @property {Boolean} options.wss WebSocket Sessions
   * connectivity is available.
   * @property {Boolean} options.ortc ObjectRTC
   * connectivity is available.
   * @property {Object} sendModel Describes how messages
   * sent to multiple recipients should be treated, either "single"
   * (single transport per recipient), or "multi" (shared transport for
   * multiple recipients). Unavailable transports will have a model of "none".
   * @property {String} sendModel.webrtc WebRTC
   * message sending model. Usually "single".
   * @property {String} sendModel.wss WebSocket Sessions
   * message sending model. Usually "multi".
   * @property {String} sendModel.ortc ObjectRTC
   * message sending model. Usually "single".
   *
   * @type {Object}
   * @static
   */
   static get supportedTransports() {
      if (P2PRouter._supportedTransports == undefined) {
         P2PRouter._supportedTransports =  new Object();
         P2PRouter._supportedTransports.preferred = ["webrtc", "wss", "ortc"];
         P2PRouter._supportedTransports.options = new Object();
         P2PRouter._supportedTransports.sendModel = new Object();
         if (DetectRTC.isWebRTCSupported == true) {
            P2PRouter._supportedTransports.options.webrtc = true;
            P2PRouter._supportedTransports.sendModel.webrtc = "single";
         } else {
            P2PRouter._supportedTransports.options.webrtc = false;
            P2PRouter._supportedTransports.sendModel.webrtc = "none";
         }
         if ((WSSClient != undefined) && (WSSClient != null)) {
            P2PRouter._supportedTransports.options.wss = true;
            P2PRouter._supportedTransports.sendModel.wss = "multi";
         } else {
            P2PRouter._supportedTransports.options.wss = false;
            P2PRouter._supportedTransports.sendModel.wss = "none";
         }
         if (DetectRTC.isORTCSupported == true) {
            P2PRouter._supportedTransports.options.ortc = true;
            P2PRouter._supportedTransports.sendModel.ortc = "single";
         } else {
            P2PRouter._supportedTransports.options.ortc = false;
            P2PRouter._supportedTransports.sendModel.ortc = "none";
         }
      }
      return (P2PRouter._supportedTransports);
   }

   /**
   * The privateID assigned to the session by the [rendezvous]{@link P2PRouter#rendezvous}.
   * This value will be <code>null</code> if no session has been established.
   * @type {String}
   */
   get privateID() {
      if ((this.rendezvous == null) && (this._privateID == undefined)) {
         this._privateID = null;
      } else {
         if (this._privateID == undefined) {
            this._privateID = this.rendezvous.privateID;
         }
      }
      return (this._privateID);
   }

   set privateID(pidSet) {
      this._privateID = pidSet;
   }

   /**
   * Internally-generated user token required by the [rendezvous]{@link P2PRouter#rendezvous}.
   * @type {String}
   */
   get userToken() {
      if ((this.rendezvous == null) && (this._userToken == undefined)) {
         this._userToken = null;
      } else {
         if (this._userToken == undefined) {
            this._userToken = this.rendezvous.userToken;
         }
      }
      return (this._userToken);
   }

   set userToken(utSet) {
      this._userToken = utSet;
   }

   /**
   * Server-generated token required by the [rendezvous]{@link P2PRouter#rendezvous}.
   * @type {String}
   */
   get serverToken() {
      if ((this.rendezvous == null) && (this._serverToken == undefined)) {
         this._serverToken = null;
      } else {
         if (this._serverToken == undefined) {
            this._serverToken = this.rendezvous.serverToken;
         }
      }
      return (this._serverToken);
   }

   set serverToken(stSet) {
      this._userToken = stSet;
   }

   /**
   * A list of currently connected peer IDs gathered from [peerConnections]{@link P2PRouter#peerConnections}.
   * @type {Array}
   * @readonly
   */
   get peers() {
      if (this.rendezvous == null) {
         return (null);
      }
      var returnPeers = new Array();
      for (var privateID in this.peerConnections) {
         returnPeers.push(privateID);
      }
      return (returnPeers);
   }

   /**
   * A reference to the P2P rendezvous, signalling, and fallback connection handler, or <code>null</code>
   * if no such connection exists or is invalid.
   * @type {*}
   */
   get rendezvous() {
      if (this._rendezvous == undefined) {
         return (null);
      }
      return (this._rendezvous);
   }

   set rendezvous(rendSet) {
      this._rendezvous = rendSet;
      //remove any existing listeners
      this._rendezvous.removeEventListener("message", this.onMessage);
      this._rendezvous.removeEventListener("update", this.onUpdate);
      this._rendezvous.removeEventListener("peerconnect", this.onPeerConnect);
      this._rendezvous.removeEventListener("peerpid", this.onPeerPIDUpdate);
      this._rendezvous.removeEventListener("peerdisconnect", this.onPeerDisconnect);
      //add new listeners
      this._rendezvous.addEventListener("message", this.onMessage, this);
      this._rendezvous.addEventListener("update", this.onUpdate, this);
      this._rendezvous.addEventListener("peerconnect", this.onPeerConnect, this);
      this._rendezvous.addEventListener("peerpid", this.onPeerPIDUpdate, this);
      this._rendezvous.addEventListener("peerdisconnect", this.onPeerDisconnect, this);
      switch (this._rendezvous.toString()) {
         case "WSSClient":
            this.updatePeerConnections(["wss"],[this._rendezvous]);
            break;
         case "WSSTunnel":
            this.updatePeerConnections(["wss"],[this._rendezvous]);
            break;
         default:
            break;
      }
   }

   /**
   * Contains {@link PeerConnectionObject} for any connected or desired peers, stored by private ID.
   * That is:<br/>
   * <code>peerConnections[<i>privateID</i>]=</code>{@link PeerConnectionObject}
   * @type {Object}
   * @readonly
   */
   get peerConnections() {
      if (this._peerConnections == undefined) {
         this._peerConnections = new Object();
      }
      return (this._peerConnections);
   }

   /**
   * Returns a {@link PeerConnectionObject} contained in [peerConnections]{@link P2PRouter#peerConnections}
   * by one of its transport references.
   *
   * @param {Object} transport A reference to a specific transport instance for which
   * to return a [peerConnections]{@link P2PRouter#peerConnections}. If this is a shared transport such
   * as WebSocket Sessions instance, the first matching [peerConnections]{@link P2PRouter#peerConnections}
   * object will be returned.
   *
   * @return {PeerConnectionObject} The first object in [peerConnections]{@link P2PRouter#peerConnections} containing a matching
   * <code>transport</code> reference. <code>null</code> is returned if no match can be found.
   */
   getPCOByTransport(transport) {
      for (var privateID in this.peerConnections) {
         var peerTransports = this.peerConnections[privateID].transport;
         for (var peerTransport in peerTransports) {
            if (peerTransports[peerTransport] === transport) {
               return (this.peerConnections[privateID]);
            }
         }
      }
      return (null);
   }

   /**
   * Returns a private ID associated with a {@link PeerConnectionObject} in
   * [peerConnections]{@link P2PRouter#peerConnections}.
   *
   * @param {Object} transport A reference to a specific transport instance for which
   * to return a private ID. If this is a shared transport such as WebSocket Sessions instance,
   * the first matching [peerConnections]{@link P2PRouter#peerConnections}'s private ID will be returned.
   *
   * @return {Object} The private ID of the first [peerConnections]{@link P2PRouter#peerConnections} containing
   * a matching <code>transport</code> reference. <code>null</code> is returned if no match can be found.
   */
   getPIDByTransport(transport) {
      for (var privateID in this.peerConnections) {
         var peerTransports = this.peerConnections[privateID].transport;
         for (var peerTransport in peerTransports) {
            if (peerTransports[peerTransport] === transport) {
               return (privateID);
            }
         }
      }
      return (null);
   }

   /**
   * Establishes a connection to a main / default rendezvous and fallback server.
   * If no such connection is established, the local [privateID]{@link P2PRouter#privateID} must be
   * set manually prior to establishing any peer-to-peer connections.
   *
   * @param {Object} connectInfo An object containing information about the
   * rendezvous/fallback server to connect to. The object must contain at least a
   * <code>type</code> property.
   * @param {String} connectInfo.transport Specifies the type of transport defined
   * by the <code>connectInfo</code> object. This parameter is case-sensitive.
   * Valid types include:<br/>
   * <ul>
   * <li><code>wss</code>: WebSocket Sessions</li>
   * <li><code>wsst</code>: WebSocket Sessions Tunnel</li>
   * </ul>
   * @param {Object} [connectInfo.tunnelParams] Tunneling parameters such as a list of
   * possible endpoints for use with the tunneling connection (if <code>connectInfo.transport="wsst"</code>,
   * for example).
   * @throws {Error} Thrown when the specified server could not be contacted or
   * if there is a problem with the <code>connectioInfo</code> parameter.
   */
   async connectRendezvous(connectionInfo) {
      if (connectionInfo == null) {
         throw (new Error("No connection info object provided."));
      }
      if (typeof(connectionInfo.transport) != "string") {
         throw (new Error("The connection info \"transport\" property must be a string."));
      }
      var connectData = new Object();
      connectData.options = P2PRouter.supportedTransports.options;
      switch (connectionInfo.transport) {
         case "wss":
            try {
               this.rendezvous = new WSSClient(connectionInfo.url);
               var result = await this.rendezvous.connect(connectionInfo.url, false, connectData);
               this.updatePeerConnections(["wss"],[this.rendezvous]);
            } catch (err) {
               this._rendezvous.destroy();
               this._rendezvous = null;
               throw(err);
            }
            return (result);
            break;
         case "wsst":
            try {
               this.rendezvous = new WSSTunnel(connectionInfo.url);
               connectData.tunnelParams = JSON.parse(connectionInfo.parameters);
               result = await this.rendezvous.connect(connectionInfo.url, false, connectData);
               this.updatePeerConnections(["wss"],[this.rendezvous]);
            } catch (err) {
               this._rendezvous.destroy();
               this._rendezvous = null;
            }
            return (result);
            break;
         default:
            throw (new Error("Unrecognized transport type \""+connectInfo.transport+"\""));
            break;
      }
   }

   /**
   * Updates the [peerConnections]{@link P2PRouter#peerConnections} object with the connectivity options
   * stored in [peerOptions]{@link P2PRouter#connection.peerOptions}.
   *
   * @param {Array} [openTypes=null] Indexed array of any connectivity types
   * to set as "open" (connected). The status of any types not appearing in this array
   * will be set as "closed" (disconnected).
   * @param {Array} [transports=null] Indexed array of any open transport references
   * matching the <code>openTypes</code> array.
   * @private
   */
   updatePeerConnections(openTypes=null, transports=null) {
      for (var privateID in this.rendezvous.peerOptions) {
         var options = this.rendezvous.peerOptions[privateID];
         this.insertPeerConnectionObject(privateID, options);
         //overwrite any defines open types
         if (openTypes != null) {
            if (openTypes.length != transports.length) {
               throw(new Error("Number of open connection types must match number of connections."));
            }
            for (var count=0; count < openTypes.length; count++) {
               this.peerConnections[privateID].status[openTypes[count]] = "open";
               this.peerConnections[privateID].transport[openTypes[count]] = transports[count];
            }
         }
         this.peerConnections[privateID].options = options;
      }
   }

   /**
   * Inserts a {@link PeerConnectionObject} for a peer into the [peerConnections]{@link P2PRouter#peerConnections} array
   * if one does not already exist.
   *
   * @param {String} privateID The private ID of the peer for which to inert a {@link PeerConnectionObject}.
   * @param {Object} options Name-value pairs defining the transports that the peer has advertised
   * as being available for them to use.
   * @param {String} defaultStatus The default <code>status</code> to set within the {@link PeerConnectionObject}
   * for each <code>option</code>.
   */
   insertPeerConnectionObject(privateID, options, defaultStatus="closed") {
      if ((this.peerConnections[privateID] == undefined) || (this.peerConnections[privateID] == null)) {
         this.peerConnections[privateID] = new Object();
         this.peerConnections[privateID].status = new Object();
         this.peerConnections[privateID].options = options;
         this.peerConnections[privateID].transport = new Object();
         this.peerConnections[privateID].connectTimeout = new Object();
         this.peerConnections[privateID].connectPromise = new Object();
         for (var option in options) {
            this.peerConnections[privateID].status[option] = defaultStatus;
            if (this.peerConnections[privateID].transport[option] == undefined) {
               this.peerConnections[privateID].transport[option] = null;
            }
            if (this.peerConnections[privateID].connectTimeout[option] == undefined) {
               this.peerConnections[privateID].connectTimeout[option] = new Object();
               if (this.peerConnections[privateID].connectTimeout[option] == undefined) {
                  this.peerConnections[privateID].connectTimeout[option] = null;
               }
               if (this.peerConnections[privateID].connectTimeout[option] == undefined) {
                  this.peerConnections[privateID].connectTimeout[option] = null;
               }
            }
            if (this.peerConnections[privateID].connectPromise[option] == undefined) {
               this.peerConnections[privateID].connectPromise[option] = new Object();
               if (this.peerConnections[privateID].connectPromise[option].resolve == undefined) {
                  this.peerConnections[privateID].connectPromise[option].resolve = null;
               }
               if (this.peerConnections[privateID].connectPromise[option].reject == undefined) {
                  this.peerConnections[privateID].connectPromise[option].reject = null;
               }
            }
         }
      }
   }

   /**
   * Changes the private ID associated with this instance. Any connected peers
   * are notified of this change.
   *
   * @param {String} newPrivateID The new private ID to set for this instance.
   * @param {Boolean} [allSuccess=false] If true, all attached peers must
   * be successfully notified of the change in order for the returned promise
   * to resolve with <code>true</code>. If false, only the <code>rendezvous</code>
   * server must be successfully updated.
   *
   * @return {Promise} The promise resolves with <code>true</code> if the private
   * ID was successfully changed, otherwise it rejects with
   * <code>false</code>. If <code>allSuccess</code> is true, all connected peers
   * must be successfully notified of the change for the promise to resolve
   * with <code>true</code>, otherwise only the [rendezvous]{@link P2PRouter#rendezvous}
   * server needs to be successfully updated.
   *
   * @async
   */
   async changePrivateID(newPrivateID, allSuccess=false) {
      var sentTransports = new Array();
      var updated = await this.rendezvous.changePrivateID(newPrivateID);
      if (updated == true) {
         this._privateID = newPrivateID;
      }
      sentTransports.push(this.rendezvous);
      for (var PID in this.peerConnections) {
         var connectionObj = this.peerConnections[PID];
         for (var transportType in connectionObj.status) {
            var status = connectionObj.status[transportType];
            var transport = connectionObj.transport[transportType];
            if (status == "open") {
               var sent = false;
               for (var count=0; count < sentTransports.length; count++) {
                  if (sentTransports[count] === transport) {
                     sent = true;
                     break;
                  }
               }
               if (sent == false) {
                  var changed = await transport.changePrivateID(newPrivateID);
                  sentTransports.push(transport);
                  if (changed == false) {
                     if (allSuccess == true) {
                        updated = false;
                     }
                     if (P2PRouter.supportedTransports[transportType].sendModel == "single") {
                        console.warn ("Could not send private ID change notification to: "+transport.peerID);
                     } else if (P2PRouter.supportedTransports[transportType].sendModel == "multi") {
                        console.warn ("Could not broadcast private ID change notification via: "+transport.toString());
                     } else {
                        console.warn ("Could not broadcast private ID change notification via unsupported transport.");
                     }
                  }
               }
            }
         }
      }
      return (updated);
   }

   /**
   * Requests a direct connection to a peer with a specific transport.
   *
   * @param {String} privateID The private ID of the peer to request the connection to.
   * @param {String} transportType The transport type to request the direct connection on. This
   * should be one of the supported transports listed in {@link P2PRouter.supportedTransports}.
   * @param {Number} requestTimeout=20 The number of seconds to wait before considering the request
   * timed out and invalid. The connection status will be set to "failed" after this timer
   * elapses without a successfull connection.
   */
   connectPeer(privateID, transportType, requestTimeout=20) {
      var promise = new Promise((resolve, reject) => {
         if (P2PRouter.supportedTransports.options[transportType] == false) {
            reject(new Error("Requested peer transport not locally supported."));
            return (false);
         }
         this.insertPeerConnectionObject(privateID, P2PRouter.supportedTransports.options); //if not exist
         if (this.peerConnections[privateID].status[transportType] == "open") {
            //already connected
            resolve(true);
            return(true);
         }
         switch (transportType) {
            case "webrtc":
               if (this.peerConnections[privateID].options[transportType] == false) {
                  reject (new Error("Peer does not support \""+transportType+"\" transport type."));
                  return (false);
               }
               var wrtcInst = new WebRTCClient(this);
               wrtcInst.addEventListener("peerconnect", this.onPeerConnect, this);
               wrtcInst.addEventListener("message", this.onMessage, this);
               wrtcInst.addEventListener("update", this.onUpdate, this);
               wrtcInst.addEventListener("peerpid", this.onPeerPIDUpdate, this);
               wrtcInst.addEventListener("peerdisconnect", this.onPeerDisconnect, this);
               this.peerConnections[privateID].transport[transportType] = wrtcInst;
               this.peerConnections[privateID].connectPromise[transportType].resolve = resolve;
               this.peerConnections[privateID].connectPromise[transportType].reject = reject;
               this.setPeerConnectTimeout(privateID, transportType, reject, requestTimeout);
               this.setConnectionStatus(privateID, "pending", transportType, wrtcInst);
               wrtcInst.connect(privateID);
               break;
            case "wss":
               //assume connection already exists so do nothing
               break;
            default:
               break;
         }
      });
      return (promise);
   }

   /**
   * Sets / starts a peer connection timeout.
   *
   * @param {String} privateID The private ID of the peer to whom the connection attempt is being made.
   * @param {String} transportType The transport connection type associated with the attempt. This should match
   * one of the {@link P2PRouter.supportedTransports}<code>.options</code>.
   * @param {Function} reject A <code>Promise</code> reject function or callback to invoke if the timeout completes.
   * @param {Number} [timeout=20] The number of seconds to delay before the timeout completes and <code>reject</code>
   * is called.
   *
   * @private
   */
   setPeerConnectTimeout(privateID, transportType, reject, timeout=20) {
      var timeoutID = setTimeout(this.onPeerConnectTimeout, (timeout*1000), privateID, transportType, reject, this);
      this.peerConnections[privateID].connectTimeout[transportType] = timeoutID;
   }

   /**
   * Sets the connection status and transport for a peer within the [peerConnections]{@link P2PRouter#peerConnections}
   * object, stopping any timeout time associated with a pending connection and optionally removing
   * the {@link PeerConnectionObject} entirely if no connection on any transport is open.
   *
   * @param {String} privateID The private ID of the peer for which to set the connection status and transport
   * reference.
   * @param {String} status The status to set for the <code>transportType</code>. Valid types include:
   * <ul>
   * <li><b>"closed"</b>: The transport is closed and and unavailable.</li>
   * <li><b>"pending"</b>: The transport is connection is pending / being established.</li>
   * <li><b>"open"</b>: The transport is open and available for bi-directional communication.</li>
   * <li><b>"failed"</b>: An attempt to open the transport for communication has failed.</li>
   * </ul>
   * @param {String} transportType The transport type to set the <code>status</code> for. Valid types
   * include:
   * <ul>
   * <li><b>"wss"</b></li>
   * <li><b>"webrtc"</b></li>
   * <li><b>"ortc"</b></li>
   * <li><b>"failed"</b></li>
   * </ul>
   * @param {Object} [transport=null] A reference to the transport associated with the <code>transportType</code>.
   * This reference is assigned to the <code>transport[transportType]</code> property of the {@link PeerConnectionObject}.
   * @param {Boolean} [removeOnDisconnect=true] If true, the {@link PeerConnectionObject} is automatically removed
   * from the [peerConnections]{@link P2PRouter#peerConnections} object and any event listeners removed if no transport
   * is "open" or "pending".
   */
   setConnectionStatus(privateID, status, transportType, transport=null, removeOnDisconnect=true) {
      var currentTransport = this.peerConnections[privateID].transport[transportType]; //save for possible event listener removal
      this.peerConnections[privateID].status[transportType] = status;
      this.peerConnections[privateID].transport[transportType] = transport;
      if ((status == "open") || (status == "failed")) {
         //stop timeout timer
         try {
            var timerID = this.peerConnections[privateID].connectTimeout[transportType];
            clearTimeout(timerID);
            this.peerConnections[privateID].connectTimeout[transportType] = null;
            delete this.peerConnections[privateID].connectTimeout[transportType];
         } catch (err) {
         }
         try {
            if (status == "open") {
               var resolveFunc = this.peerConnections[privateID].connectPromise[transportType].resolve;
               if (typeof(resolveFunc) == "function") {
                  var resolveObj = new Object();
                  resolveObj.transportType = transportType;
                  resolveObj.peerConnectionObject = this.peerConnections[privateID];
                  resolveFunc(resolveObj);
               }
               this.peerConnections[privateID].connectPromise[transportType].resolve = null;
               this.peerConnections[privateID].connectPromise[transportType].reject = null;
            } else if (status == "failed") {
               var rejectFunc = this.peerConnections[privateID].connectPromise[transportType].resolve;
               if (typeof(rejectFunc) == "function") {
                  var rejectObj = new Error("Connection failed.");
                  rejectObj.transportType = transportType;
                  rejectObj.peerConnectionObject = this.peerConnections[privateID];
                  rejectFunc(rejectObj);
               }
               this.peerConnections[privateID].connectPromise[transportType].resolve = null;
               this.peerConnections[privateID].connectPromise[transportType].reject = null;
            }
         } catch (err) {
            console.error (err);
         }
      }
      if (removeOnDisconnect == true) {
         var connected = false; //is peer connected on any transport?
         for (var transportTypeStr in this.peerConnections[privateID].status) {
            var transportStatus = this.peerConnections[privateID].status[transportTypeStr];
            if ((transportStatus != "closed") && (transportStatus != "failed")) {
               connected = true;
            }
         }
         //don't remove listeners if connected ot transport is a shared connection
         if (connected == false) {
            if (currentTransport != this.rendezvous) {
               //remove event listeners
               currentTransport.removeEventListener("message", this.onMessage, this);
               currentTransport.removeEventListener("update", this.onUpdate, this);
               currentTransport.removeEventListener("peerconnect", this.onPeerConnect, this);
               currentTransport.removeEventListener("peerpid", this.onPeerPIDUpdate, this);
               currentTransport.removeEventListener("peerdisconnect", this.onPeerDisconnect, this);
            }
            //remove entry
            this.peerConnections[privateID] = null;
            delete this.peerConnections[privateID];
         }
      }
   }

   /**
   * Creates an internal P2PRouter message. Since the format of this message
   * may change, this is the preferred way to create a message rather than
   * creating your own object.
   *
   * @param {String} messageType The P2PRouter message type to create.
   *
   * @return {Object} A formatted P2PRouter table message. Additional data
   * can be appended to this object before sending it to other peers.
   * @private
   */
   buildRouterMessage(messageType) {
      var messageObj=new Object();
      messageObj.routerMsg = messageType;
      return (messageObj);
   }

   /**
   * Sends a routed broadcast message to all connected peers using the
   * first preferred and available peer-to-peer communication transport foe each
   * peer as efficiently as possible.
   *
   * @param {*} data The data / message to send.
   * @param {Array} [prefTransports=P2PRouter.supportedTransports.preferred] Indexed
   * array of preferred transports to use to send the data / message. Each recipient
   * is evaluated and the first available (lowest index), transport matching this
   * list is used. Shared transports such as WebSocket Sessions are used as
   * efficiently as possible (e.g. sending to multiple recipients with one request).

   *
   * @return {Promise} An asynchronous Promise that will contain the results of
   * all send operations or will throw an error on failure.
   */
   async broadcast(data, prefTransports=P2PRouter.supportedTransports.preferred) {
      var transportGroups = this.createTransportGroups(this.peers, prefTransports);
      var results = new Array();
      for (var transportType in transportGroups) {
         try {
            var transportObj = transportGroups[transportType];
            var recipientsArr = transportObj.recipients;
            var transportsArr = transportObj.transports;
            var excludeArr = transportObj.exclude;
            var sendModel = P2PRouter.supportedTransports.sendModel[transportType];
            if (sendModel == "single") {
               for (var count = 0; count < recipientsArr.length; count++) {
                  var recipient = recipientsArr[count];
                  var transport = transportsArr[count];
                  var exclude = excludeArr.some(element => {
                     return (element == recipient);
                  });
                  if (exclude == false) {
                     var result = await transport.broadcast(data);
                  }
                  results.push(result);
               }
            } else if (sendModel == "multi") {
               //further break down shared transports
               var sharedGroups = this.createSharedTransportGroups(recipientsArr, transportsArr, excludeArr);
               for (count = 0; count < sharedGroups.length; count++) {
                  var sharedRecipients = sharedGroups[count].recipients;
                  var sharedTransport = sharedGroups[count].transport;
                  var sharedExclude = excludeArr.concat(sharedGroups[count].exclude);
                  result = await sharedTransport.broadcast(data, sharedExclude);
                  results.push(result);
               }
            } else if (sendModel == "none") {
               //try fallback connection as a last resort
               result = await this.rendezvous.broadcast(data, excludeArr);
               results.push(result);
            }
         } catch (err) {
            console.error (err);
         }
      }
      return (results);
   }

   /**
   * Sends a routed direct message to one or more connected peers using the
   * first preferred and available peer-to-peer communication transport for
   * each peer as efficiently as possible.
   *
   * @param {*} data The data / message to send.
   * @param {(Object|Array)} recipients An object or an array of recipient private
   * IDs. If this parameter is an object, one or more additional properties are
   * expected:
   * @param {Array} [recipients.rcp] An indexed array of recipient private IDs.
   * If this list is provided as the <code>recipients</code> parameter this
   * structure is dynamicaly generated before sending.
   * @param {Array} [prefTransports=P2PRouter.supportedTransports.preferred] Indexed
   * array of preferred transports to use to send the data / message. Each recipient
   * is evaluated and the first available (lowest index), transport matching this
   * list is used. Shared transports such as WebSocket Sessions are used as
   * efficiently as possible (e.g. sending to multiple recipients with one request).
   *
   * @return {Promise} An asynchronous Promise that will contain the results of
   * all send operations or will throw an error on failure.
   */
   async send(data, recipients, prefTransports=P2PRouter.supportedTransports.preferred) {
      if (typeof(recipients) != "object") {
         throw (new Error(`"recipients" parameter must be an array!`));
      }
      if (typeof(recipients["length"]) == "number") {
         var transportGroups = this.createTransportGroups(recipients, prefTransports);
      } else {
         transportGroups = this.createTransportGroups(recipients.rcp, prefTransports);
      }
      var results = new Array();
      for (var transportType in transportGroups) {
         try {
            var transportObj = transportGroups[transportType];
            var recipientsArr = transportObj.recipients;
            var transportsArr = transportObj.transports;
            var sendModel = P2PRouter.supportedTransports.sendModel[transportType];
            if (sendModel == "single") {
               for (var count = 0; count < recipientsArr.length; count++) {
                  var recipient = recipientsArr[count];
                  var transport = transportsArr[count];
                  var result = await transport.send(data, [recipient]);
                  results.push(result);
               }
            } else if (sendModel == "multi") {
               //further break down shared transports
               var sharedGroups = this.createSharedTransportGroups(recipientsArr, transportsArr);
               for (count = 0; count < sharedGroups.length; count++) {
                  var sharedRecipients = sharedGroups[count].recipients;
                  var sharedTransport = sharedGroups[count].transport;
                  result = await sharedTransport.send(data, sharedRecipients);
                  results.push(result);
               }
            } else if (sendModel == "none") {
               //try fallback connection as a last resort
               result = await this.rendezvous.send(data, recipients);
               results.push(result);
            }
         } catch (err) {
            console.error (err);
         }
      }
   }

   /**
   * Groups private IDs by their preferred or first available transports.
   *
   * @param {Array} recipients Indexed array of recipient private IDs to group
   * by preferred / available transport.
   * @param {Array} [preferred=P2PRouter.supportedTransports.preferred] Indexed array
   * of preferred transport ordering to use for creating groups.
   * @param {Boolean} [onlyAvail=true] If true, only private IDs with available
   * transports are returned. If false, a special <code>none</code> transport type
   * array is included with all private IDs that have no available transport.
   *
   * @return {Object} Contains child objects accessible via their transport types (names),
   * with each object containing an indexed <code>recipients</code> array and accompanying
   * <code>transports</code> array. A special object named <code>none</code> contains any
   * <code>recipients</code> without available <code>transports</code> (all null),
   * if <code>onlyAvail=false</code>.
   */
   createTransportGroups(recipients, preferred=P2PRouter.supportedTransports.preferred, onlyAvail=true) {
      var groups = new Object();
      if (onlyAvail == false) {
         groups.none = new Array();
         groups.none.recipients = new Array();
         groups.none.transports = new Array();
      }
      for (var count=0; count < recipients.length; count++) {
         var privateID = recipients[count];
         var transportObj = this.getPreferredTransport(privateID, preferred);
         if (transportObj != null) {
            var transport = transportObj.transport;
            var type = transportObj.transportType;
            if (groups[type] == undefined) {
               groups[type] = new Object();
               groups[type].recipients = new Array();
               groups[type].transports = new Array();
            }
            groups[type].recipients.push(privateID);
            groups[type].transports.push(transport);
            this.addGroupsExclusion(privateID, type, groups);
         } else {
            if (onlyAvail == false) {
               groups.none.recipients.push(privateID);
               groups.none.transports.push(null);
               this.addGroupsExclusion(privateID, "none", groups);
            }
         }
      }
      return (groups);
   }

   /**
   * Creates exclusion groups for various transport types for a
   * specific private ID.
   *
   * @param {String} privateID The private ID to exclude from all
   * transport <code>groups</code>, except those specified by the
   * <code>includedTransportType</code>.
   * @param {String} includedTransportType The transport group type
   * within the <code>groups</code> array to <b>not</b> exclude
   * <code>privateID</code> from.
   * @param {Object} groups Named transport groups containing the
   * indexed arrays <code>recipients</code> and <code>transports</code>.
   * Each group will have a new <code>exclude</code> array created, if
   * one doesn't exist, and the <code>privateID</code> appended to it
   * unless the group is in an <code>includedTransportType</code>.
   *
   * @private
   */
   addGroupsExclusion(privateID, includedTransportType, groups) {
      for (var type in groups) {
         if (groups[type].exclude == undefined) {
            groups[type].exclude = new Array();
         }
         if (type != includedTransportType) {
            groups[type].exclude.push(privateID);
         }
      }
   }

   /**
   * Creates arrays of privateIDs based on their shared transports.
   *
   * @param {Array} recipients An indexed array of recipient private IDs. The length
   * of this array <b<must</b> match the length of the <code>transports</code> one.
   * @param {Array} transports References to the transports used by the private IDs
   * of the <code>recipients</code> array.
   *
   * @return {Array} The returned indexed array contains objects, each
   * containing a unique <code>transport</code> reference shared by the private IDs
   * in a <code>recipients</code> array. A null object is returned if the
   * <code>recipients</code> and <code>transports</code> parameter lengths don't match.
   */
   createSharedTransportGroups(recipients, transports, exclusions) {
      if (recipients.length != transports.length) {
         return (null);
      }
      var sharedGroups = new Array();
      var groupExists;
      for (var count=0; count < recipients.length; count++) {
         var privateID = recipients[count];
         var transport = transports[count];
         groupExists = false;
         for (var groupNum=0; groupNum < sharedGroups.length; groupNum++) {
            var groupObj = sharedGroups[groupNum];
            if (groupObj.transport === transport) {
               groupObj.recipients.push(privateID);
               groupExists = true;
            }
         }
         if (groupExists == false) {
            groupObj = new Object();
            groupObj.transport = transport;
            groupObj.recipients = new Array();
            groupObj.recipients.push(privateID);
            sharedGroups.push(groupObj);
         }
      }
      for (count=0; count < recipients.length; count++) {
         privateID = recipients[count];
         for (groupNum=0; groupNum < sharedGroups.length; groupNum++) {
            groupObj = sharedGroups[groupNum];
            this.addSharedGroupsExclusion(privateID, groupObj, sharedGroups);
         }
      }
      return (sharedGroups);
   }

   /**
   * Add exclusions in shared transport groups for a private ID.
   *
   * @param {String} privateID The private ID to add exclusions for.
   * @param {Object} includeGroup The group in which <code>privateID</code>
   * is included. The <code>privateID</code> will be excluded out of all
   * the other <code>sharedGroups</code>.
   * @param {Array} sharedGroups An indexed array of shared transport groups
   * to exclude the <code>privateID</code> from, except the <code>includeGroup</code>.
   * Each object in this array will have an <code>exclude</code> array added, if
   * it doesn't already exist, and the <code>privateID</code> appended if unless
   * included.
   *
   * @private
   */
   addSharedGroupsExclusion(privateID, includeGroup, sharedGroups) {
      for (var count = 0; count < sharedGroups.length; count++) {
         if (sharedGroups[count].exclude == undefined) {
            sharedGroups[count].exclude = new Array();
         }
         if (sharedGroups[count] !== includeGroup) {
            sharedGroups[count].exclude.push(privateID);
         }
      }
   }

   /**
   * Returns the preferred or next available transport for a peer.
   *
   * @param {String} privateID The private ID of the peer for which to
   * retrieve the transport.
   * @param {Array} [preferred=["webrtc","wss","ortc"]] The preferred transport
   * order with the first transport being the most preferred. Any transports
   * not included in this list will not be considered.
   *
   * @return {Object} An object containing a reference to the first preferred
   * <code>transport</code> with an "open" status and its <code>type</code>. If no
   * transport is open then <code>null</code> is returned.
   */
   getPreferredTransport(privateID, preferred=P2PRouter.supportedTransports.preferred) {
      var status = this.peerConnections[privateID].status;
      for (var count=0; count < preferred.length; count++) {
         var transportType = preferred[count];
         if (status[transportType] == "open") {
            var returnObj = new Object();
            returnObj.transport = this.peerConnections[privateID].transport[transportType];
            returnObj.transportType = transportType;
            return (returnObj);
         }
      }
      return (null);
   }

   /**
   * Verifies if a supplied message event object contains a valid P2PRouter message.
   *
   * @param {Event} event The "message" event, as usually dispatched by the
   * peer-to-peer interface, to examine.
   *
   * @return {Boolean} True if the event contains a valid P2PRouter message
   * (though its type may not be supported).
   * @private
   */
   isRouterMsgEvent(event) {
      try {
         if (typeof(event["data"]) != "object") {
            //not sure what this is
            return (false);
         }
         if (typeof(event.data["result"]) != "object") {
            //may not be a JSON-RPC message
            return (false);
         }
         if (typeof(event.data.result["data"]) != "object") {
            //not a router-formatted message
            return (false);
         }
         return (this.isRouterMessage(event.data.result.data));
      } catch (err) {
         return (false);
      }
   }

   /**
   * Verifies if a supplied object is a valid P2PRouter message.
   *
   * @param {Object} message The object to examine.
   *
   * @return {Boolean} True if the object seems to be a valid P2PRouter message
   * (though it may not be supported).
   * @private
   */
   isRouterMessage(message) {
      if ((message["routerMsg"] == undefined) || (message["routerMsg"] == null) || (message["routerMsg"] == "")) {
         //not a P2PRouter message or it's blank (mo message type)
         return (false);
      }
      return (true);
   }

   /**
   * Handles any "message" events received on routed transports. Any P2PRouter messages are intercepted,
   * otherwise "message" events are propagated to onward listeners.
   *
   * @param {Event} event A message event received from a supported transport.
   *
   * @fires P2PRouter#message
   * @private
   */
   async onMessage(event) {
      if (this.isRouterMsgEvent(event)) {
         //TODO: add check for "from" property and add (if being sent via WebRTC, for example)
         var fromPID = event.data.result.from;
         var msgData = event.data.result.data;
         var messageType = msgData.routerMsg;
         var transportType = msgData.transportType;
         switch (messageType) {
            case "peerconnectreq":
               this.onConnectPeerRequest(fromPID, msgData).catch(err => {
                  console.warn(err);
               })
               break;
            default:
               //not a recognized P2PRouter message
               break;
         }
      } else {
         var source = event.target.toString();
         var newEvent = new Event("message");
         if ((source == "WSSClient") || (source == "WSSTunnel")) {
            newEvent.data = event.data;
            newEvent.data.result.transport = "wss";
            newEvent.transport = event.target;
         } else if (source == "WebRTCClient") {
            var jsonObj = buildJSONRPC("notification");
            jsonObj.data = event.data;
            jsonObj.data.result.from = this.getPIDByTransport(event.target);
            jsonObj.data.result.transport = "webrtc";
            newEvent.data = jsonObj.data;
            newEvent.transport = event.target;
         }
         this.dispatchEvent(newEvent);
      }
   }

   /**
   * Handles any "update" message events received on routed transports.
   *
   * @param {Event} event An update message event received from a supported transport.
   *
   * @fires P2PRouter#update
   * @private
   */
   async onUpdate(event) {
      if (this.isRouterMsgEvent(event)) {
         var fromPID = event.data.result.from;
         var msgData = event.data.result.data;
         var messageType = msgData.routerMsg;
         switch (messageType) {
            default:
               //not a recognized P2PRouter update
               break;
         }
      } else {
         var source = event.target.toString();
         var newEvent = new Event("update");
         newEvent.data = event.data;
         newEvent.transport = event.target;
         if ((source == "WSSClient") || (source == "WSSTunnel")) {
            newEvent.transportType = "wss";
         } else if (source == "WebRTCClient") {
            newEvent.transportType = "webrtc";
         }
         this.dispatchEvent(newEvent);
      }
   }

   /**
   * Handles any "peerpid" message events received on routed transports. The
   * [peerConnections]{@link P2PRouter#peerConnections} is automatically
   * updated to reflect the changed private ID.
   *
   * @param {Event} event An update message event received from a supported transport.
   *
   * @fires P2PRouter#peerpid
   * @private
   */
   async onPeerPIDUpdate(event) {
      var oldPID = event.data.result.change.oldPrivateID;
      var newPID = event.data.result.change.newPrivateID;
      if (oldPID == newPID) {
         return;
      }
      if ((this.peerConnections[oldPID] == undefined) || (this.peerConnections[oldPID] == null)) {
         //may ne a duplicated notification
         return;
      }
      //update peerConnections object
      this.peerConnections[newPID] = this.peerConnections[oldPID];
      delete this.peerConnections[oldPID];
      var newEvent = new Event("peerpid");
      newEvent.oldPrivateID = oldPID;
      newEvent.newPrivateID = newPID;
      this.dispatchEvent(newEvent);
   }

   /**
   * Invoked when peer requests a direct peer-to-peer connection.
   *
   * @param {String} privateID The private ID of the peer requesting the connection.
   * @param {Object} requestObj An object containing the details of the connection request.
   * @param {String} requestObj.transportType The transport type on which the direct connection is
   * being requested. The request will be rejected or ignored if the transport doesn't match
   * an available one specified in {@link P2PRouter.supportedTransports}.
   * @param {Number} [requestTimeout=20] The number of seconds to wait for this connection to
   * be established before considering it failed (timed out).
   *
   * @private
   */
   onConnectPeerRequest(privateID, requestObj, requestTimeout=20) {
      var promise = new Promise((resolve, reject) => {
         var transportType = requestObj.transportType;
         switch (transportType) {
            case "webrtc":
               if (this.peerConnections[privateID].options[transportType] == false) {
                  reject (new Error("Peer does not support \""+transportType+"\" transport type."));
                  return (false);
               }
               var wrtcInst = new WebRTCClient(this);
               wrtcInst.addEventListener("peerconnect", this.onPeerConnect, this);
               wrtcInst.addEventListener("message", this.onMessage, this);
               wrtcInst.addEventListener("update", this.onUpdate, this);
               wrtcInst.addEventListener("peerpid", this.onPeerPIDUpdate, this);
               wrtcInst.addEventListener("peerdisconnect", this.onPeerDisconnect, this);
               this.insertPeerConnectionObject(privateID, P2PRouter.supportedTransports.options); //if not exist
               this.peerConnections[privateID].transport[transportType] = wrtcInst;
               this.peerConnections[privateID].connectPromise[transportType].resolve = resolve;
               this.peerConnections[privateID].connectPromise[transportType].reject = reject;
               this.setPeerConnectTimeout(privateID, transportType, reject, requestTimeout);
               this.setConnectionStatus(privateID, "pending", transportType, wrtcInst);
               wrtcInst.setRemoteOffer(privateID, requestObj.offer);
               break;
            case "wss":
               //assume connection already exists so do nothing
               break;
            default:
               this.setConnectionStatus(privateID, "failed", transportType);
               reject (new Error("Unrecognized connection type \""+transportType+"\""));
               break;
         }
      });
      return (promise);
   }

   /**
   * Handles any new peer connection events on routed transports.
   *
   * @param {Event} event A peer connect event received from a supported transport.
   *
   * @fires P2PRouter#peerconnect
   * @private
   */
   async onPeerConnect(event) {
      var source = event.target.toString();
      var newEvent = new Event("peerconnect");
      newEvent.transport = event.target;
      if ((source == "WSSClient") || (source == "WSSTunnel")) {
         console.dir (event.data);
         var privateID = event.data.result.connect;
         console.log ("A new peer has connected on ("+source+"): "+privateID);
         if ((event.data.result.options == undefined) || (event.data.result.options == null)) {
            //added for pre-v0.4.1 compatibility
            event.data.result.options = {"wss":true,"webrtc":false,"ortc":false};
         }
         this.insertPeerConnectionObject(privateID, event.data.result.options);
         this.setConnectionStatus(privateID, "open", "wss", event.target);
         newEvent.data = event.data;
         newEvent.transportType = "wss";
      } else if (source == "WebRTCClient") {
         var privateID = this.getPIDByTransport(event.target);
         this.setConnectionStatus(privateID, "open", "webrtc", event.target);
         newEvent.data = buildJSONRPC("notification");
         newEvent.data.result.connect = privateID;
         newEvent.data.result.options = this.peerConnections[privateID].options;
         newEvent.data.result.type = "session";
         newEvent.transportType = "webrtc";

      }
      this.dispatchEvent(newEvent);
   }

   /**
   * Handles any peer disconnection events on routed transports.
   *
   * @param {Event} event A peer disconnect event received from a supported transport.
   *
   * @fires P2PRouter#peerdisconnect
   * @private
   */
   async onPeerDisconnect(event) {
      var source = event.target.toString();
      var newEvent = new Event("peerdisconnect");
      newEvent.transport = event.target;
      if ((source == "WSSClient") || (source == "WSSTunnel")) {
         var privateID = event.data.result.disconnect;
         this.setConnectionStatus(privateID, "closed", "wss");
         newEvent.data = event.data;
         newEvent.transportType = "wss";
         newEvent._event = event._event;
         this.dispatchEvent(newEvent);
      } else if (source == "WebRTCClient") {
         var privateID = this.getPIDByTransport(event.target);
         this.setConnectionStatus(privateID, "closed", "webrtc");
         newEvent.data = buildJSONRPC("notification");
         newEvent.data.result.disconnect = privateID;
         newEvent.data.result.type = "session";
         newEvent.transportType = "webrtc";
      }
      this.dispatchEvent(newEvent);
   }

   /**
   * Handles a peer connection timeout started by [setPeerConnectTimeout]{@link P2PRouter#setPeerConnectTimeout}.
   *
   * @param {String} privateID The private ID of the peer to whom the connection attempt failed.
   * @param {String} transportType The transport connection type associated with the attempt. This should match
   * one of the {@link P2PRouter.supportedTransports}<code>.options</code>.
   * @param {Function} reject A <code>Promise</code> reject function or callback to invoke.
   * @param {P2PRouter} conext The execution context to use in place of the <code>this</code> reference.
   *
   * @private
   */
   onPeerConnectTimeout(privateID, transportType, reject, context) {
      console.error ("Attempt to establish peer connection using \""+transportType+"\" transport to \""+privateID+"\" has timed out.");
      console.error ("Using fallback transport \""+context.getPreferredTransport(privateID).transportType+"\".");
      context.peerConnections[privateID].connectTimeout[transportType] = null;
      context.setConnectionStatus(privateID, "failed", transportType);
   }

   /**
   * Prepares the instance for destruction by closing any open transports, = null;
   * removing references and event listeners, and otherwise cleaning up.
   *
   * @async
   */
   async destroy() {
         console.log ("P2PRouter.destroy()");
      if (this.rendezvous != null) {
         this.rendezvous.removeEventListener("message", this.onMessage);
         this.rendezvous.removeEventListener("update", this.onUpdate);
         this.rendezvous.removeEventListener("peerconnect", this.onPeerConnect);
         this.rendezvous.removeEventListener("peerpid", this.onPeerPIDUpdate);
         this.rendezvous.removeEventListener("peerdisconnect", this.onPeerDisconnect);
         try {
            var result = await this.rendezvous.disconnect();
         } catch (err) {
         }
         this._rendezvous = null;
      }
      for (var privateID in this.peerConnections) {
         try {
            this.peerConnections[privateID].transport.removeEventListener("peerconnect", this.onPeerConnect);
            this.peerConnections[privateID].transport.removeEventListener("message", this.onMessage);
            this.peerConnections[privateID].transport.removeEventListener("update", this.onUpdate);
            this.peerConnections[privateID].transport.removeEventListener("peerpid", this.onPeerPIDUpdate);
            this.peerConnections[privateID].transport.removeEventListener("peerdisconnect", this.onPeerDisconnect);
            if (this.peerConnections[privateID].connectTimeout[transportType] != null) {
               this.clearTimeout(this.peerConnections[privateID].connectTimeout[transportType]);
               this.peerConnections[privateID].connectTimeout[transportType] = null;
            }
            var result = await this.peerConnections[privateID].transport.disconnect();
         } catch (err) {
         } finally {
            this.peerConnections[privateID].transport = null;
            this.peerConnections[privateID].status = "closed";
         }
      }
   }

   toString() {
      return ("P2PRouter");
   }

}