252 lines
5.7 KiB
TypeScript
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');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|