Skip to main content

libpetri_event/
net_event.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::token_payload::TokenPayload;
5
6/// All observable events during Petri net execution.
7///
8/// 13 event types as a discriminated union matching the TypeScript/Java implementations.
9#[derive(Debug, Clone)]
10pub enum NetEvent {
11    /// Net execution started.
12    ExecutionStarted { net_name: Arc<str>, timestamp: u64 },
13    /// Net execution completed (no more enabled transitions, no in-flight actions).
14    ExecutionCompleted { net_name: Arc<str>, timestamp: u64 },
15    /// Transition became enabled (all input/inhibitor/read conditions satisfied).
16    TransitionEnabled {
17        transition_name: Arc<str>,
18        timestamp: u64,
19    },
20    /// Transition's enabling clock restarted (input place tokens changed while enabled).
21    TransitionClockRestarted {
22        transition_name: Arc<str>,
23        timestamp: u64,
24    },
25    /// Transition started firing (tokens consumed, action dispatched).
26    TransitionStarted {
27        transition_name: Arc<str>,
28        timestamp: u64,
29    },
30    /// Transition completed successfully (outputs produced).
31    TransitionCompleted {
32        transition_name: Arc<str>,
33        timestamp: u64,
34    },
35    /// Transition action failed with error.
36    TransitionFailed {
37        transition_name: Arc<str>,
38        error: String,
39        timestamp: u64,
40    },
41    /// Transition exceeded its timing deadline and was force-disabled.
42    TransitionTimedOut {
43        transition_name: Arc<str>,
44        timestamp: u64,
45    },
46    /// Transition action exceeded its action timeout.
47    ActionTimedOut {
48        transition_name: Arc<str>,
49        timeout_ms: u64,
50        timestamp: u64,
51    },
52    /// Token added to a place.
53    ///
54    /// The optional `token` payload is populated only by event stores that opt in
55    /// via [`EventStore::CAPTURES_TOKENS`](crate::event_store::EventStore::CAPTURES_TOKENS).
56    /// Production stores (`NoopEventStore`, `InMemoryEventStore`) leave it `None`
57    /// so the capture path is monomorphized out — preserving the zero-cost event
58    /// recording invariant.
59    ///
60    /// Marked `#[non_exhaustive]` so future fields (e.g. token identity,
61    /// source-transition attribution) can be added without a breaking change;
62    /// downstream matches must use `{ .., .. }`.
63    #[non_exhaustive]
64    TokenAdded {
65        place_name: Arc<str>,
66        timestamp: u64,
67        token: Option<Arc<dyn TokenPayload>>,
68    },
69    /// Token removed from a place.
70    ///
71    /// See [`TokenAdded`](Self::TokenAdded) for payload semantics and for the
72    /// `#[non_exhaustive]` contract.
73    #[non_exhaustive]
74    TokenRemoved {
75        place_name: Arc<str>,
76        timestamp: u64,
77        token: Option<Arc<dyn TokenPayload>>,
78    },
79    /// Log message emitted by a transition action.
80    LogMessage {
81        transition_name: Arc<str>,
82        level: String,
83        message: String,
84        timestamp: u64,
85    },
86    /// Snapshot of the current marking (token counts per place).
87    MarkingSnapshot {
88        marking: HashMap<Arc<str>, usize>,
89        timestamp: u64,
90    },
91}
92
93/// Returns the instance prefix carried by a node name, or [`None`] when the
94/// name has no `/` (i.e. is not part of any composed subnet instance) per
95/// `spec/11-modular-composition.md` **MOD-041**.
96///
97/// The prefix is the substring up to (but not including) the **last** `/`
98/// — so `producer1/internal` returns `Some("producer1")` and
99/// `outer/inner/leaf` returns `Some("outer/inner")`.
100///
101/// Duplicated locally inside the event package (rather than delegated to
102/// `libpetri_export::subnet_prefixes`) to keep the event subsystem
103/// dependency-free of the export layer per the package-isolation contract.
104/// Mirrors `org.libpetri.event.NetEvent#instancePrefixOf`.
105pub fn instance_prefix_of(name: &str) -> Option<&str> {
106    let idx = name.rfind('/')?;
107    if idx == 0 {
108        return None;
109    }
110    Some(&name[..idx])
111}
112
113impl NetEvent {
114    /// Constructs a [`TokenAdded`](Self::TokenAdded) event without a payload —
115    /// the default for production event stores.
116    pub fn token_added(place_name: Arc<str>, timestamp: u64) -> Self {
117        Self::TokenAdded { place_name, timestamp, token: None }
118    }
119
120    /// Constructs a [`TokenAdded`](Self::TokenAdded) event carrying a token
121    /// payload — used by debug-aware event stores that set
122    /// [`EventStore::CAPTURES_TOKENS = true`](crate::event_store::EventStore::CAPTURES_TOKENS).
123    pub fn token_added_with(
124        place_name: Arc<str>,
125        timestamp: u64,
126        token: Arc<dyn TokenPayload>,
127    ) -> Self {
128        Self::TokenAdded { place_name, timestamp, token: Some(token) }
129    }
130
131    /// Constructs a [`TokenRemoved`](Self::TokenRemoved) event without a payload.
132    pub fn token_removed(place_name: Arc<str>, timestamp: u64) -> Self {
133        Self::TokenRemoved { place_name, timestamp, token: None }
134    }
135
136    /// Constructs a [`TokenRemoved`](Self::TokenRemoved) event carrying a token
137    /// payload. See [`token_added_with`](Self::token_added_with).
138    pub fn token_removed_with(
139        place_name: Arc<str>,
140        timestamp: u64,
141        token: Arc<dyn TokenPayload>,
142    ) -> Self {
143        Self::TokenRemoved { place_name, timestamp, token: Some(token) }
144    }
145
146    /// Returns the timestamp of this event.
147    pub fn timestamp(&self) -> u64 {
148        match self {
149            NetEvent::ExecutionStarted { timestamp, .. }
150            | NetEvent::ExecutionCompleted { timestamp, .. }
151            | NetEvent::TransitionEnabled { timestamp, .. }
152            | NetEvent::TransitionClockRestarted { timestamp, .. }
153            | NetEvent::TransitionStarted { timestamp, .. }
154            | NetEvent::TransitionCompleted { timestamp, .. }
155            | NetEvent::TransitionFailed { timestamp, .. }
156            | NetEvent::TransitionTimedOut { timestamp, .. }
157            | NetEvent::ActionTimedOut { timestamp, .. }
158            | NetEvent::TokenAdded { timestamp, .. }
159            | NetEvent::TokenRemoved { timestamp, .. }
160            | NetEvent::LogMessage { timestamp, .. }
161            | NetEvent::MarkingSnapshot { timestamp, .. } => *timestamp,
162        }
163    }
164
165    /// Returns the place name if this event is place-related.
166    pub fn place_name(&self) -> Option<&str> {
167        match self {
168            NetEvent::TokenAdded { place_name, .. }
169            | NetEvent::TokenRemoved { place_name, .. } => Some(place_name),
170            _ => None,
171        }
172    }
173
174    /// Returns the derived instance prefix per **MOD-041** for this event,
175    /// computed from whichever of [`Self::transition_name`] or
176    /// [`Self::place_name`] is populated. Returns [`None`] for events that
177    /// don't carry a node name (e.g. [`Self::ExecutionStarted`],
178    /// [`Self::MarkingSnapshot`]) or when the carried name is flat (no `/`).
179    pub fn instance_prefix(&self) -> Option<&str> {
180        let name = self.transition_name().or_else(|| self.place_name())?;
181        instance_prefix_of(name)
182    }
183
184    /// Returns the transition name if this event is transition-related.
185    pub fn transition_name(&self) -> Option<&str> {
186        match self {
187            NetEvent::TransitionEnabled {
188                transition_name, ..
189            }
190            | NetEvent::TransitionClockRestarted {
191                transition_name, ..
192            }
193            | NetEvent::TransitionStarted {
194                transition_name, ..
195            }
196            | NetEvent::TransitionCompleted {
197                transition_name, ..
198            }
199            | NetEvent::TransitionFailed {
200                transition_name, ..
201            }
202            | NetEvent::TransitionTimedOut {
203                transition_name, ..
204            }
205            | NetEvent::ActionTimedOut {
206                transition_name, ..
207            }
208            | NetEvent::LogMessage {
209                transition_name, ..
210            } => Some(transition_name),
211            _ => None,
212        }
213    }
214
215    /// Returns true if this is a failure event.
216    pub fn is_failure(&self) -> bool {
217        matches!(
218            self,
219            NetEvent::TransitionFailed { .. }
220                | NetEvent::TransitionTimedOut { .. }
221                | NetEvent::ActionTimedOut { .. }
222        )
223    }
224
225    /// Returns true if this event carries any error signal — superset of
226    /// [`is_failure`](Self::is_failure) that additionally treats a
227    /// [`LogMessage`](NetEvent::LogMessage) at level `ERROR` (case-insensitive)
228    /// as an error. Used by the v2 archive writer to pre-compute the
229    /// `hasErrors` flag so listing/sampling tools can filter archives without
230    /// scanning their event bodies. (libpetri 1.7.0+)
231    pub fn has_error_signal(&self) -> bool {
232        match self {
233            NetEvent::TransitionFailed { .. }
234            | NetEvent::TransitionTimedOut { .. }
235            | NetEvent::ActionTimedOut { .. } => true,
236            NetEvent::LogMessage { level, .. } => level.eq_ignore_ascii_case("ERROR"),
237            _ => false,
238        }
239    }
240}