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}