//! The connection side of the per-agent schedule KV bucket: opening it for //! writing and the agent's own direct read. The bucket's names, subjects and //! stored types are `swarm_wire::agent_schedules`. /// Open the schedule bucket for writing, creating it if nothing has yet. /// /// **Controller-side only.** `history: 1`: an agent wants its current list. #[cfg(feature = "kv")] pub async fn open_or_create( client: &async_nats::Client, ) -> Result { let js = async_nats::jetstream::new(client.clone()); crate::kv::open_or_create( &js, swarm_wire::agent_schedules::BUCKET, "agent-schedule", "Scheduled prompts the swarm controller declares for each agent".to_owned(), ) .await } /// What reading one agent's key found. #[cfg(feature = "agent-schedules")] #[derive(Debug)] pub enum Read { /// The bucket answered; an absent or deleted key is an empty list. Current(swarm_wire::agent_schedules::AgentSchedules), /// Nothing answers for the bucket's stream: no schedule has been written /// yet, or `JetStream` is not serving it right now. Not an empty list, /// since the second case says nothing about the agent's schedules. NoBucket, } /// Read `agent`'s current list with the one subject its grant covers. /// /// # Errors /// /// A failed request or an undecodable value. A request the queue refused for /// permissions reaches the client as a timeout, not as a refusal. #[cfg(feature = "agent-schedules")] pub async fn read_own( client: &async_nats::Client, agent: &str, ) -> Result> { use async_nats::jetstream::stream::DirectGetErrorKind; use swarm_wire::agent_schedules::{AgentSchedules, STREAM, key_subject}; let js = async_nats::jetstream::new(client.clone()); let stream = js.get_stream_no_info(STREAM).await?; let message = match stream.direct_get_last_for_subject(key_subject(agent)).await { Ok(message) => message, Err(e) if matches!(e.kind(), DirectGetErrorKind::NotFound) => { return Ok(Read::Current(AgentSchedules::default())); } Err(e) if is_no_responders(&e) => return Ok(Read::NoBucket), Err(e) => return Err(e.into()), }; // A KV delete or purge is stored as an empty marker carrying this header. let removed = message .headers .get("KV-Operation") .is_some_and(|op| matches!(op.as_str(), "DEL" | "PURGE")); if removed { return Ok(Read::Current(AgentSchedules::default())); } Ok(Read::Current(serde_json::from_slice(&message.payload)?)) } #[cfg(feature = "agent-schedules")] fn is_no_responders(e: &async_nats::jetstream::stream::DirectGetError) -> bool { matches!( e.kind(), async_nats::jetstream::stream::DirectGetErrorKind::ErrorResponse(status, _) if status == async_nats::StatusCode::NO_RESPONDERS ) }