tdm_server_rust/collaboration/
signalr.rs1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9pub const RECORD_SEPARATOR: char = '\u{1e}';
11
12#[derive(Debug, thiserror::Error)]
14pub enum SignalRError {
15 #[error("JSON 协议错误:{0}")]
17 Json(#[from] serde_json::Error),
18 #[error("不支持的 SignalR 消息")]
20 Unsupported,
21}
22
23#[derive(Clone, Debug, Deserialize, Serialize)]
25pub struct InvocationMessage {
26 #[serde(rename = "type")]
28 pub message_type: i32,
29 #[serde(rename = "invocationId", skip_serializing_if = "Option::is_none")]
31 pub invocation_id: Option<String>,
32 pub target: String,
34 #[serde(default)]
36 pub arguments: Vec<Value>,
37}
38
39#[derive(Clone, Debug, Deserialize, Serialize)]
41pub struct CompletionMessage {
42 #[serde(rename = "type")]
44 pub message_type: i32,
45 #[serde(rename = "invocationId")]
47 pub invocation_id: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub result: Option<Value>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub error: Option<String>,
54}
55
56#[derive(Clone, Debug)]
58pub enum HubMessage {
59 Invocation(InvocationMessage),
61 Completion(CompletionMessage),
63 Ping,
65}
66
67impl HubMessage {
68 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 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 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
111pub fn handshake_response() -> String {
113 format!("{{}}{RECORD_SEPARATOR}")
114}
115
116pub fn ping_frame() -> String {
118 format!("{{\"type\":6}}{RECORD_SEPARATOR}")
119}
120
121pub fn is_handshake_request(payload: &str) -> bool {
126 payload.contains("\"protocol\"") && payload.contains("\"version\"")
127}
128
129pub 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 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 Some(_) => {}
154 None => return Err(SignalRError::Unsupported),
155 }
156 }
157 Ok(messages)
158}
159
160pub 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}