@mothepro/fancy-p2p
Version:
A quick and efficient way to form p2p groups in the browser
288 lines (208 loc) • 8.16 kB
Markdown
# [Fancy P2P](https://mothepro.github.io/fancy-p2p)
> A simple way to discovery P2P (peer to peer) connections
## Projects
+ [Fancy P2P Demo](https://mothepro.github.io/fancy-p2p)
+ [Amazons](https://amazons.parkshade.com)
## Why
Direct connections between browsers is well supported with WebRTC, but this is difficult to set up and use.
## Caveats
Devices behind [strict NAT networks](https://developers.google.com/talk/libjingle/important_concepts?csw=1#portssocketsconnections) (roughly 8% of devices worldwide) can **not** create a direct peer to peer connection.
## Terminology
**Peer** A direct connection from one browser to another
**Client** Another browser in the same lobby. We can become peers if we both accept
## Install
`yarn add @mothepro/fancy-p2p`
## How to Use
Include the ES module on your page.
```html
<script type="module" src="//unpkg.com/@mothepro/fancy-p2p"></script>
```
Then in your application, initialize a P2P to find peers and connect with them.
```typescript
const
/** My public server running `@mothepro/signaling-lobby`. */
address = 'wss://ws.parkshade.com:443',
/** STUNS to useful for testing. */
stuns = [
"stun:stun.stunprotocol.org",
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302",
"stun:stun3.l.google.com:19302",
"stun:stun4.l.google.com:19302",
],
/** The version of `@mothepro/signaling-lobby` the signaling server is running */
version = '0.2.0',
/** Only clients who use this string will be found in the lobby. */
lobby = 'app-name@v1.0',
/** P2P instance */
p2p = new P2P({
/** Name used to connect to lobby with */
name: 'Mo',
/** STUN servers to use to initialize P2P connections */
stuns,
/** Lobby ID to use for this app */
lobby,
/** Settings for the signaling server */
server: {
/** The address of the signaling server */
address,
/** The version of `@mothepro/signaling-lobby` the signaling server is running */
version,
},
/** Whether to use the signaling server as a fallback when a direct connection to peer can not be established. */
fallback: false,
/** Number of times to attempt to make an RTC connection, if negative direct p2p connections will not be attempted. */
retries: 1,
/** The number of milliseconds to wait before giving up on the connection. */
timeout: 10 * 1000,
})
```
The `p2p` has 4 possible states represented by the following.
```typescript
enum State {
OFFLINE = 0,
LOBBY = 1,
LOADING = 2,
READY = 3
}
```
The `p2p` instance exposes the following to listen for state changes.
```typescript
class P2P {
readonly state: State
/** Activated when the state changes, Cancels when finalized, Deactivates when error is throw. */
readonly stateChange: Listener<State>
}
```
### Offline
No connection to server or peers.
Next state will be `LOBBY` or to fail.
### Lobby
We are now connected to the lobby. Now, listening to `Client`s connect and waiting to make or join a group.
```typescript
interface Client {
/** Name of this client. */
readonly name: Name
/** Activated when a initiating a new group. Closed when client leaves. */
readonly proposals: Listener<{
/** The other members in this group, including me. */
members: Client[]
/** Function to accept or reject the group, not present if you created the group */
action?(accept: boolean): void
/** Activated with the Client who just accepted the group proposal. Deactivates when someone rejects. */
ack: Listener<Client>
}>
/**
* Whether this client represents you in the lobby.
* When false this is another client and proposals are initiated by them.
*/
readonly isYou: boolean
}
```
*The first client to connect is always "you".*
The `p2p` instance provides a Listener to find new clients and a `proposeGroup` method which takes a list of clients to group with.
```typescript
class P2P {
/** Activated when a client joins the lobby. */
readonly lobbyConnection: SafeListener<SimpleClient>
/** Propose a group with other clients connected to this lobby. */
proposeGroup(...members: SimpleClient[]): void
/** Whether a group with the following memebers has been proposed or answered. */
groupExists(...members: SimpleClient[]): boolean
}
```
Which can be used to find clients and monitor when they propose, accept or reject groups or leave the lobby.
<details>
<summary>Example: Listening to clients</summary>
```typescript
async function bindClientProposals(client: Client) {
for await (const { members, ack, action } of client.proposals) {
const groupName = members.map(client => client.name).join(', ') + ' & you'
console.log('Group proposed for ', groupName)
this.bindProposalAcks(groupName, ack)
if (action) // not present if I created the group
action(confirm('Want to join group with ' + groupName))
}
console.log(client.name, 'has left the lobby')
}
async function bindProposalAcks(groupName: string, ack: Listener<Client>) {
try {
for await (const client of ack)
console.log(client.name, 'accepted invitation with', groupName)
} catch (err) {
if (err.client) // if present, this is the client who rejected
console.error(err, err.client.name, 'rejected invitation to group with', groupName)
else
console.error(err, 'Group closed with', groupName)
}
}
for await (const client of p2p.lobbyConnection) {
console.log(client.name, 'has joined the lobby')
this.bindClientProposals(client)
}
```
</details>
Next state will be `LOADING` if group is made or `OFFLINE` if kicked from server for inactivity.
### Loading
Happens once every client accepts the group.
Time to create direct P2P connections with everyone who accepted
Next state will be `READY` if successful or fail if a direct connection with all isn't made.
### Ready
The direct connections with peers are set and we can now broadcast messages and generate random numbers together.
```typescript
interface Peer<T extends string | ArrayBuffer | Blob> {
/** Name of the new peer. */
readonly name: Name
/** Send data to activate the `message` listener for the peer. */
send(data: T): void
/** Activates when a message is received for this peer. Cancels once the connection is closed. */
readonly message: Listener<T>
/**
* Whether this peer represents a "connection" to you.
* When false this is another peer and data is sent through the wire.
*/
readonly isYou: boolean
/** Close the connection with this peer. */
close(): void;
}
```
The `peers` member in the `p2p` instance is now a list of `Peer`s in a random order.
This order is consistent for all the peers though (Useful for turn based applications).
The `p2p` instance also provides the `broadcast` & `random` helper functions.
```typescript
class P2P<T extends ArrayBuffer | string | Blob> {
/** The peers who's connections are still open */
readonly peers: Peer<T>[]
/**
* Generates a random number in [0,1). Same as Math.random()
* If `isInt` is true, than a integer in range [-2 ** 31, 2 ** 31) is generated.
*
* This value will be the same across all the other connected peers.
*/
random(isInt?: boolean): number
/** Send data to all connected peers. Including you by default */
broadcast(data: T, includeSelf?: boolean): void
}
```
<details>
<summary>Example: Listening to peers</summary>
```typescript
async function bindPeerMessages(peer: Peer) {
try {
for await (const data of peer.message)
console.log(peer.name, 'sent', data)
} catch (err) {
console.error(err)
}
console.log('Closed direct connection with', peer.name)
}
for (const peer of p2p.peers)
this.bindPeerMessages(peer)
```
</details>
## Roadmap
+ Test RTC possibility before starting server connection
+ Support trickle ICE
+ Improve peer lib `simple-peer` is messes with buffer
+ Undo https://github.com/mothepro/fancy-p2p/commit/527da616bf1982bac84ed66f55d3295df8074ff1 ??