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