Skip to main content

subc_protocol/
session.rs

1//! Session route control wire contract.
2//!
3//! subc has two distinct channel-0 handshakes. Module registration is the
4//! module-to-subc `HELLO`/`HELLO_ACK` handshake that registers the manifest and
5//! liveness. Route bind is the client-to-subc-to-module request/response
6//! handshake that binds one client route to a module route channel.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::{BindIdentity, Principal, RouteTarget};
12
13pub const MODULE_CONTROL_OP_HEALTH_CHECK: &str = "health.check";
14
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum HealthStatus {
18    Ok,
19    Degraded,
20    Failing,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub struct HealthReport {
25    pub status: HealthStatus,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub detail: Option<String>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub metrics: Option<Value>,
30}
31
32impl HealthReport {
33    pub fn ok() -> Self {
34        Self {
35            status: HealthStatus::Ok,
36            detail: None,
37            metrics: None,
38        }
39    }
40}
41
42/// subc-to-module channel-0 control RPC body.
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44#[serde(tag = "op")]
45pub enum ModuleControlRequest {
46    #[serde(rename = "route.bind")]
47    RouteBind {
48        route_channel: u16,
49        target: RouteTarget,
50        identity: BindIdentity,
51        #[serde(default, skip_serializing_if = "Option::is_none")]
52        principal: Option<Principal>,
53    },
54    #[serde(rename = "health.check")]
55    HealthCheck {},
56}
57
58/// Module-to-subc channel-0 response body.
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
60#[serde(tag = "op")]
61pub enum ModuleControlResponse {
62    /// ACK-only success. Rejections use the `FrameType::Error` lane.
63    #[serde(rename = "route.bind")]
64    RouteBindAck {},
65    #[serde(rename = "health.check")]
66    HealthCheck {
67        status: HealthStatus,
68        #[serde(default, skip_serializing_if = "Option::is_none")]
69        detail: Option<String>,
70        #[serde(default, skip_serializing_if = "Option::is_none")]
71        metrics: Option<Value>,
72    },
73}
74
75impl From<HealthReport> for ModuleControlResponse {
76    fn from(report: HealthReport) -> Self {
77        Self::HealthCheck {
78            status: report.status,
79            detail: report.detail,
80            metrics: report.metrics,
81        }
82    }
83}
84
85impl ModuleControlResponse {
86    pub fn health_report(&self) -> Option<HealthReport> {
87        match self {
88            Self::HealthCheck {
89                status,
90                detail,
91                metrics,
92            } => Some(HealthReport {
93                status: *status,
94                detail: detail.clone(),
95                metrics: metrics.clone(),
96            }),
97            Self::RouteBindAck {} => None,
98        }
99    }
100}
101
102/// Module-to-subc channel-0 push body.
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
104#[serde(tag = "op")]
105pub enum ModuleControlPush {
106    #[serde(rename = "route.status")]
107    RouteStatus { route_channel: u16, status: String },
108}