Skip to main content

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}