1mod connection;
18mod push_client;
19mod subscription;
20
21pub use push_client::{OBSERVABILITY_CHANNEL, PushClient, PushWriter, PushedFrame};
22pub use subscription::{DeliveredMessage, SubscriptionStream};
23
24use alloc::format;
25use alloc::string::ToString;
26use alloc::sync::Arc;
27use alloc::vec::Vec;
28use core::fmt;
29
30use liminal::protocol::{
31 CausalContext, Frame, MessageEnvelope, PUBLISH_DELIVERED_FLAG, PUBLISH_IDEMPOTENCY_KEY_FLAG,
32 SchemaId,
33};
34use spin::Mutex;
35
36use crate::{DeliveryAck, PressureResponse, SdkError};
37
38use self::connection::{Connection, unexpected_frame};
39use super::ServerAddress;
40use super::protocol::{
41 RemoteTransport, WireConversationRequest, WirePublishRequest, WireResumeRequest,
42 WireSubscribeRequest,
43};
44
45const APPLICATION_STREAM_ID: u32 = 1;
47const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
49const SCHEMALESS_SCHEMA: &[u8] = &[];
51
52pub struct TcpRemoteTransport {
54 connection: Arc<Mutex<Connection>>,
55}
56
57impl fmt::Debug for TcpRemoteTransport {
58 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59 formatter
60 .debug_struct("TcpRemoteTransport")
61 .finish_non_exhaustive()
62 }
63}
64
65impl TcpRemoteTransport {
66 pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
74 let connection = Connection::connect(server_address.as_str())?;
75 Ok(Self {
76 connection: Arc::new(Mutex::new(connection)),
77 })
78 }
79
80 pub fn connect_with_auth(
92 server_address: &ServerAddress,
93 auth_token: &[u8],
94 ) -> Result<Self, SdkError> {
95 let connection = Connection::connect_with_auth(server_address.as_str(), auth_token)?;
96 Ok(Self {
97 connection: Arc::new(Mutex::new(connection)),
98 })
99 }
100
101 fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
102 let mut connection = self.connection.lock();
103 connection.round_trip(request)
104 }
105}
106
107impl RemoteTransport for TcpRemoteTransport {
108 fn publish(
109 &self,
110 _server_address: &ServerAddress,
111 request: &WirePublishRequest,
112 ) -> Result<PressureResponse, SdkError> {
113 let frame = build_publish_frame(request);
114 let response = self.round_trip(&frame)?;
115 publish_response(response)
116 }
117
118 fn publish_with_delivery(
119 &self,
120 _server_address: &ServerAddress,
121 request: &WirePublishRequest,
122 ) -> Result<DeliveryAck, SdkError> {
123 let frame = build_publish_frame(request);
124 let response = self.round_trip(&frame)?;
125 publish_delivery_response(response)
126 }
127
128 fn subscribe(
149 &self,
150 _server_address: &ServerAddress,
151 request: &WireSubscribeRequest,
152 ) -> Result<(), SdkError> {
153 let frame = Frame::Subscribe {
154 flags: 0,
155 stream_id: request.stream_id(),
156 channel: request.channel().to_string(),
157 accepted_schemas: Vec::new(),
160 max_in_flight: DEFAULT_MAX_IN_FLIGHT,
161 };
162 let response = self.round_trip(&frame)?;
163 subscribe_response(response)
164 }
165
166 fn send_conversation(
167 &self,
168 _server_address: &ServerAddress,
169 request: &WireConversationRequest,
170 ) -> Result<(), SdkError> {
171 let conversation_label = request.conversation_id().as_str();
172 let conversation_id = conversation_wire_id(conversation_label);
173 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
174 let mut connection = self.connection.lock();
175 connection.send_conversation_message(conversation_id, conversation_label, envelope)
176 }
177
178 fn request_reply_conversation(
179 &self,
180 _server_address: &ServerAddress,
181 request: &WireConversationRequest,
182 ) -> Result<Vec<u8>, SdkError> {
183 let conversation_label = request.conversation_id().as_str();
184 let conversation_id = conversation_wire_id(conversation_label);
185 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
186 let mut connection = self.connection.lock();
187 connection.conversation_request_reply(conversation_id, conversation_label, envelope)
188 }
189
190 fn resume(
191 &self,
192 _server_address: &ServerAddress,
193 request: &WireResumeRequest,
194 ) -> Result<(), SdkError> {
195 let _ = (request.subscription_id(), request.resume_from_sequence());
202 Err(SdkError::Protocol {
203 description:
204 "resume is not yet supported over the TCP transport; re-subscribe to trigger \
205 server replay"
206 .to_string(),
207 })
208 }
209}
210
211fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
212 MessageEnvelope::new(
213 schema_id_from_bytes(schema_bytes),
214 CausalContext::independent(),
215 payload.to_vec(),
216 )
217}
218
219fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
225 let mut id = [0_u8; SchemaId::WIRE_LEN];
226 let mut hash = fnv1a(schema_bytes).to_be_bytes();
227 for (index, slot) in id.iter_mut().enumerate() {
229 *slot = hash[index % hash.len()];
230 if index % hash.len() == hash.len() - 1 {
231 hash = fnv1a(&hash).to_be_bytes();
232 }
233 }
234 SchemaId::new(id)
235}
236
237fn conversation_wire_id(conversation_id: &str) -> u64 {
238 fnv1a(conversation_id.as_bytes())
239}
240
241fn fnv1a(bytes: &[u8]) -> u64 {
243 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
244 const PRIME: u64 = 0x0000_0100_0000_01b3;
245 let mut hash = OFFSET_BASIS;
246 for byte in bytes {
247 hash ^= u64::from(*byte);
248 hash = hash.wrapping_mul(PRIME);
249 }
250 hash
251}
252
253fn build_publish_frame(request: &WirePublishRequest) -> Frame {
257 let envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
258 let flags = match request.idempotency_key() {
259 Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
260 None => 0,
261 };
262 Frame::Publish {
263 flags,
264 stream_id: APPLICATION_STREAM_ID,
265 channel: request.channel().to_string(),
266 envelope,
267 idempotency_key: request.idempotency_key().map(ToString::to_string),
268 }
269}
270
271fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
272 match frame {
273 Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
274 Frame::PublishError {
275 reason_code,
276 message,
277 ..
278 } => Err(SdkError::Backpressure {
279 reason: format!(
280 "server rejected publish (reason {reason_code}): {}",
281 message.unwrap_or_else(|| "no detail".to_string())
282 ),
283 }),
284 other => Err(unexpected_frame("PublishAck", &other)),
285 }
286}
287
288fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
291 match frame {
292 Frame::PublishAck { flags, .. } => {
293 let accepted = flags & PUBLISH_DELIVERED_FLAG != 0;
294 Ok(DeliveryAck::new(PressureResponse::Accept, accepted))
295 }
296 Frame::PublishError {
297 reason_code,
298 message,
299 ..
300 } => Err(SdkError::Backpressure {
301 reason: format!(
302 "server rejected publish (reason {reason_code}): {}",
303 message.unwrap_or_else(|| "no detail".to_string())
304 ),
305 }),
306 other => Err(unexpected_frame("PublishAck", &other)),
307 }
308}
309
310fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
311 match frame {
312 Frame::SubscribeAck { .. } => Ok(()),
313 Frame::SubscribeError {
314 reason_code,
315 message,
316 ..
317 } => Err(SdkError::Protocol {
318 description: format!(
319 "server rejected subscribe (reason {reason_code}): {}",
320 message.unwrap_or_else(|| "no detail".to_string())
321 ),
322 }),
323 other => Err(unexpected_frame("SubscribeAck", &other)),
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn schema_ids_are_deterministic_and_distinct() {
333 assert_eq!(schema_id_from_bytes(b"a"), schema_id_from_bytes(b"a"));
334 assert_ne!(schema_id_from_bytes(b"a"), schema_id_from_bytes(b"b"));
335 }
336
337 #[test]
338 fn conversation_ids_are_stable() {
339 assert_eq!(conversation_wire_id("chat"), conversation_wire_id("chat"));
340 assert_ne!(conversation_wire_id("chat"), conversation_wire_id("other"));
341 }
342
343 #[test]
344 fn publish_ack_maps_to_accept() -> Result<(), SdkError> {
345 let frame = Frame::PublishAck {
346 flags: 0,
347 stream_id: 1,
348 message_id: 7,
349 };
350 assert_eq!(publish_response(frame)?, PressureResponse::Accept);
351 Ok(())
352 }
353
354 #[test]
355 fn publish_error_maps_to_backpressure() {
356 let frame = Frame::PublishError {
357 flags: 0,
358 stream_id: 1,
359 reason_code: 9,
360 message: Some("nope".to_string()),
361 };
362 assert!(matches!(
363 publish_response(frame),
364 Err(SdkError::Backpressure { .. })
365 ));
366 }
367}