-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
base: master
Are you sure you want to change the base?
Changes from 6 commits
b2016a5
7e18b44
0319432
2cba46d
bee2bdb
e40007a
54e2ac6
15311d9
b49ef4a
5edbfd5
b26a8e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
try { | ||
const peerA = await new Peer(); | ||
// Create 10 new `Peer`s that will try to steel A's id | ||
let peers_try_to_take = Array.from({ length: 10 }, async (_, i) => { | ||
try { | ||
await new Peer(peerA.id); | ||
throw `Peer ${i} failed! Connection got established.`; | ||
} catch (error) { | ||
if (error.type === "unavailable-id") { | ||
return `ID already taken. (${i})`; | ||
} else { | ||
throw error; | ||
} | ||
} | ||
}); | ||
await Promise.all(peers_try_to_take); | ||
messages.textContent = "No ID takeover"; | ||
} catch (error) { | ||
errorMessage.textContent += JSON.stringify(error); | ||
} | ||
})(); | ||
</script> | ||
</body> | ||
</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> |
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> |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -11,6 +11,7 @@ import { BaseConnection, type BaseConnectionEvents } from "../baseconnection"; | |||||||||||||||||||||
import type { ServerMessage } from "../servermessage"; | ||||||||||||||||||||||
import type { EventsWithError } from "../peerError"; | ||||||||||||||||||||||
import { randomToken } from "../utils/randomToken"; | ||||||||||||||||||||||
import { PeerError } from "../peerError"; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export interface DataConnectionEvents | ||||||||||||||||||||||
extends EventsWithError<DataConnectionErrorType | BaseConnectionErrorType>, | ||||||||||||||||||||||
|
@@ -25,6 +26,14 @@ export interface DataConnectionEvents | |||||||||||||||||||||
open: () => void; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
export interface IDataConnection | ||||||||||||||||||||||
extends BaseConnection<DataConnectionEvents, DataConnectionErrorType> { | ||||||||||||||||||||||
/** 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. | ||||||||||||||||||||||
*/ | ||||||||||||||||||||||
|
@@ -38,6 +47,10 @@ export abstract class DataConnection extends BaseConnection< | |||||||||||||||||||||
private _negotiator: Negotiator<DataConnectionEvents, 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; | ||||||||||||||||||||||
|
@@ -46,6 +59,20 @@ 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); | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though I believe the listeners would still remain in memory (a.k.a. a memory leak). Though currently There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great point. Now removing the other listener when firing an event: peerjs/lib/eventEmitterWithPromise.ts Lines 41 to 50 in 15311d9
|
||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
this.connectionId = | ||||||||||||||||||||||
this.options.connectionId || DataConnection.ID_PREFIX + randomToken(); | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
@@ -87,7 +114,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 +152,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( | ||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this mean you cannot do e.g.
and
? Also I think this will behave in an unexpected way, because the second
await
would finish before the connection is actually open:While I think the first two examples are fine as long as it's clear to the users that it's not a perfect Promise implementation, the third one seems dangerous to me, it would be very hard to catch such a bug.
Here's a doc I found on how to make a good thenable: https://promisesaplus.com/#point-36
I think there must be an easy way to implement this by proxying
then
(andcatch
?) to a realPromise
object.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original intention was to use
then
as a hack to be able to useawait
. It was even markedprivate
as to explicitly disallow the first two.But great suggestion, there's no good reason for this. All PeerJS objects now
implement Promise
(includingcatch/finally
).then
internally constructs and returns a realPromise
, meaning the first two work correctly.You are totally right, 3 is confusing. I hope I adequately addressed this with this
peerjs/lib/eventEmitterWithPromise.ts
Lines 43 to 45 in 15311d9
Instead of patching
this
at runtime I create aProxy
without athen
property. Subsequent calls tothen
will have the some consistent behavior.