Skip to main content

tdm_server_rust/collaboration/
signalr.rs

1//! SignalR JSON Hub 协议子集
2//!
3//! 仅实现与 `@microsoft/signalr` 前端互通所需的最小协议:握手、Invocation(1)、Completion(3)、Ping(6)。
4//! 帧之间以记录分隔符 `0x1e` 分隔。
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// SignalR JSON Hub 协议记录分隔符(0x1e)。
10pub const RECORD_SEPARATOR: char = '\u{1e}';
11
12/// SignalR 协议错误。
13#[derive(Debug, thiserror::Error)]
14pub enum SignalRError {
15    /// JSON 序列化或反序列化失败。
16    #[error("JSON 协议错误:{0}")]
17    Json(#[from] serde_json::Error),
18    /// 不支持的协议消息类型。
19    #[error("不支持的 SignalR 消息")]
20    Unsupported,
21}
22
23/// SignalR Invocation 消息(type=1)。
24#[derive(Clone, Debug, Deserialize, Serialize)]
25pub struct InvocationMessage {
26    /// 消息类型,Invocation 固定为 1。
27    #[serde(rename = "type")]
28    pub message_type: i32,
29    /// 调用 ID(无返回值的事件推送可为空)。
30    #[serde(rename = "invocationId", skip_serializing_if = "Option::is_none")]
31    pub invocation_id: Option<String>,
32    /// Hub 方法名或客户端事件名。
33    pub target: String,
34    /// 调用参数列表。
35    #[serde(default)]
36    pub arguments: Vec<Value>,
37}
38
39/// SignalR Completion 消息(type=3)。
40#[derive(Clone, Debug, Deserialize, Serialize)]
41pub struct CompletionMessage {
42    /// 消息类型,Completion 固定为 3。
43    #[serde(rename = "type")]
44    pub message_type: i32,
45    /// 对应的 Invocation ID。
46    #[serde(rename = "invocationId")]
47    pub invocation_id: String,
48    /// 成功结果。
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub result: Option<Value>,
51    /// 错误消息。
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub error: Option<String>,
54}
55
56/// SignalR Hub 消息枚举(仅覆盖本协作所需类型)。
57#[derive(Clone, Debug)]
58pub enum HubMessage {
59    /// Invocation 消息(客户端调用或服务端事件推送)。
60    Invocation(InvocationMessage),
61    /// Completion 消息(服务端对调用的应答)。
62    Completion(CompletionMessage),
63    /// Ping 心跳消息。
64    Ping,
65}
66
67impl HubMessage {
68    /// 构造成功 Completion 应答。
69    ///
70    /// ## 参数
71    /// - `invocation_id`:客户端调用 ID
72    /// - `result`:调用结果(可为 `None` 表示无返回值)
73    pub fn completion(invocation_id: String, result: Option<Value>) -> Self {
74        Self::Completion(CompletionMessage {
75            message_type: 3,
76            invocation_id,
77            result,
78            error: None,
79        })
80    }
81
82    /// 构造失败 Completion 应答。
83    ///
84    /// ## 参数
85    /// - `invocation_id`:客户端调用 ID
86    /// - `error`:错误消息文本
87    pub fn completion_error(invocation_id: String, error: String) -> Self {
88        Self::Completion(CompletionMessage {
89            message_type: 3,
90            invocation_id,
91            result: None,
92            error: Some(error),
93        })
94    }
95
96    /// 构造服务端事件推送(无调用 ID 的 Invocation)。
97    ///
98    /// ## 参数
99    /// - `target`:客户端事件名(如 `ProjectStateUpdated`)
100    /// - `payload`:事件载荷
101    pub fn event(target: &str, payload: Value) -> Self {
102        Self::Invocation(InvocationMessage {
103            message_type: 1,
104            invocation_id: None,
105            target: target.to_owned(),
106            arguments: vec![payload],
107        })
108    }
109}
110
111/// 生成 SignalR JSON Hub 握手成功响应帧 `{}\x1e`。
112pub fn handshake_response() -> String {
113    format!("{{}}{RECORD_SEPARATOR}")
114}
115
116/// 预编码的 Keep-Alive Ping 文本帧 `{"type":6}\x1e`。
117pub fn ping_frame() -> String {
118    format!("{{\"type\":6}}{RECORD_SEPARATOR}")
119}
120
121/// 判断文本帧是否为 SignalR JSON Hub 握手请求。
122///
123/// ## 参数
124/// - `payload`:WebSocket 文本帧
125pub fn is_handshake_request(payload: &str) -> bool {
126    payload.contains("\"protocol\"") && payload.contains("\"version\"")
127}
128
129/// 解码一个文本帧中的全部 Hub 消息(按记录分隔符切分,跳过握手帧)。
130///
131/// ## 参数
132/// - `payload`:WebSocket 文本帧(可能含多条消息)
133///
134/// ## 返回
135/// - `Ok(Vec<HubMessage>)`:解析出的消息列表
136/// - `Err(SignalRError)`:出现不支持的消息类型或 JSON 错误
137pub fn decode_messages(payload: &str) -> Result<Vec<HubMessage>, SignalRError> {
138    let mut messages = Vec::new();
139    for raw in payload
140        .split(RECORD_SEPARATOR)
141        .filter(|part| !part.trim().is_empty())
142    {
143        // 跳过握手帧(含 protocol/version 字段)。
144        if raw.contains("\"protocol\"") && raw.contains("\"version\"") {
145            continue;
146        }
147        let value: Value = serde_json::from_str(raw)?;
148        match value.get("type").and_then(Value::as_i64) {
149            Some(1) => messages.push(HubMessage::Invocation(serde_json::from_value(value)?)),
150            Some(3) => messages.push(HubMessage::Completion(serde_json::from_value(value)?)),
151            Some(6) => messages.push(HubMessage::Ping),
152            // type=7 CloseMessage 等其他类型直接忽略,避免误判断连。
153            Some(_) => {}
154            None => return Err(SignalRError::Unsupported),
155        }
156    }
157    Ok(messages)
158}
159
160/// 编码 Hub 消息为带记录分隔符的文本帧。
161///
162/// ## 参数
163/// - `message`:待编码 Hub 消息
164pub fn encode_message(message: &HubMessage) -> Result<String, SignalRError> {
165    let raw = match message {
166        HubMessage::Invocation(value) => serde_json::to_string(value)?,
167        HubMessage::Completion(value) => serde_json::to_string(value)?,
168        HubMessage::Ping => return Ok(ping_frame()),
169    };
170    Ok(format!("{raw}{RECORD_SEPARATOR}"))
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use serde_json::json;
177
178    #[test]
179    fn decode_messages_splits_record_separator_frames() {
180        let payload =
181            "{\"type\":1,\"invocationId\":\"1\",\"target\":\"JoinProject\",\"arguments\":[{}]}\u{1e}{\"type\":6}\u{1e}";
182        let messages = decode_messages(payload).expect("decode");
183        assert_eq!(messages.len(), 2);
184    }
185
186    #[test]
187    fn encode_event_appends_record_separator() {
188        let payload = encode_message(&HubMessage::event(
189            "ProjectStateUpdated",
190            json!({"project_key":"p1"}),
191        ))
192        .expect("encode");
193        assert!(payload.ends_with(RECORD_SEPARATOR));
194    }
195
196    #[test]
197    fn handshake_response_matches_protocol() {
198        assert!(is_handshake_request(
199            "{\"protocol\":\"json\",\"version\":1}\u{1e}"
200        ));
201        assert_eq!(handshake_response(), "{}\u{1e}");
202    }
203
204    #[test]
205    fn ping_frame_matches_keep_alive() {
206        assert_eq!(ping_frame(), "{\"type\":6}\u{1e}");
207    }
208}