Skip to main content

aster_forge_mail/
component.rs

1//! Runtime component integration for mail outbox draining.
2//!
3//! Aster products own the concrete outbox store, template rendering, sender
4//! configuration, audit hooks, and product error mapping. Forge owns the
5//! repeated runtime lifecycle contract: after background workers stop, drain the
6//! mail outbox before database handles close. Keeping the component names and
7//! dependency edge here gives all products the same shutdown graph.
8
9use std::future::Future;
10
11use aster_forge_runtime::{RuntimeComponentBundleRegistration, RuntimeComponentKind};
12
13use crate::MAIL_OUTBOX_COMPONENT;
14/// Stable shutdown phase name for mail outbox draining.
15pub const MAIL_OUTBOX_DRAIN_SHUTDOWN_PHASE: &str = "mail_outbox_drain";
16
17/// Creates the mail outbox runtime component used by product entrypoints.
18///
19/// The `resources` value is product-defined. It usually contains a database
20/// handle, runtime config snapshot, and mail sender. The `drain` callback keeps
21/// product-specific rendering, repository, audit, and error mapping outside
22/// Forge while still using the shared component lifecycle.
23pub fn mail_outbox_component<T, F, Fut>(
24    resources: T,
25    drain: F,
26) -> RuntimeComponentBundleRegistration<aster_forge_runtime::ShutdownResourceComponent<T>>
27where
28    T: Send + 'static,
29    F: FnOnce(T) -> Fut + Send + Sync + 'static,
30    Fut: Future<Output = Result<(), String>> + Send + 'static,
31{
32    aster_forge_runtime::shutdown_resource_component_after(
33        MAIL_OUTBOX_COMPONENT,
34        RuntimeComponentKind::Mail,
35        MAIL_OUTBOX_DRAIN_SHUTDOWN_PHASE,
36        &[aster_forge_tasks::BACKGROUND_TASKS_COMPONENT],
37        resources,
38        drain,
39    )
40}
41
42#[cfg(test)]
43mod tests {
44    use std::sync::{
45        Arc,
46        atomic::{AtomicUsize, Ordering},
47    };
48
49    use aster_forge_runtime::RuntimeComponentBundle;
50
51    use super::{MAIL_OUTBOX_COMPONENT, MAIL_OUTBOX_DRAIN_SHUTDOWN_PHASE, mail_outbox_component};
52
53    #[test]
54    fn mail_outbox_component_registers_standard_shutdown_dependency() {
55        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
56            mail_outbox_component((), |()| async { Ok(()) }).register(registry);
57        });
58
59        let descriptor = registry
60            .descriptor(MAIL_OUTBOX_COMPONENT)
61            .expect("mail outbox component should be registered");
62        assert_eq!(
63            descriptor.kind,
64            aster_forge_runtime::RuntimeComponentKind::Mail
65        );
66        assert_eq!(
67            descriptor.dependencies,
68            vec![aster_forge_tasks::BACKGROUND_TASKS_COMPONENT]
69        );
70        assert_eq!(
71            descriptor
72                .shutdown
73                .first()
74                .expect("mail outbox shutdown should be registered")
75                .phase_name,
76            MAIL_OUTBOX_DRAIN_SHUTDOWN_PHASE
77        );
78    }
79
80    #[tokio::test]
81    async fn mail_outbox_component_consumes_resource_once_during_shutdown() {
82        let calls = Arc::new(AtomicUsize::new(0));
83        let calls_for_drain = calls.clone();
84        let mut registry = aster_forge_runtime::RuntimeComponentRegistry::new();
85        registry.register_bundle(mail_outbox_component(7usize, move |resource| {
86            let calls = calls_for_drain.clone();
87            async move {
88                assert_eq!(resource, 7);
89                calls.fetch_add(1, Ordering::SeqCst);
90                Ok(())
91            }
92        }));
93        let report = registry.shutdown().await;
94
95        assert!(!report.has_failures());
96        assert_eq!(calls.load(Ordering::SeqCst), 1);
97    }
98}