MWSE/sdk/index.js

275 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
// 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 <script> that
// refers to MWSE after the module has loaded).
if (typeof window !== 'undefined') {
window.MWSE = MWSE;
}