@mothepro/fancy-p2p
Version:
A quick and efficient way to form p2p groups in the browser
102 lines • 5.02 kB
JavaScript
import { Emitter } from 'fancy-emitter';
import Client from './Client.js';
import Peer, { MockPeer } from './Peer.js';
import Signaling from './Signaling.js';
import rng from '../util/random.js';
export default class {
constructor({ name, stuns, lobby, server: { address, version }, fallback = false, retries = 0, timeout = -1 }) {
this.state = 0 /* OFFLINE */;
/** Activated when the state changes, Cancels when finalized, Deactivates when error is throw. */
this.stateChange = new Emitter(newState => this.state = newState);
/** The peers who's connections are still open */
this.peers = [];
/**
* Generates a random number in [0,1), same as Math.random()
* If `isInt` is true, then an integer in range [-2 ** 31, 2 ** 31) is generated instead.
*
* `state` must be `State.READY`.
*/
this.random = (isInt = false) => this.assert(3 /* READY */) &&
isInt
? this.rng.next().value
: 0.5 + this.rng.next().value / 4294967295 /* INT */;
/**
* Propose a group with other clients connected to this lobby.
*
* `state` must be `State.LOBBY`.
*/
this.proposeGroup = (...members) => this.assert(1 /* LOBBY */) &&
this.server.proposeGroup(...members);
/**
* Whether a group with the following memebers has been proposed or answered.
*
* `state` must be `State.LOBBY`.
*/
this.groupExists = (...members) => this.assert(1 /* LOBBY */) &&
this.server.groupExists(...members);
/**
* Send data to all connected peers.
*
* `state` must be `State.READY`.
*/
this.broadcast = (data, includeSelf = true) => {
this.assert(3 /* READY */);
for (const peer of this.peers)
if (peer.message.isAlive && (includeSelf || !peer.isYou))
peer.send(data);
};
this.server = new Signaling(address, lobby, name, version);
// Bind Emitters
this.lobbyConnection = this.server.connection;
this.bindServerState(stuns, retries, timeout, fallback);
this.stateChange.on(() => { })
.finally(this.server.stateChange.cancel) // close server when state is done
.catch(() => { }); // handle elsewhere
}
// TODO allow READY state even tho the state doesn't change until the next tick
assert(valid, message = `Expected state to be ${valid} but was ${this.state}`) {
if (this.state != valid)
throw Error(message);
return true;
}
async bindServerState(stuns, retries, timeout, fallback) {
try {
for await (const state of this.server.stateChange)
switch (state) {
case 1 /* READY */:
this.stateChange.activate(1 /* LOBBY */);
break;
case 2 /* FINALIZED */:
this.stateChange.activate(2 /* LOADING */);
this.rng = rng(this.server.code);
const members = [
...this.server.members,
{ id: this.server.myId }
];
// sort the IDs then use a consistent fisher yates shuffle on them
members.sort(({ id: firstID }, { id: secondID }) => firstID - secondID);
for (let i = members.length - 1; i > 0; i--) {
const j = Math.abs(this.rng.next().value) % i;
[members[j], members[i]] = [members[i], members[j]];
}
for (const client of members)
this.peers.push(client instanceof Client
? new Peer(stuns, client, retries, timeout, fallback ? this.server : undefined)
: new MockPeer(this.server.self.name));
this.server.peersForFallbackLogging = this.peers;
// Every connection is connected successfully, ready up & close connection with server
// TODO, does every promise need to resolve? Or can broken peers just be kicked?
const peerConnectionStatuses = await Promise.all(this.peers.map(peer => peer.ready));
this.server.stateChange.activate(fallback && !peerConnectionStatuses.every(isUsingDirectConnection => isUsingDirectConnection)
? 3 /* FALLBACK */
: 0 /* CLOSED */);
this.stateChange.activate(3 /* READY */);
}
this.assert(3 /* READY */, 'Connection with server closed prematurely');
}
catch (err) {
this.stateChange.deactivate(err);
}
}
}
//# sourceMappingURL=P2P.js.map