websockets
This commit is contained in:
parent
b44e4ed6f9
commit
f0b53b67ce
9 changed files with 268 additions and 28 deletions
|
|
@ -22,7 +22,7 @@ pub struct UpdateTaskRequest {
|
|||
pub cron: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct TaskResponse {
|
||||
pub id: Uuid,
|
||||
pub goal: String,
|
||||
|
|
@ -31,7 +31,7 @@ pub struct TaskResponse {
|
|||
pub runs: Vec<TaskRunResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct TaskRunResponse {
|
||||
pub id: Uuid,
|
||||
pub status: String,
|
||||
|
|
@ -40,7 +40,7 @@ pub struct TaskRunResponse {
|
|||
pub created_at: chrono::DateTime<chrono::FixedOffset>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct RecentRunResponse {
|
||||
pub id: Uuid,
|
||||
pub task_id: Uuid,
|
||||
|
|
@ -75,6 +75,18 @@ pub async fn execute_agent_run(
|
|||
.await
|
||||
.map_err(crate::error::AppError::Database)?;
|
||||
|
||||
let _ = _scheduler
|
||||
.tx
|
||||
.send(crate::server::notifications::WsEvent::RunStarted(
|
||||
RecentRunResponse {
|
||||
id: run_id,
|
||||
task_id,
|
||||
goal: goal.clone(),
|
||||
status: "running".to_string(),
|
||||
created_at: Utc::now().into(),
|
||||
},
|
||||
));
|
||||
|
||||
let mut agent = Agent::new(
|
||||
db.clone(),
|
||||
config.zen_api_key.clone(),
|
||||
|
|
@ -113,7 +125,14 @@ pub async fn execute_agent_run(
|
|||
.await
|
||||
.map_err(crate::error::AppError::Database)?;
|
||||
|
||||
get_task_inner(task_id, db).await
|
||||
let task_response = get_task_inner(task_id, db).await?;
|
||||
let _ = _scheduler
|
||||
.tx
|
||||
.send(crate::server::notifications::WsEvent::RunFinished(
|
||||
task_response.clone(),
|
||||
));
|
||||
|
||||
Ok(task_response)
|
||||
}
|
||||
|
||||
pub async fn get_task_inner(id: Uuid, db: &DatabaseConnection) -> AppResult<TaskResponse> {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ pub struct Scheduler {
|
|||
db: DatabaseConnection,
|
||||
tasks_to_jobs: DashMap<Uuid, Uuid>,
|
||||
config: Arc<crate::config::Config>,
|
||||
pub tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub async fn new(
|
||||
db: DatabaseConnection,
|
||||
config: Arc<crate::config::Config>,
|
||||
tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
|
||||
) -> AppResult<Self> {
|
||||
let scheduler = JobScheduler::new()
|
||||
.await
|
||||
|
|
@ -32,6 +34,7 @@ impl Scheduler {
|
|||
db,
|
||||
tasks_to_jobs: DashMap::new(),
|
||||
config,
|
||||
tx,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -43,12 +46,14 @@ impl Scheduler {
|
|||
|
||||
let db = self.db.clone();
|
||||
let config = self.config.clone();
|
||||
let tx = self.tx.clone();
|
||||
|
||||
let job = Job::new_async(cron_expr, move |_uuid, _l| {
|
||||
let db = db.clone();
|
||||
let config = config.clone();
|
||||
let tx = tx.clone();
|
||||
Box::pin(async move {
|
||||
if let Err(e) = Self::run_task(db, config, task_id).await {
|
||||
if let Err(e) = Self::run_task(db, config, tx, task_id).await {
|
||||
tracing::error!("Error in scheduled task {}: {}", task_id, e);
|
||||
}
|
||||
})
|
||||
|
|
@ -81,6 +86,7 @@ impl Scheduler {
|
|||
async fn run_task(
|
||||
db: DatabaseConnection,
|
||||
config: Arc<crate::config::Config>,
|
||||
tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
|
||||
task_id: Uuid,
|
||||
) -> AppResult<()> {
|
||||
let task = Task::find_by_id(task_id)
|
||||
|
|
@ -105,6 +111,16 @@ impl Scheduler {
|
|||
use sea_orm::ActiveModelTrait;
|
||||
run.insert(&db).await.map_err(AppError::Database)?;
|
||||
|
||||
let _ = tx.send(crate::server::notifications::WsEvent::RunStarted(
|
||||
crate::domain::tasks::RecentRunResponse {
|
||||
id: run_id,
|
||||
task_id,
|
||||
goal: task.goal.clone(),
|
||||
status: "running".to_string(),
|
||||
created_at: chrono::Utc::now().into(),
|
||||
},
|
||||
));
|
||||
|
||||
// Start agent in background
|
||||
let mut agent = Agent::new(
|
||||
db.clone(),
|
||||
|
|
@ -143,6 +159,12 @@ impl Scheduler {
|
|||
e
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(task_response) = crate::domain::tasks::get_task_inner(task_id, &db).await {
|
||||
let _ = tx.send(crate::server::notifications::WsEvent::RunFinished(
|
||||
task_response,
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod auth;
|
||||
pub mod chat;
|
||||
pub mod notifications;
|
||||
pub mod tasks;
|
||||
|
||||
use axum::{
|
||||
|
|
@ -24,6 +25,7 @@ pub struct AppState {
|
|||
pub config: Arc<crate::config::Config>,
|
||||
pub verifier: Arc<crate::domain::auth::JwksVerifier>,
|
||||
pub authenticator: Arc<crate::domain::auth::Authenticator>,
|
||||
pub tx: tokio::sync::broadcast::Sender<crate::server::notifications::WsEvent>,
|
||||
}
|
||||
|
||||
pub async fn start(config: crate::config::Config) -> AppResult<()> {
|
||||
|
|
@ -31,8 +33,10 @@ pub async fn start(config: crate::config::Config) -> AppResult<()> {
|
|||
|
||||
let config = Arc::new(config);
|
||||
|
||||
let (tx, _) = tokio::sync::broadcast::channel(100);
|
||||
|
||||
let scheduler = Arc::new(
|
||||
Scheduler::new(db.clone(), config.clone())
|
||||
Scheduler::new(db.clone(), config.clone(), tx.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))?,
|
||||
);
|
||||
|
|
@ -56,6 +60,7 @@ pub async fn start(config: crate::config::Config) -> AppResult<()> {
|
|||
config: config.clone(),
|
||||
verifier,
|
||||
authenticator,
|
||||
tx,
|
||||
});
|
||||
|
||||
let app = build_app(state, &config);
|
||||
|
|
@ -122,6 +127,7 @@ fn build_app(state: Arc<AppState>, config: &crate::config::Config) -> Router {
|
|||
.route("/api/auth/refresh", post(auth::auth_refresh))
|
||||
.route("/api/auth/logout", post(auth::auth_logout))
|
||||
.route("/api/chat", post(chat::chat_handler))
|
||||
.route("/api/ws", get(notifications::ws_handler))
|
||||
.layer(cors)
|
||||
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
|
||||
axum::http::header::CONTENT_SECURITY_POLICY,
|
||||
|
|
|
|||
46
src/server/notifications.rs
Normal file
46
src/server/notifications.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use crate::domain::tasks::{RecentRunResponse, TaskResponse};
|
||||
use crate::server::AppState;
|
||||
use axum::{
|
||||
extract::{
|
||||
State,
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
pub enum WsEvent {
|
||||
TaskCreated(TaskResponse),
|
||||
TaskUpdated(TaskResponse),
|
||||
RunStarted(RecentRunResponse),
|
||||
RunFinished(TaskResponse),
|
||||
}
|
||||
|
||||
pub async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| handle_socket(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_socket(mut socket: WebSocket, state: Arc<AppState>) {
|
||||
let mut rx = state.tx.subscribe();
|
||||
|
||||
while let Ok(event) = rx.recv().await {
|
||||
let msg = match serde_json::to_string(&event) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to serialize WsEvent: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if socket.send(Message::Text(msg)).await.is_err() {
|
||||
// Client disconnected
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,13 @@ pub async fn create_task(
|
|||
let _ = state.scheduler.add_task_job(task_id, cron).await;
|
||||
}
|
||||
|
||||
tasks::get_task_inner(task_id, &state.db).await.map(Json)
|
||||
let task_response = tasks::get_task_inner(task_id, &state.db).await?;
|
||||
let _ = state
|
||||
.tx
|
||||
.send(crate::server::notifications::WsEvent::TaskCreated(
|
||||
task_response.clone(),
|
||||
));
|
||||
Ok(Json(task_response))
|
||||
}
|
||||
|
||||
pub async fn rerun_task(
|
||||
|
|
@ -150,7 +156,13 @@ pub async fn update_task(
|
|||
let _ = state.scheduler.remove_task_job(id).await;
|
||||
}
|
||||
|
||||
tasks::get_task_inner(id, &state.db).await.map(Json)
|
||||
let task_response = tasks::get_task_inner(id, &state.db).await?;
|
||||
let _ = state
|
||||
.tx
|
||||
.send(crate::server::notifications::WsEvent::TaskUpdated(
|
||||
task_response.clone(),
|
||||
));
|
||||
Ok(Json(task_response))
|
||||
}
|
||||
|
||||
pub async fn get_task(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue