randomp2p/p2p.js
Ole c29df1ca80
All checks were successful
Pin to IPFS / pin (push) Successful in 9s
Eigenen Namen eingeben vor Raum-Erstellen/Beitreten
2026-06-14 16:55:37 +02:00

374 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
/* ─── 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', signal => {
if (signalSent) return;
signalSent = true;
this._currentQRData = JSON.stringify({ v: 1, id: this.myPeerId, s: signal });
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 };
}
feedAnswer(answerStr) {
if (!this._pendingPeer || this._pendingPeer.signalReceived) return;
try {
const data = JSON.parse(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 ─── */
joinFromQR(qrContent) {
const data = JSON.parse(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', signal => {
if (answerSent) return;
answerSent = true;
const answerData = JSON.stringify({
v: 1, id: this.myPeerId, s: signal, to: remoteId
});
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 (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;
}
}
}