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