UNPKG

@mothepro/fancy-p2p

Version:

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

245 lines 12 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fancy_emitter_1 = require("fancy-emitter"); const parsers_js_1 = require("../util/parsers.js"); const builders_js_1 = require("../util/builders.js"); const Client_js_1 = __importStar(require("./Client.js")); const HashableSet_js_1 = __importDefault(require("../util/HashableSet.js")); class LeaveError extends Error { constructor(client, message) { super(message); this.client = client; } } /** * Handle the communication with the signaling server. * * Joins the lobby on server upon construction. * Allows for creation, approval and rejection of groups. * Listening on the `groupFinal` emitter will tell caller which offers/answers to create/accept */ class default_1 { constructor(address, lobby, name, protocol) { /** Map of all clients connected to this signaling server. */ this.allClients = new Map; this.groups = new Map; this.state = 0 /* CLOSED */; this.stateChange = new fancy_emitter_1.Emitter(newState => this.state = newState); /** Activates when a new client joins the lobby. */ this.connection = new fancy_emitter_1.Emitter; /** Activates when the server sends a message on behalf of a failed peer connection. */ this.fallbackMessage = new fancy_emitter_1.Emitter; /** The direct, indirect & mock peers, it's only passed here for server logging. */ this.peersForFallbackLogging = []; /** Activates when receiving some data from the signaling server. */ this.message = new fancy_emitter_1.SafeEmitter(data => { try { switch (this.state) { case 3 /* FALLBACK */: this.fallbackMessage.activate(parsers_js_1.parseFallback(data)); break; // Accept the SDP from the client after they have created. case 2 /* FINALIZED */: const { from, sdp } = parsers_js_1.parseSdp(data); this.getClient(from).acceptor.activate(sdp); break; case 1 /* READY */: switch (data.getUint8(0)) { case 5 /* YOUR_NAME */: this.connection.activate(this.self = new Client_js_1.MockClient(parsers_js_1.parseYourName(data))); return; case 1 /* CLIENT_JOIN */: this.handleClientJoin(parsers_js_1.parseClientJoin(data)); return; case 0 /* CLIENT_LEAVE */: // TODO Also close peer's connection if in fallback // server needs to send message since we are in group not lobby state this.getClient(parsers_js_1.parseClientLeave(data)).proposals.cancel(); return; case 3 /* GROUP_REJECT */: case 2 /* GROUP_REQUEST */: this.handleGroupChange(parsers_js_1.parseGroupChange(data)); return; case 4 /* GROUP_FINAL */: this.handleGroupFinalize(parsers_js_1.parseGroupFinalize(data)); return; default: throw Error(`Unexpected data from server ${data}`); } } } catch (err) { this.stateChange.deactivate(err); } }); /** Attempts to get a client that has connected. Throws if unable to. */ this.getClient = (id) => { if (!this.allClients.has(id)) throw Error(`Received data from unknown client ${id}.`); return this.allClients.get(id); }; if (typeof address == 'string') address = new URL(address); address.searchParams.set('lobby', lobby); if (name) address.searchParams.set('name', name); this.server = new WebSocket(address.toString(), protocol); this.server.binaryType = 'arraybuffer'; this.server.addEventListener('open', () => this.stateChange.activate(1 /* READY */)); this.server.addEventListener('close', () => this.stateChange.activate(0 /* CLOSED */)); this.server.addEventListener('error', () => this.stateChange.deactivate(Error('Connection to Server closed unexpectedly.'))); this.server.addEventListener('message', ({ data }) => this.message.activate(new DataView(data))); this.bindStateChanges(name); } async handleClientJoin({ id, name }) { const client = new Client_js_1.default(id, name); this.allClients.set(id, client); this.sendSdpOnCreation(client); if (this.connection.count == 0) // Ensure the SelfPeer comes out first, otherwise, just wait for it await this.connection.next; this.connection.activate(client); // Clean up on disconnect for await (const _ of client.proposals) ; this.allClients.delete(id); } handleGroupChange({ approve, actor, members }) { const rejectGroup = (reason) => { var _a; return ((_a = this.groups.get(members.hash)) === null || _a === void 0 ? void 0 : _a.deactivate(reason)) && this.groups.delete(members.hash); }; if (approve) { // Initiate the group if it hasn't been propopsed before if (!this.groups.has(members.hash)) { // Used to keep track of clients when they accept or reject this.groups.set(members.hash, new fancy_emitter_1.Emitter); // Initiate on behalf of the client this.getClient(actor).proposals.activate({ members: [...members].map(this.getClient), ack: this.groups.get(members.hash), action: accept => { this.serverSend(builders_js_1.buildProposal(accept, ...members)); if (!accept) rejectGroup(new Error(`Rejected group with ${[...members]}.`)); } }); } // TODO decide if that should be in an else this.groups.get(members.hash).activate(this.getClient(actor)); } else rejectGroup(new LeaveError(this.allClients.get(actor), `Group with ${[...members]} was rejected.`)); } handleGroupFinalize({ code, members, cmp }) { this.code = code; this.myId = cmp; this.members = [...members].map(this.getClient); for (const member of this.members) // The `cmp` is sent from the server as a way to determine // What expression will evaluate the same on both sides of the equation... member.isOpener = cmp < member.id; this.stateChange.activate(2 /* FINALIZED */); } /** A wrapper around socket send since that method doesn't throw, for some reason. */ serverSend(data) { if (this.server.readyState != WebSocket.OPEN) throw Error('WebSocket is not in an OPEN state.'); this.server.send(data); } async sendSdpOnCreation({ id, creator }) { for await (const sdp of creator) this.serverSend(builders_js_1.buildSdp(id, sdp)); } async bindStateChanges(presetName) { try { for await (const state of this.stateChange) switch (state) { case 0 /* CLOSED */: this.stateChange.cancel(); break; // Log which connections worked and failed. case 3 /* FALLBACK */: const fails = []; for (const peer of this.peersForFallbackLogging) if (!peer.isYou && !(await peer.ready)) // this is a promise, so don't use `.filter`, even tho it's already been resolved fails.push(peer); if (fails.length) // sanity check console.warn(`${fails.length} out of ${this.peersForFallbackLogging.length - 1} direct connections broke. Failed peers IDs: ${fails.map(({ fallbackId }) => fallbackId).join(', ')}`); // TODO log on server break; case 1 /* READY */: if (presetName) { await Promise.resolve(); // Wait a tick; Allow `this.connection` listener to be bound first this.connection.activate(this.self = new Client_js_1.MockClient(presetName)); } break; } } finally { this.fallbackMessage.cancel(); this.connection.cancel(); this.server.close(); } } /** Proposes a group to the server and returns the emitter that will be activated when clients accept it. */ proposeGroup(...members) { const ids = new HashableSet_js_1.default; for (const [id, client] of this.allClients) if (members.includes(client)) ids.add(id); if (this.state != 1 /* READY */) throw Error('Can not propose a group before connecting.'); if (this.groups.has(ids.hash)) throw Error('Can not propose a group that is already formed.'); if (ids.size < 1) throw Error('Can not propose a group without members.'); const ack = new fancy_emitter_1.Emitter; this.serverSend(builders_js_1.buildProposal(true, ...ids)); this.groups.set(ids.hash, ack); this.self.proposals.activate({ members, ack }); return this.groups.get(ids.hash); } groupExists(...members) { const ids = new HashableSet_js_1.default; // TODO improve this?? // for some reason this allows members to include self. for (const [id, client] of this.allClients) if (members.includes(client)) ids.add(id); return !!ids.size && this.groups.has(ids.hash); } /** Sends an indirect message to a peer thru signaling server. */ sendFallback(id, data) { if (this.state != 3 /* FALLBACK */) return; const view = new DataView(new ArrayBuffer(2 /* SHORT */ + data.byteLength)); view.setUint16(0, id, true); new Uint8Array(view.buffer, 2 /* SHORT */).set(new Uint8Array(data)); // optimize? this.serverSend(view.buffer); } } exports.default = default_1; //# sourceMappingURL=Signaling.js.map