MWSE/sdk/Connection.js

108 lines
3.4 KiB
JavaScript

// WebSocket lifecycle management.
// import.meta.url is used for the 'auto' endpoint mode so the SDK always
// connects back to the same server it was downloaded from.
export class Connection {
constructor(mwse, options) {
this.mwse = mwse;
this.connected = false;
this.autoPair = false;
this.autoReconnect = true;
this.autoReconnectTimeout = 3000;
this.autoReconnectTimer = undefined;
this._activeCallbacks = [];
this._passiveCallbacks = [];
this._packCallbacks = [];
if (options.endpoint === 'auto') {
// In ES modules document.currentScript is null; use import.meta.url
// instead — it resolves to the URL the SDK was actually loaded from.
const scriptURL = new URL(import.meta.url);
const isSecure = scriptURL.protocol === 'https:';
scriptURL.protocol = isSecure ? 'wss:' : 'ws:';
// Strip /index.js (or any filename) so we connect to the server root.
scriptURL.pathname = scriptURL.pathname.replace(/\/[^/]+$/, '/');
this.endpoint = scriptURL;
} else {
try {
this.endpoint = new URL(options.endpoint);
} catch {
throw new Error('MWSE: endpoint is required and must be a valid URL');
}
}
if (typeof options.autoReconnect === 'boolean') {
this.autoReconnect = options.autoReconnect;
} else if (options.autoReconnect) {
this.autoReconnect = true;
this.autoReconnectTimeout = options.autoReconnect.timeout;
}
}
connect() {
if (this.autoReconnectTimer) clearTimeout(this.autoReconnectTimer);
this.ws = new WebSocket(this.endpoint.href);
this._attachEvents();
}
disconnect() {
// Prevent auto-reconnect when the caller explicitly closes.
this.autoReconnect = false;
this.ws.close();
}
_attachEvents() {
this.ws.addEventListener('open', () => this._onOpen());
this.ws.addEventListener('close', () => this._onClose());
this.ws.addEventListener('error', () => this._onError());
this.ws.addEventListener('message', ({ data }) => this._onMessage(data));
}
_onOpen() {
this.connected = true;
for (const cb of this._activeCallbacks) cb();
}
_onClose() {
for (const cb of this._passiveCallbacks) cb();
this.connected = false;
if (this.autoReconnect) {
this.autoReconnectTimer = setTimeout(
() => this.connect(),
this.autoReconnectTimeout
);
}
}
_onError() {
this.connected = false;
}
_onMessage(data) {
if (typeof data === 'string') {
const parsed = JSON.parse(data);
for (const cb of this._packCallbacks) cb(parsed);
} else if (data instanceof ArrayBuffer) {
// Binary frame — passed raw to WSTSProtocol for codec decoding.
for (const cb of this._packCallbacks) cb(data);
}
}
onRecaivePack(fn) { this._packCallbacks.push(fn); }
onActive(fn) {
if (this.connected) fn();
else this._activeCallbacks.push(fn);
}
onPassive(fn) {
if (!this.connected) fn();
else this._passiveCallbacks.push(fn);
}
tranferToServer(data) {
if (this.connected) this.ws.send(data);
}
}