Skip to main content

dcontext_dactor/
outbound.rs

1use std::any::Any;
2
3use dactor::{Disposition, Headers, OutboundContext, OutboundInterceptor, Outcome, RuntimeHeaders};
4
5use crate::header::{ContextHeader, ContextSnapshotHeader};
6use crate::ErrorPolicy;
7
8/// Outbound interceptor that captures the current dcontext and attaches it
9/// to outgoing actor messages as headers.
10///
11/// - **Local targets**: attaches a [`ContextSnapshotHeader`] only — no
12///   serialization is performed, preserving local-only context values.
13/// - **Remote targets**: serializes context to bytes and attaches a
14///   [`ContextHeader`] for wire transport.
15///
16/// # Error Handling
17///
18/// Controlled by [`ErrorPolicy`]:
19/// - [`LogAndContinue`](ErrorPolicy::LogAndContinue) (default) — log and
20///   send the message without context.
21/// - [`Reject`](ErrorPolicy::Reject) — reject the message.
22///
23/// # Usage
24///
25/// ```ignore
26/// use dcontext_dactor::{ContextOutboundInterceptor, ErrorPolicy};
27///
28/// // Default: log and continue on errors
29/// runtime.add_outbound_interceptor(Box::new(ContextOutboundInterceptor::default()));
30///
31/// // Strict: reject messages if context serialization fails
32/// runtime.add_outbound_interceptor(Box::new(
33///     ContextOutboundInterceptor::new(ErrorPolicy::Reject),
34/// ));
35/// ```
36#[derive(Default)]
37pub struct ContextOutboundInterceptor {
38    error_policy: ErrorPolicy,
39}
40
41impl ContextOutboundInterceptor {
42    /// Create with a specific error policy.
43    pub fn new(error_policy: ErrorPolicy) -> Self {
44        Self { error_policy }
45    }
46}
47
48impl OutboundInterceptor for ContextOutboundInterceptor {
49    fn name(&self) -> &'static str {
50        "dcontext-outbound"
51    }
52
53    fn on_send(
54        &self,
55        ctx: &OutboundContext<'_>,
56        _runtime_headers: &RuntimeHeaders,
57        headers: &mut Headers,
58        _message: &dyn Any,
59    ) -> Disposition {
60        if ctx.remote {
61            // Remote target: serialize context to wire bytes.
62            match dcontext::capture().serialize() {
63                Ok(bytes) => {
64                    headers.insert(ContextHeader { bytes });
65                }
66                Err(e) => {
67                    tracing::warn!(
68                        target: "dcontext_dactor",
69                        error = %e,
70                        target_actor = %ctx.target_name,
71                        "failed to serialize dcontext for outbound message"
72                    );
73                    if self.error_policy == ErrorPolicy::Reject {
74                        return Disposition::Reject(format!("dcontext serialization failed: {e}"));
75                    }
76                }
77            }
78        } else {
79            // Local target: snapshot only — no serialization needed.
80            let snap = dcontext::capture();
81            headers.insert(ContextSnapshotHeader { snapshot: snap });
82        }
83
84        Disposition::Continue
85    }
86
87    fn on_reply(
88        &self,
89        _ctx: &OutboundContext<'_>,
90        _runtime_headers: &RuntimeHeaders,
91        _headers: &Headers,
92        _outcome: &Outcome<'_>,
93    ) {
94        // No action on reply — context flows forward, not backward.
95    }
96}