UNPKG

@helios-starling/starling

Version:

Modern WebSocket client for the Helios-Starling protocol

272 lines (233 loc) 6.92 kB
// managers/state.js import { getCurrentTimestamp } from '@helios-starling/utils'; /** * @typedef {Object} StateManagerOptions * @property {number} [refreshInterval=300000] Refresh interval (5min) * @property {number} [minRefreshInterval=60000] Minimum interval between refreshes (1min) * @property {number} [retryAttempts=3] Number of retry attempts on failure * @property {number} [retryDelay=1000] Delay between retry attempts (ms) * @property {boolean} [forceRefreshOnReconnect=true] Force a refresh after reconnect * @property {boolean} [debug=false] Enable debug logs */ /** * Manages connection state and synchronization with the server */ export class StateManager { /** * @param {import('../core/starling').Starling} starling Starling instance * @param {StateManagerOptions} [options] Configuration options */ constructor(starling, options = {}) { /** @private */ this._starling = starling; /** @private */ this._options = { refreshInterval: 5 * 60 * 1000, // 5 minutes minRefreshInterval: 60 * 1000, // 1 minute retryAttempts: 3, retryDelay: 1000, forceRefreshOnReconnect: true, debug: false, ...options }; /** @private */ this._token = null; /** @private */ this._lastRefresh = null; /** @private */ this._refreshTimer = null; /** @private */ this._refreshing = false; /** @private */ this._metrics = { connectionAttempts: 0, reconnections: 0, lastDisconnect: null, totalDowntime: 0, refreshes: 0, refreshFailures: 0 }; // Bind event handlers this._bindEvents(); } /** * Refresh the state token * @param {Object} [options] Refresh options * @param {boolean} [options.force=false] Ignore minimum interval * @param {number} [options.timeout] Custom timeout * @returns {Promise<string>} Recovery token */ async refresh(options = {}) { if (this._refreshing) { throw new Error('Refresh already in progress'); } const now = getCurrentTimestamp(); // Vérifier l'intervalle minimum sauf si forcé if (!options.force && this._lastRefresh) { const elapsed = now - this._lastRefresh; if (elapsed < this._options.minRefreshInterval) { throw new Error(`Minimum refresh interval not reached (${Math.floor((this._options.minRefreshInterval - elapsed) / 1000)}s remaining)`); } } this._refreshing = true; let attempts = 0; try { while (attempts < this._options.retryAttempts) { try { const response = await this._starling.request('starling:state', null, { timeout: options.timeout }); this._token = response.token; this._lastRefresh = now; this._metrics.refreshes++; this._debug('Token refreshed successfully'); this._scheduleNextRefresh(); // Émettre l'événement de succès this._emitRefreshSuccess(); return this._token; } catch (error) { attempts++; this._metrics.refreshFailures++; this._debug(`Refresh attempt ${attempts} failed: ${error.message}`); if (attempts >= this._options.retryAttempts) { throw error; } // Attendre avant de réessayer await new Promise(resolve => setTimeout(resolve, this._options.retryDelay)); } } } finally { this._refreshing = false; } } /** * Ensure a fresh token * @returns {Promise<string>} Recovery token */ async ensureFreshToken() { const now = getCurrentTimestamp(); // If no token or last refresh time, refresh immediately if (!this._token || !this._lastRefresh) { return this.refresh({ force: true }); } // If refresh interval has passed, refresh immediately if (now - this._lastRefresh >= this._options.refreshInterval) { return this.refresh(); } return this._token; } /** * Get the current token * @returns {string|null} Recovery token or null */ get token() { return this._token; } /** * Get connection metrics * @returns {Object} Metrics object */ get metrics() { return { ...this._metrics, lastRefresh: this._lastRefresh, refreshing: this._refreshing, connected: this._starling.connected, uptime: this._calculateUptime() }; } /** * Reset connection metrics */ resetMetrics() { this._metrics = { connectionAttempts: 0, reconnections: 0, lastDisconnect: null, totalDowntime: 0, refreshes: 0, refreshFailures: 0 }; } /** * Bind event listeners * @private */ _bindEvents() { // Handle reconnections this._starling.events.on('starling:connected', () => { if (this._metrics.lastDisconnect) { this._metrics.reconnections++; this._metrics.totalDowntime += getCurrentTimestamp() - this._metrics.lastDisconnect; // Start the refresh timer if (this._options.forceRefreshOnReconnect) { this.refresh({ force: true }).catch(error => { this._debug(`Force refresh after reconnect failed: ${error.message}`); }); } } }); this._starling.events.on('starling:disconnected', () => { this._metrics.lastDisconnect = getCurrentTimestamp(); // Stop the refresh timer if (this._refreshTimer) { clearTimeout(this._refreshTimer); this._refreshTimer = null; } }); } /** * Schedule the next refresh * @private */ _scheduleNextRefresh() { if (this._refreshTimer) { clearTimeout(this._refreshTimer); } this._refreshTimer = setTimeout(() => { this.refresh().catch(error => { this._debug(`Scheduled refresh failed: ${error.message}`); }); }, this._options.refreshInterval); } /** * Calculate the current uptime * @returns {number} Uptime in milliseconds * @private */ _calculateUptime() { const totalTime = getCurrentTimestamp() - this._starling.createdAt; return totalTime - this._metrics.totalDowntime; } /** * Emit an event of success for refresh * @private */ _emitRefreshSuccess() { this._starling.events.emit('state:refreshed', { token: this._token, metrics: this.getMetrics(), debug: { type: 'info', message: 'State token refreshed successfully' } }); } /** * Debug log * @param {string} message Debug message * @private */ _debug(message) { if (this._options.debug) { this._starling.events.emit('state:debug', { message, timestamp: getCurrentTimestamp(), debug: { type: 'debug', message: `[StateManager] ${message}` } }); } } }