@layr-labs/hourglass-performer
Version:
TypeScript SDK for building Hourglass AVS performers
353 lines (304 loc) • 9.69 kB
text/typescript
// Enhanced task processing framework with serialization and validation
import { TaskRequest, TaskResponse, TaskValidationError, TaskExecutionError } from '../types/performer';
import { IWorker } from '../worker/iWorker';
import { TaskContext, TaskContextBuilder, TaskPipeline, ValidationStage, TaskMetrics } from './taskContext';
import { ILogger } from '../utils/logger';
/**
* Serialization strategy interface
*/
export interface SerializationStrategy {
/** Strategy name */
name: string;
/** Serialize data to bytes */
serialize(data: any): Uint8Array;
/** Deserialize bytes to data */
deserialize<T = any>(bytes: Uint8Array): T;
/** Validate if bytes can be deserialized */
canDeserialize(bytes: Uint8Array): boolean;
}
/**
* JSON serialization strategy
*/
export class JsonSerializationStrategy implements SerializationStrategy {
name = 'json';
serialize(data: any): Uint8Array {
const json = JSON.stringify(data);
return new TextEncoder().encode(json);
}
deserialize<T = any>(bytes: Uint8Array): T {
const json = new TextDecoder().decode(bytes);
return JSON.parse(json);
}
canDeserialize(bytes: Uint8Array): boolean {
try {
const json = new TextDecoder().decode(bytes);
JSON.parse(json);
return true;
} catch {
return false;
}
}
}
/**
* Raw bytes serialization strategy
*/
export class RawSerializationStrategy implements SerializationStrategy {
name = 'raw';
serialize(data: any): Uint8Array {
if (data instanceof Uint8Array) {
return data;
}
if (typeof data === 'string') {
return new TextEncoder().encode(data);
}
throw new Error('Raw serialization only supports Uint8Array or string');
}
deserialize<T = any>(bytes: Uint8Array): T {
return bytes as T;
}
canDeserialize(bytes: Uint8Array): boolean {
return true;
}
}
/**
* Number serialization strategy (64-bit big-endian)
*/
export class NumberSerializationStrategy implements SerializationStrategy {
name = 'number';
serialize(data: any): Uint8Array {
const num = Number(data);
if (isNaN(num)) {
throw new Error('Invalid number for serialization');
}
const bytes = new Uint8Array(8);
const view = new DataView(bytes.buffer);
view.setBigUint64(0, BigInt(num), false); // big-endian
return bytes;
}
deserialize<T = any>(bytes: Uint8Array): T {
if (bytes.length !== 8) {
throw new Error('Number deserialization requires exactly 8 bytes');
}
const view = new DataView(bytes.buffer);
return Number(view.getBigUint64(0, false)) as T; // big-endian
}
canDeserialize(bytes: Uint8Array): boolean {
return bytes.length === 8;
}
}
/**
* Task processing configuration
*/
export interface TaskProcessorConfig {
/** Default timeout in milliseconds */
timeout?: number;
/** Enable metrics collection */
enableMetrics?: boolean;
/** Custom validation stages */
validationStages?: ValidationStage[];
/** Default serialization strategy */
defaultSerialization?: SerializationStrategy;
/** Available serialization strategies */
serializationStrategies?: Map<string, SerializationStrategy>;
}
/**
* Enhanced task processor with pipeline support
*/
export class TaskProcessor {
private config: Required<TaskProcessorConfig>;
private pipeline: TaskPipeline | undefined;
private metrics: TaskMetrics[] = [];
constructor(
private worker: IWorker,
private logger: ILogger,
config: TaskProcessorConfig = {}
) {
// Set up default configuration
this.config = {
timeout: config.timeout ?? 5000,
enableMetrics: config.enableMetrics ?? true,
validationStages: config.validationStages ?? [],
defaultSerialization: config.defaultSerialization ?? new JsonSerializationStrategy(),
serializationStrategies: config.serializationStrategies ?? new Map([
['json', new JsonSerializationStrategy()],
['raw', new RawSerializationStrategy()],
['number', new NumberSerializationStrategy()],
]),
};
this.setupPipeline();
}
/**
* Process a task with full pipeline support
*/
async processTask(request: TaskRequest): Promise<TaskResponse> {
const contextBuilder = new TaskContextBuilder(request, this.logger, this.config.timeout);
const context = contextBuilder.build();
try {
// Execute the processing pipeline
if (this.pipeline) {
await this.pipeline.execute(context);
}
// Execute the worker task
const response = await this.executeWorkerTask(context);
// Record success metrics
if (this.config.enableMetrics) {
this.recordTaskCompletion(context, response);
}
return response;
} catch (error) {
// Record failure metrics
if (this.config.enableMetrics) {
this.recordTaskFailure(context, error);
}
// Re-throw the error for gRPC handling
throw error;
}
}
/**
* Execute the worker task with enhanced error handling
*/
private async executeWorkerTask(context: TaskContext): Promise<TaskResponse> {
const { request } = context;
try {
// Validate task using worker
await this.worker.validateTask(request);
context.logger.debug('Task validation passed');
// Handle task using worker
const response = await this.worker.handleTask(request);
context.logger.debug('Task execution completed', {
resultSize: response.result.length
});
// Validate response
this.validateResponse(response, request);
return response;
} catch (error) {
if (error instanceof TaskValidationError || error instanceof TaskExecutionError) {
throw error;
}
// Wrap unknown errors
throw new TaskExecutionError(
error instanceof Error ? error.message : 'Unknown task execution error',
request.taskId
);
}
}
/**
* Validate task response
*/
private validateResponse(response: TaskResponse, request: TaskRequest): void {
if (response.taskId !== request.taskId) {
throw new TaskExecutionError(
`Response task ID (${response.taskId}) does not match request task ID (${request.taskId})`,
request.taskId
);
}
if (!response.result) {
throw new TaskExecutionError(
'Task response must include a result',
request.taskId
);
}
}
/**
* Set up the processing pipeline
*/
private setupPipeline(): void {
this.pipeline = new TaskPipeline();
// Add built-in validation stages
this.pipeline.addStage(new ValidationStage(async (context) => {
this.validateTaskRequest(context.request);
}));
// Add custom validation stages
for (const stage of this.config.validationStages) {
this.pipeline.addStage(stage);
}
// Add metrics collection stage if enabled
if (this.config.enableMetrics) {
this.pipeline.addStage({
name: 'metrics-start',
execute: async (context) => {
const metrics: TaskMetrics = {
taskId: context.request.taskId,
executionId: context.executionId,
startTime: context.startTime,
payloadSize: context.request.payload.length,
};
context.metadata.set('metrics', metrics);
},
});
}
}
/**
* Validate task request
*/
private validateTaskRequest(request: TaskRequest): void {
if (!request.taskId) {
throw new TaskValidationError('Task ID is required', '');
}
if (!request.payload) {
throw new TaskValidationError('Task payload is required', request.taskId);
}
if (request.payload.length === 0) {
throw new TaskValidationError('Task payload cannot be empty', request.taskId);
}
}
/**
* Record successful task completion metrics
*/
private recordTaskCompletion(context: TaskContext, response: TaskResponse): void {
const metrics = context.metadata.get('metrics') as TaskMetrics;
if (metrics) {
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.success = true;
metrics.resultSize = response.result.length;
this.metrics.push(metrics);
context.logger.info('Task completed successfully', {
duration: `${metrics.duration}ms`,
payloadSize: metrics.payloadSize,
resultSize: metrics.resultSize,
});
}
}
/**
* Record task failure metrics
*/
private recordTaskFailure(context: TaskContext, error: any): void {
const metrics = context.metadata.get('metrics') as TaskMetrics;
if (metrics) {
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.success = false;
metrics.error = error instanceof Error ? error.message : 'Unknown error';
this.metrics.push(metrics);
context.logger.error('Task execution failed', {
duration: `${metrics.duration}ms`,
error: metrics.error,
});
}
}
/**
* Get serialization strategy by name
*/
getSerializationStrategy(name: string): SerializationStrategy | undefined {
return this.config.serializationStrategies.get(name);
}
/**
* Add custom serialization strategy
*/
addSerializationStrategy(strategy: SerializationStrategy): void {
this.config.serializationStrategies.set(strategy.name, strategy);
}
/**
* Get task execution metrics
*/
getMetrics(): TaskMetrics[] {
return [...this.metrics];
}
/**
* Clear accumulated metrics
*/
clearMetrics(): void {
this.metrics = [];
}
}