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}