// 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 }) => {
const peer = this.peer(id, true);
this.pairs.delete(id);
peer.rtc?.destroy(); // WebSocket kopunca (sayfa yenileme vb.) RTC da kapat
peer.emit('disconnect');
this.peer('me').emit('peer/disconnect', peer);
});
ep.signal('accepted/pair', ({ from, info }) => {
const peer = this.peer(from, true);
peer.info.info = info;
// İstek gönderen tarafta pairs haritasını doldur.
this.pairs.set(from, peer);
peer.emit('accepted/pair', peer);
this.peer('me').emit('accepted/pair', peer);
});
ep.signal('end/pair', ({ from, info }) => {
const peer = this.peer(from, true);
this.pairs.delete(from);
peer.rtc?.destroy(); // karşı taraf endPair çağırdığında da RTC kapat
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