UNPKG

@helios-starling/starling

Version:

Modern WebSocket client for the Helios-Starling protocol

253 lines (207 loc) 7.06 kB
import { BaseStarling, getCurrentTimestamp, NetworkNode } from "@helios-starling/utils"; import { StateManager } from "../managers/state"; import { ReconnectionManager } from "../managers/reconnection"; import { proxyConfiguration } from "../config/proxy.config"; /** * @typedef {import('@helios-starling/utils').BaseStarlingOptions} BaseStarlingOptions */ /** * @typedef {import("@helios-starling/utils").NetworkNodeOptions & { * connectTimeout: number=10000, * reconnect: boolean=true, * reconnectDelay: number=10000, * maxReconnectAttempts: number=5, * state: import("../managers/state").StateManagerOptions, * reconnection: import("../managers/reconnection").ReconnectionOptions * }} StarlingOptions */ /** * Client Starling instance */ export class Starling extends NetworkNode { /** * @param {string|URL} url * @param {StarlingOptions} options */ constructor(url, options = {}) { super({ builtInMethods: {}, proxyConfiguration }, {...options}); this._options = { connectTimeout: 10000, reconnect: true, reconnectDelay: 10000, maxReconnectAttempts: 5, ...options } this.url = url instanceof URL ? url : new URL(url); this._starling = new BaseStarling({ id: this.id, networkNode: this, ...options }, this.events); this._starling.events = this.events; this._stateManager = new StateManager(this, this._options?.state || {}); this._reconnection = new ReconnectionManager(this, this._options.reconnection || {}); this.send = this._starling.send.bind(this._starling); this.notify = this._starling.notify.bind(this._starling); this.request = this._starling.request.bind(this._starling); this.sendError = this._starling.sendError.bind(this._starling); } get _ws() { return this._starling._ws; } set _ws(ws) { this._starling._ws = ws; } /** * Connects to the Helios server * @returns {Promise<void>} * @throws {Error} */ async connect() { if (this.connected || this.connecting) { throw new Error('Already connected or connecting'); } try { const token = this.state.token; if (token) { this.url.searchParams.set('recover', token); } return await new Promise((resolve, reject) => { this._ws = new WebSocket(this.url); this._ws.onopen = () => { console.log('✨ Starling connected to Helios server'); this._lastConnected = getCurrentTimestamp(); console.log('Starling connected'); this._starling.events.emit('starling:connected', { debug: { type: 'connection', message: `Starling connected` } }); this._starling._buffer.flush(); resolve(); }; this._ws.onclose = () => this._handleClose(); this._ws.onerror = error => this._handleError(error); this._ws.onmessage = message => this._starling.handleMessage(message.data); // Timeout setTimeout(() => { if (!this.connected) { reject(new Error('Connection timeout')); } }, this._options.connectTimeout || 10000); }) } catch (error) { this.events.emit('starling:connect:failed', { error, debug: { type: 'error', message: `Failed to connect to Starling: ${error.message}` } }); throw error; } } /** * Closes the WebSocket connection * @param {string} [reason] Reason for closing * @returns {Promise<void>} */ async disconnect(reason = 'Connection closed') { if (!this._ws) return; this._reconnection.stop(); this._starling._requests.cancelAll(reason); this._starling._buffer.flush(); this._starling._data.clear(); return new Promise((resolve) => { const cleanup = () => { this._ws = null; resolve(); }; try { this._ws.onclose = cleanup; this._ws.close(1000, reason); } catch (error) { console.error('Failed to close connection:', error); cleanup(); } }) } async sync() { try { const token = await this._stateManager.refresh(); return token; } catch (error) { throw new Error(`Failed to get recovery token: ${error.message}`); } } /** * Gère la fermeture de la connexion * @private */ _handleClose() { this._ws = null; console.log('❌ Starling disconnected'); this._emitEvent('starling:disconnected', { lastConnected: this._lastConnected, debug: { type: 'disconnection', message: `Starling disconnected` } }); if (this._options.reconnect) { this._reconnection.start(); } } /** * Gère les erreurs de connexion * @private */ _handleError(error) { console.error('Starling error:', error); this._emitEvent('error', { error, timestamp: getCurrentTimestamp() }); } /** * Émet un événement * @private */ _emitEvent(type, data) { this.events.emit(type, { ...data, timestamp: getCurrentTimestamp() }); } get createdAt() { return this._starling.createdAt; } get state() { return this._starling.state; } get isConnected() { return this._starling.isConnected; } get lastConnected() { return this._starling.lastConnected; } get data() { return this._starling.data; } get connected() { return this._ws && this._ws.readyState === WebSocket.OPEN; } get connecting() { return this._ws && this._ws.readyState === WebSocket.CONNECTING; } get closing() { return this._ws && this._ws.readyState === WebSocket.CLOSING; } get closed() { return this._ws && this._ws.readyState === WebSocket.CLOSED; } }