/** * Abstract Feature base class * Features are composable capabilities that can be attached to devices */ import * as plugins from '../plugins.js'; import type { TFeatureType, TFeatureState, IFeature, IFeatureInfo, IFeatureOptions, } from '../interfaces/feature.interfaces.js'; import type { IRetryOptions } from '../interfaces/index.js'; import { withRetry } from '../helpers/helpers.retry.js'; /** * Forward reference to Device to avoid circular dependency * The actual Device class will set this reference */ export type TDeviceReference = { id: string; name: string; address: string; port: number; }; /** * Abstract base class for all features * Provides common functionality for connection management, state tracking, and retry logic */ export abstract class Feature extends plugins.events.EventEmitter implements IFeature { /** * The feature type identifier */ public abstract readonly type: TFeatureType; /** * The protocol used by this feature */ public abstract readonly protocol: string; /** * The port used by this feature */ protected _port: number; /** * Current feature state */ protected _state: TFeatureState = 'disconnected'; /** * Last error encountered */ protected _lastError?: Error; /** * Reference to the parent device */ protected _device: TDeviceReference; /** * Retry configuration */ protected _retryConfig: Required; /** * Additional metadata from discovery */ protected _metadata: Record; constructor( device: TDeviceReference, port: number, options?: IFeatureOptions ) { super(); this._device = device; this._port = port; this._metadata = options?.metadata ?? {}; // Setup retry config const retryOpts = options?.retryOptions ?? {}; this._retryConfig = { maxRetries: retryOpts.maxRetries ?? 3, baseDelay: retryOpts.baseDelay ?? 1000, maxDelay: retryOpts.maxDelay ?? 30000, multiplier: retryOpts.multiplier ?? 2, jitter: retryOpts.jitter ?? true, }; } // ============================================================================ // Public Properties // ============================================================================ /** * Get the feature port */ public get port(): number { return this._port; } /** * Get the current feature state */ public get state(): TFeatureState { return this._state; } /** * Check if the feature is connected */ public get isConnected(): boolean { return this._state === 'connected'; } /** * Get the last error */ public get lastError(): Error | undefined { return this._lastError; } /** * Get the device address */ public get address(): string { return this._device.address; } /** * Get the device ID */ public get deviceId(): string { return this._device.id; } // ============================================================================ // Connection Management // ============================================================================ /** * Connect to the feature endpoint */ public async connect(): Promise { if (this._state === 'connected') { return; } if (this._state === 'connecting') { throw new Error(`Feature ${this.type} is already connecting`); } this.setState('connecting'); this._lastError = undefined; try { await this.withRetry(() => this.doConnect()); this.setState('connected'); this.emit('connected'); } catch (error) { this._lastError = error instanceof Error ? error : new Error(String(error)); this.setState('error'); this.emit('error', this._lastError); throw this._lastError; } } /** * Disconnect from the feature endpoint */ public async disconnect(): Promise { if (this._state === 'disconnected') { return; } try { await this.doDisconnect(); } finally { this.setState('disconnected'); this.emit('disconnected'); } } // ============================================================================ // Abstract Methods (implemented by subclasses) // ============================================================================ /** * Perform the actual connection logic * Implemented by each feature subclass */ protected abstract doConnect(): Promise; /** * Perform the actual disconnection logic * Implemented by each feature subclass */ protected abstract doDisconnect(): Promise; /** * Get feature-specific info for serialization */ public abstract getFeatureInfo(): IFeatureInfo; // ============================================================================ // Helper Methods // ============================================================================ /** * Set the feature state and emit change event */ protected setState(state: TFeatureState): void { if (this._state !== state) { const oldState = this._state; this._state = state; this.emit('state:changed', { oldState, newState: state }); } } /** * Execute an operation with retry logic */ protected async withRetry(operation: () => Promise): Promise { return withRetry(operation, this._retryConfig); } /** * Get base feature info */ protected getBaseFeatureInfo(): IFeatureInfo { return { type: this.type, protocol: this.protocol, port: this._port, state: this._state, }; } /** * Clear error state */ public clearError(): void { this._lastError = undefined; if (this._state === 'error') { this.setState('disconnected'); } } }