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