1mod connection;
18mod participant;
19mod push_client;
20mod subscription;
21
22pub use push_client::{OBSERVABILITY_CHANNEL, PushClient, PushWriter, PushedFrame};
23pub use subscription::{DeliveredMessage, SubscriptionStream};
24
25use alloc::format;
26use alloc::string::{String, ToString};
27use alloc::sync::Arc;
28use alloc::vec::Vec;
29use core::fmt;
30
31use liminal::protocol::{
32 CausalContext, Frame, MessageEnvelope, PUBLISH_DELIVERED_FLAG, PUBLISH_IDEMPOTENCY_KEY_FLAG,
33 SchemaId,
34};
35use spin::Mutex;
36
37use crate::{DeliveryAck, PressureResponse, SdkError};
38
39use self::connection::{Connection, unexpected_frame};
40use super::ServerAddress;
41use super::participant::ParticipantResponseProvenance;
42use super::protocol::{
43 ParticipantRemoteTransport, ParticipantTransportFrame, RemoteTransport,
44 WireConversationRequest, WirePublishRequest, WireResumeRequest, WireSubscribeRequest,
45};
46
47const APPLICATION_STREAM_ID: u32 = 1;
49const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
51const SCHEMALESS_SCHEMA: &[u8] = &[];
53
54struct ConnectionSlot {
55 connection: Connection,
56 provenance: ParticipantResponseProvenance,
57 next_attempt_id: u64,
58 next_connection_id: u64,
59}
60
61pub struct TcpRemoteTransport {
63 connection: Arc<Mutex<ConnectionSlot>>,
64 address: String,
65 auth_token: Vec<u8>,
66}
67
68impl fmt::Debug for TcpRemoteTransport {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter
71 .debug_struct("TcpRemoteTransport")
72 .finish_non_exhaustive()
73 }
74}
75
76impl TcpRemoteTransport {
77 pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
85 Self::connect_with_auth(server_address, &[])
86 }
87
88 pub fn connect_with_auth(
100 server_address: &ServerAddress,
101 auth_token: &[u8],
102 ) -> Result<Self, SdkError> {
103 let address = server_address.as_str().to_string();
104 let connection = Connection::connect_with_auth(&address, auth_token)?;
105 Ok(Self {
106 connection: Arc::new(Mutex::new(ConnectionSlot {
107 connection,
108 provenance: ParticipantResponseProvenance::new(1, 1),
109 next_attempt_id: 2,
110 next_connection_id: 2,
111 })),
112 address,
113 auth_token: auth_token.to_vec(),
114 })
115 }
116
117 fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
118 self.connection.lock().connection.round_trip(request)
119 }
120}
121
122impl ParticipantRemoteTransport for TcpRemoteTransport {
123 fn send_participant(
124 &self,
125 _server_address: &ServerAddress,
126 request: &liminal_protocol::wire::ClientRequest,
127 ) -> Result<ParticipantResponseProvenance, SdkError> {
128 let mut slot = self.connection.lock();
129 slot.connection.send_participant(request)?;
130 Ok(slot.provenance)
131 }
132
133 fn receive_participant(
134 &self,
135 _server_address: &ServerAddress,
136 ) -> Result<ParticipantTransportFrame, SdkError> {
137 let mut slot = self.connection.lock();
138 let frame = slot.connection.receive_participant()?;
139 Ok(ParticipantTransportFrame {
140 frame,
141 provenance: slot.provenance,
142 })
143 }
144
145 fn reconnect_participant(
146 &self,
147 _server_address: &ServerAddress,
148 ) -> Result<ParticipantResponseProvenance, SdkError> {
149 let mut slot = self.connection.lock();
150 let attempt_id = slot.next_attempt_id;
151 slot.next_attempt_id =
152 slot.next_attempt_id
153 .checked_add(1)
154 .ok_or_else(|| SdkError::Connection {
155 description: "participant transport attempt identity exhausted".to_string(),
156 })?;
157 let connection = Connection::connect_with_auth(&self.address, &self.auth_token)?;
158 let connection_id = slot.next_connection_id;
159 slot.next_connection_id =
160 slot.next_connection_id
161 .checked_add(1)
162 .ok_or_else(|| SdkError::Connection {
163 description: "participant transport connection identity exhausted".to_string(),
164 })?;
165 let provenance = ParticipantResponseProvenance::new(connection_id, attempt_id);
166 slot.connection = connection;
167 slot.provenance = provenance;
168 Ok(provenance)
169 }
170}
171
172impl RemoteTransport for TcpRemoteTransport {
173 fn publish(
174 &self,
175 _server_address: &ServerAddress,
176 request: &WirePublishRequest,
177 ) -> Result<PressureResponse, SdkError> {
178 let frame = build_publish_frame(request);
179 let response = self.round_trip(&frame)?;
180 publish_response(response)
181 }
182
183 fn publish_with_delivery(
184 &self,
185 _server_address: &ServerAddress,
186 request: &WirePublishRequest,
187 ) -> Result<DeliveryAck, SdkError> {
188 let frame = build_publish_frame(request);
189 let response = self.round_trip(&frame)?;
190 publish_delivery_response(response)
191 }
192
193 fn subscribe(
214 &self,
215 _server_address: &ServerAddress,
216 request: &WireSubscribeRequest,
217 ) -> Result<(), SdkError> {
218 let frame = Frame::Subscribe {
219 flags: 0,
220 stream_id: request.stream_id(),
221 channel: request.channel().to_string(),
222 accepted_schemas: Vec::new(),
225 max_in_flight: DEFAULT_MAX_IN_FLIGHT,
226 };
227 let response = self.round_trip(&frame)?;
228 subscribe_response(response)
229 }
230
231 fn send_conversation(
232 &self,
233 _server_address: &ServerAddress,
234 request: &WireConversationRequest,
235 ) -> Result<(), SdkError> {
236 let conversation_label = request.conversation_id().as_str();
237 let conversation_id = conversation_wire_id(conversation_label);
238 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
239 self.connection.lock().connection.send_conversation_message(
240 conversation_id,
241 conversation_label,
242 envelope,
243 )
244 }
245
246 fn request_reply_conversation(
247 &self,
248 _server_address: &ServerAddress,
249 request: &WireConversationRequest,
250 ) -> Result<Vec<u8>, SdkError> {
251 let conversation_label = request.conversation_id().as_str();
252 let conversation_id = conversation_wire_id(conversation_label);
253 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
254 self.connection
255 .lock()
256 .connection
257 .conversation_request_reply(conversation_id, conversation_label, envelope)
258 }
259
260 fn resume(
261 &self,
262 _server_address: &ServerAddress,
263 request: &WireResumeRequest,
264 ) -> Result<(), SdkError> {
265 let _ = (request.subscription_id(), request.resume_from_sequence());
272 Err(SdkError::Protocol {
273 description:
274 "resume is not yet supported over the TCP transport; re-subscribe to trigger \
275 server replay"
276 .to_string(),
277 })
278 }
279}
280
281fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
282 MessageEnvelope::new(
283 schema_id_from_bytes(schema_bytes),
284 CausalContext::independent(),
285 payload.to_vec(),
286 )
287}
288
289fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
295 let mut id = [0_u8; SchemaId::WIRE_LEN];
296 let mut hash = fnv1a(schema_bytes).to_be_bytes();
297 for (index, slot) in id.iter_mut().enumerate() {
299 *slot = hash[index % hash.len()];
300 if index % hash.len() == hash.len() - 1 {
301 hash = fnv1a(&hash).to_be_bytes();
302 }
303 }
304 SchemaId::new(id)
305}
306
307fn conversation_wire_id(conversation_id: &str) -> u64 {
308 fnv1a(conversation_id.as_bytes())
309}
310
311fn fnv1a(bytes: &[u8]) -> u64 {
313 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
314 const PRIME: u64 = 0x0000_0100_0000_01b3;
315 let mut hash = OFFSET_BASIS;
316 for byte in bytes {
317 hash ^= u64::from(*byte);
318 hash = hash.wrapping_mul(PRIME);
319 }
320 hash
321}
322
323fn build_publish_frame(request: &WirePublishRequest) -> Frame {
327 let envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
328 let flags = match request.idempotency_key() {
329 Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
330 None => 0,
331 };
332 Frame::Publish {
333 flags,
334 stream_id: APPLICATION_STREAM_ID,
335 channel: request.channel().to_string(),
336 envelope,
337 idempotency_key: request.idempotency_key().map(ToString::to_string),
338 }
339}
340
341fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
342 match frame {
343 Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
344 Frame::PublishError {
345 reason_code,
346 message,
347 ..
348 } => Err(SdkError::Backpressure {
349 reason: format!(
350 "server rejected publish (reason {reason_code}): {}",
351 message.unwrap_or_else(|| "no detail".to_string())
352 ),
353 }),
354 other => Err(unexpected_frame("PublishAck", &other)),
355 }
356}
357
358fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
361 match frame {
362 Frame::PublishAck { flags, .. } => {
363 let accepted = flags & PUBLISH_DELIVERED_FLAG != 0;
364 Ok(DeliveryAck::new(PressureResponse::Accept, accepted))
365 }
366 Frame::PublishError {
367 reason_code,
368 message,
369 ..
370 } => Err(SdkError::Backpressure {
371 reason: format!(
372 "server rejected publish (reason {reason_code}): {}",
373 message.unwrap_or_else(|| "no detail".to_string())
374 ),
375 }),
376 other => Err(unexpected_frame("PublishAck", &other)),
377 }
378}
379
380fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
381 match frame {
382 Frame::SubscribeAck { .. } => Ok(()),
383 Frame::SubscribeError {
384 reason_code,
385 message,
386 ..
387 } => Err(SdkError::Protocol {
388 description: format!(
389 "server rejected subscribe (reason {reason_code}): {}",
390 message.unwrap_or_else(|| "no detail".to_string())
391 ),
392 }),
393 other => Err(unexpected_frame("SubscribeAck", &other)),
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn schema_ids_are_deterministic_and_distinct() {
403 assert_eq!(schema_id_from_bytes(b"a"), schema_id_from_bytes(b"a"));
404 assert_ne!(schema_id_from_bytes(b"a"), schema_id_from_bytes(b"b"));
405 }
406
407 #[test]
408 fn conversation_ids_are_stable() {
409 assert_eq!(conversation_wire_id("chat"), conversation_wire_id("chat"));
410 assert_ne!(conversation_wire_id("chat"), conversation_wire_id("other"));
411 }
412
413 #[test]
414 fn publish_ack_maps_to_accept() -> Result<(), SdkError> {
415 let frame = Frame::PublishAck {
416 flags: 0,
417 stream_id: 1,
418 message_id: 7,
419 };
420 assert_eq!(publish_response(frame)?, PressureResponse::Accept);
421 Ok(())
422 }
423
424 #[test]
425 fn publish_error_maps_to_backpressure() {
426 let frame = Frame::PublishError {
427 flags: 0,
428 stream_id: 1,
429 reason_code: 9,
430 message: Some("nope".to_string()),
431 };
432 assert!(matches!(
433 publish_response(frame),
434 Err(SdkError::Backpressure { .. })
435 ));
436 }
437}