// MWSE SDK — ES module entry point. // // Load via: // // or through the /sdk.js redirect: // // // Because this is an ES module, all imports below resolve relative to // import.meta.url (= the URL of this file on the MWSE server). Every other SDK // file therefore loads from the same origin automatically — no bundler needed. // // Version handshake: // On connect the server sends a wsts/hello signal carrying its version string. // If it does not match SDK_VERSION the SDK fires an 'error' event, closes the // connection, and never fires 'scope'. This prevents accidental use of an SDK // against an incompatible engine. import { SDK_VERSION } from './version.js'; import { Connection } from './Connection.js'; import WSTSProtocol from './WSTSProtocol.js'; import EventPool from './EventPool.js'; import { IPPressure } from './IPPressure.js'; import Peer from './Peer.js'; import Room from './Room.js'; export default class MWSE { constructor(options) { this.rooms = new Map(); this.pairs = new Map(); this.peers = new Map(); this.writable = 1; this.readable = 1; this._events = {}; this.activeScope = false; // Default endpoint to 'auto' (SDK reads import.meta.url → same origin). const opts = typeof options === 'string' ? { endpoint: options } : { endpoint: 'auto', ...options }; this.server = new Connection(this, opts); this.WSTSProtocol = new WSTSProtocol(this); this.EventPooling = new EventPool(this); this.virtualPressure = new IPPressure(this); this.me = new Peer(this); this.me.scope(() => { this.peers.set('me', this.me); this.peers.set(this.me.socketId, this.me); }); this._wireSignals(); this.server.connect(); // Version handshake happens before scope. onActive waits for wsts/hello; // only on success does it fire the user's scope callbacks. this.server.onActive(async () => { try { await this._awaitHello(); } catch (err) { this.emit('error', err); return; } this.me.setSocketId('me'); await this.me.metadata(); this.emit('scope'); this.activeScope = true; }); this.server.onPassive(() => { this.emit('close'); }); } // ---- Version handshake ---------------------------------------------- // _awaitHello waits for the server's wsts/hello signal and validates the // version. Resolves on success; rejects (and closes the connection) on // mismatch or timeout. _awaitHello() { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('MWSE: wsts/hello timeout — server did not send a version handshake')); }, 5000); this.EventPooling.signal('wsts/hello', ({ v, codecs }) => { clearTimeout(timer); if (v !== SDK_VERSION) { this.server.disconnect(); reject(new Error( `MWSE version mismatch — server: ${v}, SDK: ${SDK_VERSION}. ` + 'Update both to the same version.' )); return; } // Negotiate the best codec both sides support. this.WSTSProtocol.codec.negotiate(codecs || [0]); resolve(); }); }); } // ---- Event emitter (base, inlined) ---------------------------------- on(eventName, callback) { (this._events[eventName] ??= []).push(callback); } emit(eventName, ...args) { for (const cb of (this._events[eventName] || [])) cb(...args); } // scope(f) fires f now if already in scope, otherwise on the next 'scope' event. scope(f) { if (this.activeScope) f(); else this.on('scope', f); } // ---- Peer helpers --------------------------------------------------- room(options) { if (typeof options === 'string') { if (this.rooms.has(options)) return this.rooms.get(options); } const r = new Room(this); r.setRoomOptions(options); this.emit('room'); return r; } peer(options, isActive = false) { if (typeof options === 'string') { if (this.peers.has(options)) return this.peers.get(options); if (this.pairs.has(options)) return this.pairs.get(options); } const p = new Peer(this); p.setPeerOptions(options); p.active = isActive; this.peers.set(p.socketId, p); this.emit('peer', p); return p; } async request(peerId, pack) { const { pack: answer } = await this.EventPooling.request({ type: 'request/to', to: peerId, pack }); return answer; } async response(peerId, requestId, pack) { this.WSTSProtocol.SendOnly({ type: 'response/to', to: peerId, pack, id: requestId }); } // ---- Session flags -------------------------------------------------- enableRecaiveData() { this.WSTSProtocol.SendOnly({ type: 'connection/packrecaive', value: 1 }); this.readable = 1; } disableRecaiveData() { this.WSTSProtocol.SendOnly({ type: 'connection/packrecaive', value: 0 }); this.readable = 0; } enableSendData() { this.WSTSProtocol.SendOnly({ type: 'connection/packsending', value: 1 }); this.writable = 1; } disableSendData() { this.WSTSProtocol.SendOnly({ type: 'connection/packsending', value: 0 }); this.writable = 0; } enableNotifyRoomInfo() { this.WSTSProtocol.SendOnly({ type: 'connection/roominfo', value: 1 }); } disableNotifyRoomInfo() { this.WSTSProtocol.SendOnly({ type: 'connection/roominfo', value: 0 }); } destroy() { this.server.disconnect(); } // ---- Signal wiring -------------------------------------------------- _wireSignals() { const ep = this.EventPooling; ep.signal('pack', ({ from, pack }) => { if (this.readable) { this.peer(from, true).emit('pack', pack); } }); ep.signal('request', ({ from, pack, id }) => { const scope = { body: pack, response: (replyPack) => this.response(from, id, replyPack), peer: this.peer(from, true) }; this.peer(from, true).emit('request', scope); this.peer('me').emit('request', scope); }); ep.signal('pack/room', ({ from, pack, sender }) => { if (this.readable) { this.room(from).emit('message', pack, this.peer(sender)); } }); ep.signal('room/joined', ({ id, roomid }) => { const room = this.room(roomid); const peer = this.peer(id, true); room.peers.set(peer.socketId, peer); room.emit('join', peer); }); ep.signal('room/info', ({ roomId, name, value }) => { this.room(roomId).emit('updateinfo', name, value); }); ep.signal('room/ejected', ({ id, roomid }) => { const room = this.room(roomid); const peer = this.peer(id, true); room.peers.delete(peer.socketId); room.emit('eject', peer); }); ep.signal('room/closed', ({ roomid }) => { const room = this.room(roomid); room.peers.clear(); room.emit('close'); this.rooms.delete(roomid); }); ep.signal('pair/info', ({ from, name, value }) => { this.peer(from, true).info.info[name] = value; this.peer(from, true).emit('info', name, value); }); ep.signal('request/pair', ({ from, info }) => { const peer = this.peer(from, true); peer.info.info = info; peer.emit('request/pair', peer); this.peer('me').emit('request/pair', peer); }); ep.signal('peer/disconnect', ({ id }) => { this.peer(id, true).emit('disconnect'); }); ep.signal('accepted/pair', ({ from, info }) => { const peer = this.peer(from, true); peer.info.info = info; peer.emit('accepted/pair', peer); this.peer('me').emit('accepted/pair', peer); }); ep.signal('end/pair', ({ from, info }) => { const peer = this.peer(from, true); peer.emit('end/pair', info); this.peer('me').emit('end/pair', from, info); }); // server/pack — message pushed by the application server via /api/client/:id/send ep.signal('server/pack', ({ from, fromServer, pack }) => { if (this.readable) { this.emit('server/pack', { from, fromServer, pack }); } }); } } // Expose on window for non-module usage patterns (e.g. inline