UNPKG

@mothepro/fancy-p2p

Version:

A quick and efficient way to form p2p groups in the browser

102 lines 5.02 kB
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