class MeshNet { constructor() { this.myPeerId = generatePeerId(); this.connections = new Map(); this._pendingMeshPeers = new Map(); this._pendingPeer = null; this._currentQRData = null; this._fullSDPData = null; this.isHost = false; this.hostPeerId = null; this.roster = []; this.readyPeers = new Set(); this._meshReadySent = false; this.playerName = ''; this.names = new Map(); this.names.set(this.myPeerId, ''); this.onpeerconnect = null; this.onpeerdisconnect = null; this.ondata = null; this.onqrupdate = null; this.onanswerready = null; this.onreadyupdate = null; this.onchat = null; } /* ─── Host: room ─── */ createRoom() { this.isHost = true; this._createPendingPeer(); } _createPendingPeer() { if (this._pendingPeer) { try { this._pendingPeer.peer.destroy(); } catch (e) {} } const peer = new SimplePeer({ initiator: true, trickle: false, config: { iceServers: [] } }); let signalSent = false; peer.on('signal', async signal => { if (signalSent) return; signalSent = true; const packed = await pack({ v: 1, id: this.myPeerId, s: signal }); this._currentQRData = encode45(packed); this._fullSDPData = this._currentQRData; if (this.onqrupdate) this.onqrupdate(this._currentQRData); }); peer.on('connect', () => { peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName })); }); peer.on('data', data => { try { const msg = JSON.parse(data.toString()); this._handleMessage(msg, peer); } catch (e) { console.error('p2p data error:', e); } }); peer.on('close', () => this._cleanupPeer(peer)); peer.on('error', err => console.error('p2p error:', err)); this._pendingPeer = { peer, signalSent, connected: false, peerId: null }; } async feedAnswer(answerStr) { if (!this._pendingPeer || this._pendingPeer.signalReceived) return; try { const data = await unpack(decode45(answerStr)); this._pendingPeer.peerId = data.id; this._pendingPeer.peer.signal(data.s); this._pendingPeer.signalReceived = true; } catch (e) { console.error('feedAnswer error:', e); } } /* ─── Peer: join ─── */ async joinFromQR(qrContent) { const data = await unpack(decode45(qrContent)); const remoteId = data.id; this.hostPeerId = remoteId; const offer = data.s; const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } }); let answerSent = false; peer.on('signal', async signal => { if (answerSent) return; answerSent = true; const packed = await pack({ v: 1, id: this.myPeerId, s: signal, to: remoteId }); const answerData = encode45(packed); if (this.onanswerready) this.onanswerready(answerData); }); peer.on('connect', () => { this.connections.set(remoteId, peer); peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName })); if (this.onpeerconnect) this.onpeerconnect(remoteId); }); peer.on('data', data => { try { const msg = JSON.parse(data.toString()); this._handleMessage(msg, peer); } catch (e) { console.error('p2p data error:', e); } }); peer.on('close', () => this._cleanupPeer(peer)); peer.on('error', err => console.error('p2p error:', err)); peer.signal(offer); this.connections.set(remoteId, peer); this._pendingPeer = { peer, peerId: remoteId, connected: answerSent }; } /* ─── Mesh helpers ─── */ _createSimplePeer(initiator, onSignal, onConnect) { const peer = new SimplePeer({ initiator, trickle: false, config: { iceServers: [] } }); peer.on('signal', signal => { if (onSignal) onSignal(signal); }); peer.on('connect', () => { if (onConnect) onConnect(); // No identity exchange – peerId known from context }); peer.on('data', data => { try { const msg = JSON.parse(data.toString()); this._handleMessage(msg, peer); } catch (e) { console.error('mesh data error:', e); } }); peer.on('close', () => this._cleanupPeer(peer)); peer.on('error', e => console.error('mesh error:', e)); return peer; } _initiateMeshConnection(targetPeerId) { if (this.connections.has(targetPeerId)) return; if (this._pendingMeshPeers.has(targetPeerId)) return; if (this.myPeerId >= targetPeerId) return; const peer = this._createSimplePeer(true, signal => { this.sendTo(this.hostPeerId, { type: 'signal_relay', to: targetPeerId, signal }); }, () => { this.connections.set(targetPeerId, peer); this._pendingMeshPeers.delete(targetPeerId); if (this.onpeerconnect) this.onpeerconnect(targetPeerId); this._checkMeshReady(); } ); this._pendingMeshPeers.set(targetPeerId, peer); } _handleRelayedSignal(fromPeerId, signal) { let peer = this.connections.get(fromPeerId) || this._pendingMeshPeers.get(fromPeerId); if (!peer) { peer = this._createSimplePeer(false, signal => { this.sendTo(this.hostPeerId, { type: 'signal_relay', to: fromPeerId, signal }); }, () => { this.connections.set(fromPeerId, peer); this._pendingMeshPeers.delete(fromPeerId); if (this.onpeerconnect) this.onpeerconnect(fromPeerId); this._checkMeshReady(); } ); this._pendingMeshPeers.set(fromPeerId, peer); } peer.signal(signal); } _checkMeshConnections() { for (const pid of this.roster) { if (pid === this.myPeerId) continue; if (this.connections.has(pid)) continue; if (this.myPeerId < pid) { this._initiateMeshConnection(pid); } } this._checkMeshReady(); } _checkMeshReady() { if (this._meshReadySent) return; if (!this.hostPeerId) return; if (!this.connections.has(this.hostPeerId)) return; const allConnected = this.roster.every(pid => pid === this.myPeerId || this.connections.has(pid) ); if (allConnected && this.roster.length > 0) { this._meshReadySent = true; this.sendTo(this.hostPeerId, { type: 'mesh_ready' }); } } _broadcastRoster() { this.roster = this.getPeerIds(); this.readyPeers.clear(); this.broadcast({ type: 'roster', peers: this.roster.map(id => ({ id, name: this.names.get(id) || '' })) }); if (this.onreadyupdate) this.onreadyupdate(0, this.roster.length); } /* ─── Message routing ─── */ _handleMessage(msg, peer) { const peerId = this._findPeerId(peer) || 'unknown'; if (msg.type === 'identity') { if (msg.name) this.names.set(msg.peerId, msg.name); const existing = this._findPeerId(peer); if (existing && existing !== msg.peerId) { this.connections.delete(existing); } this.connections.set(msg.peerId, peer); if (this._pendingPeer && this._pendingPeer.peer === peer) { this._pendingPeer.peerId = msg.peerId; this._pendingPeer.connected = true; this._pendingPeer = null; if (this.onpeerconnect) this.onpeerconnect(msg.peerId); if (this.isHost) { setTimeout(() => this._createPendingPeer(), 300); this._broadcastRoster(); } } else { if (this.onpeerconnect) this.onpeerconnect(msg.peerId); } return; } if (msg.type === 'signal_relay') { const target = this.connections.get(msg.to) || this._pendingMeshPeers.get(msg.to); if (target) { target.send(JSON.stringify({ type: 'signal_relayed', from: peerId, signal: msg.signal })); } return; } if (msg.type === 'signal_relayed') { this._handleRelayedSignal(msg.from, msg.signal); return; } if (msg.type === 'roster') { this.roster = (msg.peers || []).map(p => p.id || p); if (msg.peers) msg.peers.forEach(p => { if (p.name) this.names.set(p.id, p.name); }); this._meshReadySent = false; this._checkMeshConnections(); return; } if (msg.type === 'mesh_ready' && this.isHost) { this.readyPeers.add(peerId); if (this.onreadyupdate) { this.onreadyupdate(this.readyPeers.size, this.roster.length); } return; } if (msg.type === 'chat') { if (this.onchat) this.onchat(peerId, msg.text); return; } if (this.ondata) { this.ondata(peerId, msg); } } /* ─── Peer management ─── */ _findPeerId(peer) { for (const [id, p] of this.connections) { if (p === peer) return id; } for (const [id, p] of this._pendingMeshPeers) { if (p === peer) return id; } return null; } _cleanupPeer(peer) { const peerId = this._findPeerId(peer); if (peerId) { this.connections.delete(peerId); if (this.onpeerdisconnect) this.onpeerdisconnect(peerId); } for (const [id, p] of this._pendingMeshPeers) { if (p === peer) { this._pendingMeshPeers.delete(id); break; } } if (this._pendingPeer && this._pendingPeer.peer === peer) { this._pendingPeer = null; } } /* ─── Send ─── */ broadcast(msg) { const str = JSON.stringify(msg); for (const [id, peer] of this.connections) { try { peer.send(str); } catch (e) { console.error('broadcast to', id, 'failed:', e); } } } sendTo(peerId, msg) { const peer = this.connections.get(peerId); if (peer) { try { peer.send(JSON.stringify(msg)); } catch (e) { console.error('sendTo', peerId, 'failed:', e); } } } relaySignal(from, to, signal) { const target = this.connections.get(to); if (target) { target.send(JSON.stringify({ type: 'signal_relay', from, to, signal })); } } getPeerIds() { return Array.from(this.connections.keys()); } getCount() { return this.connections.size; } getPeerName(peerId) { return this.names.get(peerId) || peerId.slice(0, 8); } getFullSDPData() { return this._fullSDPData; } destroy() { for (const peer of this.connections.values()) { try { peer.destroy(); } catch (e) {} } for (const peer of this._pendingMeshPeers.values()) { try { peer.destroy(); } catch (e) {} } this.connections.clear(); this._pendingMeshPeers.clear(); if (this._pendingPeer) { try { this._pendingPeer.peer.destroy(); } catch (e) {} this._pendingPeer = null; } } }