myrmic_common/cells/mod.rs
1//! Cell identity, messaging, spawning, and lifecycle wire types.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use serde::{Deserialize, Serialize};
6
7pub use ids::{Sri, Srn};
8pub use names::{Command, Event};
9pub use naming::{NameError, ROOT_NS, child_sri, resolve_target, sri_of_path, validate_segment};
10
11mod ids;
12mod names;
13pub mod naming;
14pub mod spawn_ref;
15
16// Cell query error codes
17/// Command error code: no response within the timeout.
18pub const ERR_CELL_CMD_TIMEOUT: i32 = -7;
19/// Command error code: the target cell is not present in the system.
20pub const ERR_CELL_CMD_CELL_NOT_PRESENT: i32 = -2;
21/// Command error code: the cell exists but has no such command, or the
22/// arguments don't match what it expects.
23pub const ERR_CELL_CMD_COMMAND_NOT_PRESENT: i32 = -3;
24/// Command error code: the receiving cell crashed or errored while handling
25/// the command.
26pub const ERR_CELL_CMD_CELL_ERROR: i32 = -4;
27/// Command error code: an internal framework error.
28pub const ERR_CELL_CMD_INTERNAL: i32 = -5;
29/// Command error code: the provided buffer was too small for the response.
30pub const ERR_CELL_CMD_SMALL_BUFFER: i32 = -6;
31/// Error code: a request or response failed to (de)serialize.
32pub const ERR_SERIALISATION: i32 = -127;
33
34/// Represents the request to send a command to a cell
35#[derive(Serialize, Deserialize, Debug)]
36#[allow(missing_docs)]
37pub struct CommandRequest {
38 pub sri: Sri,
39 pub command: Command,
40 /// The encoded command payload; `None` for payload-less commands.
41 pub payload: Option<Vec<u8>>,
42}
43
44/// Represents the request to publish an event
45#[derive(Serialize, Deserialize, Debug)]
46#[allow(missing_docs)]
47pub struct EventPublishRequest {
48 pub event: Event,
49 /// The encoded event payload; `None` for payload-less events.
50 pub payload: Option<Vec<u8>>,
51}
52
53/// Represents the request to create a timer (periodic interval or one-shot delay).
54#[derive(Serialize, Deserialize, Debug)]
55pub struct CreateTimerRequest {
56 /// The command export invoked on each tick.
57 pub export_name: String,
58 /// Delay before the first tick, in milliseconds.
59 pub delay_ms: u64,
60 /// Tick period in milliseconds; `0` for a one-shot delay.
61 pub period_ms: u64,
62 /// Maximum number of ticks; `None` runs until the cell stops.
63 pub count: Option<u32>,
64 /// When `true`, the period is measured from the end of one invocation to
65 /// the start of the next (fixed delay) rather than tick-to-tick (fixed
66 /// rate).
67 pub fixed_delay: bool,
68}
69
70/// A request to spawn a new cell instance at runtime.
71///
72/// Identifies a cell class either by content hash or by registered name.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub enum ClassRef {
75 /// SHA-256 content hash of the Wasm binary.
76 Hash([u8; 32]),
77 /// Registered class name.
78 Name(String),
79}
80
81/// Used by cells to create and deploy other cells via the `spawn_cell` host
82/// function.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SpawnRequest {
85 /// The cell class to instantiate.
86 pub class: ClassRef,
87 /// Local name for the new cell, unique among the caller's children. The
88 /// host derives the child's SRI as `child_sri(caller_sri, local_name)` (see
89 /// [`crate::cells::naming`]); the caller never supplies a raw SRI.
90 pub local_name: Option<String>,
91 /// Optional placement tags constraining where the cell is deployed. If
92 /// `None`, no placement constraints are applied.
93 pub tags: Option<Vec<String>>,
94 /// Optional payload delivered to the child's `#[init]` handler as its
95 /// argument buffer. `None` (or empty) means the child must have a
96 /// no-payload `#[init]`; a non-empty buffer is decoded by the init
97 /// handler's `Decoder` param, exactly like a command payload.
98 pub arguments: Option<Vec<u8>>,
99 /// Decouples the child's lifetime from the caller's: no fencing against
100 /// the parent, excluded from cascades, no `cell_lost` on either side.
101 /// Declared only by the spawning parent.
102 pub detached: bool,
103 /// Per-edge fencing tolerance: how long the child outlives this cell's
104 /// silence before the runtime kills it. `None` = the cluster default.
105 pub grace_ms: Option<u64>,
106 /// Per-edge death deadline: how long the child's node must be silent
107 /// before it is declared dead (rows released, `cell_lost` sent). Short =
108 /// fast failover but misfires on slow reboots; long lets the child ride
109 /// one out. Clamped to at least twice the lease renewal period; `None` =
110 /// the cluster default.
111 pub deadline_ms: Option<u64>,
112}
113
114/// Host status code: the call succeeded.
115pub const STATUS_OK: core::ffi::c_int = 0;
116/// Spawn error code: no cell class with the given hash or name is registered.
117pub const SPAWN_ERR_CLASS_NOT_FOUND: core::ffi::c_int = -10;
118/// Spawn error code: the caller already has a child with that local name.
119pub const SPAWN_ERR_ALREADY_EXISTS: core::ffi::c_int = -11;
120/// Spawn error code: the deploy failed after the child's name was claimed.
121pub const SPAWN_ERR_DEPLOY_FAILED: core::ffi::c_int = -12;
122
123/// Errors that can occur when spawning a cell.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum SpawnError {
126 /// No cell class with the given hash or name is registered.
127 ClassNotFound,
128 /// The caller already has a child with that local name.
129 AlreadyExists,
130 /// The deploy failed after the child's name was claimed.
131 DeployFailed,
132}
133
134impl TryFrom<core::ffi::c_int> for SpawnError {
135 type Error = core::ffi::c_int;
136
137 fn try_from(code: core::ffi::c_int) -> Result<Self, Self::Error> {
138 match code {
139 SPAWN_ERR_CLASS_NOT_FOUND => Ok(Self::ClassNotFound),
140 SPAWN_ERR_ALREADY_EXISTS => Ok(Self::AlreadyExists),
141 SPAWN_ERR_DEPLOY_FAILED => Ok(Self::DeployFailed),
142 _ => Err(code),
143 }
144 }
145}
146
147impl From<SpawnError> for &'static str {
148 fn from(err: SpawnError) -> Self {
149 match err {
150 SpawnError::ClassNotFound => "class not found",
151 SpawnError::AlreadyExists => "already exists",
152 SpawnError::DeployFailed => "deploy failed",
153 }
154 }
155}
156
157/// Reserved system command name carrying a [`CellLost`] payload. Routed to
158/// the guest's `on_cell_lost` export, never to a `command_*` handler. The
159/// guest-facing send path must reject names with this prefix so cells cannot
160/// spoof system notifications.
161pub const SYS_CELL_LOST: &str = "__sys_cell_lost";
162
163/// Prefix reserved for host-emitted system commands.
164pub const SYS_COMMAND_PREFIX: &str = "__sys";
165
166/// Why a cell died (spec §5).
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub enum LostReason {
169 /// The cell's node lease expired.
170 NodeLost,
171 /// The cell's task ended without a deliberate undeploy (trap, panic,
172 /// runtime error). Its subtree died with it.
173 Crashed,
174 /// Deliberate `stop_self(code)`; the code is the cell's stated reason.
175 Stopped {
176 /// The exit code the cell passed to `stop_self`.
177 code: Option<u32>,
178 },
179 /// Killed by an ancestor's terminate or a cascade passing through.
180 Terminated,
181 /// A spawn this cell requested never came up: the deploy failed after
182 /// the child's name was claimed (placement, transfer, or exec failure).
183 /// Not sent for detached spawns; the spawn call also returns the error.
184 SpawnFailed,
185}
186
187/// Notification that a watched cell died. Today only parents receive these
188/// (for their children); the shape is deliberately cell-, not child-,
189/// centric so general monitoring can reuse it. Delivered as the payload of
190/// the reserved [`SYS_CELL_LOST`] command.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct CellLost {
193 /// The cell that died (never the receiver of the notification).
194 pub cell: Sri,
195 /// Spawn-time local name when known; `None` for external deploys.
196 /// Helpers need not rely on it — deterministic naming lets a parent
197 /// derive its children's SRIs from the names it spawned.
198 pub local_name: Option<String>,
199 /// Why the cell died.
200 pub reason: LostReason,
201}
202
203/// Terminate error code: no cell with the given SRI exists.
204pub const TERMINATE_ERR_NOT_FOUND: core::ffi::c_int = -20;
205/// Terminate error code: the undeploy step failed.
206pub const TERMINATE_ERR_UNDEPLOY_FAILED: core::ffi::c_int = -21;
207/// Terminate error code: erasing the cell's records failed.
208pub const TERMINATE_ERR_ERASE_FAILED: core::ffi::c_int = -22;
209/// Terminate error code: the target is not the caller or one of its
210/// descendants.
211pub const TERMINATE_ERR_NOT_PERMITTED: core::ffi::c_int = -23;
212
213/// Errors that can occur when terminating a cell.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum TerminateError {
216 /// No cell with the given SRI exists.
217 NotFound,
218 /// The undeploy step failed.
219 UndeployFailed,
220 /// Erasing the cell's records failed.
221 EraseFailed,
222 /// The target is not the caller or one of its descendants — kill
223 /// authority is ancestry (spec §6).
224 NotPermitted,
225}
226
227impl TryFrom<core::ffi::c_int> for TerminateError {
228 type Error = core::ffi::c_int;
229
230 fn try_from(code: core::ffi::c_int) -> Result<Self, Self::Error> {
231 match code {
232 TERMINATE_ERR_NOT_FOUND => Ok(Self::NotFound),
233 TERMINATE_ERR_UNDEPLOY_FAILED => Ok(Self::UndeployFailed),
234 TERMINATE_ERR_ERASE_FAILED => Ok(Self::EraseFailed),
235 TERMINATE_ERR_NOT_PERMITTED => Ok(Self::NotPermitted),
236 _ => Err(code),
237 }
238 }
239}
240
241impl From<TerminateError> for &'static str {
242 fn from(err: TerminateError) -> Self {
243 match err {
244 TerminateError::NotFound => "not found",
245 TerminateError::UndeployFailed => "undeploy failed",
246 TerminateError::EraseFailed => "erase failed",
247 TerminateError::NotPermitted => "not permitted",
248 }
249 }
250}
251
252#[cfg(test)]
253mod cell_lost_tests {
254 use alloc::string::ToString;
255
256 use super::*;
257
258 #[test]
259 fn cell_lost_round_trips_all_reasons() {
260 for reason in [
261 LostReason::NodeLost,
262 LostReason::Crashed,
263 LostReason::Stopped { code: Some(3) },
264 LostReason::Stopped { code: None },
265 LostReason::Terminated,
266 LostReason::SpawnFailed,
267 ] {
268 let v = CellLost {
269 cell: sri_of_path("round-trip-test").unwrap().into(),
270 local_name: Some("pump".to_string()),
271 reason: reason.clone(),
272 };
273 let bytes = postcard::to_allocvec(&v).unwrap();
274 assert_eq!(postcard::from_bytes::<CellLost>(&bytes).unwrap(), v);
275 }
276 }
277
278 #[test]
279 fn sys_cell_lost_is_a_valid_command_name() {
280 assert!(Command::new(SYS_CELL_LOST.to_string()).is_ok());
281 assert!(SYS_CELL_LOST.starts_with(SYS_COMMAND_PREFIX));
282 }
283}