263 lines
9.0 KiB
JavaScript
263 lines
9.0 KiB
JavaScript
// MWSE SDK — ES module entry point.
|
|
//
|
|
// Load via:
|
|
// <script type="module" src="https://ws.example.com/sdk/index.js"></script>
|
|
// or through the /sdk.js redirect:
|
|
// <script type="module" src="https://ws.example.com/sdk.js"></script>
|
|
//
|
|
// 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;
|
|
|
|
this.server = new Connection(this, options);
|
|
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 <script> that
|
|
// refers to MWSE after the module has loaded).
|
|
if (typeof window !== 'undefined') {
|
|
window.MWSE = MWSE;
|
|
}
|