feat(#90): implement EVENT subscriptions for federation

Implement event pub/sub messaging for federation to enable real-time
event streaming between federated instances.

Features:
- Event subscription management (subscribe/unsubscribe)
- Event publishing to subscribed instances
- Event acknowledgment protocol
- Server-side event filtering based on subscriptions
- Full signature verification and connection validation

Implementation:
- FederationEventSubscription model for storing subscriptions
- EventService with complete event lifecycle management
- EventController with authenticated and public endpoints
- EventMessage, EventAck, and SubscriptionDetails types
- Comprehensive DTOs for all event operations

API Endpoints:
- POST /api/v1/federation/events/subscribe
- POST /api/v1/federation/events/unsubscribe
- POST /api/v1/federation/events/publish
- GET /api/v1/federation/events/subscriptions
- GET /api/v1/federation/events/messages
- POST /api/v1/federation/incoming/event (public)
- POST /api/v1/federation/incoming/event/ack (public)

Testing:
- 18 unit tests for EventService (89.09% coverage)
- 11 unit tests for EventController (83.87% coverage)
- All 29 tests passing
- Follows TDD red-green-refactor cycle

Technical Notes:
- Reuses existing FederationMessage model with eventType field
- Follows patterns from QueryService and CommandService
- Uses existing signature and connection infrastructure
- Supports hierarchical event type naming (e.g., "task.created")

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-03 13:45:00 -06:00
parent 9501aa3867
commit ca4f5ec011
10 changed files with 2395 additions and 5 deletions

View File

@@ -0,0 +1,109 @@
/**
* Event DTOs
*
* Data Transfer Objects for event subscription and publishing.
*/
import { IsString, IsNotEmpty, IsOptional, IsObject } from "class-validator";
/**
* DTO for subscribing to an event type
*/
export class SubscribeToEventDto {
@IsString()
@IsNotEmpty()
connectionId!: string;
@IsString()
@IsNotEmpty()
eventType!: string;
@IsOptional()
@IsObject()
metadata?: Record<string, unknown>;
}
/**
* DTO for unsubscribing from an event type
*/
export class UnsubscribeFromEventDto {
@IsString()
@IsNotEmpty()
connectionId!: string;
@IsString()
@IsNotEmpty()
eventType!: string;
}
/**
* DTO for publishing an event
*/
export class PublishEventDto {
@IsString()
@IsNotEmpty()
eventType!: string;
@IsObject()
@IsNotEmpty()
payload!: Record<string, unknown>;
}
/**
* DTO for incoming event request
*/
export class IncomingEventDto {
@IsString()
@IsNotEmpty()
messageId!: string;
@IsString()
@IsNotEmpty()
instanceId!: string;
@IsString()
@IsNotEmpty()
eventType!: string;
@IsObject()
@IsNotEmpty()
payload!: Record<string, unknown>;
@IsNotEmpty()
timestamp!: number;
@IsString()
@IsNotEmpty()
signature!: string;
}
/**
* DTO for incoming event acknowledgment
*/
export class IncomingEventAckDto {
@IsString()
@IsNotEmpty()
messageId!: string;
@IsString()
@IsNotEmpty()
correlationId!: string;
@IsString()
@IsNotEmpty()
instanceId!: string;
@IsNotEmpty()
received!: boolean;
@IsOptional()
@IsString()
error?: string;
@IsNotEmpty()
timestamp!: number;
@IsString()
@IsNotEmpty()
signature!: string;
}