randomp2p/p2p.js

217 lines
5.5 KiB
JavaScript

class MeshNet {
constructor() {
this.myPeerId = generatePeerId();
this.connections = new Map();
this.isHost = false;
this._pendingPeer = null;
this._currentQRData = null;
this.onpeerconnect = null;
this.onpeerdisconnect = null;
this.ondata = null;
this.onqrupdate = null;
this.onanswerready = null;
}
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 });
let signalSent = false;
let connected = false;
peer.on('signal', signal => {
if (signalSent) return;
signalSent = true;
this._currentQRData = JSON.stringify({ v: 1, id: this.myPeerId, s: signal });
if (this.onqrupdate) this.onqrupdate(this._currentQRData);
});
peer.on('connect', () => {
connected = true;
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId }));
});
peer.on('data', data => {
try {
const msg = JSON.parse(data.toString());
this._handleMessage(msg, peer);
} catch (e) {
console.error('p2p data parse error:', e);
}
});
peer.on('close', () => {
this._cleanupPeer(peer);
});
peer.on('error', err => {
console.error('p2p error:', err);
});
this._pendingPeer = { peer, signalSent, connected, 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);
}
}
joinFromQR(qrContent) {
const data = JSON.parse(qrContent);
const remoteId = data.id;
const offer = data.s;
const peer = new SimplePeer({ initiator: false, trickle: false });
let answerSent = false;
let connected = 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', () => {
connected = true;
this.connections.set(remoteId, peer);
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId }));
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 parse 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 };
}
_handleMessage(msg, peer) {
if (msg.type === 'identity') {
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;
if (this.onpeerconnect) this.onpeerconnect(msg.peerId);
if (this.isHost) {
setTimeout(() => this._createPendingPeer(), 300);
}
} else {
if (this.onpeerconnect) this.onpeerconnect(msg.peerId);
}
return;
}
if (msg.type === 'signal_relay') {
const target = this.connections.get(msg.to);
if (target) {
target.send(JSON.stringify({
type: 'signal_relayed',
from: msg.from,
signal: msg.signal
}));
}
return;
}
if (this.ondata) {
const peerId = this._findPeerId(peer) || 'unknown';
this.ondata(peerId, msg);
}
}
_findPeerId(peer) {
for (const [id, p] of this.connections) {
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);
}
if (this._pendingPeer && this._pendingPeer.peer === peer) {
this._pendingPeer = null;
}
}
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;
}
destroy() {
for (const peer of this.connections.values()) {
try { peer.destroy(); } catch (e) {}
}
this.connections.clear();
if (this._pendingPeer) {
try { this._pendingPeer.peer.destroy(); } catch (e) {}
this._pendingPeer = null;
}
}
}