Selaa lähdekoodia

refactor: `implement Promise` for all PeerJS objects

`Proxy` instead of patching `this`
Jonas Gloning 1 vuosi sitten
vanhempi
commit
54e2ac63ca

+ 40 - 18
lib/baseconnection.ts

@@ -2,16 +2,14 @@ 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 { PeerError, type PromiseEvents } from "./peerError";
 import type { ValidEventTypes } from "eventemitter3";
+import EventEmitter from "eventemitter3";
+import { EventEmitterWithPromise } from "./eventEmitterWithPromise";
 
 export interface BaseConnectionEvents<
 	ErrorType extends string = BaseConnectionErrorType,
-> extends EventsWithError<ErrorType> {
+> extends PromiseEvents<never, ErrorType> {
 	/**
 	 * Emitted when either you or the remote peer closes the connection.
 	 *
@@ -29,13 +27,44 @@ export interface BaseConnectionEvents<
 	iceStateChanged: (state: RTCIceConnectionState) => void;
 }
 
-export abstract class BaseConnection<
+export interface IBaseConnection<
 	SubClassEvents extends ValidEventTypes,
 	ErrorType extends string = never,
-> extends EventEmitterWithError<
-	ErrorType | BaseConnectionErrorType,
-	SubClassEvents & BaseConnectionEvents<BaseConnectionErrorType | ErrorType>
-> {
+> extends EventEmitter<
+		| (SubClassEvents &
+				BaseConnectionEvents<BaseConnectionErrorType | ErrorType>)
+		| BaseConnectionEvents<BaseConnectionErrorType | ErrorType>
+	> {
+	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 & BaseConnectionEvents<BaseConnectionErrorType | ErrorType>
+		>,
+		SubClassEvents extends ValidEventTypes,
+		ErrorType extends string = never,
+	>
+	extends EventEmitterWithPromise<
+		AwaitType,
+		never,
+		ErrorType | BaseConnectionErrorType,
+		SubClassEvents & BaseConnectionEvents<BaseConnectionErrorType | ErrorType>
+	>
+	implements IBaseConnection<SubClassEvents, ErrorType>
+{
 	protected _open = false;
 
 	/**
@@ -50,15 +79,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;
 	}

+ 18 - 24
lib/dataconnection/DataConnection.ts

@@ -7,14 +7,20 @@ 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 type { PromiseEvents } from "../peerError";
 import { randomToken } from "../utils/randomToken";
-import { PeerError } from "../peerError";
 
 export interface DataConnectionEvents
-	extends EventsWithError<DataConnectionErrorType | BaseConnectionErrorType>,
+	extends PromiseEvents<
+			never,
+			DataConnectionErrorType | BaseConnectionErrorType
+		>,
 		BaseConnectionEvents<DataConnectionErrorType | BaseConnectionErrorType> {
 	/**
 	 * Emitted when data is received from the remote peer.
@@ -27,7 +33,8 @@ export interface DataConnectionEvents
 }
 
 export interface IDataConnection
-	extends BaseConnection<DataConnectionEvents, DataConnectionErrorType> {
+	extends IBaseConnection<DataConnectionEvents, DataConnectionErrorType> {
+	get type(): ConnectionType.Data;
 	/** Allows user to close connection. */
 	close(options?: { flush?: boolean }): void;
 	/** Allows user to send data. */
@@ -38,19 +45,20 @@ export interface IDataConnection
  * 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<DataConnectionEvents, this>;
+	private _negotiator: Negotiator<
+		DataConnectionEvents,
+		DataConnectionErrorType,
+		this
+	>;
 	abstract readonly serialization: string;
 	readonly reliable: boolean;
-	private then: (
-		onfulfilled?: (value: IDataConnection) => any,
-		onrejected?: (reason: PeerError<DataConnectionErrorType>) => any,
-	) => void;
 
 	public get type() {
 		return ConnectionType.Data;
@@ -59,20 +67,6 @@ export abstract class DataConnection extends BaseConnection<
 	constructor(peerId: string, provider: Peer, options: any) {
 		super(peerId, provider, options);
 
-		this.then = (
-			onfulfilled?: (value: IDataConnection) => any,
-			onrejected?: (reason: PeerError<DataConnectionErrorType>) => any,
-		) => {
-			// Remove 'then' to prevent potential recursion issues
-			// `await` will wait for a Promise-like to resolve recursively
-			delete this.then;
-
-			// We don’t need to worry about cleaning up listeners here
-			// `await`ing a Promise will make sure only one of the paths executes
-			this.once("open", () => onfulfilled(this));
-			this.once("error", onrejected);
-		};
-
 		this.connectionId =
 			this.options.connectionId || DataConnection.ID_PREFIX + randomToken();
 

+ 82 - 0
lib/eventEmitterWithPromise.ts

@@ -0,0 +1,82 @@
+import EventEmitter from "eventemitter3";
+import logger from "./logger";
+import { PeerError, PromiseEvents } from "./peerError";
+
+export class EventEmitterWithPromise<
+		AwaitType extends EventEmitter<Events>,
+		OpenType,
+		ErrorType extends string,
+		Events extends PromiseEvents<OpenType, ErrorType>,
+	>
+	extends EventEmitter<Events | PromiseEvents<OpenType, ErrorType>, never>
+	implements Promise<AwaitType>
+{
+	protected _open = false;
+	readonly [Symbol.toStringTag]: string;
+
+	catch<TResult = never>(
+		onrejected?:
+			| ((reason: PeerError<`${ErrorType}`>) => PromiseLike<TResult> | TResult)
+			| undefined
+			| null,
+	): Promise<AwaitType | TResult> {
+		return this.then(undefined, onrejected);
+	}
+
+	finally(onfinally?: (() => void) | undefined | null): Promise<AwaitType> {
+		return this.then().finally(onfinally);
+	}
+
+	then<TResult1 = AwaitType, TResult2 = never>(
+		onfulfilled?:
+			| ((value: AwaitType) => PromiseLike<TResult1> | TResult1)
+			| undefined
+			| null,
+		onrejected?:
+			| ((reason: any) => PromiseLike<TResult2> | TResult2)
+			| undefined
+			| null,
+	): Promise<TResult1 | TResult2> {
+		const p = new Promise((resolve, reject) => {
+			const onOpen = () => {
+				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}`>) => {
+				this.removeListener("open", onOpen);
+				reject(err);
+			};
+			if (this._open) {
+				onOpen();
+				return;
+			}
+			this.once("open", onOpen);
+			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);
+
+		this.emit("error", new PeerError<`${ErrorType}`>(`${type}`, err));
+	}
+}
+
+function proxyWithoutThen<T extends object>(obj: T) {
+	return new Proxy(obj, {
+		get(target, p, receiver) {
+			if (p === "then") {
+				return undefined;
+			}
+			return Reflect.get(target, p, receiver);
+		},
+	});
+}

+ 27 - 20
lib/mediaconnection.ts

@@ -24,15 +24,39 @@ export interface MediaConnectionEvents extends BaseConnectionEvents<never> {
 	willCloseOnRemote: () => void;
 }
 
+export interface IMediaConnection
+	extends BaseConnection<IMediaConnection, MediaConnectionEvents> {
+	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<MediaConnectionEvents> {
+export class MediaConnection extends BaseConnection<
+	IMediaConnection,
+	MediaConnectionEvents
+> {
 	private static readonly ID_PREFIX = "mc_";
-	readonly label: string;
 
-	private _negotiator: Negotiator<MediaConnectionEvents, this>;
+	private _negotiator: Negotiator<MediaConnectionEvents, never, this>;
 	private _localStream: MediaStream;
 	private _remoteStream: MediaStream;
 
@@ -112,16 +136,6 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 		}
 	}
 
-	/**
-     * 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 +164,6 @@ export class MediaConnection extends BaseConnection<MediaConnectionEvents> {
 		this._open = true;
 	}
 
-	/**
-	 * Exposed functionality for users.
-	 */
-
-	/**
-	 * Closes the media connection.
-	 */
 	close(): void {
 		if (this._negotiator) {
 			this._negotiator.cleanup();

+ 6 - 1
lib/negotiator.ts

@@ -15,7 +15,12 @@ import type { ValidEventTypes } from "eventemitter3";
  */
 export class Negotiator<
 	Events extends ValidEventTypes,
-	ConnectionType extends BaseConnection<Events | BaseConnectionEvents>,
+	ErrorType extends string,
+	ConnectionType extends BaseConnection<
+		any,
+		Events | BaseConnectionEvents,
+		ErrorType
+	>,
 > {
 	constructor(readonly connection: ConnectionType) {}
 

+ 6 - 23
lib/peer.ts

@@ -21,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 } from "./peerError";
+import { EventEmitterWithPromise } from "./eventEmitterWithPromise";
+import EventEmitter from "eventemitter3";
 
 class PeerOptions implements PeerJSOption {
 	/**
@@ -109,8 +111,7 @@ export interface PeerEvents {
 	error: (error: PeerError<`${PeerErrorType}`>) => void;
 }
 
-export interface IPeer
-	extends EventEmitterWithError<PeerErrorType, PeerEvents> {
+export interface IPeer extends EventEmitter<PeerEvents> {
 	/**
 	 * The brokering ID of this peer
 	 *
@@ -183,7 +184,7 @@ export interface IPeer
  * A peer who can initiate connections with other peers.
  */
 export class Peer
-	extends EventEmitterWithError<PeerErrorType, PeerEvents>
+	extends EventEmitterWithPromise<IPeer, string, PeerErrorType, PeerEvents>
 	implements IPeer
 {
 	private static readonly DEFAULT_KEY = "peerjs";
@@ -206,16 +207,11 @@ export class Peer
 	// 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<string, ServerMessage[]> = new Map(); // src => [list of messages]
-	private then: (
-		onfulfilled?: (value: IPeer) => any,
-		onrejected?: (reason: PeerError<PeerErrorType>) => any,
-	) => void;
 
 	get id() {
 		return this._id;
@@ -276,20 +272,6 @@ export class Peer
 	constructor(id?: string | PeerOptions, options?: PeerOptions) {
 		super();
 
-		this.then = (
-			onfulfilled?: (value: IPeer) => any,
-			onrejected?: (reason: PeerError<PeerErrorType>) => any,
-		) => {
-			// Remove 'then' to prevent potential recursion issues
-			// `await` will wait for a Promise-like to resolve recursively
-			delete this.then;
-
-			// We don’t need to worry about cleaning up listeners here
-			// `await`ing a Promise will make sure only one of the paths executes
-			this.once("open", () => onfulfilled(this));
-			this.once("error", onrejected);
-		};
-
 		let userId: string | undefined;
 
 		// Deal with overloading
@@ -594,6 +576,7 @@ export class Peer
 			options,
 		);
 		this._addConnection(peer, dataConnection);
+
 		return dataConnection;
 	}
 

+ 2 - 20
lib/peerError.ts

@@ -1,26 +1,8 @@
-import { EventEmitter } from "eventemitter3";
-import logger from "./logger";
-
-export interface EventsWithError<ErrorType extends string> {
+export interface PromiseEvents<OpenType, ErrorType extends string> {
+	open: (open?: OpenType) => void;
 	error: (error: PeerError<`${ErrorType}`>) => void;
 }
 
-export class EventEmitterWithError<
-	ErrorType extends string,
-	Events extends EventsWithError<ErrorType>,
-> extends EventEmitter<Events, never> {
-	/**
-	 * 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.

+ 1 - 0
tsconfig.json

@@ -2,6 +2,7 @@
 	"compilerOptions": {
 		"target": "es5",
 		"module": "commonjs",
+		"esModuleInterop": true,
 		"downlevelIteration": true,
 		"noUnusedLocals": true,
 		"noUnusedParameters": true,