Skip to main content

core_api/
delivery.rs

1//! Core -> Host delivery contract. Host executes a resolved destination; it never resolves
2//! recipients or chooses fallback routes. Completion describes evidence, not user receipt.
3use crate::MwsMessage;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase", deny_unknown_fields)]
8pub struct HostDeliveryRequest {
9    /// Stable across retransmission of the same attempt. A new attempt needs a new ID.
10    pub delivery_id: String,
11    pub expires_at_unix_ms: i64,
12    pub destination: HostDeliveryDestination,
13}
14
15impl HostDeliveryRequest {
16    pub fn remaining(&self) -> std::time::Duration {
17        let now = std::time::SystemTime::now()
18            .duration_since(std::time::UNIX_EPOCH)
19            .map(|duration| duration.as_millis())
20            .unwrap_or(u128::MAX);
21        let remaining = (self.expires_at_unix_ms.max(0) as u128).saturating_sub(now);
22        std::time::Duration::from_millis(remaining.min(u64::MAX as u128) as u64)
23    }
24    pub fn is_expired(&self) -> bool {
25        let now = std::time::SystemTime::now()
26            .duration_since(std::time::UNIX_EPOCH)
27            .map(|duration| duration.as_millis())
28            .unwrap_or(u128::MAX);
29        self.expires_at_unix_ms <= 0 || now >= self.expires_at_unix_ms as u128
30    }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(
35    tag = "kind",
36    rename_all = "camelCase",
37    rename_all_fields = "camelCase",
38    deny_unknown_fields
39)]
40pub enum HostDeliveryDestination {
41    AppConnection {
42        client_id: String,
43        message: MwsMessage,
44    },
45    NodeConnection {
46        connection_key: String,
47        message: MwsMessage,
48    },
49    Messaging {
50        request: crate::MessagingDeliveryRequest,
51    },
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(tag = "status", rename_all = "camelCase", deny_unknown_fields)]
56pub enum DeliveryOutcome {
57    Accepted { boundary: DeliveryAcceptance },
58    Failed { code: String, message: String },
59    Unknown { message: String },
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub enum DeliveryAcceptance {
65    /// The authenticated connection's writer accepted the frame, not the remote application.
66    ConnectionQueue,
67    /// The provider API accepted the operation, not a user-read acknowledgement.
68    Provider,
69}
70
71impl DeliveryOutcome {
72    pub fn is_accepted(&self) -> bool {
73        matches!(self, Self::Accepted { .. })
74    }
75
76    pub fn connection(accepted: bool) -> Self {
77        if accepted {
78            Self::Accepted {
79                boundary: DeliveryAcceptance::ConnectionQueue,
80            }
81        } else {
82            Self::Failed {
83                code: "UNREACHABLE".into(),
84                message: "Connection did not accept delivery".into(),
85            }
86        }
87    }
88}
89
90/// Connection fencing is control, not delivery. The key must identify an exact session,
91/// so delayed control requests cannot close a replacement connection.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(
94    tag = "kind",
95    rename_all = "camelCase",
96    rename_all_fields = "camelCase",
97    deny_unknown_fields
98)]
99pub enum HostConnectionControl {
100    DisconnectNode { connection_key: String },
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn acceptance_is_not_a_boolean_receipt() {
109        let result = DeliveryOutcome::connection(true);
110        assert_eq!(
111            serde_json::to_value(result).unwrap(),
112            serde_json::json!({
113                "status": "accepted", "boundary": "connectionQueue"
114            })
115        );
116        assert!(
117            serde_json::from_value::<DeliveryOutcome>(serde_json::json!({"delivered": true}))
118                .is_err()
119        );
120    }
121
122    #[test]
123    fn obsolete_output_effects_are_rejected() {
124        assert!(serde_json::from_value::<crate::ServiceCoreOutput>(
125            serde_json::json!({"effects": []})
126        )
127        .is_err());
128    }
129
130    #[test]
131    fn unknown_is_not_failure_or_acceptance() {
132        let result = DeliveryOutcome::Unknown {
133            message: "Timed out after dispatch".into(),
134        };
135        assert!(!result.is_accepted());
136        let encoded = serde_json::to_value(&result).unwrap();
137        assert_eq!(
138            serde_json::from_value::<DeliveryOutcome>(encoded).unwrap(),
139            result
140        );
141    }
142}