randomp2p/p2p.js
Ole 14aa83ff6e
All checks were successful
Pin to IPFS / pin (push) Successful in 11s
Debug-Chat per Data-Channel (nur ?debug)
2026-06-14 17:11:17 +02:00

380 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;
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;
}
}
}