Files
devicemanager/ts/features/feature.abstract.ts

252 lines
5.7 KiB
TypeScript

/**
* 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<IRetryOptions>;
/**
* Additional metadata from discovery
*/
protected _metadata: Record<string, unknown>;
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<void> {
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<void> {
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<void>;
/**
* Perform the actual disconnection logic
* Implemented by each feature subclass
*/
protected abstract doDisconnect(): Promise<void>;
/**
* 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<T>(operation: () => Promise<T>): Promise<T> {
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');
}
}
}