pub struct WorkflowDefinition { /* private fields */ }Expand description
One opaque, parsed app-kit.workflow/v2 definition.
Construct definitions through Self::parse; their internals cannot be
copied through struct update syntax:
use app_kit::workflow::WorkflowDefinition;
fn reconstruct(definition: WorkflowDefinition) -> WorkflowDefinition {
WorkflowDefinition { ..definition }
}Only the strict versioned language is accepted. Unknown/duplicate fields, invalid expressions, graph references and finite-bound violations fail before runtime work. Inspection returns passive metadata; composition runs the same compiler rather than exposing a mutable executable AST.
§Example
use app_kit::workflow::WorkflowDefinition;
let definition = WorkflowDefinition::parse(r#"{
"schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
"state":{"count":0}, "required":["count"],
"reducers":{"count":"reject_concurrent"}, "entry":"increment",
"nodes":[{"id":"increment","step":{"kind":"state","mutations":[
{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
]}}], "routes":{}
}"#)?;
assert_eq!(definition.graph().entry, "increment");Implementations§
Source§impl WorkflowDefinition
impl WorkflowDefinition
Sourcepub fn graph(&self) -> GraphDescription
pub fn graph(&self) -> GraphDescription
Describes authored nodes, ordered edges and declared state access.
This projection owns metadata only; it never retains executable bindings or lower handles. Callable metadata is resolved when bindings are supplied.
§Example
use app_kit::workflow::WorkflowDefinition;
let definition = WorkflowDefinition::parse(r#"{
"schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
"state":{"count":0}, "required":["count"],
"reducers":{"count":"reject_concurrent"}, "entry":"increment",
"nodes":[{"id":"increment","step":{"kind":"state","mutations":[
{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
]}}], "routes":{}
}"#)?;
let graph = definition.graph();
assert_eq!(graph.nodes[0].id, "increment");
assert_eq!(graph.nodes[0].writes, ["count"]);Examples found in repository?
10fn main() -> Result<(), AppError> {
11 let mut imports = Vec::new();
12 for (id, prefix) in [("first-counter", "first_"), ("second-counter", "second_")] {
13 let definition = WorkflowDefinition::parse(
14 &json!({
15 "schema":"app-kit.workflow/v2", "id":id, "revision":"r1",
16 "state":{"count":0}, "required":["count"],
17 "reducers":{"count":"reject_concurrent"}, "entry":"add",
18 "nodes":[{"id":"add", "name":"Add one", "step":{
19 "kind":"state", "mutations":[{
20 "kind":"increment", "target":"count",
21 "value":{"kind":"literal", "value":1}
22 }]
23 }}],
24 "routes":{}
25 })
26 .to_string(),
27 )?;
28 imports.push(GraphImport {
29 definition,
30 prefix: prefix.into(),
31 });
32 }
33 let definition = WorkflowDefinition::compose(GraphComposition {
34 id: "two-counters".parse()?,
35 revision: "r1".into(),
36 entry: "first_add".into(),
37 imports,
38 connections: vec![GraphConnection {
39 from: "first_add".into(),
40 to: "second_add".into(),
41 }],
42 })?;
43 let graph = definition.graph();
44 assert_eq!(graph.entry, "first_add");
45 assert_eq!(graph.nodes.len(), 2);
46 assert_eq!(graph.nodes[0].id, "first_add");
47 assert_eq!(graph.nodes[1].id, "second_add");
48 assert!(
49 graph
50 .edges
51 .iter()
52 .any(|edge| { edge.from == "first_add" && edge.to.as_deref() == Some("second_add") })
53 );
54
55 let inspection = definition.export(GraphFormat::Json)?;
56 let inspection: serde_json::Value = serde_json::from_str(&inspection)
57 .map_err(|_| AppError::Integration("example graph JSON was invalid".into()))?;
58 assert_eq!(inspection["nodes"].as_array().map(Vec::len), Some(2));
59 assert!(definition.export(GraphFormat::Dot)?.starts_with("digraph "));
60 println!("{}", definition.export(GraphFormat::Mermaid)?);
61
62 // Built-in nodes retain their canonical authored source. Tracking requires
63 // actual source for every node and is a separate content-export format.
64 let tracking = graph.tracking_record()?;
65 assert_eq!(tracking["type"], "application");
66 assert_eq!(tracking["actions"].as_array().map(Vec::len), Some(2));
67 Ok(())
68}Sourcepub fn export(&self, format: GraphFormat) -> Result<String, AppError>
pub fn export(&self, format: GraphFormat) -> Result<String, AppError>
Renders deterministic inspection JSON, DOT or Mermaid without a subprocess.
Caller text is escaped for the chosen format. This is an inspection
document, distinct from GraphDescription::tracking_record.
§Errors
Returns InvalidInput when the projection exceeds JSON or output-size limits,
or Integration if graph metadata cannot be encoded.
§Example
use app_kit::workflow::WorkflowDefinition;
let definition = WorkflowDefinition::parse(r#"{
"schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
"state":{"count":0}, "required":["count"],
"reducers":{"count":"reject_concurrent"}, "entry":"increment",
"nodes":[{"id":"increment","step":{"kind":"state","mutations":[
{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
]}}], "routes":{}
}"#)?;
use app_kit::workflow::GraphFormat;
let mermaid = definition.export(GraphFormat::Mermaid)?;
assert!(mermaid.starts_with("flowchart TD"));Examples found in repository?
10fn main() -> Result<(), AppError> {
11 let mut imports = Vec::new();
12 for (id, prefix) in [("first-counter", "first_"), ("second-counter", "second_")] {
13 let definition = WorkflowDefinition::parse(
14 &json!({
15 "schema":"app-kit.workflow/v2", "id":id, "revision":"r1",
16 "state":{"count":0}, "required":["count"],
17 "reducers":{"count":"reject_concurrent"}, "entry":"add",
18 "nodes":[{"id":"add", "name":"Add one", "step":{
19 "kind":"state", "mutations":[{
20 "kind":"increment", "target":"count",
21 "value":{"kind":"literal", "value":1}
22 }]
23 }}],
24 "routes":{}
25 })
26 .to_string(),
27 )?;
28 imports.push(GraphImport {
29 definition,
30 prefix: prefix.into(),
31 });
32 }
33 let definition = WorkflowDefinition::compose(GraphComposition {
34 id: "two-counters".parse()?,
35 revision: "r1".into(),
36 entry: "first_add".into(),
37 imports,
38 connections: vec![GraphConnection {
39 from: "first_add".into(),
40 to: "second_add".into(),
41 }],
42 })?;
43 let graph = definition.graph();
44 assert_eq!(graph.entry, "first_add");
45 assert_eq!(graph.nodes.len(), 2);
46 assert_eq!(graph.nodes[0].id, "first_add");
47 assert_eq!(graph.nodes[1].id, "second_add");
48 assert!(
49 graph
50 .edges
51 .iter()
52 .any(|edge| { edge.from == "first_add" && edge.to.as_deref() == Some("second_add") })
53 );
54
55 let inspection = definition.export(GraphFormat::Json)?;
56 let inspection: serde_json::Value = serde_json::from_str(&inspection)
57 .map_err(|_| AppError::Integration("example graph JSON was invalid".into()))?;
58 assert_eq!(inspection["nodes"].as_array().map(Vec::len), Some(2));
59 assert!(definition.export(GraphFormat::Dot)?.starts_with("digraph "));
60 println!("{}", definition.export(GraphFormat::Mermaid)?);
61
62 // Built-in nodes retain their canonical authored source. Tracking requires
63 // actual source for every node and is a separate content-export format.
64 let tracking = graph.tracking_record()?;
65 assert_eq!(tracking["type"], "application");
66 assert_eq!(tracking["actions"].as_array().map(Vec::len), Some(2));
67 Ok(())
68}Sourcepub fn compose(composition: GraphComposition) -> Result<Self, AppError>
pub fn compose(composition: GraphComposition) -> Result<Self, AppError>
Imports graph pieces with explicit node prefixes and appended connections.
The ordinary compiler checks the assembled definition. Conflicting state, reducers, reused revisions, node names, entrypoints and edges fail before any runtime or lower operation is admitted. Behavior binding keys retain their original meaning; only graph-local node references are prefixed.
§Errors
Returns InvalidInput for conflicting state/reducers, incompatible revisions,
colliding prefixed nodes, invalid connections/entrypoints, graph errors, or
exceeded compiler bounds. No runtime or lower operation is admitted.
§Example
use app_kit::workflow::WorkflowDefinition;
const SOURCE: &str = r#"{
"schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
"state":{"count":0}, "required":["count"],
"reducers":{"count":"reject_concurrent"}, "entry":"increment",
"nodes":[{"id":"increment","step":{"kind":"state","mutations":[
{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
]}}], "routes":{}
}"#;
let definition = WorkflowDefinition::parse(SOURCE)?;
use app_kit::workflow::{GraphComposition, GraphConnection, GraphImport};
let composition = GraphComposition {
id: "composed".parse()?, revision: "r1".into(), entry: "a_increment".into(),
imports: vec![
GraphImport { definition, prefix: "a_".into() },
GraphImport { definition: WorkflowDefinition::parse(SOURCE)?, prefix: "b_".into() },
],
connections: vec![GraphConnection { from: "a_increment".into(), to: "b_increment".into() }],
};
let composed = WorkflowDefinition::compose(composition)?;
assert_eq!(composed.graph().nodes.len(), 2);
assert_eq!(composed.graph().entry, "a_increment");Examples found in repository?
10fn main() -> Result<(), AppError> {
11 let mut imports = Vec::new();
12 for (id, prefix) in [("first-counter", "first_"), ("second-counter", "second_")] {
13 let definition = WorkflowDefinition::parse(
14 &json!({
15 "schema":"app-kit.workflow/v2", "id":id, "revision":"r1",
16 "state":{"count":0}, "required":["count"],
17 "reducers":{"count":"reject_concurrent"}, "entry":"add",
18 "nodes":[{"id":"add", "name":"Add one", "step":{
19 "kind":"state", "mutations":[{
20 "kind":"increment", "target":"count",
21 "value":{"kind":"literal", "value":1}
22 }]
23 }}],
24 "routes":{}
25 })
26 .to_string(),
27 )?;
28 imports.push(GraphImport {
29 definition,
30 prefix: prefix.into(),
31 });
32 }
33 let definition = WorkflowDefinition::compose(GraphComposition {
34 id: "two-counters".parse()?,
35 revision: "r1".into(),
36 entry: "first_add".into(),
37 imports,
38 connections: vec![GraphConnection {
39 from: "first_add".into(),
40 to: "second_add".into(),
41 }],
42 })?;
43 let graph = definition.graph();
44 assert_eq!(graph.entry, "first_add");
45 assert_eq!(graph.nodes.len(), 2);
46 assert_eq!(graph.nodes[0].id, "first_add");
47 assert_eq!(graph.nodes[1].id, "second_add");
48 assert!(
49 graph
50 .edges
51 .iter()
52 .any(|edge| { edge.from == "first_add" && edge.to.as_deref() == Some("second_add") })
53 );
54
55 let inspection = definition.export(GraphFormat::Json)?;
56 let inspection: serde_json::Value = serde_json::from_str(&inspection)
57 .map_err(|_| AppError::Integration("example graph JSON was invalid".into()))?;
58 assert_eq!(inspection["nodes"].as_array().map(Vec::len), Some(2));
59 assert!(definition.export(GraphFormat::Dot)?.starts_with("digraph "));
60 println!("{}", definition.export(GraphFormat::Mermaid)?);
61
62 // Built-in nodes retain their canonical authored source. Tracking requires
63 // actual source for every node and is a separate content-export format.
64 let tracking = graph.tracking_record()?;
65 assert_eq!(tracking["type"], "application");
66 assert_eq!(tracking["actions"].as_array().map(Vec::len), Some(2));
67 Ok(())
68}Source§impl WorkflowDefinition
impl WorkflowDefinition
Sourcepub fn parse(source: &str) -> Result<Self, AppError>
pub fn parse(source: &str) -> Result<Self, AppError>
Parses and validates one strict versioned workflow document.
§Errors
Returns AppError::InvalidInput for malformed JSON, unsupported schema,
unknown or duplicate members, invalid identities or expressions, inconsistent
state/reducers, graph errors, or exceeded source/collection/depth limits.
§Example
use app_kit::workflow::WorkflowDefinition;
let definition = WorkflowDefinition::parse(r#"{
"schema":"app-kit.workflow/v2", "id":"counter", "revision":"r1",
"state":{"count":0}, "required":["count"],
"reducers":{"count":"reject_concurrent"}, "entry":"increment",
"nodes":[{"id":"increment","step":{"kind":"state","mutations":[
{"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
]}}], "routes":{}
}"#)?;
assert_eq!(definition.graph().id.to_string(), "counter");
assert!(WorkflowDefinition::parse(r#"{"schema":"app-kit.workflow/v1"}"#).is_err());Examples found in repository?
31 async fn open_app(&self) -> Result<App<StateStore, Agent>, AppError> {
32 let definition = WorkflowDefinition::parse(
33 &json!({
34 "schema":"app-kit.workflow/v2", "id":"history-counter", "revision":"r1",
35 "state":{"count":0}, "required":["count"],
36 "reducers":{"count":"reject_concurrent"}, "entry":"first",
37 "nodes":[
38 {"id":"first", "step":{"kind":"state", "mutations":[{
39 "kind":"increment", "target":"count",
40 "value":{"kind":"literal", "value":1}
41 }]}},
42 {"id":"second", "step":{"kind":"state", "mutations":[{
43 "kind":"increment", "target":"count",
44 "value":{"kind":"literal", "value":1}
45 }]}}
46 ],
47 "routes":{"first":{"mode":"first", "branches":[{
48 "target":{"kind":"node", "id":"second"}
49 }]}}
50 })
51 .to_string(),
52 )?;
53 // Agent supplies the static type for an empty inventory. No Agent value
54 // is constructed, and these native State nodes need no lower service.
55 App::open(AppConfig::<_, Agent>::new(
56 self.store.clone(),
57 Vec::new(),
58 vec![definition],
59 ))
60 .await
61 }More examples
136 fn definition(linear: bool, actions: u64) -> Result<(WorkflowDefinition, usize), AppError> {
137 let mut nodes = Vec::new();
138 let mut routes = Map::new();
139 for index in 0..if linear { actions } else { 1 } {
140 let node = if linear {
141 format!("n{index:04}")
142 } else {
143 "count".to_owned()
144 };
145 nodes.push(json!({"id":node,"deadline_ms":NODE_DEADLINE_MS,
146 "step":{"kind":"state","mutations":[
147 {"kind":"increment","target":"count","value":{"kind":"literal","value":1}}
148 ]}}));
149 if linear && index + 1 < actions {
150 routes.insert(
151 node,
152 json!({"mode":"first","branches":[
153 {"target":{"kind":"node","id":format!("n{:04}",index + 1)}}
154 ]}),
155 );
156 } else if !linear {
157 routes.insert(
158 node,
159 json!({"mode":"first","branches":[
160 {"when":{"kind":"less_than","left":{"kind":"state","path":"count"},
161 "right":{"kind":"literal","value":actions}},
162 "target":{"kind":"node","id":"count"}},
163 {"target":{"kind":"end"}}
164 ]}),
165 );
166 }
167 }
168 let source = json!({
169 "schema":"app-kit.workflow/v2","id":"performance","revision":"r1",
170 "state":{"count":0},"required":["count"],"reducers":{"count":"reject_concurrent"},
171 "entry":if linear {"n0000"} else {"count"}, "nodes":nodes,"routes":routes
172 })
173 .to_string();
174 assert!(
175 source.len() <= 1_048_576,
176 "generated definition must fit the real source bound"
177 );
178 Ok((WorkflowDefinition::parse(&source)?, source.len()))
179 }
180
181 fn read_manifest(&self) -> Result<Value, Box<dyn Error + Send + Sync>> {
182 // Reopen must never silently create a replacement empty store. Reject
183 // links and missing directories before passing the caller-owned path to LanceDB.
184 for name in [".performance-root", "manifest.json"] {
185 if !fs::symlink_metadata(self.root.join(name))?
186 .file_type()
187 .is_file()
188 {
189 return Err(format!("performance root needs a regular {name} file").into());
190 }
191 }
192 let mut marker = String::new();
193 File::open(self.root.join(".performance-root"))?
194 .take(MANIFEST_SCHEMA.len() as u64 + 1)
195 .read_to_string(&mut marker)?;
196 if marker != MANIFEST_SCHEMA {
197 return Err("root is not owned by this performance consumer".into());
198 }
199 if !fs::symlink_metadata(self.root.join("state"))?
200 .file_type()
201 .is_dir()
202 || fs::read_dir(self.root.join("state"))?
203 .next()
204 .transpose()?
205 .is_none()
206 {
207 return Err("performance state directory is absent or empty".into());
208 }
209 let mut encoded = String::new();
210 File::open(self.root.join("manifest.json"))?
211 .take(16_385)
212 .read_to_string(&mut encoded)?;
213 if encoded.len() > 16_384 {
214 return Err("performance manifest exceeds its bounded size".into());
215 }
216 let manifest: Value = serde_json::from_str(&encoded)?;
217 if manifest["schema"] != MANIFEST_SCHEMA
218 || manifest["dataset"] != "history"
219 || manifest["cleanup_joined"] != true
220 {
221 return Err("unsupported performance manifest".into());
222 }
223 let actions = manifest["actions"]
224 .as_u64()
225 .ok_or("manifest has no action count")?;
226 let rows = manifest["retained_rows"]
227 .as_u64()
228 .ok_or("manifest has no retained row count")?;
229 if !matches!(rows, 1_000 | 10_000)
230 || actions != (rows - 4) / 3
231 || manifest["terminal_checkpoint"] != rows - 1
232 || manifest["settings"] != Self::settings(actions)
233 {
234 return Err("manifest workload or settings do not match this consumer".into());
235 }
236 Ok(manifest)
237 }
238
239 fn samples(mut samples: Vec<Duration>) -> Value {
240 samples.sort_unstable();
241 let length = samples.len();
242 if length == 0 {
243 return json!({"count":0,"p50_ns":null,"p95_ns":null,"max_ns":null});
244 }
245 json!({"count":length,
246 "p50_ns":samples[(length * 50).div_ceil(100) - 1].as_nanos(),
247 "p95_ns":samples[(length * 95).div_ceil(100) - 1].as_nanos(),
248 "max_ns":samples[length - 1].as_nanos(),
249 "percentile":"nearest rank: ceil(p * n), one-based"})
250 }
251
252 fn verify_snapshot(snapshot: &RunSnapshot, actions: u64) {
253 assert!(matches!(snapshot.status, RunStatus::Succeeded));
254 assert!(snapshot.interactions.is_empty());
255 assert_eq!(
256 snapshot.state.values(),
257 &Map::from_iter([("count".into(), json!(actions))])
258 );
259 assert_eq!(snapshot.last_action_ordinal, Some(actions - 1));
260 assert_eq!(snapshot.outcomes.len(), 1);
261 assert_eq!(snapshot.outcomes[0].ordinal, actions - 1);
262 assert!(snapshot.outcomes[0].result.is_empty());
263 }
264
265 async fn history(
266 &self,
267 run: &WorkflowRun<StateStore, Agent, WorkloadAction>,
268 actions: u64,
269 linear: bool,
270 terminal_sequence: u64,
271 ) -> Result<Value, AppError> {
272 let ceiling = actions * 16 + 16;
273 let mut cursor = None;
274 let mut prior_sequence = None;
275 let mut rows = 0_u64;
276 let mut observed_actions = 0_u64;
277 let mut initial = 0;
278 let mut terminal = 0;
279 let mut controls = 0;
280 let mut halts = 0;
281 let mut samples = Vec::new();
282 let started = Instant::now();
283 loop {
284 assert!(
285 samples.len() < ceiling.div_ceil(PAGE_SIZE as u64) as usize + 1,
286 "history page ceiling exhausted without cursor completion"
287 );
288 let page_started = Instant::now();
289 let page = run
290 .history(cursor, NonZeroUsize::new(PAGE_SIZE).unwrap())
291 .await?;
292 samples.push(page_started.elapsed());
293 assert!(page.checkpoints.len() <= PAGE_SIZE);
294 assert!(
295 !page.checkpoints.is_empty() || page.next.is_none(),
296 "history cursor made no progress"
297 );
298 for row in &page.checkpoints {
299 assert_eq!(&row.run, run.id());
300 assert!(prior_sequence.is_none_or(|prior| row.sequence > prior));
301 prior_sequence = Some(row.sequence);
302 rows += 1;
303 assert!(
304 rows <= ceiling,
305 "declared retained-row workload ceiling exceeded"
306 );
307 match row.kind {
308 CheckpointKind::Initial => {
309 initial += 1;
310 assert_eq!(observed_actions, 0);
311 assert_eq!(row.sequence, 0);
312 }
313 CheckpointKind::Action => {
314 assert_eq!(row.outcomes.len(), 1);
315 let outcome = &row.outcomes[0];
316 assert_eq!(outcome.ordinal, observed_actions);
317 assert_eq!(
318 outcome.node,
319 if linear {
320 format!("n{observed_actions:04}")
321 } else {
322 "count".into()
323 }
324 );
325 assert!(outcome.result.is_empty());
326 observed_actions += 1;
327 assert_eq!(row.action_ordinal, Some(observed_actions - 1));
328 }
329 CheckpointKind::Terminal => {
330 terminal += 1;
331 assert!(matches!(row.status, RunStatus::Succeeded));
332 }
333 CheckpointKind::Control => {
334 assert!(row.outcomes.is_empty());
335 controls += 1;
336 if let RunStatus::Halted { node, result } = &row.status {
337 assert!(!linear);
338 assert_eq!(row.sequence, 4);
339 assert_eq!(node, "count");
340 assert_eq!(result, &Some(Map::new()));
341 halts += 1;
342 }
343 }
344 CheckpointKind::Failed { .. } => {
345 panic!("workload history contains a failed action")
346 }
347 }
348 assert_eq!(
349 row.state.values(),
350 &Map::from_iter([("count".into(), json!(observed_actions))])
351 );
352 if !linear && matches!(row.sequence, 4 | 5) {
353 assert!(matches!(row.kind, CheckpointKind::Control));
354 assert_eq!(observed_actions, 1);
355 assert_eq!(row.action_ordinal, Some(0));
356 assert_eq!(row.position.as_deref(), Some("count"));
357 if row.sequence == 5 {
358 assert!(matches!(row.status, RunStatus::Paused));
359 }
360 }
361 }
362 cursor = page.next;
363 // Each complete Checkpoint/WorkflowState is released with this page.
364 if cursor.is_none() {
365 break;
366 }
367 }
368 assert_eq!(initial, 1);
369 assert_eq!(terminal, 1);
370 assert_eq!(observed_actions, actions);
371 assert_eq!(prior_sequence, Some(terminal_sequence));
372 if !linear {
373 assert_eq!(rows, actions * 3 + 4);
374 assert_eq!(terminal_sequence, rows - 1);
375 assert_eq!(controls, actions * 2 + 2);
376 assert_eq!(halts, 1);
377 assert_eq!(samples.len(), (rows as usize).div_ceil(PAGE_SIZE));
378 }
379 let elapsed = started.elapsed();
380 let first = samples.first().copied().unwrap_or_default();
381 let subsequent: Duration = samples.iter().skip(1).copied().sum();
382 Ok(
383 json!({"rows":rows,"action_rows":observed_actions,"pages":samples.len(),
384 "elapsed_ns":elapsed.as_nanos(),"first_page_ns":first.as_nanos(),
385 "subsequent_pages_total_ns":subsequent.as_nanos(),
386 "rows_per_second":rows as f64 / elapsed.as_secs_f64(),"page_latency":Self::samples(samples)}),
387 )
388 }
389
390 async fn seed(
391 &self,
392 app: &App<StateStore, Agent, WorkloadAction>,
393 actions: u64,
394 linear: bool,
395 ) -> Result<Value, Box<dyn Error + Send + Sync>> {
396 let workspace = app
397 .create_workspace(WorkspaceConfig {
398 name: Some("performance consumer".into()),
399 root: self.root.join("workspace"),
400 })
401 .await?;
402 let workflow = workspace.workflow("performance".parse()?).await?;
403 let mut run = workflow.prepare(WorkflowState::default()).await?;
404 let mut execution = run
405 .execution(ExecutionOptions {
406 halt_after: if linear {
407 Vec::new()
408 } else {
409 vec!["count".into()]
410 },
411 ..ExecutionOptions::default()
412 })
413 .await?;
414 let mut samples = Vec::with_capacity(actions as usize);
415 let started = Instant::now();
416 for ordinal in 0..actions {
417 let step_started = Instant::now();
418 let event = execution.step().await?;
419 samples.push(step_started.elapsed());
420 let Some(ExecutionEvent::Committed { outcomes, .. }) = event else {
421 panic!("each serial workload step must commit one action");
422 };
423 assert_eq!(outcomes.len(), 1);
424 assert_eq!(outcomes[0].ordinal, ordinal);
425 assert_eq!(
426 outcomes[0].node,
427 if linear {
428 format!("n{ordinal:04}")
429 } else {
430 "count".into()
431 }
432 );
433 assert!(outcomes[0].result.is_empty());
434 if !linear && ordinal == 0 {
435 // One public halt and explicit resume add two control rows to
436 // Initial + three checkpoints per action + Terminal.
437 let halted = execution.finish().await?;
438 assert!(matches!(&halted.status, RunStatus::Halted { node, result }
439 if node == "count" && result == &Some(Map::new())));
440 assert_eq!(halted.checkpoint, 4);
441 assert_eq!(
442 halted.state.values(),
443 &Map::from_iter([("count".into(), json!(1))])
444 );
445 assert_eq!(halted.last_action_ordinal, Some(0));
446 assert!(halted.outcomes == outcomes);
447 assert!(halted.interactions.is_empty());
448 execution = run.execution(ExecutionOptions::default()).await?;
449 }
450 }
451 let snapshot = execution.finish().await?;
452 Self::verify_snapshot(&snapshot, actions);
453 let execution_elapsed = started.elapsed();
454 let history = self
455 .history(&run, actions, linear, snapshot.checkpoint)
456 .await?;
457 if let Command::SeedHistory { rows } = self.command {
458 assert_eq!(history["rows"], rows);
459 }
460 Ok(
461 json!({"dataset":if linear {"linear"} else {"history"},"actions":actions,
462 "workspace":workspace.id().to_string(),"run":run.id().to_string(),
463 "occurrence":snapshot.occurrence,
464 "expected_state":snapshot.state.values(),"last_action_ordinal":snapshot.last_action_ordinal,
465 "terminal_checkpoint":snapshot.checkpoint,"retained_rows":history["rows"],
466 "execution_ns":execution_elapsed.as_nanos(),
467 "actions_per_second":actions as f64 / execution_elapsed.as_secs_f64(),
468 "step_latency":Self::samples(samples),"history":history}),
469 )
470 }
471
472 async fn reopen(
473 &self,
474 app: &App<StateStore, Agent, WorkloadAction>,
475 manifest: &Value,
476 actions: u64,
477 open_only: bool,
478 ) -> Result<Value, Box<dyn Error + Send + Sync>> {
479 let started = Instant::now();
480 let workspace: WorkspaceId = manifest["workspace"]
481 .as_str()
482 .ok_or("missing workspace ID")?
483 .parse()?;
484 let run_id: RunId = manifest["run"].as_str().ok_or("missing run ID")?.parse()?;
485 let occurrence = manifest["occurrence"]
486 .as_str()
487 .ok_or("missing run occurrence")?
488 .to_owned();
489 let workspace = app.workspace(workspace).await?;
490 let workflow = workspace.workflow("performance".parse()?).await?;
491 let run = workflow
492 .open_run(RunSelector::Exact {
493 id: run_id,
494 occurrence: occurrence.clone(),
495 })
496 .await?;
497 let snapshot = run.snapshot().await?;
498 Self::verify_snapshot(&snapshot, actions);
499 assert_eq!(snapshot.occurrence, occurrence);
500 assert_eq!(json!(snapshot.checkpoint), manifest["terminal_checkpoint"]);
501 assert_eq!(json!(snapshot.state.values()), manifest["expected_state"]);
502 assert_eq!(
503 json!(snapshot.last_action_ordinal),
504 manifest["last_action_ordinal"]
505 );
506 let lookup = started.elapsed();
507 if open_only {
508 return Ok(
509 json!({"dataset":"history","actions":actions,"mode":"open-only",
510 "lookup_and_snapshot_ns":lookup.as_nanos(),
511 "seed_retained_rows":manifest["retained_rows"],"observed_history_rows":null}),
512 );
513 }
514 let history = self
515 .history(&run, actions, false, snapshot.checkpoint)
516 .await?;
517 assert_eq!(history["rows"], manifest["retained_rows"]);
518 Ok(
519 json!({"dataset":"history","actions":actions,"mode":"full-history",
520 "lookup_and_snapshot_ns":lookup.as_nanos(),"history":history}),
521 )
522 }
523
524 fn definitions(&self, actions: u64) -> Result<(Vec<WorkflowDefinition>, usize), AppError> {
525 if matches!(
526 self.command,
527 Command::Linear | Command::SeedHistory { .. } | Command::ReopenHistory { .. }
528 ) {
529 let (definition, bytes) =
530 Self::definition(matches!(self.command, Command::Linear), actions)?;
531 return Ok((vec![definition], bytes));
532 }
533 let leaf = json!({
534 "schema":"app-kit.workflow/v2","id":"performance-leaf","revision":"r1",
535 "state":{"value":0,"result":0},"required":["value","result"],
536 "reducers":{"value":"reject_concurrent","result":"reject_concurrent"},
537 "entry":"work","nodes":[{"id":"work","deadline_ms":NODE_DEADLINE_MS,
538 "step":{"kind":"action","action":"performance-action","revision":"r1"}}],"routes":{}
539 });
540 let documents = match self.command {
541 Command::Ready => {
542 let mut nodes = vec![json!({"id":"fork","step":{"kind":"state","mutations":[]}})];
543 let mut branches = Vec::new();
544 for index in 0..32 {
545 let node = format!("n{index:02}");
546 nodes.push(json!({"id":node,"deadline_ms":NODE_DEADLINE_MS,
547 "step":{"kind":"action","action":"performance-action","revision":"r1"}}));
548 branches.push(json!({"target":{"kind":"node","id":node}}));
549 }
550 vec![json!({
551 "schema":"app-kit.workflow/v2","id":"performance","revision":"r1",
552 "state":{"value":0,"result":0},"required":["value","result"],
553 "reducers":{"value":"reject_concurrent","result":"collect"},
554 "entry":"fork","nodes":nodes,
555 "routes":{"fork":{"mode":"broadcast","branches":branches}}
556 })]
557 }
558 Command::Map | Command::NestedCancel => {
559 let nested = matches!(self.command, Command::NestedCancel);
560 let items = if nested { 2 } else { 4_096 };
561 let mut documents = vec![
562 json!({
563 "schema":"app-kit.workflow/v2","id":"performance","revision":"r1",
564 "state":{"value":0,"result":0},"required":["value","result"],
565 "reducers":{"value":"reject_concurrent","result":"sum_integer"},
566 "entry":"map","nodes":[{"id":"map","deadline_ms":if nested {NODE_DEADLINE_MS} else {WORKLOAD_BUDGET.as_millis() as u64},
567 "step":{"kind":"map","workflow":if nested {"performance-mid"} else {"performance-leaf"},
568 "items":{"kind":"literal","value":(0..items).collect::<Vec<_>>()},
569 "input_key":"value","max_parallel":if nested {2} else {8},
570 "reductions":[{"source":"result","target":"result","reducer":"sum_integer"}]}}],"routes":{}
571 }),
572 leaf,
573 ];
574 if nested {
575 documents.push(json!({
576 "schema":"app-kit.workflow/v2","id":"performance-mid","revision":"r1",
577 "state":{"value":0,"result":0},"required":["value","result"],
578 "reducers":{"value":"reject_concurrent","result":"sum_integer"},
579 "entry":"map","nodes":[{"id":"map","deadline_ms":NODE_DEADLINE_MS,
580 "step":{"kind":"map","workflow":"performance-leaf",
581 "items":{"kind":"literal","value":(0..8).collect::<Vec<_>>()},
582 "input_key":"value","max_parallel":8,
583 "reductions":[{"source":"result","target":"result","reducer":"sum_integer"}]}}],"routes":{}
584 }));
585 }
586 documents
587 }
588 Command::Repeated | Command::DelayedObserver => vec![leaf],
589 Command::Linear | Command::SeedHistory { .. } | Command::ReopenHistory { .. } => {
590 unreachable!()
591 }
592 };
593 let mut bytes = 0;
594 let mut definitions = Vec::with_capacity(documents.len());
595 for document in documents {
596 let source = document.to_string();
597 assert!(source.len() <= 1_048_576);
598 bytes += source.len();
599 definitions.push(WorkflowDefinition::parse(&source)?);
600 }
601 Ok((definitions, bytes))
602 }10fn main() -> Result<(), AppError> {
11 let mut imports = Vec::new();
12 for (id, prefix) in [("first-counter", "first_"), ("second-counter", "second_")] {
13 let definition = WorkflowDefinition::parse(
14 &json!({
15 "schema":"app-kit.workflow/v2", "id":id, "revision":"r1",
16 "state":{"count":0}, "required":["count"],
17 "reducers":{"count":"reject_concurrent"}, "entry":"add",
18 "nodes":[{"id":"add", "name":"Add one", "step":{
19 "kind":"state", "mutations":[{
20 "kind":"increment", "target":"count",
21 "value":{"kind":"literal", "value":1}
22 }]
23 }}],
24 "routes":{}
25 })
26 .to_string(),
27 )?;
28 imports.push(GraphImport {
29 definition,
30 prefix: prefix.into(),
31 });
32 }
33 let definition = WorkflowDefinition::compose(GraphComposition {
34 id: "two-counters".parse()?,
35 revision: "r1".into(),
36 entry: "first_add".into(),
37 imports,
38 connections: vec![GraphConnection {
39 from: "first_add".into(),
40 to: "second_add".into(),
41 }],
42 })?;
43 let graph = definition.graph();
44 assert_eq!(graph.entry, "first_add");
45 assert_eq!(graph.nodes.len(), 2);
46 assert_eq!(graph.nodes[0].id, "first_add");
47 assert_eq!(graph.nodes[1].id, "second_add");
48 assert!(
49 graph
50 .edges
51 .iter()
52 .any(|edge| { edge.from == "first_add" && edge.to.as_deref() == Some("second_add") })
53 );
54
55 let inspection = definition.export(GraphFormat::Json)?;
56 let inspection: serde_json::Value = serde_json::from_str(&inspection)
57 .map_err(|_| AppError::Integration("example graph JSON was invalid".into()))?;
58 assert_eq!(inspection["nodes"].as_array().map(Vec::len), Some(2));
59 assert!(definition.export(GraphFormat::Dot)?.starts_with("digraph "));
60 println!("{}", definition.export(GraphFormat::Mermaid)?);
61
62 // Built-in nodes retain their canonical authored source. Tracking requires
63 // actual source for every node and is a separate content-export format.
64 let tracking = graph.tracking_record()?;
65 assert_eq!(tracking["type"], "application");
66 assert_eq!(tracking["actions"].as_array().map(Vec::len), Some(2));
67 Ok(())
68}23 async fn run() -> Result<(), Box<dyn Error + Send + Sync>> {
24 let definition = WorkflowDefinition::parse(
25 &json!({
26 "schema":"app-kit.workflow/v2", "id":"invoice", "revision":"r1",
27 "state":{"unit_price":7,"total":0}, "required":["unit_price","total"],
28 "reducers":{"unit_price":"reject_concurrent","total":"reject_concurrent"},
29 "entry":"calculate",
30 "nodes":[{"id":"calculate","step":{
31 "kind":"action","action":"invoice","revision":"r1","project_result":false
32 }}], "routes":{}
33 })
34 .to_string(),
35 )?;
36 let store =
37 StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
38 // Native caller actions need no Agent value or provider service.
39 let mut app = App::open(
40 AppConfig::<_, Agent>::new(store, Vec::new(), vec![definition])
41 .with_actions(vec![InvoiceAction::new()]),
42 )
43 .await?;
44 let observed = tokio::time::timeout(Duration::from_secs(15), async {
45 let workspace = app
46 .create_workspace(WorkspaceConfig {
47 name: Some("invoice example".into()),
48 root: PathBuf::from(env!("CARGO_MANIFEST_DIR")),
49 })
50 .await?;
51 let workflow = workspace.workflow("invoice".parse()?).await?;
52 let mut run = workflow.prepare(WorkflowState::default()).await?;
53 let mut execution = run
54 .execution(ExecutionOptions {
55 inputs: Map::from_iter([
56 ("quantity".into(), json!(6)),
57 ("trace".into(), json!("sample")),
58 ]),
59 ..ExecutionOptions::default()
60 })
61 .await?;
62 let Some(ExecutionEvent::Partial { result, .. }) = execution.next().await? else {
63 return Err(AppError::Integration(
64 "expected the real action partial".into(),
65 ));
66 };
67 if result.get("progress") != Some(&json!("calculated")) {
68 return Err(AppError::Integration("partial changed".into()));
69 }
70 let Some(ExecutionEvent::Committed { outcomes, .. }) = execution.step().await? else {
71 return Err(AppError::Integration(
72 "expected one committed boundary".into(),
73 ));
74 };
75 let expected =
76 json!({"quantity":6,"label":"order","currency":"USD","trace":"sample","total":42});
77 if outcomes.len() != 1 || Value::Object(outcomes[0].result.clone()) != expected {
78 return Err(AppError::Integration(
79 "resolved inputs or action result changed".into(),
80 ));
81 }
82 let terminal = execution.finish().await?;
83 if !matches!(terminal.status, RunStatus::Succeeded)
84 || terminal.state.values()
85 != json!({"unit_price":7,"total":42}).as_object().unwrap()
86 {
87 return Err(AppError::Integration(
88 "only the explicit total write should persist".into(),
89 ));
90 }
91 println!(
92 "Partial: calculated; committed total: 42; transient inputs stayed out of state"
93 );
94 Ok::<(), AppError>(())
95 })
96 .await;
97 let closed = tokio::time::timeout(
98 Duration::from_secs(15),
99 app.shutdown(Shutdown::CancelAndDrain {
100 timeout: Duration::from_secs(10),
101 }),
102 )
103 .await;
104 drop(app);
105 observed.map_err(|_| AppError::Timeout("invoice execution timed out".into()))??;
106 closed.map_err(|_| AppError::Timeout("invoice cleanup timed out".into()))??;
107 Ok(())
108 }Auto Trait Implementations§
impl Freeze for WorkflowDefinition
impl RefUnwindSafe for WorkflowDefinition
impl Send for WorkflowDefinition
impl Sync for WorkflowDefinition
impl Unpin for WorkflowDefinition
impl UnsafeUnpin for WorkflowDefinition
impl UnwindSafe for WorkflowDefinition
Blanket Implementations§
impl<T> Allocation for T
Source§impl<T> ArchivePointee for T
impl<T> ArchivePointee for T
Source§type ArchivedMetadata = ()
type ArchivedMetadata = ()
Source§fn pointer_metadata(
_: &<T as ArchivePointee>::ArchivedMetadata,
) -> <T as Pointee>::Metadata
fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
impl<T> ErasedDestructor for Twhere
T: 'static,
Source§impl<T> FmtForward for T
impl<T> FmtForward for T
Source§fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
self to use its Binary implementation when Debug-formatted.Source§fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
self to use its Display implementation when
Debug-formatted.Source§fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
self to use its LowerExp implementation when
Debug-formatted.Source§fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
self to use its LowerHex implementation when
Debug-formatted.Source§fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
self to use its Octal implementation when Debug-formatted.Source§fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
self to use its Pointer implementation when
Debug-formatted.Source§fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
self to use its UpperExp implementation when
Debug-formatted.Source§fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
self to use its UpperHex implementation when
Debug-formatted.Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::RequestSource§impl<T> LayoutRaw for T
impl<T> LayoutRaw for T
Source§fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
Source§impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
Source§unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
Source§fn resolve_niched(out: Place<NichedOption<T, N1>>)
fn resolve_niched(out: Place<NichedOption<T, N1>>)
out indicating that a T is niched.Source§impl<T> Pipe for Twhere
T: ?Sized,
impl<T> Pipe for Twhere
T: ?Sized,
Source§fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
Source§fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
Source§fn pipe_borrow_mut<'a, B, R>(
&'a mut self,
func: impl FnOnce(&'a mut B) -> R,
) -> R
fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
Source§fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
self, then passes self.as_ref() into the pipe function.Source§fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
self, then passes self.as_mut() into the pipe
function.Source§fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
self, then passes self.deref() into the pipe function.Source§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<T> Tap for T
impl<T> Tap for T
Source§fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
Borrow<B> of a value. Read moreSource§fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
BorrowMut<B> of a value. Read moreSource§fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
AsRef<R> view of a value. Read moreSource§fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
AsMut<R> view of a value. Read moreSource§fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
.tap() only in debug builds, and is erased in release builds.Source§fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
.tap_mut() only in debug builds, and is erased in release
builds.Source§fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
.tap_borrow() only in debug builds, and is erased in release
builds.Source§fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
.tap_borrow_mut() only in debug builds, and is erased in release
builds.Source§fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
.tap_ref() only in debug builds, and is erased in release
builds.Source§fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
.tap_ref_mut() only in debug builds, and is erased in release
builds.Source§fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
.tap_deref() only in debug builds, and is erased in release
builds.