aion/workloop/hatch.rs
1//! The detached hatch (R13.1): a top-level workflow start with a mandatory
2//! dedupe identity, from workflow and workloop documents alike.
3//!
4//! A hatch is NOT a child: no lifecycle tie, no supervision edge, no awaited
5//! terminal. The dedupe identity is defined ONCE for both document kinds —
6//! `(namespace + target workflow type + key)`, minted deterministically by
7//! [`aion_core::hatch_workflow_id`] — so an iteration retry or replay re-mints
8//! the SAME [`WorkflowId`] and the same observed subject hatches ONE workflow,
9//! never two. The event store itself is the dedupe index: a workflow id with
10//! recorded history IS the prior hatch, and two racing first-hatches are
11//! settled by the store's optimistic append (the loser's `SequenceConflict`
12//! resolves to the winner's workflow).
13
14use aion_core::{WorkflowId, WorkloopSpecError};
15
16/// Re-export of the one identity derivation both document kinds share.
17pub use aion_core::hatch_workflow_id as hatch_identity;
18
19/// Outcome of a hatch request.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum HatchOutcome {
22 /// A new detached workflow was started under the derived identity.
23 Hatched(WorkflowId),
24 /// The identity already had a workflow — the recorded no-op (R13.1): the
25 /// existing workflow id is returned and nothing was started.
26 Existing(WorkflowId),
27}
28
29impl HatchOutcome {
30 /// The workflow id the hatch resolved to, whichever way it went.
31 #[must_use]
32 pub const fn workflow_id(&self) -> &WorkflowId {
33 match self {
34 Self::Hatched(id) | Self::Existing(id) => id,
35 }
36 }
37}
38
39/// Validates and derives the hatch identity, mapping refusals to the shared
40/// declaration-error type.
41///
42/// # Errors
43///
44/// Refuses empty parts and NUL bytes ([`WorkloopSpecError`]).
45pub fn derive_identity(
46 namespace: &str,
47 workflow_type: &str,
48 key: &str,
49) -> Result<WorkflowId, WorkloopSpecError> {
50 hatch_identity(namespace, workflow_type, key)
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn outcome_exposes_the_resolved_id_both_ways() -> Result<(), Box<dyn std::error::Error>> {
59 let id = derive_identity("default", "process_task", "task-1")?;
60 assert_eq!(HatchOutcome::Hatched(id.clone()).workflow_id(), &id);
61 assert_eq!(HatchOutcome::Existing(id.clone()).workflow_id(), &id);
62 Ok(())
63 }
64}