Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Promise-like API for PeerJS #1127

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
48 changes: 48 additions & 0 deletions e2e/peer/id-taken.await.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<link rel="stylesheet" href="../style.css" />
</head>
<body>
<h1>ID-TAKEN</h1>
<div id="messages"></div>
<div id="error-message"></div>
<script src="/dist/peerjs.js"></script>
<script type="application/javascript">
(async () => {
/**
* @type {typeof import("../../lib/exports.ts").Peer}
*/
const Peer = window.peerjs.Peer;

const messages = document.getElementById("messages");
const errorMessage = document.getElementById("error-message");

// Peer A should be created without an error
const peerA = await new Peer().catch(
(error) => (errorMessage.textContent += JSON.stringify(error)),
);

// Create 10 new `Peer`s that will try to steel A's id
// Wait for all peers to finish
const steeling_peers = await Promise.allSettled(
Array.from({ length: 10 }, () => new Peer(peerA.id)),
);

if (
steeling_peers.every(
({ reason, status }) =>
status === "rejected" && reason.type === "unavailable-id",
)
) {
messages.textContent = "No ID takeover";
} else {
errorMessage.textContent += JSON.stringify(steeling_peers);
}
})();
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions e2e/peer/id-taken.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ <h1>ID-TAKEN</h1>
)
.once("open", (id) => {
// Create 10 new `Peer`s that will try to steel A's id
let peers_try_to_take = Array.from(
const steeling_peers = Array.from(
{ length: 10 },
(_, i) =>
new Promise((resolve, reject) =>
Expand All @@ -45,7 +45,7 @@ <h1>ID-TAKEN</h1>
}),
),
);
Promise.all(peers_try_to_take)
Promise.all(steeling_peers)
.then(() => (messages.textContent = "No ID takeover"))
.catch(
(error) => (errorMessage.textContent += JSON.stringify(error)),
Expand Down
41 changes: 41 additions & 0 deletions e2e/peer/peer-unavailable.async.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<link rel="stylesheet" href="../style.css" />
</head>
<body>
<h1>PEER-UNAVAILABLE</h1>
<div id="messages"></div>
<div id="error-message"></div>
<script src="/dist/peerjs.js"></script>
<script type="application/javascript">
(async () => {
/**
* @type {typeof import("../../lib/exports.ts").Peer}
*/
const Peer = window.peerjs.Peer;

const messages = document.getElementById("messages");
const errors = document.getElementById("error-message");

const not_existing_peer = crypto
.getRandomValues(new Uint8Array(16))
.join("");

try {
const peer = await new Peer();
await peer.connect(not_existing_peer);
} catch (error) {
if (error.type === "peer-unavailable") {
messages.textContent = "Success: Peer unavailable";
} else {
errors.textContent += JSON.stringify(error);
}
}
})();
</script>
</body>
</html>
43 changes: 43 additions & 0 deletions e2e/peer/peer-unavailable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<link rel="stylesheet" href="../style.css" />
</head>
<body>
<h1>PEER-UNAVAILABLE</h1>
<div id="messages"></div>
<div id="error-message"></div>
<script src="/dist/peerjs.js"></script>
<script type="application/javascript">
/**
* @type {typeof import("../..").Peer}
*/
const Peer = window.peerjs.Peer;

const connectionErrors = document.getElementById("messages");
const peerErrors = document.getElementById("error-message");

const not_existing_peer = crypto
.getRandomValues(new Uint8Array(16))
.join("");

const peer = new Peer();
peer
.once(
"error",
(error) => void (peerErrors.textContent += JSON.stringify(error)),
)
.once("open", (id) => {
const connection = peer.connect(not_existing_peer);
connection.once(
"error",
(error) =>
void (connectionErrors.textContent += JSON.stringify(error)),
);
});
</script>
</body>
</html>
17 changes: 17 additions & 0 deletions e2e/peer/peer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
});
});
59 changes: 39 additions & 20 deletions lib/baseconnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ErrorType> {
> extends PromiseEvents<never, ErrorType> {
/**
* Emitted when either you or the remote peer closes the connection.
*
Expand All @@ -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<BaseConnectionErrorType | ErrorType>
> {
> extends EventEmitter<SubClassEvents> {
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>,
SubClassEvents extends BaseConnectionEvents<
BaseConnectionErrorType | ErrorType
>,
ErrorType extends string = never,
>
extends EventEmitterWithPromise<
AwaitType,
never,
ErrorType | BaseConnectionErrorType,
SubClassEvents
>
implements IBaseConnection<SubClassEvents, ErrorType>
{
protected _open = false;

/**
Expand All @@ -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;
}
Expand Down
32 changes: 24 additions & 8 deletions lib/dataconnection/DataConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataConnectionErrorType | BaseConnectionErrorType>,
BaseConnectionEvents<DataConnectionErrorType | BaseConnectionErrorType> {
extends BaseConnectionEvents<
DataConnectionErrorType | BaseConnectionErrorType
> {
/**
* Emitted when data is received from the remote peer.
*/
Expand All @@ -25,21 +29,35 @@ export interface DataConnectionEvents
open: () => void;
}

export interface IDataConnection
extends IBaseConnection<DataConnectionEvents, DataConnectionErrorType> {
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<DataConnectionEvents, this>;
private _negotiator: Negotiator<
DataConnectionEvents,
DataConnectionErrorType,
this
>;
abstract readonly serialization: string;
readonly reliable: boolean;

public get type() {
public get type(): ConnectionType.Data {
return ConnectionType.Data;
}

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions lib/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export enum PeerErrorType {
}

export enum BaseConnectionErrorType {
PeerUnavailable = "peer-unavailable",
NegotiationFailed = "negotiation-failed",
ConnectionClosed = "connection-closed",
}
Expand Down
Loading