diff --git a/packages/spec/src/system/events.test.ts b/packages/spec/src/system/events.test.ts index 2a3e20ca3..830778a00 100644 --- a/packages/spec/src/system/events.test.ts +++ b/packages/spec/src/system/events.test.ts @@ -1,16 +1,48 @@ import { describe, it, expect } from 'vitest'; import { + EventPriority, + EVENT_PRIORITY_VALUES, EventMetadataSchema, EventSchema, EventHandlerSchema, EventRouteSchema, EventPersistenceSchema, + EventTypeDefinitionSchema, + EventQueueConfigSchema, + EventReplayConfigSchema, + EventSourcingConfigSchema, + DeadLetterQueueEntrySchema, + EventLogEntrySchema, + EventWebhookConfigSchema, + EventMessageQueueConfigSchema, + RealTimeNotificationConfigSchema, + EventBusConfigSchema, type Event, type EventHandler, type EventRoute, type EventPersistence, + type EventTypeDefinition, + type EventBusConfig, } from './events.zod'; +describe('EventPriority', () => { + it('should accept valid event priorities', () => { + expect(() => EventPriority.parse('critical')).not.toThrow(); + expect(() => EventPriority.parse('high')).not.toThrow(); + expect(() => EventPriority.parse('normal')).not.toThrow(); + expect(() => EventPriority.parse('low')).not.toThrow(); + expect(() => EventPriority.parse('background')).not.toThrow(); + }); + + it('should have correct priority values', () => { + expect(EVENT_PRIORITY_VALUES.critical).toBe(0); + expect(EVENT_PRIORITY_VALUES.high).toBe(1); + expect(EVENT_PRIORITY_VALUES.normal).toBe(2); + expect(EVENT_PRIORITY_VALUES.low).toBe(3); + expect(EVENT_PRIORITY_VALUES.background).toBe(4); + }); +}); + describe('EventMetadataSchema', () => { it('should accept valid metadata', () => { const metadata = { @@ -27,11 +59,36 @@ describe('EventMetadataSchema', () => { timestamp: '2024-01-15T10:30:00Z', userId: 'user-123', tenantId: 'tenant-456', + correlationId: 'corr-789', + causationId: 'cause-456', }; const parsed = EventMetadataSchema.parse(metadata); expect(parsed.userId).toBe('user-123'); expect(parsed.tenantId).toBe('tenant-456'); + expect(parsed.correlationId).toBe('corr-789'); + expect(parsed.causationId).toBe('cause-456'); + }); + + it('should apply default priority', () => { + const metadata = EventMetadataSchema.parse({ + source: 'plugin', + timestamp: '2024-01-15T10:30:00Z', + }); + + expect(metadata.priority).toBe('normal'); + }); + + it('should accept different priorities', () => { + const priorities: Array = ['critical', 'high', 'normal', 'low', 'background']; + priorities.forEach(priority => { + const metadata = EventMetadataSchema.parse({ + source: 'plugin', + timestamp: '2024-01-15T10:30:00Z', + priority, + }); + expect(metadata.priority).toBe(priority); + }); }); it('should validate datetime format', () => { @@ -433,3 +490,333 @@ describe('Event System Integration', () => { expect(() => EventPersistenceSchema.parse(persistence)).not.toThrow(); }); }); + +describe('EventTypeDefinitionSchema', () => { + it('should accept valid event type definition', () => { + const eventType: EventTypeDefinition = { + name: 'order.created', + version: '1.0.0', + schema: { + type: 'object', + properties: { + orderId: { type: 'string' }, + customerId: { type: 'string' }, + total: { type: 'number' }, + }, + }, + }; + + expect(() => EventTypeDefinitionSchema.parse(eventType)).not.toThrow(); + }); + + it('should apply default values', () => { + const eventType = EventTypeDefinitionSchema.parse({ + name: 'user.created', + }); + + expect(eventType.version).toBe('1.0.0'); + expect(eventType.deprecated).toBe(false); + }); +}); + +describe('EventQueueConfigSchema', () => { + it('should accept queue config', () => { + const config = { + name: 'events', + concurrency: 20, + retryPolicy: { + maxRetries: 5, + backoffStrategy: 'exponential', + }, + deadLetterQueue: 'failed_events', + }; + + const parsed = EventQueueConfigSchema.parse(config); + expect(parsed.concurrency).toBe(20); + }); + + it('should apply default values', () => { + const config = EventQueueConfigSchema.parse({}); + expect(config.name).toBe('events'); + expect(config.concurrency).toBe(10); + expect(config.priorityEnabled).toBe(true); + }); +}); + +describe('EventReplayConfigSchema', () => { + it('should accept replay config', () => { + const config = { + fromTimestamp: '2024-01-01T00:00:00Z', + toTimestamp: '2024-01-31T23:59:59Z', + eventTypes: ['order.created', 'order.updated'], + speed: 10, + }; + + const parsed = EventReplayConfigSchema.parse(config); + expect(parsed.speed).toBe(10); + expect(parsed.eventTypes).toHaveLength(2); + }); + + it('should apply default speed', () => { + const config = EventReplayConfigSchema.parse({ + fromTimestamp: '2024-01-01T00:00:00Z', + }); + + expect(config.speed).toBe(1); + }); +}); + +describe('EventSourcingConfigSchema', () => { + it('should accept event sourcing config', () => { + const config = { + enabled: true, + snapshotInterval: 100, + retention: 365, + aggregateTypes: ['order', 'customer'], + }; + + const parsed = EventSourcingConfigSchema.parse(config); + expect(parsed.snapshotInterval).toBe(100); + }); + + it('should apply defaults', () => { + const config = EventSourcingConfigSchema.parse({}); + expect(config.enabled).toBe(false); + expect(config.snapshotInterval).toBe(100); + expect(config.snapshotRetention).toBe(10); + expect(config.retention).toBe(365); + }); +}); + +describe('DeadLetterQueueEntrySchema', () => { + it('should accept dead letter queue entry', () => { + const entry = { + id: 'dlq-123', + event: { + name: 'user.created', + payload: { userId: '123' }, + metadata: { + source: 'system', + timestamp: '2024-01-15T10:00:00Z', + }, + }, + error: { + message: 'Handler timeout', + code: 'TIMEOUT', + }, + retries: 3, + firstFailedAt: '2024-01-15T10:00:00Z', + lastFailedAt: '2024-01-15T10:30:00Z', + failedHandler: 'email_handler', + }; + + expect(() => DeadLetterQueueEntrySchema.parse(entry)).not.toThrow(); + }); +}); + +describe('EventLogEntrySchema', () => { + it('should accept event log entry', () => { + const log = { + id: 'log-123', + event: { + name: 'order.created', + payload: { orderId: '789' }, + metadata: { + source: 'ecommerce', + timestamp: '2024-01-15T10:00:00Z', + }, + }, + status: 'completed', + handlersExecuted: [ + { + handlerId: 'email_handler', + status: 'success', + durationMs: 150, + }, + { + handlerId: 'analytics_handler', + status: 'success', + durationMs: 80, + }, + ], + receivedAt: '2024-01-15T10:00:00Z', + processedAt: '2024-01-15T10:00:01Z', + totalDurationMs: 1000, + }; + + const parsed = EventLogEntrySchema.parse(log); + expect(parsed.handlersExecuted).toHaveLength(2); + }); +}); + +describe('EventWebhookConfigSchema', () => { + it('should accept webhook config', () => { + const webhook = { + eventPattern: 'order.*', + url: 'https://api.example.com/webhooks', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + authentication: { + type: 'bearer', + credentials: { token: 'secret' }, + }, + retryPolicy: { + maxRetries: 3, + backoffStrategy: 'exponential', + }, + timeoutMs: 30000, + }; + + const parsed = EventWebhookConfigSchema.parse(webhook); + expect(parsed.method).toBe('POST'); + expect(parsed.timeoutMs).toBe(30000); + }); + + it('should apply defaults', () => { + const webhook = EventWebhookConfigSchema.parse({ + eventPattern: 'test.*', + url: 'https://example.com/hook', + }); + + expect(webhook.method).toBe('POST'); + expect(webhook.timeoutMs).toBe(30000); + expect(webhook.enabled).toBe(true); + }); +}); + +describe('EventMessageQueueConfigSchema', () => { + it('should accept message queue config', () => { + const config = { + provider: 'kafka', + topic: 'events', + eventPattern: 'order.*', + partitionKey: 'metadata.tenantId', + format: 'json', + compression: 'gzip', + batchSize: 100, + }; + + const parsed = EventMessageQueueConfigSchema.parse(config); + expect(parsed.provider).toBe('kafka'); + expect(parsed.batchSize).toBe(100); + }); + + it('should apply defaults', () => { + const config = EventMessageQueueConfigSchema.parse({ + provider: 'rabbitmq', + topic: 'events', + }); + + expect(config.eventPattern).toBe('*'); + expect(config.format).toBe('json'); + expect(config.compression).toBe('none'); + expect(config.batchSize).toBe(1); + }); +}); + +describe('RealTimeNotificationConfigSchema', () => { + it('should accept realtime config', () => { + const config = { + enabled: true, + protocol: 'websocket', + eventPattern: 'notification.*', + userFilter: true, + tenantFilter: true, + channels: [ + { + name: 'notifications', + eventPattern: 'notification.*', + }, + ], + rateLimit: { + maxEventsPerSecond: 100, + windowMs: 1000, + }, + }; + + const parsed = RealTimeNotificationConfigSchema.parse(config); + expect(parsed.protocol).toBe('websocket'); + expect(parsed.channels).toHaveLength(1); + }); + + it('should apply defaults', () => { + const config = RealTimeNotificationConfigSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.protocol).toBe('websocket'); + expect(config.eventPattern).toBe('*'); + expect(config.userFilter).toBe(true); + expect(config.tenantFilter).toBe(true); + }); +}); + +describe('EventBusConfigSchema', () => { + it('should accept complete event bus config', () => { + const config: EventBusConfig = { + persistence: { + enabled: true, + retention: 365, + }, + queue: { + concurrency: 20, + priorityEnabled: true, + }, + eventSourcing: { + enabled: true, + snapshotInterval: 100, + }, + webhooks: [ + { + eventPattern: 'order.*', + url: 'https://example.com/webhook', + }, + ], + messageQueue: { + provider: 'kafka', + topic: 'events', + }, + realtime: { + enabled: true, + protocol: 'websocket', + }, + }; + + expect(() => EventBusConfigSchema.parse(config)).not.toThrow(); + }); + + it('should accept minimal config', () => { + const config = EventBusConfigSchema.parse({}); + expect(config).toBeDefined(); + }); +}); + +describe('Enhanced Event Handler', () => { + it('should accept handler with retry and timeout', () => { + const handler = { + eventName: 'user.created', + handler: async () => {}, + priority: 1, + async: true, + retry: { + maxRetries: 5, + backoffMs: 2000, + backoffMultiplier: 3, + }, + timeoutMs: 30000, + }; + + const parsed = EventHandlerSchema.parse(handler); + expect(parsed.retry?.maxRetries).toBe(5); + expect(parsed.timeoutMs).toBe(30000); + }); + + it('should accept handler with filter function', () => { + const handler = { + eventName: 'order.*', + handler: async () => {}, + filter: (event: Event) => event.metadata.priority === 'critical', + }; + + expect(() => EventHandlerSchema.parse(handler)).not.toThrow(); + }); +}); diff --git a/packages/spec/src/system/events.zod.ts b/packages/spec/src/system/events.zod.ts index b1d9116a9..4809fd1dc 100644 --- a/packages/spec/src/system/events.zod.ts +++ b/packages/spec/src/system/events.zod.ts @@ -1,6 +1,41 @@ import { z } from 'zod'; import { EventNameSchema } from '../shared/identifiers.zod'; +// ========================================== +// Event Priority +// ========================================== + +/** + * Event Priority Enum + * Priority levels for event processing + * Lower numbers = higher priority + */ +export const EventPriority = z.enum([ + 'critical', // 0 - Process immediately, block if necessary + 'high', // 1 - Process soon, minimal delay + 'normal', // 2 - Default priority + 'low', // 3 - Process when resources available + 'background', // 4 - Process during idle time +]); + +export type EventPriority = z.infer; + +/** + * Event Priority Values + * Maps priority names to numeric values for sorting + */ +export const EVENT_PRIORITY_VALUES: Record = { + critical: 0, + high: 1, + normal: 2, + low: 3, + background: 4, +}; + +// ========================================== +// Event Metadata +// ========================================== + /** * Event Metadata Schema * Metadata associated with every event @@ -10,8 +45,44 @@ export const EventMetadataSchema = z.object({ timestamp: z.string().datetime().describe('ISO 8601 datetime when event was created'), userId: z.string().optional().describe('User who triggered the event'), tenantId: z.string().optional().describe('Tenant identifier for multi-tenant systems'), + correlationId: z.string().optional().describe('Correlation ID for event tracing'), + causationId: z.string().optional().describe('ID of the event that caused this event'), + priority: EventPriority.optional().default('normal').describe('Event priority'), }); +// ========================================== +// Event Schema +// ========================================== + +/** + * Event Type Definition Schema + * Defines the structure of an event type + * + * @example + * { + * "name": "order.created", + * "version": "1.0.0", + * "schema": { + * "type": "object", + * "properties": { + * "orderId": { "type": "string" }, + * "customerId": { "type": "string" }, + * "total": { "type": "number" } + * } + * } + * } + */ +export const EventTypeDefinitionSchema = z.object({ + name: EventNameSchema.describe('Event type name (lowercase with dots)'), + version: z.string().default('1.0.0').describe('Event schema version'), + schema: z.any().optional().describe('JSON Schema for event payload validation'), + description: z.string().optional().describe('Event type description'), + deprecated: z.boolean().optional().default(false).describe('Whether this event type is deprecated'), + tags: z.array(z.string()).optional().describe('Event type tags'), +}); + +export type EventTypeDefinition = z.infer; + /** * Event Schema * Base schema for all events in the system @@ -20,22 +91,82 @@ export const EventMetadataSchema = z.object({ * This aligns with industry standards for event-driven architectures and message queues. */ export const EventSchema = z.object({ + /** + * Event identifier (for tracking and deduplication) + */ + id: z.string().optional().describe('Unique event identifier'), + + /** + * Event name + */ name: EventNameSchema.describe('Event name (lowercase with dots, e.g., user.created, order.paid)'), + + /** + * Event payload + */ payload: z.any().describe('Event payload schema'), + + /** + * Event metadata + */ metadata: EventMetadataSchema.describe('Event metadata'), }); export type Event = z.infer; +// ========================================== +// Event Handlers +// ========================================== + /** * Event Handler Schema * Defines how to handle a specific event */ export const EventHandlerSchema = z.object({ + /** + * Handler identifier + */ + id: z.string().optional().describe('Unique handler identifier'), + + /** + * Event name pattern + */ eventName: z.string().describe('Name of event to handle (supports wildcards like user.*)'), + + /** + * Handler function + */ handler: z.function().args(EventSchema).returns(z.promise(z.void())).describe('Handler function'), + + /** + * Execution priority + */ priority: z.number().int().default(0).describe('Execution priority (lower numbers execute first)'), + + /** + * Async execution + */ async: z.boolean().default(true).describe('Execute in background (true) or block (false)'), + + /** + * Retry configuration + */ + retry: z.object({ + maxRetries: z.number().int().min(0).default(3).describe('Maximum retry attempts'), + backoffMs: z.number().int().positive().default(1000).describe('Initial backoff delay'), + backoffMultiplier: z.number().positive().default(2).describe('Backoff multiplier'), + }).optional().describe('Retry policy for failed handlers'), + + /** + * Timeout + */ + timeoutMs: z.number().int().positive().optional().describe('Handler timeout in milliseconds'), + + /** + * Filter function + */ + filter: z.function().args(EventSchema).returns(z.boolean()).optional() + .describe('Optional filter to determine if handler should execute'), }); export type EventHandler = z.infer; @@ -60,6 +191,569 @@ export const EventPersistenceSchema = z.object({ enabled: z.boolean().default(false).describe('Enable event persistence'), retention: z.number().int().positive().describe('Days to retain persisted events'), filter: z.function().optional().describe('Optional filter function to select which events to persist'), + storage: z.enum(['database', 'file', 's3', 'custom']).default('database') + .describe('Storage backend for persisted events'), }); export type EventPersistence = z.infer; + +// ========================================== +// Event Queue +// ========================================== + +/** + * Event Queue Configuration Schema + * Configuration for async event processing queue + * + * @example + * { + * "name": "event_queue", + * "concurrency": 10, + * "retryPolicy": { + * "maxRetries": 3, + * "backoffStrategy": "exponential" + * } + * } + */ +export const EventQueueConfigSchema = z.object({ + /** + * Queue name + */ + name: z.string().default('events').describe('Event queue name'), + + /** + * Concurrency + */ + concurrency: z.number().int().min(1).default(10).describe('Max concurrent event handlers'), + + /** + * Retry policy + */ + retryPolicy: z.object({ + maxRetries: z.number().int().min(0).default(3).describe('Max retries for failed events'), + backoffStrategy: z.enum(['fixed', 'linear', 'exponential']).default('exponential') + .describe('Backoff strategy'), + initialDelayMs: z.number().int().positive().default(1000).describe('Initial retry delay'), + maxDelayMs: z.number().int().positive().default(60000).describe('Maximum retry delay'), + }).optional().describe('Default retry policy for events'), + + /** + * Dead letter queue + */ + deadLetterQueue: z.string().optional().describe('Dead letter queue name for failed events'), + + /** + * Enable priority processing + */ + priorityEnabled: z.boolean().default(true).describe('Process events based on priority'), +}); + +export type EventQueueConfig = z.infer; + +// ========================================== +// Event Replay +// ========================================== + +/** + * Event Replay Configuration Schema + * Configuration for replaying historical events + * + * @example + * { + * "fromTimestamp": "2024-01-01T00:00:00Z", + * "toTimestamp": "2024-01-31T23:59:59Z", + * "eventTypes": ["order.created", "order.updated"], + * "speed": 10 + * } + */ +export const EventReplayConfigSchema = z.object({ + /** + * Start timestamp + */ + fromTimestamp: z.string().datetime().describe('Start timestamp for replay (ISO 8601)'), + + /** + * End timestamp + */ + toTimestamp: z.string().datetime().optional().describe('End timestamp for replay (ISO 8601)'), + + /** + * Event types to replay + */ + eventTypes: z.array(z.string()).optional().describe('Event types to replay (empty = all)'), + + /** + * Event filters + */ + filters: z.record(z.string(), z.any()).optional().describe('Additional filters for event selection'), + + /** + * Replay speed multiplier + */ + speed: z.number().positive().default(1).describe('Replay speed multiplier (1 = real-time)'), + + /** + * Target handlers + */ + targetHandlers: z.array(z.string()).optional().describe('Handler IDs to execute (empty = all)'), +}); + +export type EventReplayConfig = z.infer; + +// ========================================== +// Event Sourcing +// ========================================== + +/** + * Event Sourcing Configuration Schema + * Configuration for event sourcing pattern + * + * Event sourcing stores all changes to application state as a sequence of events. + * The current state can be reconstructed by replaying the events. + * + * @example + * { + * "enabled": true, + * "snapshotInterval": 100, + * "retention": 365 + * } + */ +export const EventSourcingConfigSchema = z.object({ + /** + * Enable event sourcing + */ + enabled: z.boolean().default(false).describe('Enable event sourcing'), + + /** + * Snapshot interval + */ + snapshotInterval: z.number().int().positive().default(100) + .describe('Create snapshot every N events'), + + /** + * Snapshot retention + */ + snapshotRetention: z.number().int().positive().default(10) + .describe('Number of snapshots to retain'), + + /** + * Event retention + */ + retention: z.number().int().positive().default(365) + .describe('Days to retain events'), + + /** + * Aggregate types + */ + aggregateTypes: z.array(z.string()).optional() + .describe('Aggregate types to enable event sourcing for'), + + /** + * Storage configuration + */ + storage: z.object({ + type: z.enum(['database', 'file', 's3', 'eventstore']).default('database') + .describe('Storage backend'), + options: z.record(z.string(), z.any()).optional().describe('Storage-specific options'), + }).optional().describe('Event store configuration'), +}); + +export type EventSourcingConfig = z.infer; + +// ========================================== +// Dead Letter Queue +// ========================================== + +/** + * Dead Letter Queue Entry Schema + * Represents a failed event in the dead letter queue + */ +export const DeadLetterQueueEntrySchema = z.object({ + /** + * Entry identifier + */ + id: z.string().describe('Unique entry identifier'), + + /** + * Original event + */ + event: EventSchema.describe('Original event'), + + /** + * Failure reason + */ + error: z.object({ + message: z.string().describe('Error message'), + stack: z.string().optional().describe('Error stack trace'), + code: z.string().optional().describe('Error code'), + }).describe('Failure details'), + + /** + * Retry count + */ + retries: z.number().int().min(0).describe('Number of retry attempts'), + + /** + * Timestamps + */ + firstFailedAt: z.string().datetime().describe('When event first failed'), + lastFailedAt: z.string().datetime().describe('When event last failed'), + + /** + * Handler that failed + */ + failedHandler: z.string().optional().describe('Handler ID that failed'), +}); + +export type DeadLetterQueueEntry = z.infer; + +// ========================================== +// Event Log +// ========================================== + +/** + * Event Log Entry Schema + * Represents a logged event + */ +export const EventLogEntrySchema = z.object({ + /** + * Log entry ID + */ + id: z.string().describe('Unique log entry identifier'), + + /** + * Event + */ + event: EventSchema.describe('The event'), + + /** + * Status + */ + status: z.enum(['pending', 'processing', 'completed', 'failed']).describe('Processing status'), + + /** + * Handlers executed + */ + handlersExecuted: z.array(z.object({ + handlerId: z.string().describe('Handler identifier'), + status: z.enum(['success', 'failed', 'timeout']).describe('Handler execution status'), + durationMs: z.number().int().optional().describe('Execution duration'), + error: z.string().optional().describe('Error message if failed'), + })).optional().describe('Handlers that processed this event'), + + /** + * Timestamps + */ + receivedAt: z.string().datetime().describe('When event was received'), + processedAt: z.string().datetime().optional().describe('When event was processed'), + + /** + * Total duration + */ + totalDurationMs: z.number().int().optional().describe('Total processing time'), +}); + +export type EventLogEntry = z.infer; + +// ========================================== +// Webhook Integration +// ========================================== + +/** + * Event Webhook Configuration Schema + * Configuration for sending events to webhooks + * + * @example + * { + * "eventPattern": "order.*", + * "url": "https://api.example.com/webhooks/orders", + * "method": "POST", + * "headers": { "Authorization": "Bearer token" } + * } + */ +export const EventWebhookConfigSchema = z.object({ + /** + * Webhook identifier + */ + id: z.string().optional().describe('Unique webhook identifier'), + + /** + * Event pattern to match + */ + eventPattern: z.string().describe('Event name pattern (supports wildcards)'), + + /** + * Target URL + */ + url: z.string().url().describe('Webhook endpoint URL'), + + /** + * HTTP method + */ + method: z.enum(['GET', 'POST', 'PUT', 'PATCH']).default('POST').describe('HTTP method'), + + /** + * Headers + */ + headers: z.record(z.string(), z.string()).optional().describe('HTTP headers'), + + /** + * Authentication + */ + authentication: z.object({ + type: z.enum(['none', 'bearer', 'basic', 'api-key']).describe('Auth type'), + credentials: z.record(z.string(), z.string()).optional().describe('Auth credentials'), + }).optional().describe('Authentication configuration'), + + /** + * Retry policy + */ + retryPolicy: z.object({ + maxRetries: z.number().int().min(0).default(3).describe('Max retry attempts'), + backoffStrategy: z.enum(['fixed', 'linear', 'exponential']).default('exponential'), + initialDelayMs: z.number().int().positive().default(1000).describe('Initial retry delay'), + maxDelayMs: z.number().int().positive().default(60000).describe('Max retry delay'), + }).optional().describe('Retry policy'), + + /** + * Timeout + */ + timeoutMs: z.number().int().positive().default(30000).describe('Request timeout in milliseconds'), + + /** + * Event transformation + */ + transform: z.function().args(EventSchema).returns(z.any()).optional() + .describe('Transform event before sending'), + + /** + * Enabled + */ + enabled: z.boolean().default(true).describe('Whether webhook is enabled'), +}); + +export type EventWebhookConfig = z.infer; + +// ========================================== +// Message Queue Integration +// ========================================== + +/** + * Event Message Queue Configuration Schema + * Configuration for publishing events to message queues + * + * @example + * { + * "provider": "kafka", + * "topic": "events", + * "eventPattern": "*", + * "partitionKey": "metadata.tenantId" + * } + */ +export const EventMessageQueueConfigSchema = z.object({ + /** + * Provider + */ + provider: z.enum(['kafka', 'rabbitmq', 'aws-sqs', 'redis-pubsub', 'google-pubsub', 'azure-service-bus']) + .describe('Message queue provider'), + + /** + * Topic/Queue name + */ + topic: z.string().describe('Topic or queue name'), + + /** + * Event pattern + */ + eventPattern: z.string().default('*').describe('Event name pattern to publish (supports wildcards)'), + + /** + * Partition key + */ + partitionKey: z.string().optional().describe('JSON path for partition key (e.g., "metadata.tenantId")'), + + /** + * Message format + */ + format: z.enum(['json', 'avro', 'protobuf']).default('json').describe('Message serialization format'), + + /** + * Include metadata + */ + includeMetadata: z.boolean().default(true).describe('Include event metadata in message'), + + /** + * Compression + */ + compression: z.enum(['none', 'gzip', 'snappy', 'lz4']).default('none').describe('Message compression'), + + /** + * Batch size + */ + batchSize: z.number().int().min(1).default(1).describe('Batch size for publishing'), + + /** + * Flush interval + */ + flushIntervalMs: z.number().int().positive().default(1000).describe('Flush interval for batching'), +}); + +export type EventMessageQueueConfig = z.infer; + +// ========================================== +// Real-time Notifications +// ========================================== + +/** + * Real-time Notification Configuration Schema + * Configuration for real-time event notifications via WebSocket/SSE + * + * @example + * { + * "enabled": true, + * "protocol": "websocket", + * "eventPattern": "notification.*", + * "userFilter": true + * } + */ +export const RealTimeNotificationConfigSchema = z.object({ + /** + * Enable real-time notifications + */ + enabled: z.boolean().default(true).describe('Enable real-time notifications'), + + /** + * Protocol + */ + protocol: z.enum(['websocket', 'sse', 'long-polling']).default('websocket') + .describe('Real-time protocol'), + + /** + * Event pattern + */ + eventPattern: z.string().default('*').describe('Event pattern to broadcast'), + + /** + * User-specific filtering + */ + userFilter: z.boolean().default(true).describe('Filter events by user'), + + /** + * Tenant-specific filtering + */ + tenantFilter: z.boolean().default(true).describe('Filter events by tenant'), + + /** + * Channels + */ + channels: z.array(z.object({ + name: z.string().describe('Channel name'), + eventPattern: z.string().describe('Event pattern for channel'), + filter: z.function().args(EventSchema).returns(z.boolean()).optional() + .describe('Additional filter function'), + })).optional().describe('Named channels for event broadcasting'), + + /** + * Rate limiting + */ + rateLimit: z.object({ + maxEventsPerSecond: z.number().int().positive().describe('Max events per second per client'), + windowMs: z.number().int().positive().default(1000).describe('Rate limit window'), + }).optional().describe('Rate limiting configuration'), +}); + +export type RealTimeNotificationConfig = z.infer; + +// ========================================== +// Complete Event Bus Configuration +// ========================================== + +/** + * Event Bus Configuration Schema + * Complete configuration for the event bus system + * + * @example + * { + * "persistence": { "enabled": true, "retention": 365 }, + * "queue": { "concurrency": 20 }, + * "eventSourcing": { "enabled": true }, + * "webhooks": [], + * "messageQueue": { "provider": "kafka", "topic": "events" }, + * "realtime": { "enabled": true, "protocol": "websocket" } + * } + */ +export const EventBusConfigSchema = z.object({ + /** + * Event persistence + */ + persistence: EventPersistenceSchema.optional().describe('Event persistence configuration'), + + /** + * Event queue + */ + queue: EventQueueConfigSchema.optional().describe('Event queue configuration'), + + /** + * Event sourcing + */ + eventSourcing: EventSourcingConfigSchema.optional().describe('Event sourcing configuration'), + + /** + * Event replay + */ + replay: z.object({ + enabled: z.boolean().default(true).describe('Enable event replay capability'), + }).optional().describe('Event replay configuration'), + + /** + * Webhooks + */ + webhooks: z.array(EventWebhookConfigSchema).optional().describe('Webhook configurations'), + + /** + * Message queue integration + */ + messageQueue: EventMessageQueueConfigSchema.optional().describe('Message queue integration'), + + /** + * Real-time notifications + */ + realtime: RealTimeNotificationConfigSchema.optional().describe('Real-time notification configuration'), + + /** + * Event type definitions + */ + eventTypes: z.array(EventTypeDefinitionSchema).optional().describe('Event type definitions'), + + /** + * Global handlers + */ + handlers: z.array(EventHandlerSchema).optional().describe('Global event handlers'), +}); + +export type EventBusConfig = z.infer; + +// ========================================== +// Helper Functions +// ========================================== + +/** + * Helper to create event bus configuration + */ +export function createEventBusConfig>(config: T): T { + return config; +} + +/** + * Helper to create event type definition + */ +export function createEventTypeDefinition>(definition: T): T { + return definition; +} + +/** + * Helper to create event webhook configuration + */ +export function createEventWebhookConfig>(config: T): T { + return config; +} diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts index 2f16c9c78..5baded7e9 100644 --- a/packages/spec/src/system/index.ts +++ b/packages/spec/src/system/index.ts @@ -11,6 +11,7 @@ export * from './audit.zod'; export * from './translation.zod'; export * from './events.zod'; export * from './job.zod'; +export * from './worker.zod'; export * from './feature.zod'; export * from './collaboration.zod'; export * from './types'; diff --git a/packages/spec/src/system/worker.test.ts b/packages/spec/src/system/worker.test.ts new file mode 100644 index 000000000..3f2513fd5 --- /dev/null +++ b/packages/spec/src/system/worker.test.ts @@ -0,0 +1,552 @@ +import { describe, it, expect } from 'vitest'; +import { + TaskPriority, + TaskStatus, + TaskSchema, + TaskRetryPolicySchema, + TaskExecutionResultSchema, + QueueConfigSchema, + BatchTaskSchema, + BatchProgressSchema, + WorkerConfigSchema, + WorkerStatsSchema, + TASK_PRIORITY_VALUES, + type Task, + type TaskRetryPolicy, + type QueueConfig, + type BatchTask, + type WorkerConfig, +} from './worker.zod'; + +describe('TaskPriority', () => { + it('should accept valid task priorities', () => { + expect(() => TaskPriority.parse('critical')).not.toThrow(); + expect(() => TaskPriority.parse('high')).not.toThrow(); + expect(() => TaskPriority.parse('normal')).not.toThrow(); + expect(() => TaskPriority.parse('low')).not.toThrow(); + expect(() => TaskPriority.parse('background')).not.toThrow(); + }); + + it('should reject invalid priorities', () => { + expect(() => TaskPriority.parse('urgent')).toThrow(); + expect(() => TaskPriority.parse('medium')).toThrow(); + }); + + it('should have correct priority value mappings', () => { + expect(TASK_PRIORITY_VALUES.critical).toBe(0); + expect(TASK_PRIORITY_VALUES.high).toBe(1); + expect(TASK_PRIORITY_VALUES.normal).toBe(2); + expect(TASK_PRIORITY_VALUES.low).toBe(3); + expect(TASK_PRIORITY_VALUES.background).toBe(4); + }); +}); + +describe('TaskStatus', () => { + it('should accept valid task statuses', () => { + const statuses = ['pending', 'queued', 'processing', 'completed', 'failed', 'cancelled', 'timeout', 'dead']; + statuses.forEach(status => { + expect(() => TaskStatus.parse(status)).not.toThrow(); + }); + }); + + it('should reject invalid statuses', () => { + expect(() => TaskStatus.parse('running')).toThrow(); + expect(() => TaskStatus.parse('success')).toThrow(); + }); +}); + +describe('TaskRetryPolicySchema', () => { + it('should accept valid retry policy', () => { + const policy: TaskRetryPolicy = { + maxRetries: 5, + backoffStrategy: 'exponential', + initialDelayMs: 2000, + maxDelayMs: 120000, + backoffMultiplier: 3, + }; + + expect(() => TaskRetryPolicySchema.parse(policy)).not.toThrow(); + }); + + it('should apply default values', () => { + const policy = TaskRetryPolicySchema.parse({}); + + expect(policy.maxRetries).toBe(3); + expect(policy.backoffStrategy).toBe('exponential'); + expect(policy.initialDelayMs).toBe(1000); + expect(policy.maxDelayMs).toBe(60000); + expect(policy.backoffMultiplier).toBe(2); + }); + + it('should accept different backoff strategies', () => { + const strategies = ['fixed', 'linear', 'exponential']; + strategies.forEach(backoffStrategy => { + const policy = TaskRetryPolicySchema.parse({ backoffStrategy }); + expect(policy.backoffStrategy).toBe(backoffStrategy); + }); + }); + + it('should accept zero retries', () => { + const policy = TaskRetryPolicySchema.parse({ maxRetries: 0 }); + expect(policy.maxRetries).toBe(0); + }); + + it('should reject negative retries', () => { + expect(() => TaskRetryPolicySchema.parse({ maxRetries: -1 })).toThrow(); + }); +}); + +describe('TaskSchema', () => { + it('should accept valid minimal task', () => { + const task: Task = { + id: 'task-123', + type: 'send_email', + payload: { to: 'user@example.com', subject: 'Welcome' }, + }; + + expect(() => TaskSchema.parse(task)).not.toThrow(); + }); + + it('should apply default values', () => { + const task = TaskSchema.parse({ + id: 'task-123', + type: 'process_data', + payload: { data: 'test' }, + }); + + expect(task.queue).toBe('default'); + expect(task.priority).toBe('normal'); + expect(task.attempts).toBe(0); + expect(task.status).toBe('pending'); + }); + + it('should validate task type format (snake_case)', () => { + const validTypes = ['send_email', 'process_payment', 'generate_report']; + validTypes.forEach(type => { + const task = { id: 'task-123', type, payload: {} }; + expect(() => TaskSchema.parse(task)).not.toThrow(); + }); + }); + + it('should reject invalid task type formats', () => { + const invalidTypes = ['SendEmail', 'send-email', 'sendEmail', '123_invalid']; + invalidTypes.forEach(type => { + expect(() => TaskSchema.parse({ + id: 'task-123', + type, + payload: {}, + })).toThrow(); + }); + }); + + it('should accept task with all fields', () => { + const task = { + id: 'task-456', + type: 'complex_task', + payload: { data: 'complex' }, + queue: 'background', + priority: 'high', + retryPolicy: { + maxRetries: 5, + backoffStrategy: 'linear', + initialDelayMs: 2000, + maxDelayMs: 60000, + }, + timeoutMs: 300000, + scheduledAt: '2024-12-31T23:59:59Z', + attempts: 2, + status: 'processing', + metadata: { + createdAt: '2024-01-15T10:00:00Z', + updatedAt: '2024-01-15T10:05:00Z', + createdBy: 'user-123', + tags: ['urgent', 'customer-facing'], + }, + }; + + const parsed = TaskSchema.parse(task); + expect(parsed.priority).toBe('high'); + expect(parsed.retryPolicy?.maxRetries).toBe(5); + expect(parsed.metadata?.tags).toEqual(['urgent', 'customer-facing']); + }); + + it('should accept different priorities', () => { + const priorities: Array = ['critical', 'high', 'normal', 'low', 'background']; + priorities.forEach(priority => { + const task = { id: 'task-123', type: 'test_task', payload: {}, priority }; + const parsed = TaskSchema.parse(task); + expect(parsed.priority).toBe(priority); + }); + }); + + it('should accept different statuses', () => { + const statuses: Array = ['pending', 'queued', 'processing', 'completed', 'failed', 'cancelled', 'timeout', 'dead']; + statuses.forEach(status => { + const task = { id: 'task-123', type: 'test_task', payload: {}, status }; + const parsed = TaskSchema.parse(task); + expect(parsed.status).toBe(status); + }); + }); +}); + +describe('TaskExecutionResultSchema', () => { + it('should accept successful execution result', () => { + const result = { + taskId: 'task-123', + status: 'completed', + result: { sent: true, messageId: 'msg-456' }, + durationMs: 1500, + startedAt: '2024-01-15T10:00:00Z', + completedAt: '2024-01-15T10:00:01.500Z', + attempt: 1, + willRetry: false, + }; + + expect(() => TaskExecutionResultSchema.parse(result)).not.toThrow(); + }); + + it('should accept failed execution result', () => { + const result = { + taskId: 'task-456', + status: 'failed', + error: { + message: 'Connection timeout', + stack: 'Error: Connection timeout\n at ...', + code: 'ETIMEDOUT', + }, + durationMs: 30000, + startedAt: '2024-01-15T10:00:00Z', + completedAt: '2024-01-15T10:00:30Z', + attempt: 2, + willRetry: true, + }; + + const parsed = TaskExecutionResultSchema.parse(result); + expect(parsed.error?.message).toBe('Connection timeout'); + expect(parsed.willRetry).toBe(true); + }); + + it('should accept timeout execution result', () => { + const result = { + taskId: 'task-789', + status: 'timeout', + error: { + message: 'Task exceeded 300000ms timeout', + code: 'TASK_TIMEOUT', + }, + startedAt: '2024-01-15T10:00:00Z', + completedAt: '2024-01-15T10:05:00Z', + attempt: 1, + willRetry: false, + }; + + expect(() => TaskExecutionResultSchema.parse(result)).not.toThrow(); + }); +}); + +describe('QueueConfigSchema', () => { + it('should accept valid minimal queue config', () => { + const config: QueueConfig = { + name: 'notifications', + }; + + expect(() => QueueConfigSchema.parse(config)).not.toThrow(); + }); + + it('should apply default values', () => { + const config = QueueConfigSchema.parse({ name: 'test_queue' }); + + expect(config.concurrency).toBe(5); + expect(config.priority).toBe(0); + }); + + it('should accept queue with rate limiting', () => { + const config = { + name: 'rate_limited_queue', + concurrency: 10, + rateLimit: { + max: 100, + duration: 60000, + }, + }; + + const parsed = QueueConfigSchema.parse(config); + expect(parsed.rateLimit?.max).toBe(100); + expect(parsed.rateLimit?.duration).toBe(60000); + }); + + it('should accept queue with auto-scaling', () => { + const config = { + name: 'auto_scale_queue', + autoScale: { + enabled: true, + minWorkers: 2, + maxWorkers: 20, + scaleUpThreshold: 200, + scaleDownThreshold: 20, + }, + }; + + const parsed = QueueConfigSchema.parse(config); + expect(parsed.autoScale?.enabled).toBe(true); + expect(parsed.autoScale?.maxWorkers).toBe(20); + }); + + it('should accept queue with dead letter queue', () => { + const config = { + name: 'main_queue', + deadLetterQueue: 'failed_tasks', + defaultRetryPolicy: { + maxRetries: 3, + backoffStrategy: 'exponential', + }, + }; + + const parsed = QueueConfigSchema.parse(config); + expect(parsed.deadLetterQueue).toBe('failed_tasks'); + }); +}); + +describe('BatchTaskSchema', () => { + it('should accept valid batch task', () => { + const batch: BatchTask = { + id: 'batch-123', + type: 'import_records', + items: [{ name: 'Item 1' }, { name: 'Item 2' }, { name: 'Item 3' }], + }; + + expect(() => BatchTaskSchema.parse(batch)).not.toThrow(); + }); + + it('should apply default values', () => { + const batch = BatchTaskSchema.parse({ + id: 'batch-456', + type: 'process_items', + items: [1, 2, 3], + }); + + expect(batch.batchSize).toBe(100); + expect(batch.queue).toBe('batch'); + expect(batch.priority).toBe('normal'); + expect(batch.parallel).toBe(true); + expect(batch.stopOnError).toBe(false); + }); + + it('should accept batch with custom configuration', () => { + const batch = { + id: 'batch-789', + type: 'export_data', + items: Array(500).fill({ data: 'test' }), + batchSize: 50, + queue: 'exports', + priority: 'high', + parallel: false, + stopOnError: true, + }; + + const parsed = BatchTaskSchema.parse(batch); + expect(parsed.batchSize).toBe(50); + expect(parsed.parallel).toBe(false); + expect(parsed.stopOnError).toBe(true); + }); +}); + +describe('BatchProgressSchema', () => { + it('should accept batch progress', () => { + const progress = { + batchId: 'batch-123', + total: 1000, + processed: 500, + succeeded: 480, + failed: 20, + percentage: 50, + status: 'running', + startedAt: '2024-01-15T10:00:00Z', + }; + + const parsed = BatchProgressSchema.parse(progress); + expect(parsed.percentage).toBe(50); + expect(parsed.status).toBe('running'); + }); + + it('should apply default values', () => { + const progress = BatchProgressSchema.parse({ + batchId: 'batch-456', + total: 100, + percentage: 0, + status: 'pending', + }); + + expect(progress.processed).toBe(0); + expect(progress.succeeded).toBe(0); + expect(progress.failed).toBe(0); + }); +}); + +describe('WorkerConfigSchema', () => { + it('should accept valid worker config', () => { + const config: WorkerConfig = { + name: 'worker-1', + queues: ['notifications', 'emails'], + }; + + expect(() => WorkerConfigSchema.parse(config)).not.toThrow(); + }); + + it('should apply default values', () => { + const config = WorkerConfigSchema.parse({ + name: 'worker-2', + queues: ['default'], + }); + + expect(config.pollIntervalMs).toBe(1000); + expect(config.visibilityTimeoutMs).toBe(30000); + expect(config.defaultTimeoutMs).toBe(300000); + expect(config.shutdownTimeoutMs).toBe(30000); + }); + + it('should accept worker with queue configurations', () => { + const config = { + name: 'worker-3', + queues: ['high_priority', 'normal'], + queueConfigs: [ + { + name: 'high_priority', + concurrency: 10, + priority: 0, + }, + { + name: 'normal', + concurrency: 5, + priority: 1, + }, + ], + pollIntervalMs: 500, + defaultTimeoutMs: 600000, + }; + + const parsed = WorkerConfigSchema.parse(config); + expect(parsed.queueConfigs).toHaveLength(2); + expect(parsed.pollIntervalMs).toBe(500); + }); + + it('should require at least one queue', () => { + expect(() => WorkerConfigSchema.parse({ + name: 'worker-4', + queues: [], + })).toThrow(); + }); +}); + +describe('WorkerStatsSchema', () => { + it('should accept worker stats', () => { + const stats = { + workerName: 'worker-1', + totalProcessed: 1000, + succeeded: 950, + failed: 50, + active: 5, + avgExecutionMs: 1500, + uptimeMs: 3600000, + queues: { + notifications: { + pending: 10, + active: 3, + completed: 500, + failed: 25, + }, + emails: { + pending: 5, + active: 2, + completed: 450, + failed: 25, + }, + }, + }; + + const parsed = WorkerStatsSchema.parse(stats); + expect(parsed.totalProcessed).toBe(1000); + expect(parsed.queues?.notifications.completed).toBe(500); + }); +}); + +describe('Worker Integration', () => { + it('should handle email sending task', () => { + const task: Task = { + id: 'email-task-123', + type: 'send_email', + payload: { + to: 'customer@example.com', + template: 'order_confirmation', + data: { orderId: 'ORD-123', amount: 99.99 }, + }, + queue: 'notifications', + priority: 'high', + retryPolicy: { + maxRetries: 3, + backoffStrategy: 'exponential', + initialDelayMs: 1000, + maxDelayMs: 60000, + }, + timeoutMs: 30000, + }; + + expect(() => TaskSchema.parse(task)).not.toThrow(); + }); + + it('should handle batch import task', () => { + const batch: BatchTask = { + id: 'import-batch-456', + type: 'import_customer', + items: Array(1000).fill(null).map((_, i) => ({ + name: `Customer ${i}`, + email: `customer${i}@example.com`, + })), + batchSize: 100, + queue: 'batch_import', + priority: 'low', + parallel: true, + stopOnError: false, + }; + + const parsed = BatchTaskSchema.parse(batch); + expect(parsed.items).toHaveLength(1000); + expect(parsed.batchSize).toBe(100); + }); + + it('should handle scheduled maintenance task', () => { + const task: Task = { + id: 'maintenance-daily', + type: 'cleanup_temp_files', + payload: { olderThanDays: 7 }, + queue: 'maintenance', + priority: 'background', + scheduledAt: '2024-12-31T02:00:00Z', + timeoutMs: 3600000, + }; + + expect(() => TaskSchema.parse(task)).not.toThrow(); + }); + + it('should demonstrate priority-based task processing', () => { + const tasks: Task[] = [ + { id: '1', type: 'task_1', payload: {}, priority: 'critical' }, + { id: '2', type: 'task_2', payload: {}, priority: 'low' }, + { id: '3', type: 'task_3', payload: {}, priority: 'high' }, + { id: '4', type: 'task_4', payload: {}, priority: 'background' }, + { id: '5', type: 'task_5', payload: {}, priority: 'normal' }, + ]; + + tasks.forEach(task => { + expect(() => TaskSchema.parse(task)).not.toThrow(); + }); + + // Sort by priority + const sorted = tasks.sort((a, b) => + TASK_PRIORITY_VALUES[a.priority] - TASK_PRIORITY_VALUES[b.priority] + ); + + expect(sorted[0].priority).toBe('critical'); + expect(sorted[4].priority).toBe('background'); + }); +}); diff --git a/packages/spec/src/system/worker.zod.ts b/packages/spec/src/system/worker.zod.ts new file mode 100644 index 000000000..89fa9d0f1 --- /dev/null +++ b/packages/spec/src/system/worker.zod.ts @@ -0,0 +1,572 @@ +import { z } from 'zod'; + +/** + * Worker System Protocol + * + * Background task processing system with queues, priorities, and retry logic. + * Provides a robust foundation for async task execution similar to: + * - Sidekiq (Ruby) + * - Celery (Python) + * - Bull/BullMQ (Node.js) + * - AWS SQS/Lambda + * + * Features: + * - Task queues with priorities + * - Task scheduling and retry logic + * - Batch processing + * - Dead letter queues + * - Task monitoring and logging + * + * @example Basic task + * ```typescript + * const task: Task = { + * id: 'task-123', + * type: 'send_email', + * payload: { to: 'user@example.com', subject: 'Welcome' }, + * queue: 'notifications', + * priority: 5 + * }; + * ``` + */ + +// ========================================== +// Task Priority +// ========================================== + +/** + * Task Priority Enum + * Lower numbers = higher priority + */ +export const TaskPriority = z.enum([ + 'critical', // 0 - Must execute immediately + 'high', // 1 - Execute soon + 'normal', // 2 - Default priority + 'low', // 3 - Execute when resources available + 'background', // 4 - Execute during low-traffic periods +]); + +export type TaskPriority = z.infer; + +/** + * Task Priority Mapping + * Maps priority names to numeric values for sorting + */ +export const TASK_PRIORITY_VALUES: Record = { + critical: 0, + high: 1, + normal: 2, + low: 3, + background: 4, +}; + +// ========================================== +// Task Status +// ========================================== + +/** + * Task Status Enum + * Lifecycle states of a task + */ +export const TaskStatus = z.enum([ + 'pending', // Waiting to be processed + 'queued', // In queue, ready for worker + 'processing', // Currently being executed + 'completed', // Successfully completed + 'failed', // Failed (may retry) + 'cancelled', // Manually cancelled + 'timeout', // Exceeded execution timeout + 'dead', // Moved to dead letter queue +]); + +export type TaskStatus = z.infer; + +// ========================================== +// Task Schema +// ========================================== + +/** + * Task Retry Policy Schema + * Configuration for task retry behavior + */ +export const TaskRetryPolicySchema = z.object({ + maxRetries: z.number().int().min(0).default(3).describe('Maximum retry attempts'), + backoffStrategy: z.enum(['fixed', 'linear', 'exponential']).default('exponential') + .describe('Backoff strategy between retries'), + initialDelayMs: z.number().int().positive().default(1000).describe('Initial retry delay in milliseconds'), + maxDelayMs: z.number().int().positive().default(60000).describe('Maximum retry delay in milliseconds'), + backoffMultiplier: z.number().positive().default(2).describe('Multiplier for exponential backoff'), +}); + +export type TaskRetryPolicy = z.infer; + +/** + * Task Schema + * Represents a background task to be executed + * + * @example + * { + * "id": "task-abc123", + * "type": "send_email", + * "payload": { "to": "user@example.com", "template": "welcome" }, + * "queue": "notifications", + * "priority": "high", + * "retryPolicy": { + * "maxRetries": 3, + * "backoffStrategy": "exponential" + * } + * } + */ +export const TaskSchema = z.object({ + /** + * Unique task identifier + */ + id: z.string().describe('Unique task identifier'), + + /** + * Task type (handler identifier) + */ + type: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Task type (snake_case)'), + + /** + * Task payload data + */ + payload: z.any().describe('Task payload data'), + + /** + * Queue name + */ + queue: z.string().default('default').describe('Queue name'), + + /** + * Task priority + */ + priority: TaskPriority.default('normal').describe('Task priority level'), + + /** + * Retry policy + */ + retryPolicy: TaskRetryPolicySchema.optional().describe('Retry policy configuration'), + + /** + * Execution timeout in milliseconds + */ + timeoutMs: z.number().int().positive().optional().describe('Task timeout in milliseconds'), + + /** + * Scheduled execution time + */ + scheduledAt: z.string().datetime().optional().describe('ISO 8601 datetime to execute task'), + + /** + * Maximum execution attempts + */ + attempts: z.number().int().min(0).default(0).describe('Number of execution attempts'), + + /** + * Task status + */ + status: TaskStatus.default('pending').describe('Current task status'), + + /** + * Task metadata + */ + metadata: z.object({ + createdAt: z.string().datetime().optional().describe('When task was created'), + updatedAt: z.string().datetime().optional().describe('Last update time'), + createdBy: z.string().optional().describe('User who created task'), + tags: z.array(z.string()).optional().describe('Task tags for filtering'), + }).optional().describe('Task metadata'), +}); + +export type Task = z.infer; + +// ========================================== +// Task Execution Result +// ========================================== + +/** + * Task Execution Result Schema + * Result of a task execution attempt + */ +export const TaskExecutionResultSchema = z.object({ + /** + * Task identifier + */ + taskId: z.string().describe('Task identifier'), + + /** + * Execution status + */ + status: TaskStatus.describe('Execution status'), + + /** + * Execution result data + */ + result: z.any().optional().describe('Execution result data'), + + /** + * Error information + */ + error: z.object({ + message: z.string().describe('Error message'), + stack: z.string().optional().describe('Error stack trace'), + code: z.string().optional().describe('Error code'), + }).optional().describe('Error details if failed'), + + /** + * Execution duration + */ + durationMs: z.number().int().optional().describe('Execution duration in milliseconds'), + + /** + * Execution timestamps + */ + startedAt: z.string().datetime().describe('When execution started'), + completedAt: z.string().datetime().optional().describe('When execution completed'), + + /** + * Retry information + */ + attempt: z.number().int().min(1).describe('Attempt number (1-indexed)'), + willRetry: z.boolean().describe('Whether task will be retried'), +}); + +export type TaskExecutionResult = z.infer; + +// ========================================== +// Queue Configuration +// ========================================== + +/** + * Queue Configuration Schema + * Configuration for a task queue + * + * @example + * { + * "name": "notifications", + * "concurrency": 10, + * "rateLimit": { + * "max": 100, + * "duration": 60000 + * } + * } + */ +export const QueueConfigSchema = z.object({ + /** + * Queue name + */ + name: z.string().describe('Queue name (snake_case)'), + + /** + * Maximum concurrent workers + */ + concurrency: z.number().int().min(1).default(5).describe('Max concurrent task executions'), + + /** + * Rate limiting + */ + rateLimit: z.object({ + max: z.number().int().positive().describe('Maximum tasks per duration'), + duration: z.number().int().positive().describe('Duration in milliseconds'), + }).optional().describe('Rate limit configuration'), + + /** + * Default retry policy + */ + defaultRetryPolicy: TaskRetryPolicySchema.optional().describe('Default retry policy for tasks'), + + /** + * Dead letter queue + */ + deadLetterQueue: z.string().optional().describe('Dead letter queue name'), + + /** + * Queue priority + */ + priority: z.number().int().min(0).default(0).describe('Queue priority (lower = higher priority)'), + + /** + * Auto-scaling configuration + */ + autoScale: z.object({ + enabled: z.boolean().default(false).describe('Enable auto-scaling'), + minWorkers: z.number().int().min(1).default(1).describe('Minimum workers'), + maxWorkers: z.number().int().min(1).default(10).describe('Maximum workers'), + scaleUpThreshold: z.number().int().positive().default(100).describe('Queue size to scale up'), + scaleDownThreshold: z.number().int().min(0).default(10).describe('Queue size to scale down'), + }).optional().describe('Auto-scaling configuration'), +}); + +export type QueueConfig = z.infer; + +// ========================================== +// Batch Processing +// ========================================== + +/** + * Batch Task Schema + * Configuration for batch processing multiple items + * + * @example + * { + * "id": "batch-import-123", + * "type": "import_records", + * "items": [{ "name": "Item 1" }, { "name": "Item 2" }], + * "batchSize": 100, + * "queue": "batch_processing" + * } + */ +export const BatchTaskSchema = z.object({ + /** + * Batch job identifier + */ + id: z.string().describe('Unique batch job identifier'), + + /** + * Task type for processing each item + */ + type: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Task type (snake_case)'), + + /** + * Items to process + */ + items: z.array(z.any()).describe('Array of items to process'), + + /** + * Batch size (items per task) + */ + batchSize: z.number().int().min(1).default(100).describe('Number of items per batch'), + + /** + * Queue name + */ + queue: z.string().default('batch').describe('Queue for batch tasks'), + + /** + * Priority + */ + priority: TaskPriority.default('normal').describe('Batch task priority'), + + /** + * Parallel processing + */ + parallel: z.boolean().default(true).describe('Process batches in parallel'), + + /** + * Stop on error + */ + stopOnError: z.boolean().default(false).describe('Stop batch if any item fails'), + + /** + * Progress callback + * + * Called after each batch completes to report progress. + * Invoked asynchronously and should not throw errors. + * If the callback throws, the error is logged but batch processing continues. + * + * @param progress - Object containing processed count, total count, and failed count + */ + onProgress: z.function() + .args(z.object({ + processed: z.number(), + total: z.number(), + failed: z.number(), + })) + .returns(z.void()) + .optional() + .describe('Progress callback function (called after each batch)'), +}); + +export type BatchTask = z.infer; + +/** + * Batch Progress Schema + * Tracks progress of a batch job + */ +export const BatchProgressSchema = z.object({ + /** + * Batch job identifier + */ + batchId: z.string().describe('Batch job identifier'), + + /** + * Total items + */ + total: z.number().int().min(0).describe('Total number of items'), + + /** + * Processed items + */ + processed: z.number().int().min(0).default(0).describe('Items processed'), + + /** + * Successful items + */ + succeeded: z.number().int().min(0).default(0).describe('Items succeeded'), + + /** + * Failed items + */ + failed: z.number().int().min(0).default(0).describe('Items failed'), + + /** + * Progress percentage + */ + percentage: z.number().min(0).max(100).describe('Progress percentage'), + + /** + * Status + */ + status: z.enum(['pending', 'running', 'completed', 'failed', 'cancelled']).describe('Batch status'), + + /** + * Timestamps + */ + startedAt: z.string().datetime().optional().describe('When batch started'), + completedAt: z.string().datetime().optional().describe('When batch completed'), +}); + +export type BatchProgress = z.infer; + +// ========================================== +// Worker Configuration +// ========================================== + +/** + * Worker Configuration Schema + * Configuration for a worker instance + */ +export const WorkerConfigSchema = z.object({ + /** + * Worker name + */ + name: z.string().describe('Worker name'), + + /** + * Queues to process + */ + queues: z.array(z.string()).min(1).describe('Queue names to process'), + + /** + * Queue configurations + */ + queueConfigs: z.array(QueueConfigSchema).optional().describe('Queue configurations'), + + /** + * Polling interval + */ + pollIntervalMs: z.number().int().positive().default(1000).describe('Queue polling interval in milliseconds'), + + /** + * Visibility timeout + */ + visibilityTimeoutMs: z.number().int().positive().default(30000) + .describe('How long a task is invisible after being claimed'), + + /** + * Task timeout + */ + defaultTimeoutMs: z.number().int().positive().default(300000).describe('Default task timeout in milliseconds'), + + /** + * Graceful shutdown timeout + */ + shutdownTimeoutMs: z.number().int().positive().default(30000) + .describe('Graceful shutdown timeout in milliseconds'), + + /** + * Task handlers + */ + handlers: z.record(z.string(), z.function()).optional().describe('Task type handlers'), +}); + +export type WorkerConfig = z.infer; + +// ========================================== +// Worker Stats +// ========================================== + +/** + * Worker Stats Schema + * Runtime statistics for a worker + */ +export const WorkerStatsSchema = z.object({ + /** + * Worker name + */ + workerName: z.string().describe('Worker name'), + + /** + * Total tasks processed + */ + totalProcessed: z.number().int().min(0).describe('Total tasks processed'), + + /** + * Successful tasks + */ + succeeded: z.number().int().min(0).describe('Successful tasks'), + + /** + * Failed tasks + */ + failed: z.number().int().min(0).describe('Failed tasks'), + + /** + * Active tasks + */ + active: z.number().int().min(0).describe('Currently active tasks'), + + /** + * Average execution time + */ + avgExecutionMs: z.number().min(0).optional().describe('Average execution time in milliseconds'), + + /** + * Uptime + */ + uptimeMs: z.number().int().min(0).describe('Worker uptime in milliseconds'), + + /** + * Queue stats + */ + queues: z.record(z.string(), z.object({ + pending: z.number().int().min(0).describe('Pending tasks'), + active: z.number().int().min(0).describe('Active tasks'), + completed: z.number().int().min(0).describe('Completed tasks'), + failed: z.number().int().min(0).describe('Failed tasks'), + })).optional().describe('Per-queue statistics'), +}); + +export type WorkerStats = z.infer; + +// ========================================== +// Helper Functions +// ========================================== + +/** + * Helper to create a task + */ +export const Task = Object.assign(TaskSchema, { + create: >(task: T) => task, +}); + +/** + * Helper to create a queue config + */ +export const QueueConfig = Object.assign(QueueConfigSchema, { + create: >(config: T) => config, +}); + +/** + * Helper to create a worker config + */ +export const WorkerConfig = Object.assign(WorkerConfigSchema, { + create: >(config: T) => config, +}); + +/** + * Helper to create a batch task + */ +export const BatchTask = Object.assign(BatchTaskSchema, { + create: >(batch: T) => batch, +});