+
+
+
+
+
+
diff --git a/e2e/peer/peer.spec.ts b/e2e/peer/peer.spec.ts
index 7545a23ce..570da5a81 100644
--- a/e2e/peer/peer.spec.ts
+++ b/e2e/peer/peer.spec.ts
@@ -17,4 +17,21 @@ describe("Peer", () => {
await P.waitForMessage('{"type":"disconnected"}');
expect(await P.errorMessage.getText()).toBe("");
});
+ it("should emit an error, when the remote peer is unavailable", async () => {
+ await P.open("peer-unavailable");
+ await P.waitForMessage('{"type":"peer-unavailable"}');
+ expect(await P.errorMessage.getText()).toBe('{"type":"peer-unavailable"}');
+ });
+});
+describe("Peer:async", () => {
+ it("should emit an error, when the ID is already taken", async () => {
+ await P.open("id-taken.await");
+ await P.waitForMessage("No ID takeover");
+ expect(await P.errorMessage.getText()).toBe("");
+ });
+ it("should emit an error, when the remote peer is unavailable", async () => {
+ await P.open("peer-unavailable.async");
+ await P.waitForMessage("Success: Peer unavailable");
+ expect(await P.errorMessage.getText()).toBe("");
+ });
});
diff --git a/lib/baseconnection.ts b/lib/baseconnection.ts
index 8c0c18402..3e500d72b 100644
--- a/lib/baseconnection.ts
+++ b/lib/baseconnection.ts
@@ -2,16 +2,13 @@ import type { Peer } from "./peer";
import type { ServerMessage } from "./servermessage";
import type { ConnectionType } from "./enums";
import { BaseConnectionErrorType } from "./enums";
-import {
- EventEmitterWithError,
- type EventsWithError,
- PeerError,
-} from "./peerError";
-import type { ValidEventTypes } from "eventemitter3";
+import { PeerError, type PromiseEvents } from "./peerError";
+import EventEmitter from "eventemitter3";
+import { EventEmitterWithPromise } from "./eventEmitterWithPromise";
export interface BaseConnectionEvents<
ErrorType extends string = BaseConnectionErrorType,
-> extends EventsWithError {
+> extends PromiseEvents {
/**
* Emitted when either you or the remote peer closes the connection.
*
@@ -29,13 +26,42 @@ export interface BaseConnectionEvents<
iceStateChanged: (state: RTCIceConnectionState) => void;
}
-export abstract class BaseConnection<
- SubClassEvents extends ValidEventTypes,
+export interface IBaseConnection<
+ SubClassEvents extends BaseConnectionEvents<
+ BaseConnectionErrorType | ErrorType
+ >,
ErrorType extends string = never,
-> extends EventEmitterWithError<
- ErrorType | BaseConnectionErrorType,
- SubClassEvents & BaseConnectionEvents
-> {
+> extends EventEmitter {
+ readonly metadata: any;
+ readonly connectionId: string;
+ get type(): ConnectionType;
+ /**
+ * The optional label passed in or assigned by PeerJS when the connection was initiated.
+ */
+ label: string;
+ /**
+ * Whether the media connection is active (e.g. your call has been answered).
+ * You can check this if you want to set a maximum wait time for a one-sided call.
+ */
+ get open(): boolean;
+ close(): void;
+}
+
+export abstract class BaseConnection<
+ AwaitType extends EventEmitter,
+ SubClassEvents extends BaseConnectionEvents<
+ BaseConnectionErrorType | ErrorType
+ >,
+ ErrorType extends string = never,
+ >
+ extends EventEmitterWithPromise<
+ AwaitType,
+ never,
+ ErrorType | BaseConnectionErrorType,
+ SubClassEvents
+ >
+ implements IBaseConnection
+{
protected _open = false;
/**
@@ -50,15 +76,8 @@ export abstract class BaseConnection<
abstract get type(): ConnectionType;
- /**
- * The optional label passed in or assigned by PeerJS when the connection was initiated.
- */
label: string;
- /**
- * Whether the media connection is active (e.g. your call has been answered).
- * You can check this if you want to set a maximum wait time for a one-sided call.
- */
get open() {
return this._open;
}
diff --git a/lib/dataconnection/DataConnection.ts b/lib/dataconnection/DataConnection.ts
index 863a37598..780e009c4 100644
--- a/lib/dataconnection/DataConnection.ts
+++ b/lib/dataconnection/DataConnection.ts
@@ -7,14 +7,18 @@ import {
ServerMessageType,
} from "../enums";
import type { Peer } from "../peer";
-import { BaseConnection, type BaseConnectionEvents } from "../baseconnection";
+import {
+ BaseConnection,
+ type BaseConnectionEvents,
+ IBaseConnection,
+} from "../baseconnection";
import type { ServerMessage } from "../servermessage";
-import type { EventsWithError } from "../peerError";
import { randomToken } from "../utils/randomToken";
export interface DataConnectionEvents
- extends EventsWithError,
- BaseConnectionEvents {
+ extends BaseConnectionEvents<
+ DataConnectionErrorType | BaseConnectionErrorType
+ > {
/**
* Emitted when data is received from the remote peer.
*/
@@ -25,21 +29,35 @@ export interface DataConnectionEvents
open: () => void;
}
+export interface IDataConnection
+ extends IBaseConnection {
+ get type(): ConnectionType.Data;
+ /** Allows user to close connection. */
+ close(options?: { flush?: boolean }): void;
+ /** Allows user to send data. */
+ send(data: any, chunked?: boolean): void;
+}
+
/**
* Wraps a DataChannel between two Peers.
*/
export abstract class DataConnection extends BaseConnection<
+ IDataConnection,
DataConnectionEvents,
DataConnectionErrorType
> {
protected static readonly ID_PREFIX = "dc_";
protected static readonly MAX_BUFFERED_AMOUNT = 8 * 1024 * 1024;
- private _negotiator: Negotiator;
+ private _negotiator: Negotiator<
+ DataConnectionEvents,
+ DataConnectionErrorType,
+ this
+ >;
abstract readonly serialization: string;
readonly reliable: boolean;
- public get type() {
+ public get type(): ConnectionType.Data {
return ConnectionType.Data;
}
@@ -87,7 +105,6 @@ export abstract class DataConnection extends BaseConnection<
* Exposed functionality for users.
*/
- /** Allows user to close connection. */
close(options?: { flush?: boolean }): void {
if (options?.flush) {
this.send({
@@ -126,7 +143,6 @@ export abstract class DataConnection extends BaseConnection<
protected abstract _send(data: any, chunked: boolean): void;
- /** Allows user to send data. */
public send(data: any, chunked = false) {
if (!this.open) {
this.emitError(
diff --git a/lib/enums.ts b/lib/enums.ts
index 0394f90aa..ee19d0cec 100644
--- a/lib/enums.ts
+++ b/lib/enums.ts
@@ -61,6 +61,7 @@ export enum PeerErrorType {
}
export enum BaseConnectionErrorType {
+ PeerUnavailable = "peer-unavailable",
NegotiationFailed = "negotiation-failed",
ConnectionClosed = "connection-closed",
}
diff --git a/lib/eventEmitterWithPromise.ts b/lib/eventEmitterWithPromise.ts
new file mode 100644
index 000000000..ce4cc66f6
--- /dev/null
+++ b/lib/eventEmitterWithPromise.ts
@@ -0,0 +1,88 @@
+import EventEmitter from "eventemitter3";
+import logger from "./logger";
+import { PeerError, PromiseEvents } from "./peerError";
+
+export class EventEmitterWithPromise<
+ AwaitType extends EventEmitter,
+ OpenType,
+ ErrorType extends string,
+ Events extends PromiseEvents,
+ >
+ extends EventEmitter
+ implements Promise
+{
+ protected _open = false;
+ readonly [Symbol.toStringTag]: string;
+
+ catch(
+ onrejected?:
+ | ((reason: PeerError<`${ErrorType}`>) => PromiseLike | TResult)
+ | undefined
+ | null,
+ ): Promise {
+ return this.then(undefined, onrejected);
+ }
+
+ finally(onfinally?: (() => void) | undefined | null): Promise {
+ return this.then().finally(onfinally);
+ }
+
+ then(
+ onfulfilled?:
+ | ((value: AwaitType) => PromiseLike | TResult1)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: any) => PromiseLike | TResult2)
+ | undefined
+ | null,
+ ): Promise {
+ const p = new Promise((resolve, reject) => {
+ const onOpen = () => {
+ // @ts-expect-error
+ this.off("error", onError);
+ // Remove 'then' to prevent potential recursion issues
+ // `await` will wait for a Promise-like to resolve recursively
+ resolve?.(proxyWithoutThen(this));
+ };
+ const onError = (err: PeerError<`${ErrorType}`>) => {
+ // @ts-expect-error
+ this.off("open", onOpen);
+ reject(err);
+ };
+ if (this._open) {
+ onOpen();
+ return;
+ }
+
+ // @ts-expect-error
+ this.once("open", onOpen);
+ // @ts-expect-error
+ this.once("error", onError);
+ });
+ return p.then(onfulfilled, onrejected);
+ }
+
+ /**
+ * Emits a typed error message.
+ *
+ * @internal
+ */
+ emitError(type: ErrorType, err: string | Error): void {
+ logger.error("Error:", err);
+
+ // @ts-expect-error
+ this.emit("error", new PeerError<`${ErrorType}`>(`${type}`, err));
+ }
+}
+
+function proxyWithoutThen(obj: T): Omit {
+ return new Proxy(obj, {
+ get(target, p, receiver) {
+ if (p === "then") {
+ return undefined;
+ }
+ return Reflect.get(target, p, receiver);
+ },
+ });
+}
diff --git a/lib/mediaconnection.ts b/lib/mediaconnection.ts
index a138a464c..7c318b4c3 100644
--- a/lib/mediaconnection.ts
+++ b/lib/mediaconnection.ts
@@ -3,11 +3,15 @@ import logger from "./logger";
import { Negotiator } from "./negotiator";
import { ConnectionType, ServerMessageType } from "./enums";
import type { Peer } from "./peer";
-import { BaseConnection, type BaseConnectionEvents } from "./baseconnection";
+import {
+ BaseConnection,
+ type BaseConnectionEvents,
+ IBaseConnection,
+} from "./baseconnection";
import type { ServerMessage } from "./servermessage";
import type { AnswerOption } from "./optionInterfaces";
-export interface MediaConnectionEvents extends BaseConnectionEvents {
+export interface MediaConnectionEvents extends BaseConnectionEvents {
/**
* Emitted when a connection to the PeerServer is established.
*
@@ -24,22 +28,46 @@ export interface MediaConnectionEvents extends BaseConnectionEvents {
willCloseOnRemote: () => void;
}
+export interface IMediaConnection
+ extends IBaseConnection {
+ get type(): ConnectionType.Media;
+ get localStream(): MediaStream;
+ get remoteStream(): MediaStream;
+ /**
+ * When receiving a {@apilink PeerEvents | `call`} event on a peer, you can call
+ * `answer` on the media connection provided by the callback to accept the call
+ * and optionally send your own media stream.
+
+ *
+ * @param stream A WebRTC media stream.
+ * @param options
+ * @returns
+ */
+ answer(stream?: MediaStream, options?: AnswerOption): void;
+
+ /**
+ * Closes the media connection.
+ */
+ close(): void;
+}
/**
* Wraps WebRTC's media streams.
* To get one, use {@apilink Peer.call} or listen for the {@apilink PeerEvents | `call`} event.
*/
-export class MediaConnection extends BaseConnection {
+export class MediaConnection extends BaseConnection<
+ IMediaConnection,
+ MediaConnectionEvents
+> {
private static readonly ID_PREFIX = "mc_";
- readonly label: string;
- private _negotiator: Negotiator;
+ private _negotiator: Negotiator;
private _localStream: MediaStream;
private _remoteStream: MediaStream;
/**
* For media connections, this is always 'media'.
*/
- get type() {
+ get type(): ConnectionType.Media {
return ConnectionType.Media;
}
@@ -112,16 +140,6 @@ export class MediaConnection extends BaseConnection {
}
}
- /**
- * When receiving a {@apilink PeerEvents | `call`} event on a peer, you can call
- * `answer` on the media connection provided by the callback to accept the call
- * and optionally send your own media stream.
-
- *
- * @param stream A WebRTC media stream.
- * @param options
- * @returns
- */
answer(stream?: MediaStream, options: AnswerOption = {}): void {
if (this._localStream) {
logger.warn(
@@ -150,13 +168,6 @@ export class MediaConnection extends BaseConnection {
this._open = true;
}
- /**
- * Exposed functionality for users.
- */
-
- /**
- * Closes the media connection.
- */
close(): void {
if (this._negotiator) {
this._negotiator.cleanup();
diff --git a/lib/negotiator.ts b/lib/negotiator.ts
index 6f5f46215..a55af35b8 100644
--- a/lib/negotiator.ts
+++ b/lib/negotiator.ts
@@ -8,14 +8,18 @@ import {
ServerMessageType,
} from "./enums";
import type { BaseConnection, BaseConnectionEvents } from "./baseconnection";
-import type { ValidEventTypes } from "eventemitter3";
/**
* Manages all negotiations between Peers.
*/
export class Negotiator<
- Events extends ValidEventTypes,
- ConnectionType extends BaseConnection,
+ Events extends BaseConnectionEvents,
+ ErrorType extends string,
+ ConnectionType extends BaseConnection<
+ any,
+ Events | BaseConnectionEvents,
+ ErrorType
+ >,
> {
constructor(readonly connection: ConnectionType) {}
diff --git a/lib/peer.ts b/lib/peer.ts
index 6fa306cbc..da0a81ce9 100644
--- a/lib/peer.ts
+++ b/lib/peer.ts
@@ -4,6 +4,7 @@ import { Socket } from "./socket";
import { MediaConnection } from "./mediaconnection";
import type { DataConnection } from "./dataconnection/DataConnection";
import {
+ BaseConnectionErrorType,
ConnectionType,
PeerErrorType,
ServerMessageType,
@@ -20,7 +21,9 @@ import { BinaryPack } from "./dataconnection/BufferedConnection/BinaryPack";
import { Raw } from "./dataconnection/BufferedConnection/Raw";
import { Json } from "./dataconnection/BufferedConnection/Json";
-import { EventEmitterWithError, PeerError } from "./peerError";
+import { PeerError, PromiseEvents } from "./peerError";
+import { EventEmitterWithPromise } from "./eventEmitterWithPromise";
+import EventEmitter from "eventemitter3";
class PeerOptions implements PeerJSOption {
/**
@@ -77,7 +80,7 @@ export interface SerializerMapping {
) => DataConnection;
}
-export interface PeerEvents {
+export interface PeerEvents extends PromiseEvents {
/**
* Emitted when a connection to the PeerServer is established.
*
@@ -107,10 +110,87 @@ export interface PeerEvents {
*/
error: (error: PeerError<`${PeerErrorType}`>) => void;
}
+
+export interface IPeer extends EventEmitter {
+ /**
+ * The brokering ID of this peer
+ *
+ * If no ID was specified in {@apilink Peer | the constructor},
+ * this will be `undefined` until the {@apilink PeerEvents | `open`} event is emitted.
+ */
+ get id(): string;
+ get open(): boolean;
+ /**
+ * A hash of all connections associated with this peer, keyed by the remote peer's ID.
+ * @deprecated
+ * Return type will change from Object to Map
+ */
+ get connections(): Object;
+ /**
+ * true if this peer and all of its connections can no longer be used.
+ */
+ get destroyed(): boolean;
+ /**
+ * Connects to the remote peer specified by id and returns a data connection.
+ *
+ * Make sure to listen to the `error` event of the resulting {@link DataConnection}
+ * in case the connection fails.
+ *
+ * @param peer The brokering ID of the remote peer (their {@link Peer.id}).
+ * @param options for specifying details about Peer Connection
+ */
+ connect(peer: string, options?: PeerConnectOption): DataConnection;
+ /**
+ * Calls the remote peer specified by id and returns a media connection.
+ * @param peer The brokering ID of the remote peer (their peer.id).
+ * @param stream The caller's media stream
+ * @param options Metadata associated with the connection, passed in by whoever initiated the connection.
+ */
+ call(
+ peer: string,
+ stream: MediaStream,
+ options?: CallOption,
+ ): MediaConnection;
+ /** Retrieve a data/media connection for this peer. */
+ getConnection(
+ peerId: string,
+ connectionId: string,
+ ): null | DataConnection | MediaConnection;
+ /**
+ * Destroys the Peer: closes all active connections as well as the connection
+ * to the server.
+ *
+ * :::caution
+ * This cannot be undone; the respective peer object will no longer be able
+ * to create or receive any connections, its ID will be forfeited on the server,
+ * and all of its data and media connections will be closed.
+ * :::
+ */
+ destroy(): void;
+ /**
+ * Disconnects the Peer's connection to the PeerServer. Does not close any
+ * active connections.
+ * Warning: The peer can no longer create or accept connections after being
+ * disconnected. It also cannot reconnect to the server.
+ */
+ disconnect(): void;
+ /** Attempts to reconnect with the same ID.
+ *
+ * Only {@apilink Peer.disconnect | disconnected peers} 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.
+ */
+ reconnect(): void;
+}
+
/**
* A peer who can initiate connections with other peers.
*/
-export class Peer extends EventEmitterWithError {
+export class Peer
+ extends EventEmitterWithPromise
+ implements IPeer
+{
private static readonly DEFAULT_KEY = "peerjs";
protected readonly _serializers: SerializerMapping = {
@@ -131,18 +211,12 @@ export class Peer extends EventEmitterWithError {
// States.
private _destroyed = false; // Connections have been killed
private _disconnected = false; // Connection to PeerServer killed but P2P connections still active
- private _open = false; // Sockets and such are not yet open.
private readonly _connections: Map<
string,
(DataConnection | MediaConnection)[]
> = new Map(); // All connections for this peer.
private readonly _lostMessages: Map = new Map(); // src => [list of messages]
- /**
- * The brokering ID of this peer
- *
- * If no ID was specified in {@apilink Peer | the constructor},
- * this will be `undefined` until the {@apilink PeerEvents | `open`} event is emitted.
- */
+
get id() {
return this._id;
}
@@ -162,11 +236,6 @@ export class Peer extends EventEmitterWithError {
return this._socket;
}
- /**
- * A hash of all connections associated with this peer, keyed by the remote peer's ID.
- * @deprecated
- * Return type will change from Object to Map
- */
get connections(): Object {
const plainConnections = Object.create(null);
@@ -177,15 +246,9 @@ export class Peer extends EventEmitterWithError {
return plainConnections;
}
- /**
- * true if this peer and all of its connections can no longer be used.
- */
get destroyed() {
return this._destroyed;
}
- /**
- * false if there is an active connection to the PeerServer.
- */
get disconnected() {
return this._disconnected;
}
@@ -379,6 +442,16 @@ export class Peer extends EventEmitterWithError {
PeerErrorType.PeerUnavailable,
`Could not connect to peer ${peerId}`,
);
+ // Emit an error on all connections with this peer.
+ const connections = (this._connections.get(peerId) ?? []).filter(
+ (c) => c.peer === peerId,
+ );
+ for (const conn of connections) {
+ conn.emitError(
+ BaseConnectionErrorType.PeerUnavailable,
+ `${peerId} is unavailable`,
+ );
+ }
break;
case ServerMessageType.Offer: {
// we should consider switching this to CALL/CONNECT, but this is the least breaking option.
@@ -482,11 +555,6 @@ export class Peer extends EventEmitterWithError {
return [];
}
- /**
- * Connects to the remote peer specified by id and returns a data connection.
- * @param peer The brokering ID of the remote peer (their {@apilink Peer.id}).
- * @param options for specifying details about Peer Connection
- */
connect(peer: string, options: PeerConnectOption = {}): DataConnection {
options = {
serialization: "default",
@@ -512,15 +580,10 @@ export class Peer extends EventEmitterWithError {
options,
);
this._addConnection(peer, dataConnection);
+
return dataConnection;
}
- /**
- * Calls the remote peer specified by id and returns a media connection.
- * @param peer The brokering ID of the remote peer (their peer.id).
- * @param stream The caller's media stream
- * @param options Metadata associated with the connection, passed in by whoever initiated the connection.
- */
call(
peer: string,
stream: MediaStream,
@@ -585,7 +648,6 @@ export class Peer extends EventEmitterWithError {
this._lostMessages.delete(connection.connectionId);
}
- /** Retrieve a data/media connection for this peer. */
getConnection(
peerId: string,
connectionId: string,
@@ -627,16 +689,6 @@ export class Peer extends EventEmitterWithError {
}
}
- /**
- * Destroys the Peer: closes all active connections as well as the connection
- * to the server.
- *
- * :::caution
- * This cannot be undone; the respective peer object will no longer be able
- * to create or receive any connections, its ID will be forfeited on the server,
- * and all of its data and media connections will be closed.
- * :::
- */
destroy(): void {
if (this.destroyed) {
return;
@@ -673,12 +725,6 @@ export class Peer extends EventEmitterWithError {
}
}
- /**
- * Disconnects the Peer's connection to the PeerServer. Does not close any
- * active connections.
- * Warning: The peer can no longer create or accept connections after being
- * disconnected. It also cannot reconnect to the server.
- */
disconnect(): void {
if (this.disconnected) {
return;
@@ -699,13 +745,6 @@ export class Peer extends EventEmitterWithError {
this.emit("disconnected", currentId);
}
- /** Attempts to reconnect with the same ID.
- *
- * Only {@apilink Peer.disconnect | disconnected peers} 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.
- */
reconnect(): void {
if (this.disconnected && !this.destroyed) {
logger.log(
diff --git a/lib/peerError.ts b/lib/peerError.ts
index c174d0c1f..b86c0cc79 100644
--- a/lib/peerError.ts
+++ b/lib/peerError.ts
@@ -1,26 +1,8 @@
-import { EventEmitter } from "eventemitter3";
-import logger from "./logger";
-
-export interface EventsWithError {
+export interface PromiseEvents {
+ open: (open?: OpenType) => void;
error: (error: PeerError<`${ErrorType}`>) => void;
}
-export class EventEmitterWithError<
- ErrorType extends string,
- Events extends EventsWithError,
-> extends EventEmitter {
- /**
- * Emits a typed error message.
- *
- * @internal
- */
- emitError(type: ErrorType, err: string | Error): void {
- logger.error("Error:", err);
-
- // @ts-ignore
- this.emit("error", new PeerError<`${ErrorType}`>(`${type}`, err));
- }
-}
/**
* A PeerError is emitted whenever an error occurs.
* It always has a `.type`, which can be used to identify the error.
diff --git a/tsconfig.json b/tsconfig.json
index 7fabb216b..78b252c1d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,7 @@
"compilerOptions": {
"target": "es5",
"module": "commonjs",
+ "esModuleInterop": true,
"downlevelIteration": true,
"noUnusedLocals": true,
"noUnusedParameters": true,