Skip to main content

pureflow_core/
lifecycle.rs

1//! Lifecycle events and observer hooks at the runtime boundary.
2//!
3//! ## Fragment: lifecycle-observer-seam
4//!
5//! The lifecycle surface exists before the runtime fully uses it because
6//! observability is part of the public direction of Pureflow, not an afterthought.
7//! The current seam is intentionally thin: it names the events that matter and
8//! leaves registration, fan-out, and buffering policy to later runtime beads.
9//!
10//! ## Fragment: lifecycle-event-vocabulary
11//!
12//! The event kinds are phrased around runtime transitions rather than around
13//! implementation details. That keeps the vocabulary stable if the execution
14//! engine changes from a sequential scaffold to structured concurrency later.
15
16use crate::{Result, context::NodeContext};
17
18/// Lifecycle event emitted at runtime boundaries.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LifecycleEventKind {
21    /// A node has been selected for execution.
22    NodeScheduled,
23    /// A node is about to start execution.
24    NodeStarted,
25    /// A node completed successfully.
26    NodeCompleted,
27    /// A node failed and returned an error.
28    NodeFailed,
29    /// A node observed or received cancellation.
30    NodeCancelled,
31}
32
33/// Runtime lifecycle event with the context needed by observers.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct LifecycleEvent {
36    kind: LifecycleEventKind,
37    context: NodeContext,
38}
39
40impl LifecycleEvent {
41    /// Create a lifecycle event for a node context.
42    #[must_use]
43    pub const fn new(kind: LifecycleEventKind, context: NodeContext) -> Self {
44        Self { kind, context }
45    }
46
47    /// Kind of lifecycle transition.
48    #[must_use]
49    pub const fn kind(&self) -> LifecycleEventKind {
50        self.kind
51    }
52
53    /// Node context associated with the lifecycle transition.
54    #[must_use]
55    pub const fn context(&self) -> &NodeContext {
56        &self.context
57    }
58}
59
60/// Observer hook for runtime lifecycle transitions.
61pub trait LifecycleHook: Sync {
62    /// Observe one lifecycle event.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error when the observer cannot record or react to the event.
67    fn observe(&self, event: &LifecycleEvent) -> Result<()>;
68}
69
70/// Default lifecycle hook that intentionally records nothing.
71#[derive(Debug, Clone, Copy, Default)]
72pub struct NoopLifecycleHook;
73
74impl LifecycleHook for NoopLifecycleHook {
75    fn observe(&self, _event: &LifecycleEvent) -> Result<()> {
76        Ok(())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::context::ExecutionMetadata;
84    use pureflow_types::{ExecutionId, NodeId, WorkflowId};
85
86    fn execution_id(value: &str) -> ExecutionId {
87        ExecutionId::new(value).expect("valid execution id")
88    }
89
90    fn node_id(value: &str) -> NodeId {
91        NodeId::new(value).expect("valid node id")
92    }
93
94    fn workflow_id(value: &str) -> WorkflowId {
95        WorkflowId::new(value).expect("valid workflow id")
96    }
97
98    fn execution() -> ExecutionMetadata {
99        ExecutionMetadata::first_attempt(execution_id("run-1"))
100    }
101
102    #[test]
103    fn lifecycle_event_carries_kind_and_context() {
104        let context: NodeContext =
105            NodeContext::new(workflow_id("flow"), node_id("node"), execution());
106        let event: LifecycleEvent = LifecycleEvent::new(LifecycleEventKind::NodeStarted, context);
107
108        assert_eq!(event.kind(), LifecycleEventKind::NodeStarted);
109        assert_eq!(event.context().node_id().as_str(), "node");
110    }
111
112    #[test]
113    fn noop_lifecycle_hook_accepts_events() {
114        let context: NodeContext =
115            NodeContext::new(workflow_id("flow"), node_id("node"), execution());
116        let event: LifecycleEvent = LifecycleEvent::new(LifecycleEventKind::NodeCompleted, context);
117
118        NoopLifecycleHook
119            .observe(&event)
120            .expect("noop hook should accept lifecycle events");
121    }
122}