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

@@ -268,9 +268,10 @@ model Workspace {
personalities Personality[]
llmSettings WorkspaceLlmSettings?
qualityGates QualityGate[]
runnerJobs RunnerJob[]
federationConnections FederationConnection[]
federationMessages FederationMessage[]
runnerJobs RunnerJob[]
federationConnections FederationConnection[]
federationMessages FederationMessage[]
federationEventSubscriptions FederationEventSubscription[]
@@index([ownerId])
@@map("workspaces")
@@ -1287,8 +1288,9 @@ model FederationConnection {
disconnectedAt DateTime? @map("disconnected_at") @db.Timestamptz
// Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
messages FederationMessage[]
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
messages FederationMessage[]
eventSubscriptions FederationEventSubscription[]
@@unique([workspaceId, remoteInstanceId])
@@index([workspaceId])
@@ -1330,6 +1332,7 @@ model FederationMessage {
// Message content
query String? @db.Text
commandType String? @map("command_type") @db.Text
eventType String? @map("event_type") @db.Text // For EVENT messages
payload Json? @default("{}")
response Json? @default("{}")
@@ -1353,5 +1356,30 @@ model FederationMessage {
@@index([connectionId])
@@index([messageId])
@@index([correlationId])
@@index([eventType])
@@map("federation_messages")
}
model FederationEventSubscription {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
connectionId String @map("connection_id") @db.Uuid
// Event subscription details
eventType String @map("event_type")
metadata Json @default("{}")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
connection FederationConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([workspaceId, connectionId, eventType])
@@index([workspaceId])
@@index([connectionId])
@@index([eventType])
@@index([workspaceId, isActive])
@@map("federation_event_subscriptions")
}