peer.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. import { EventEmitter } from "eventemitter3";
  2. import { util } from "./util";
  3. import logger, { LogLevel } from "./logger";
  4. import { Socket } from "./socket";
  5. import { MediaConnection } from "./mediaconnection";
  6. import { DataConnection } from "./dataconnection";
  7. import {
  8. ConnectionType,
  9. PeerErrorType,
  10. SocketEventType,
  11. ServerMessageType,
  12. } from "./enums";
  13. import { ServerMessage } from "./servermessage";
  14. import { API } from "./api";
  15. import type {
  16. PeerConnectOption,
  17. PeerJSOption,
  18. CallOption,
  19. } from "./optionInterfaces";
  20. class PeerOptions implements PeerJSOption {
  21. debug?: LogLevel; // 1: Errors, 2: Warnings, 3: All logs
  22. host?: string;
  23. port?: number;
  24. path?: string;
  25. key?: string;
  26. token?: string;
  27. config?: any;
  28. secure?: boolean;
  29. pingInterval?: number;
  30. referrerPolicy?: ReferrerPolicy;
  31. logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
  32. }
  33. type PeerEvents = {
  34. /**
  35. * Emitted when a connection to the PeerServer is established.
  36. */
  37. open: (id: string) => void;
  38. /**
  39. * Emitted when a new data connection is established from a remote peer.
  40. */
  41. connection: (dataConnection: DataConnection) => void;
  42. /**
  43. * Emitted when a remote peer attempts to call you.
  44. */
  45. call: (mediaConnection: MediaConnection) => void;
  46. /**
  47. * Emitted when the peer is destroyed and can no longer accept or create any new connections.
  48. */
  49. close: () => void;
  50. /**
  51. * Emitted when the peer is disconnected from the signalling server
  52. */
  53. disconnected: (currentId: string) => void;
  54. /**
  55. * Errors on the peer are almost always fatal and will destroy the peer.
  56. */
  57. error: (error: Error) => void;
  58. };
  59. /**
  60. * A peer who can initiate connections with other peers.
  61. */
  62. export class Peer extends EventEmitter<PeerEvents> {
  63. private static readonly DEFAULT_KEY = "peerjs";
  64. private readonly _options: PeerOptions;
  65. private readonly _api: API;
  66. private readonly _socket: Socket;
  67. private _id: string | null = null;
  68. private _lastServerId: string | null = null;
  69. // States.
  70. private _destroyed = false; // Connections have been killed
  71. private _disconnected = false; // Connection to PeerServer killed but P2P connections still active
  72. private _open = false; // Sockets and such are not yet open.
  73. private readonly _connections: Map<
  74. string,
  75. (DataConnection | MediaConnection)[]
  76. > = new Map(); // All connections for this peer.
  77. private readonly _lostMessages: Map<string, ServerMessage[]> = new Map(); // src => [list of messages]
  78. /**
  79. * The brokering ID of this peer
  80. */
  81. get id() {
  82. return this._id;
  83. }
  84. get options() {
  85. return this._options;
  86. }
  87. get open() {
  88. return this._open;
  89. }
  90. get socket() {
  91. return this._socket;
  92. }
  93. /**
  94. * A hash of all connections associated with this peer, keyed by the remote peer's ID.
  95. * @deprecated
  96. * Return type will change from Object to Map<string,[]>
  97. */
  98. get connections(): Object {
  99. const plainConnections = Object.create(null);
  100. for (let [k, v] of this._connections) {
  101. plainConnections[k] = v;
  102. }
  103. return plainConnections;
  104. }
  105. /**
  106. * true if this peer and all of its connections can no longer be used.
  107. */
  108. get destroyed() {
  109. return this._destroyed;
  110. }
  111. /**
  112. * false if there is an active connection to the PeerServer.
  113. */
  114. get disconnected() {
  115. return this._disconnected;
  116. }
  117. /**
  118. * A peer can connect to other peers and listen for connections.
  119. */
  120. constructor();
  121. /**
  122. * A peer can connect to other peers and listen for connections.
  123. * @param options for specifying details about PeerServer
  124. */
  125. constructor(options: PeerOptions);
  126. /**
  127. * A peer can connect to other peers and listen for connections.
  128. * @param id Other peers can connect to this peer using the provided ID.
  129. * If no ID is given, one will be generated by the brokering server.
  130. * @param options for specifying details about PeerServer
  131. */
  132. constructor(id: string, options?: PeerOptions);
  133. constructor(id?: string | PeerOptions, options?: PeerOptions) {
  134. super();
  135. let userId: string | undefined;
  136. // Deal with overloading
  137. if (id && id.constructor == Object) {
  138. options = id as PeerOptions;
  139. } else if (id) {
  140. userId = id.toString();
  141. }
  142. // Configurize options
  143. options = {
  144. debug: 0, // 1: Errors, 2: Warnings, 3: All logs
  145. host: util.CLOUD_HOST,
  146. port: util.CLOUD_PORT,
  147. path: "/",
  148. key: Peer.DEFAULT_KEY,
  149. token: util.randomToken(),
  150. config: util.defaultConfig,
  151. referrerPolicy: "strict-origin-when-cross-origin",
  152. ...options,
  153. };
  154. this._options = options;
  155. // Detect relative URL host.
  156. if (this._options.host === "/") {
  157. this._options.host = window.location.hostname;
  158. }
  159. // Set path correctly.
  160. if (this._options.path) {
  161. if (this._options.path[0] !== "/") {
  162. this._options.path = "/" + this._options.path;
  163. }
  164. if (this._options.path[this._options.path.length - 1] !== "/") {
  165. this._options.path += "/";
  166. }
  167. }
  168. // Set whether we use SSL to same as current host
  169. if (
  170. this._options.secure === undefined &&
  171. this._options.host !== util.CLOUD_HOST
  172. ) {
  173. this._options.secure = util.isSecure();
  174. } else if (this._options.host == util.CLOUD_HOST) {
  175. this._options.secure = true;
  176. }
  177. // Set a custom log function if present
  178. if (this._options.logFunction) {
  179. logger.setLogFunction(this._options.logFunction);
  180. }
  181. logger.logLevel = this._options.debug || 0;
  182. this._api = new API(options);
  183. this._socket = this._createServerConnection();
  184. // Sanity checks
  185. // Ensure WebRTC supported
  186. if (!util.supports.audioVideo && !util.supports.data) {
  187. this._delayedAbort(
  188. PeerErrorType.BrowserIncompatible,
  189. "The current browser does not support WebRTC",
  190. );
  191. return;
  192. }
  193. // Ensure alphanumeric id
  194. if (!!userId && !util.validateId(userId)) {
  195. this._delayedAbort(PeerErrorType.InvalidID, `ID "${userId}" is invalid`);
  196. return;
  197. }
  198. if (userId) {
  199. this._initialize(userId);
  200. } else {
  201. this._api
  202. .retrieveId()
  203. .then((id) => this._initialize(id))
  204. .catch((error) => this._abort(PeerErrorType.ServerError, error));
  205. }
  206. }
  207. private _createServerConnection(): Socket {
  208. const socket = new Socket(
  209. this._options.secure,
  210. this._options.host!,
  211. this._options.port!,
  212. this._options.path!,
  213. this._options.key!,
  214. this._options.pingInterval,
  215. );
  216. socket.on(SocketEventType.Message, (data: ServerMessage) => {
  217. this._handleMessage(data);
  218. });
  219. socket.on(SocketEventType.Error, (error: string) => {
  220. this._abort(PeerErrorType.SocketError, error);
  221. });
  222. socket.on(SocketEventType.Disconnected, () => {
  223. if (this.disconnected) {
  224. return;
  225. }
  226. this.emitError(PeerErrorType.Network, "Lost connection to server.");
  227. this.disconnect();
  228. });
  229. socket.on(SocketEventType.Close, () => {
  230. if (this.disconnected) {
  231. return;
  232. }
  233. this._abort(
  234. PeerErrorType.SocketClosed,
  235. "Underlying socket is already closed.",
  236. );
  237. });
  238. return socket;
  239. }
  240. /** Initialize a connection with the server. */
  241. private _initialize(id: string): void {
  242. this._id = id;
  243. this.socket.start(id, this._options.token!);
  244. }
  245. /** Handles messages from the server. */
  246. private _handleMessage(message: ServerMessage): void {
  247. const type = message.type;
  248. const payload = message.payload;
  249. const peerId = message.src;
  250. switch (type) {
  251. case ServerMessageType.Open: // The connection to the server is open.
  252. this._lastServerId = this.id;
  253. this._open = true;
  254. this.emit("open", this.id);
  255. break;
  256. case ServerMessageType.Error: // Server error.
  257. this._abort(PeerErrorType.ServerError, payload.msg);
  258. break;
  259. case ServerMessageType.IdTaken: // The selected ID is taken.
  260. this._abort(PeerErrorType.UnavailableID, `ID "${this.id}" is taken`);
  261. break;
  262. case ServerMessageType.InvalidKey: // The given API key cannot be found.
  263. this._abort(
  264. PeerErrorType.InvalidKey,
  265. `API KEY "${this._options.key}" is invalid`,
  266. );
  267. break;
  268. case ServerMessageType.Leave: // Another peer has closed its connection to this peer.
  269. logger.log(`Received leave message from ${peerId}`);
  270. this._cleanupPeer(peerId);
  271. this._connections.delete(peerId);
  272. break;
  273. case ServerMessageType.Expire: // The offer sent to a peer has expired without response.
  274. this.emitError(
  275. PeerErrorType.PeerUnavailable,
  276. `Could not connect to peer ${peerId}`,
  277. );
  278. break;
  279. case ServerMessageType.Offer: {
  280. // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
  281. const connectionId = payload.connectionId;
  282. let connection = this.getConnection(peerId, connectionId);
  283. if (connection) {
  284. connection.close();
  285. logger.warn(
  286. `Offer received for existing Connection ID:${connectionId}`,
  287. );
  288. }
  289. // Create a new connection.
  290. if (payload.type === ConnectionType.Media) {
  291. const mediaConnection = new MediaConnection(peerId, this, {
  292. connectionId: connectionId,
  293. _payload: payload,
  294. metadata: payload.metadata,
  295. });
  296. connection = mediaConnection;
  297. this._addConnection(peerId, connection);
  298. this.emit("call", mediaConnection);
  299. } else if (payload.type === ConnectionType.Data) {
  300. const dataConnection = new DataConnection(peerId, this, {
  301. connectionId: connectionId,
  302. _payload: payload,
  303. metadata: payload.metadata,
  304. label: payload.label,
  305. serialization: payload.serialization,
  306. reliable: payload.reliable,
  307. });
  308. connection = dataConnection;
  309. this._addConnection(peerId, connection);
  310. this.emit("connection", dataConnection);
  311. } else {
  312. logger.warn(`Received malformed connection type:${payload.type}`);
  313. return;
  314. }
  315. // Find messages.
  316. const messages = this._getMessages(connectionId);
  317. for (let message of messages) {
  318. connection.handleMessage(message);
  319. }
  320. break;
  321. }
  322. default: {
  323. if (!payload) {
  324. logger.warn(
  325. `You received a malformed message from ${peerId} of type ${type}`,
  326. );
  327. return;
  328. }
  329. const connectionId = payload.connectionId;
  330. const connection = this.getConnection(peerId, connectionId);
  331. if (connection && connection.peerConnection) {
  332. // Pass it on.
  333. connection.handleMessage(message);
  334. } else if (connectionId) {
  335. // Store for possible later use
  336. this._storeMessage(connectionId, message);
  337. } else {
  338. logger.warn("You received an unrecognized message:", message);
  339. }
  340. break;
  341. }
  342. }
  343. }
  344. /** Stores messages without a set up connection, to be claimed later. */
  345. private _storeMessage(connectionId: string, message: ServerMessage): void {
  346. if (!this._lostMessages.has(connectionId)) {
  347. this._lostMessages.set(connectionId, []);
  348. }
  349. this._lostMessages.get(connectionId).push(message);
  350. }
  351. /** Retrieve messages from lost message store */
  352. //TODO Change it to private
  353. public _getMessages(connectionId: string): ServerMessage[] {
  354. const messages = this._lostMessages.get(connectionId);
  355. if (messages) {
  356. this._lostMessages.delete(connectionId);
  357. return messages;
  358. }
  359. return [];
  360. }
  361. /**
  362. * Connects to the remote peer specified by id and returns a data connection.
  363. * @param peer The brokering ID of the remote peer (their peer.id).
  364. * @param options for specifying details about Peer Connection
  365. */
  366. connect(peer: string, options: PeerConnectOption = {}): DataConnection {
  367. if (this.disconnected) {
  368. logger.warn(
  369. "You cannot connect to a new Peer because you called " +
  370. ".disconnect() on this Peer and ended your connection with the " +
  371. "server. You can create a new Peer to reconnect, or call reconnect " +
  372. "on this peer if you believe its ID to still be available.",
  373. );
  374. this.emitError(
  375. PeerErrorType.Disconnected,
  376. "Cannot connect to new Peer after disconnecting from server.",
  377. );
  378. return;
  379. }
  380. const dataConnection = new DataConnection(peer, this, options);
  381. this._addConnection(peer, dataConnection);
  382. return dataConnection;
  383. }
  384. /**
  385. * Calls the remote peer specified by id and returns a media connection.
  386. * @param peer The brokering ID of the remote peer (their peer.id).
  387. * @param stream The caller's media stream
  388. * @param options Metadata associated with the connection, passed in by whoever initiated the connection.
  389. */
  390. call(
  391. peer: string,
  392. stream: MediaStream,
  393. options: CallOption = {},
  394. ): MediaConnection {
  395. if (this.disconnected) {
  396. logger.warn(
  397. "You cannot connect to a new Peer because you called " +
  398. ".disconnect() on this Peer and ended your connection with the " +
  399. "server. You can create a new Peer to reconnect.",
  400. );
  401. this.emitError(
  402. PeerErrorType.Disconnected,
  403. "Cannot connect to new Peer after disconnecting from server.",
  404. );
  405. return;
  406. }
  407. if (!stream) {
  408. logger.error(
  409. "To call a peer, you must provide a stream from your browser's `getUserMedia`.",
  410. );
  411. return;
  412. }
  413. const mediaConnection = new MediaConnection(peer, this, {
  414. ...options,
  415. _stream: stream,
  416. });
  417. this._addConnection(peer, mediaConnection);
  418. return mediaConnection;
  419. }
  420. /** Add a data/media connection to this peer. */
  421. private _addConnection(
  422. peerId: string,
  423. connection: MediaConnection | DataConnection,
  424. ): void {
  425. logger.log(
  426. `add connection ${connection.type}:${connection.connectionId} to peerId:${peerId}`,
  427. );
  428. if (!this._connections.has(peerId)) {
  429. this._connections.set(peerId, []);
  430. }
  431. this._connections.get(peerId).push(connection);
  432. }
  433. //TODO should be private
  434. _removeConnection(connection: DataConnection | MediaConnection): void {
  435. const connections = this._connections.get(connection.peer);
  436. if (connections) {
  437. const index = connections.indexOf(connection);
  438. if (index !== -1) {
  439. connections.splice(index, 1);
  440. }
  441. }
  442. //remove from lost messages
  443. this._lostMessages.delete(connection.connectionId);
  444. }
  445. /** Retrieve a data/media connection for this peer. */
  446. getConnection(
  447. peerId: string,
  448. connectionId: string,
  449. ): null | DataConnection | MediaConnection {
  450. const connections = this._connections.get(peerId);
  451. if (!connections) {
  452. return null;
  453. }
  454. for (let connection of connections) {
  455. if (connection.connectionId === connectionId) {
  456. return connection;
  457. }
  458. }
  459. return null;
  460. }
  461. private _delayedAbort(type: PeerErrorType, message: string | Error): void {
  462. setTimeout(() => {
  463. this._abort(type, message);
  464. }, 0);
  465. }
  466. /**
  467. * Emits an error message and destroys the Peer.
  468. * The Peer is not destroyed if it's in a disconnected state, in which case
  469. * it retains its disconnected state and its existing connections.
  470. */
  471. private _abort(type: PeerErrorType, message: string | Error): void {
  472. logger.error("Aborting!");
  473. this.emitError(type, message);
  474. if (!this._lastServerId) {
  475. this.destroy();
  476. } else {
  477. this.disconnect();
  478. }
  479. }
  480. /** Emits a typed error message. */
  481. emitError(type: PeerErrorType, err: string | Error): void {
  482. logger.error("Error:", err);
  483. let error: Error & { type?: PeerErrorType };
  484. if (typeof err === "string") {
  485. error = new Error(err);
  486. } else {
  487. error = err as Error;
  488. }
  489. error.type = type;
  490. this.emit("error", error);
  491. }
  492. /**
  493. * Destroys the Peer: closes all active connections as well as the connection
  494. * to the server.
  495. * Warning: The peer can no longer create or accept connections after being
  496. * destroyed.
  497. */
  498. destroy(): void {
  499. if (this.destroyed) {
  500. return;
  501. }
  502. logger.log(`Destroy peer with ID:${this.id}`);
  503. this.disconnect();
  504. this._cleanup();
  505. this._destroyed = true;
  506. this.emit("close");
  507. }
  508. /** Disconnects every connection on this peer. */
  509. private _cleanup(): void {
  510. for (let peerId of this._connections.keys()) {
  511. this._cleanupPeer(peerId);
  512. this._connections.delete(peerId);
  513. }
  514. this.socket.removeAllListeners();
  515. }
  516. /** Closes all connections to this peer. */
  517. private _cleanupPeer(peerId: string): void {
  518. const connections = this._connections.get(peerId);
  519. if (!connections) return;
  520. for (let connection of connections) {
  521. connection.close();
  522. }
  523. }
  524. /**
  525. * Disconnects the Peer's connection to the PeerServer. Does not close any
  526. * active connections.
  527. * Warning: The peer can no longer create or accept connections after being
  528. * disconnected. It also cannot reconnect to the server.
  529. */
  530. disconnect(): void {
  531. if (this.disconnected) {
  532. return;
  533. }
  534. const currentId = this.id;
  535. logger.log(`Disconnect peer with ID:${currentId}`);
  536. this._disconnected = true;
  537. this._open = false;
  538. this.socket.close();
  539. this._lastServerId = currentId;
  540. this._id = null;
  541. this.emit("disconnected", currentId);
  542. }
  543. /** Attempts to reconnect with the same ID. */
  544. reconnect(): void {
  545. if (this.disconnected && !this.destroyed) {
  546. logger.log(
  547. `Attempting reconnection to server with ID ${this._lastServerId}`,
  548. );
  549. this._disconnected = false;
  550. this._initialize(this._lastServerId!);
  551. } else if (this.destroyed) {
  552. throw new Error(
  553. "This peer cannot reconnect to the server. It has already been destroyed.",
  554. );
  555. } else if (!this.disconnected && !this.open) {
  556. // Do nothing. We're still connecting the first time.
  557. logger.error(
  558. "In a hurry? We're still trying to make the initial connection!",
  559. );
  560. } else {
  561. throw new Error(
  562. `Peer ${this.id} cannot reconnect because it is not disconnected from the server!`,
  563. );
  564. }
  565. }
  566. /**
  567. * Get a list of available peer IDs. If you're running your own server, you'll
  568. * want to set allow_discovery: true in the PeerServer options. If you're using
  569. * the cloud server, email team@peerjs.com to get the functionality enabled for
  570. * your key.
  571. */
  572. listAllPeers(cb = (_: any[]) => {}): void {
  573. this._api
  574. .listAllPeers()
  575. .then((peers) => cb(peers))
  576. .catch((error) => this._abort(PeerErrorType.ServerError, error));
  577. }
  578. }