import MWSEEventTarget from './EventTarget.js'; import { PeerInfo } from './PeerInfo.js'; import WebRTC from './WebRTC.js'; export default class Peer extends MWSEEventTarget { constructor(mwse) { super(); this.mwse = mwse; this.options = {}; this.socketId = undefined; this.selfSocket = false; this.active = false; this.peerConnection = false; this.primaryChannel = 'datachannel'; this.info = new PeerInfo(this); this.rtc = this._createRTC(); // Route incoming packs: RTC signaling goes to the RTC engine, // everything else surfaces as a 'message' event. this.on('pack', data => { if (data.type === ':rtcpack:') { this.rtc.emit('input', data.payload); } else { this.emit('message', data); } }); } _createRTC(rtcConfig, rtcServers) { const rtc = new WebRTC(rtcConfig, rtcServers); rtc.peer = this; rtc.on('connected', () => { this.peerConnection = true; }); rtc.on('disconnected', () => { this.peerConnection = false; }); // RTC output → relay opaque `:rtcpack:` to the paired peer via server. rtc.on('output', payload => { this.send({ type: ':rtcpack:', payload }); }); // RTC data-channel message → surface as peer 'pack' event. rtc.on('message', payload => { this.emit('pack', payload); }); this.rtc = rtc; return rtc; } // createRTC allows callers to (re)create the RTC object with custom config. createRTC(rtcConfig, rtcServers) { return this._createRTC(rtcConfig, rtcServers); } setPeerOptions(options) { if (typeof options === 'string') { this.setSocketId(options); } else { this.options = options; } } setSocketId(uuid) { this.socketId = uuid; } async metadata() { if (this.socketId === 'me') { const result = await this.mwse.EventPooling.request({ type: 'my/socketid' }); this.selfSocket = true; this.active = this.active || true; this.socketId = result; this.emit('scope'); this.activeScope = true; return result; } } async request(pack) { if (this.active) { return this.mwse.request(this.socketId, pack); } } equalTo(peer) { return this.socketId === peer.socketId; } async isReachable() { return this.mwse.EventPooling.request({ type: 'is/reachable', to: this.socketId }); } async enablePairAuth() { await this.mwse.EventPooling.request({ type: 'auth/pair-system', value: 'everybody' }); } async disablePairAuth() { await this.mwse.EventPooling.request({ type: 'auth/pair-system', value: 'disable' }); } async enablePairInfo() { await this.mwse.EventPooling.request({ type: 'connection/pairinfo', value: true }); } async disablePairInfo() { await this.mwse.EventPooling.request({ type: 'connection/pairinfo', value: false }); } async requestPair() { const { message, status } = await this.mwse.EventPooling.request({ type: 'request/pair', to: this.socketId }); if (message === 'ALREADY-PAIRED' || message === 'ALREADY-REQUESTED') { console.warn('MWSE: already paired or pair requested'); } if (status === 'fail') { console.error('MWSE: requestPair failed', status, message); return false; } return true; } async endPair() { await this.mwse.EventPooling.request({ type: 'end/pair', to: this.socketId }); this.mwse.pairs.delete(this.socketId); this.rtc?.destroy(); // WebRTC bağlantısını kapat, akışları durdur this.forget(); } async acceptPair() { const { message, status } = await this.mwse.EventPooling.request({ type: 'accept/pair', to: this.socketId }); if (status === 'fail') { console.error('MWSE: acceptPair failed', status, message); return false; } // Kabul eden tarafta da pairs haritasını güncelle. this.mwse.pairs.set(this.socketId, this); return true; } async rejectPair() { const { message, status } = await this.mwse.EventPooling.request({ type: 'reject/pair', to: this.socketId }); if (status === 'fail') { console.error('MWSE: rejectPair failed', status, message); return false; } this.mwse.pairs.delete(this.socketId); return true; } async getPairedList() { const { value } = await this.mwse.EventPooling.request({ type: 'pair/list', to: this.socketId }); return value; } async send(pack) { const serverOpen = this.mwse.server.connected; // WebRTC signaling (:rtcpack:) MUST always travel via the WebSocket relay — // never over the DataChannel. This is true even after the p2p connection is // established (renegotiation, new-stream ICE candidates, etc.). The DataChannel // does not exist yet when the first offer/answer exchange happens, and may not // be the right path for out-of-band signaling later either. const forceWS = pack.type === ':rtcpack:'; const p2pOpen = !forceWS && this.peerConnection && this.rtc?.active; const channel = (p2pOpen && serverOpen) ? (this.primaryChannel === 'websocket' ? 'websocket' : 'datachannel') : 'websocket'; if (channel === 'websocket') { if (!serverOpen) return; if (!this.mwse.writable && !forceWS) { console.warn('MWSE: socket is not writable'); return; } // WOM — no waiter registered; the engine returns nil for pack/to (#33). this.mwse.EventPooling.only({ type: 'pack/to', pack, to: this.socketId }); } else { this.rtc?.sendMessage(pack); } } forget() { this.mwse.peers.delete(this.socketId); this.mwse.pairs.delete(this.socketId); } }