Skip to content

Commit 39b0ccf

Browse files
committed
feat: add challenge custom events for WebSocket broadcast
- Add ChallengeCustomEvent to WsEvent enum - Add POST /api/v1/events/broadcast endpoint - Challenges can broadcast custom events to all connected validators - Validators filter by challenge_id to receive relevant events
1 parent 41d9d8b commit 39b0ccf

4 files changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Challenge Events API
2+
//!
3+
//! Allows challenges to broadcast custom events to validators via WebSocket.
4+
5+
use crate::models::{ChallengeCustomEvent, WsEvent};
6+
use crate::state::AppState;
7+
use axum::{extract::State, http::StatusCode, Json};
8+
use serde::{Deserialize, Serialize};
9+
use std::sync::Arc;
10+
use tracing::info;
11+
12+
#[derive(Debug, Deserialize)]
13+
pub struct BroadcastEventRequest {
14+
/// Challenge ID (must match a registered challenge)
15+
pub challenge_id: String,
16+
/// Event name (e.g., "new_submission", "evaluation_needed")
17+
pub event_name: String,
18+
/// Event payload - challenge-specific JSON data
19+
pub payload: serde_json::Value,
20+
}
21+
22+
#[derive(Debug, Serialize)]
23+
pub struct BroadcastEventResponse {
24+
pub success: bool,
25+
pub connections_notified: usize,
26+
pub error: Option<String>,
27+
}
28+
29+
/// POST /api/v1/events/broadcast - Broadcast a custom challenge event
30+
///
31+
/// Called by challenge containers to notify validators of events.
32+
/// Validators filter events by challenge_id to receive only relevant ones.
33+
pub async fn broadcast_event(
34+
State(state): State<Arc<AppState>>,
35+
Json(req): Json<BroadcastEventRequest>,
36+
) -> Result<Json<BroadcastEventResponse>, (StatusCode, String)> {
37+
// Create the custom event
38+
let event = ChallengeCustomEvent {
39+
challenge_id: req.challenge_id.clone(),
40+
event_name: req.event_name.clone(),
41+
payload: req.payload,
42+
timestamp: chrono::Utc::now().timestamp(),
43+
};
44+
45+
// Get connection count before broadcast
46+
let connections = state.broadcaster.connection_count();
47+
48+
// Broadcast to all connected clients
49+
state.broadcaster.broadcast(WsEvent::ChallengeEvent(event));
50+
51+
info!(
52+
"Broadcast challenge event: {}:{} to {} connections",
53+
req.challenge_id, req.event_name, connections
54+
);
55+
56+
Ok(Json(BroadcastEventResponse {
57+
success: true,
58+
connections_notified: connections,
59+
error: None,
60+
}))
61+
}

crates/platform-server/src/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod auth;
44
pub mod bridge;
55
pub mod challenges;
66
pub mod evaluations;
7+
pub mod events;
78
pub mod jobs;
89
pub mod leaderboard;
910
pub mod llm;

crates/platform-server/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ async fn main() -> anyhow::Result<()> {
232232
)
233233
// === CENTRALIZED LLM PROXY ===
234234
.route("/api/v1/llm/chat", post(api::llm::chat))
235+
// === CHALLENGE EVENTS (broadcast to validators) ===
236+
.route(
237+
"/api/v1/events/broadcast",
238+
post(api::events::broadcast_event),
239+
)
235240
.layer(TraceLayer::new_for_http())
236241
.layer(
237242
CorsLayer::new()

crates/platform-server/src/models/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,11 @@ pub enum WsEvent {
308308
#[serde(rename = "job_assigned")]
309309
JobAssigned(JobAssignedEvent),
310310

311+
/// Custom challenge event - each challenge can define its own event types
312+
/// Validators filter by challenge_id to receive only relevant events
313+
#[serde(rename = "challenge_event")]
314+
ChallengeEvent(ChallengeCustomEvent),
315+
311316
#[serde(rename = "job_progress")]
312317
JobProgress(JobProgressEvent),
313318

@@ -461,6 +466,20 @@ pub struct ChallengeStoppedEvent {
461466
pub id: String,
462467
}
463468

469+
/// Custom event from a challenge - allows challenges to broadcast their own events
470+
/// Validators subscribe and filter by challenge_id
471+
#[derive(Debug, Clone, Serialize, Deserialize)]
472+
pub struct ChallengeCustomEvent {
473+
/// Challenge identifier (e.g., "term-challenge")
474+
pub challenge_id: String,
475+
/// Event name within the challenge (e.g., "new_submission", "evaluation_needed")
476+
pub event_name: String,
477+
/// Event payload as JSON - challenge-specific data
478+
pub payload: serde_json::Value,
479+
/// Timestamp when event was created
480+
pub timestamp: i64,
481+
}
482+
464483
#[derive(Debug, Clone, Serialize, Deserialize)]
465484
pub struct ValidatorEvent {
466485
pub hotkey: String,

0 commit comments

Comments
 (0)