Skip to main content

orcs_runtime/channel/
command.rs

1//! World commands for concurrent state modification.
2//!
3//! [`WorldCommand`] represents state changes to the [`World`](super::World)
4//! that are applied asynchronously via a command queue. This enables
5//! lock-free concurrent access from multiple [`ChannelRunner`](super::ChannelRunner)s.
6//!
7//! # Design
8//!
9//! Instead of directly mutating World with locks, channel runners send
10//! commands to a dedicated manager task that applies them sequentially.
11//!
12//! ```text
13//! ChannelRunner A ──┐
14//!                   │
15//! ChannelRunner B ──┼──► mpsc::Sender<WorldCommand> ──► WorldManager
16//!                   │                                        │
17//! ChannelRunner C ──┘                                        ▼
18//!                                                     World (sequential apply)
19//! ```
20//!
21//! # Example
22//!
23//! ```ignore
24//! use orcs_runtime::channel::{WorldCommand, ChannelConfig};
25//! use tokio::sync::oneshot;
26//!
27//! let (reply_tx, reply_rx) = oneshot::channel();
28//! let cmd = WorldCommand::Spawn {
29//!     parent: parent_id,
30//!     config: ChannelConfig::default(),
31//!     reply: reply_tx,
32//! };
33//!
34//! world_tx.send(cmd).await?;
35//! let new_channel_id = reply_rx.await?;
36//! ```
37
38use super::config::ChannelConfig;
39use super::ChannelState;
40use orcs_types::ChannelId;
41use tokio::sync::oneshot;
42
43/// Commands for modifying [`World`](super::World) state.
44///
45/// These commands are sent to the [`WorldManager`](super::WorldManager)
46/// which applies them sequentially to maintain consistency.
47#[derive(Debug)]
48pub enum WorldCommand {
49    /// Spawn a new child channel.
50    ///
51    /// Creates a new channel under the specified parent with the given config.
52    /// The new channel's ID is returned via the reply channel.
53    Spawn {
54        /// Parent channel ID.
55        parent: ChannelId,
56        /// Configuration for the new channel.
57        config: ChannelConfig,
58        /// Reply channel for the new channel's ID.
59        /// Returns `None` if parent doesn't exist.
60        reply: oneshot::Sender<Option<ChannelId>>,
61    },
62
63    /// Spawn a new child channel with a pre-determined [`ChannelId`].
64    ///
65    /// Same as `Spawn` but the caller supplies the ID.
66    /// Used by `spawn_runner` which needs the ID before the World is updated.
67    SpawnWithId {
68        /// Parent channel ID.
69        parent: ChannelId,
70        /// Pre-determined channel ID for the new channel.
71        id: ChannelId,
72        /// Configuration for the new channel.
73        config: ChannelConfig,
74        /// Reply channel: `true` on success, `false` if parent doesn't exist.
75        reply: oneshot::Sender<bool>,
76    },
77
78    /// Kill a channel and all its descendants.
79    ///
80    /// Removes the channel from the World entirely.
81    Kill {
82        /// Channel to kill.
83        id: ChannelId,
84        /// Reason for killing (for logging/debugging).
85        reason: String,
86    },
87
88    /// Complete a channel successfully.
89    ///
90    /// Transitions the channel to Completed state but keeps it in the World.
91    Complete {
92        /// Channel to complete.
93        id: ChannelId,
94        /// Reply with success/failure.
95        reply: oneshot::Sender<bool>,
96    },
97
98    /// Update a channel's state.
99    ///
100    /// Used for state transitions like Pause, Resume, AwaitApproval.
101    UpdateState {
102        /// Channel to update.
103        id: ChannelId,
104        /// New state to transition to.
105        transition: StateTransition,
106        /// Reply with success/failure.
107        reply: oneshot::Sender<bool>,
108    },
109
110    /// Query a channel's current state (read-only).
111    ///
112    /// For read operations that need consistency with pending writes.
113    GetState {
114        /// Channel to query.
115        id: ChannelId,
116        /// Reply with the current state, or None if not found.
117        reply: oneshot::Sender<Option<ChannelState>>,
118    },
119
120    /// Shutdown the WorldManager.
121    ///
122    /// Signals the manager to stop processing commands.
123    Shutdown,
124}
125
126/// State transition operations.
127///
128/// These map to the state machine transitions in [`Channel`](super::Channel).
129#[derive(Debug, Clone)]
130pub enum StateTransition {
131    /// Pause a running channel.
132    Pause,
133    /// Resume a paused channel.
134    Resume,
135    /// Enter approval-waiting state.
136    AwaitApproval {
137        /// ID of the approval request.
138        request_id: String,
139    },
140    /// Resolve an approval (approved).
141    ResolveApproval {
142        /// ID of the approval being resolved.
143        approval_id: String,
144    },
145    /// Abort with reason.
146    Abort {
147        /// Reason for aborting.
148        reason: String,
149    },
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn spawn_command_creation() {
158        let parent = ChannelId::new();
159        let (tx, _rx) = oneshot::channel();
160
161        let cmd = WorldCommand::Spawn {
162            parent,
163            config: ChannelConfig::default(),
164            reply: tx,
165        };
166
167        assert!(matches!(cmd, WorldCommand::Spawn { .. }));
168    }
169
170    #[test]
171    fn kill_command_creation() {
172        let id = ChannelId::new();
173        let cmd = WorldCommand::Kill {
174            id,
175            reason: "test".into(),
176        };
177
178        assert!(matches!(cmd, WorldCommand::Kill { .. }));
179    }
180
181    #[test]
182    fn state_transition_variants() {
183        let pause = StateTransition::Pause;
184        let resume = StateTransition::Resume;
185        let await_approval = StateTransition::AwaitApproval {
186            request_id: "req-123".into(),
187        };
188        let abort = StateTransition::Abort {
189            reason: "cancelled".into(),
190        };
191
192        assert!(matches!(pause, StateTransition::Pause));
193        assert!(matches!(resume, StateTransition::Resume));
194        assert!(matches!(
195            await_approval,
196            StateTransition::AwaitApproval { .. }
197        ));
198        assert!(matches!(abort, StateTransition::Abort { .. }));
199    }
200
201    #[tokio::test]
202    async fn spawn_reply_channel_works() {
203        let parent = ChannelId::new();
204        let (tx, rx) = oneshot::channel();
205
206        let _cmd = WorldCommand::Spawn {
207            parent,
208            config: ChannelConfig::default(),
209            reply: tx,
210        };
211
212        // Simulate manager sending reply
213        // (In real code, WorldManager would send this)
214        // tx.send(Some(ChannelId::new())).unwrap();
215        // let result = rx.await.unwrap();
216        // assert!(result.is_some());
217
218        // Just verify channel is droppable without panic
219        drop(rx);
220    }
221}