lightstreamer_rs/client/message.rs
1//! Sending a message *to* the server.
2//!
3//! TLCP is not only a download: a client may push a text message up to the
4//! server's Metadata Adapter, which interprets it however the application
5//! wants — a chat line, an order, a command
6//! [`docs/spec/03-requests.md` §12]. This crate carries the text and reports
7//! what became of it; it has no opinion on the content.
8
9use std::num::NonZeroU32;
10use std::time::Duration;
11
12use crate::config::ConfigError;
13use crate::error::ServerError;
14use crate::session::{
15 MessageResult as WireResult, MessageSequence, OutgoingMessage, SequenceName as WireSequenceName,
16};
17
18/// The name of an ordered message sequence
19/// [`docs/spec/03-requests.md` §12.1].
20///
21/// Messages sharing a sequence are delivered to the Metadata Adapter **in
22/// order of their progressive number**, and a message in no sequence is
23/// processed as soon as it arrives, possibly alongside others. A sequence is
24/// therefore how an application says "these messages must not overtake each
25/// other".
26///
27/// # Examples
28///
29/// ```
30/// use lightstreamer_rs::SequenceName as Sequence;
31///
32/// let orders = Sequence::try_new("ORDERS")?;
33/// assert_eq!(orders.as_str(), "ORDERS");
34/// # Ok::<(), lightstreamer_rs::ConfigError>(())
35/// ```
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct SequenceName(WireSequenceName);
38
39impl SequenceName {
40 /// Checks and wraps a sequence name.
41 ///
42 /// # Errors
43 ///
44 /// [`ConfigError::InvalidSequenceName`] if the name is empty, contains
45 /// anything but ASCII letters, digits and underscores, or is
46 /// `UNORDERED_MESSAGES` — which earlier versions of the protocol used and
47 /// which this one reserves, so the server would refuse it
48 /// [`docs/spec/03-requests.md` §12.1].
49 ///
50 /// # Examples
51 ///
52 /// ```
53 /// use lightstreamer_rs::{ConfigError, SequenceName as Sequence};
54 ///
55 /// assert!(matches!(
56 /// Sequence::try_new("UNORDERED_MESSAGES"),
57 /// Err(ConfigError::InvalidSequenceName { .. })
58 /// ));
59 /// ```
60 #[must_use = "a checked sequence name does nothing unless it is used"]
61 pub fn try_new(name: impl Into<String>) -> Result<Self, ConfigError> {
62 let name = name.into();
63 WireSequenceName::try_new(name.clone())
64 .map(Self)
65 .map_err(|_| ConfigError::InvalidSequenceName { name })
66 }
67
68 /// The name as it goes on the wire.
69 #[must_use]
70 #[inline]
71 pub fn as_str(&self) -> &str {
72 self.0.as_str()
73 }
74}
75
76/// A message on its way to the server's Metadata Adapter.
77///
78/// There are two shapes, and the difference is not cosmetic:
79///
80/// - [`Message::new`] — numbered, so the server can deduplicate it and so its
81/// outcome can be reported back to you.
82/// - [`Message::fire_and_forget`] — unnumbered and unreported. The server
83/// cannot tell a resend from a new message and you will never learn whether
84/// it worked.
85///
86/// # The number is yours to choose
87///
88/// The progressive starts at 1 and orders the messages of a sequence. This
89/// crate deliberately does **not** allocate it: the server tracks the
90/// numbering per session, so a counter kept here would silently restart the
91/// moment a session was replaced — exactly when a caller most needs to know
92/// what happened to a message. You own the numbering, and this crate never
93/// rewrites it.
94///
95/// # Examples
96///
97/// ```
98/// use std::num::NonZeroU32;
99/// use lightstreamer_rs::{Message, SequenceName as Sequence};
100///
101/// let message = Message::new("BUY 100", NonZeroU32::MIN)
102/// .in_sequence(Sequence::try_new("ORDERS")?);
103/// # Ok::<(), lightstreamer_rs::ConfigError>(())
104/// ```
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct Message {
107 inner: OutgoingMessage,
108}
109
110impl Message {
111 /// A numbered message whose outcome will be reported.
112 ///
113 /// The outcome arrives on the session event stream as
114 /// [`SessionEvent::Message`](crate::SessionEvent::Message).
115 #[must_use = "builders do nothing unless the message is sent"]
116 pub fn new(text: impl Into<String>, progressive: NonZeroU32) -> Self {
117 Self {
118 inner: OutgoingMessage::numbered(text, progressive),
119 }
120 }
121
122 /// A message with no number and no outcome report.
123 ///
124 /// The protocol makes these two properties inseparable: a message may omit
125 /// its progressive only if it also declines the outcome notification
126 /// [`docs/spec/03-requests.md` §12.1]. So this is genuinely
127 /// fire-and-forget — no ordering, no deduplication, no confirmation, and
128 /// no error if the Metadata Adapter rejects it.
129 #[must_use = "builders do nothing unless the message is sent"]
130 pub fn fire_and_forget(text: impl Into<String>) -> Self {
131 Self {
132 inner: OutgoingMessage::fire_and_forget(text),
133 }
134 }
135
136 /// Places the message in an ordered sequence.
137 ///
138 /// Messages of one sequence are processed in progressive order; messages
139 /// of different sequences do not constrain each other.
140 #[must_use = "builders do nothing unless the message is sent"]
141 pub fn in_sequence(mut self, sequence: SequenceName) -> Self {
142 self.inner = self.inner.in_sequence(sequence.0);
143 self
144 }
145
146 /// How long the server may wait for earlier messages of the same sequence
147 /// before giving up on them and processing this one
148 /// [`docs/spec/03-requests.md` §12.1].
149 ///
150 /// Only meaningful alongside [`Message::in_sequence`]. When the wait
151 /// expires the skipped progressives are reported as failures with
152 /// Appendix B codes `38` and `39` [`docs/spec/05-error-codes.md` §2].
153 ///
154 /// # Errors
155 ///
156 /// [`ConfigError::DurationTooLarge`] if the duration does not fit a whole
157 /// number of milliseconds.
158 pub fn with_max_wait(mut self, wait: Duration) -> Result<Self, ConfigError> {
159 let millis = u64::try_from(wait.as_millis())
160 .map_err(|_| ConfigError::DurationTooLarge { field: "max_wait" })?;
161 self.inner.max_wait_millis = Some(millis);
162 Ok(self)
163 }
164
165 /// The text that will be delivered.
166 #[must_use]
167 #[inline]
168 pub fn text(&self) -> &str {
169 &self.inner.message
170 }
171
172 /// Checks that the parts of this message can be sent together.
173 ///
174 /// [`Client::send_message`](crate::Client::send_message) calls this first,
175 /// so a caller never has to; it is public for the same reason
176 /// [`Subscription::validate`](crate::Subscription::validate) is.
177 ///
178 /// # Errors
179 ///
180 /// - [`ConfigError::SequencedMessageNeedsAProgressive`] if a
181 /// [`Message::fire_and_forget`] message was placed in a sequence. A
182 /// sequence orders messages *by their progressive*, and a
183 /// fire-and-forget message has none: `LS_msg_prog` is "mandatory
184 /// whenever `LS_sequence` is specified"
185 /// [`docs/spec/03-requests.md` §12.1]. The two are not a combination the
186 /// protocol leaves open.
187 /// - [`ConfigError::MaxWaitNeedsASequence`] if
188 /// [`Message::with_max_wait`] was used on a message in no sequence,
189 /// where it names how long to wait for messages that do not exist. The
190 /// server ignores it there [`docs/spec/03-requests.md` §12.1]; this
191 /// crate refuses it, so that a caller who set a bound is never silently
192 /// running without one.
193 ///
194 /// # Examples
195 ///
196 /// ```
197 /// use lightstreamer_rs::{ConfigError, Message, SequenceName};
198 ///
199 /// let impossible = Message::fire_and_forget("BUY 100")
200 /// .in_sequence(SequenceName::try_new("ORDERS")?);
201 ///
202 /// assert!(matches!(
203 /// impossible.validate(),
204 /// Err(ConfigError::SequencedMessageNeedsAProgressive)
205 /// ));
206 /// # Ok::<(), ConfigError>(())
207 /// ```
208 pub fn validate(&self) -> Result<(), ConfigError> {
209 let sequenced = self.inner.sequence.is_some();
210 if sequenced && self.inner.msg_prog.is_none() {
211 return Err(ConfigError::SequencedMessageNeedsAProgressive);
212 }
213 if self.inner.max_wait_millis.is_some() && !sequenced {
214 return Err(ConfigError::MaxWaitNeedsASequence);
215 }
216 Ok(())
217 }
218
219 /// Hands the payload to the session layer.
220 pub(crate) fn into_outgoing(self) -> OutgoingMessage {
221 self.inner
222 }
223}
224
225/// What became of a message you sent.
226///
227/// A message can fail in three different places and this crate keeps them
228/// apart, because the right response differs: a message that never left this
229/// client can be retried immediately, one the *server* refused will be refused
230/// again, and one the *Metadata Adapter* rejected is an application-level
231/// answer.
232#[derive(Debug, Clone, PartialEq, Eq)]
233#[non_exhaustive]
234pub enum MessageResult {
235 /// The Metadata Adapter processed the message without raising
236 /// [`docs/spec/04-notifications.md` §4.1].
237 Delivered {
238 /// The adapter's own response text, empty when it supplied none. Its
239 /// meaning is entirely the application's.
240 response: String,
241 },
242
243 /// The message reached the server but was not delivered, or the Metadata
244 /// Adapter failed while processing it
245 /// [`docs/spec/04-notifications.md` §4.2].
246 ///
247 /// Appendix B codes `32`, `33`, `38` and `39` concern the sequence
248 /// numbering specifically — a progressive already used, or one skipped
249 /// because the server stopped waiting for it
250 /// [`docs/spec/05-error-codes.md` §2].
251 Failed(ServerError),
252
253 /// The server refused the request outright, so no delivery outcome will
254 /// ever follow [`docs/spec/03-requests.md` §13.2].
255 Refused(ServerError),
256
257 /// The message never reached the server.
258 ///
259 /// **Messages are not buffered and not resent.** A subscription is a
260 /// statement of desired state and is safely re-created on whatever session
261 /// comes next; a message is a one-shot side effect whose ordering and
262 /// deduplication the server tracks *per session*. Replaying one across a
263 /// session replacement would restart the numbering the server
264 /// deduplicates against, so this crate fails the send rather than promise
265 /// an ordering it cannot keep. Whether to retry is yours to decide,
266 /// because you are the only one who can decide it correctly.
267 NotSent {
268 /// What stopped it. Never contains a credential.
269 reason: String,
270 },
271}
272
273impl From<WireResult> for MessageResult {
274 fn from(result: WireResult) -> Self {
275 match result {
276 WireResult::Done { response } => Self::Delivered { response },
277 WireResult::Failed { cause } => Self::Failed(cause.into()),
278 WireResult::Refused { cause } => Self::Refused(cause.into()),
279 WireResult::NotSent { reason } => Self::NotSent { reason },
280 }
281 }
282}
283
284/// The report of one message's fate.
285#[derive(Debug, Clone, PartialEq, Eq)]
286#[non_exhaustive]
287pub struct MessageOutcome {
288 /// The sequence the message was sent in, or `None` when it was sent in
289 /// none.
290 pub sequence: Option<String>,
291 /// The progressive that identifies the message, or `None` for a
292 /// fire-and-forget one — which can only ever be reported as
293 /// [`MessageResult::NotSent`] or [`MessageResult::Refused`].
294 pub progressive: Option<u64>,
295 /// What became of it.
296 pub result: MessageResult,
297}
298
299impl MessageOutcome {
300 /// Builds the public report from the session layer's.
301 pub(crate) fn new(
302 sequence: MessageSequence,
303 progressive: Option<u64>,
304 result: WireResult,
305 ) -> Self {
306 Self {
307 sequence: match sequence {
308 MessageSequence::Named(name) => Some(name),
309 MessageSequence::Unspecified => None,
310 },
311 progressive,
312 result: result.into(),
313 }
314 }
315
316 /// Whether the Metadata Adapter processed the message.
317 #[must_use]
318 #[inline]
319 pub const fn is_delivered(&self) -> bool {
320 matches!(self.result, MessageResult::Delivered { .. })
321 }
322
323 /// Assembles one field by field, for [`crate::test_util`].
324 #[cfg(feature = "test-util")]
325 #[must_use]
326 pub(crate) const fn from_parts(
327 sequence: Option<String>,
328 progressive: Option<u64>,
329 result: MessageResult,
330 ) -> Self {
331 Self {
332 sequence,
333 progressive,
334 result,
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::session::ServerCause;
343
344 #[test]
345 fn test_sequence_name_accepts_an_alphanumeric_identifier() {
346 assert!(matches!(
347 SequenceName::try_new("ORDERS_1"),
348 Ok(name) if name.as_str() == "ORDERS_1"
349 ));
350 }
351
352 #[test]
353 fn test_sequence_name_rejects_the_reserved_identifier() {
354 assert!(matches!(
355 SequenceName::try_new("UNORDERED_MESSAGES"),
356 Err(ConfigError::InvalidSequenceName { name }) if name == "UNORDERED_MESSAGES"
357 ));
358 }
359
360 #[test]
361 fn test_sequence_name_rejects_an_empty_or_punctuated_name() {
362 assert!(matches!(
363 SequenceName::try_new(""),
364 Err(ConfigError::InvalidSequenceName { .. })
365 ));
366 assert!(matches!(
367 SequenceName::try_new("with space"),
368 Err(ConfigError::InvalidSequenceName { .. })
369 ));
370 assert!(matches!(
371 SequenceName::try_new("dash-ed"),
372 Err(ConfigError::InvalidSequenceName { .. })
373 ));
374 }
375
376 #[test]
377 fn test_fire_and_forget_carries_no_progressive() {
378 let message = Message::fire_and_forget("hello");
379 assert_eq!(message.text(), "hello");
380 assert_eq!(message.into_outgoing().msg_prog, None);
381 }
382
383 #[test]
384 fn test_a_numbered_message_keeps_the_callers_number() {
385 let outgoing = Message::new("hello", NonZeroU32::MIN).into_outgoing();
386 assert_eq!(outgoing.msg_prog, Some(NonZeroU32::MIN));
387 }
388
389 #[test]
390 fn test_max_wait_rejects_a_duration_that_does_not_fit_milliseconds() {
391 assert!(matches!(
392 Message::fire_and_forget("x").with_max_wait(Duration::MAX),
393 Err(ConfigError::DurationTooLarge { field: "max_wait" })
394 ));
395 }
396
397 // -----------------------------------------------------------------------
398 // A-03: an impossible message is refused before it is sent
399 // -----------------------------------------------------------------------
400
401 fn sequence() -> SequenceName {
402 match SequenceName::try_new("ORDERS") {
403 Ok(name) => name,
404 Err(error) => unreachable!("the fixture sequence name is valid: {error}"),
405 }
406 }
407
408 #[test]
409 fn test_a_fire_and_forget_message_cannot_be_sequenced() {
410 // A sequence orders messages by a progressive this one does not have
411 // [`docs/spec/03-requests.md` §12.1].
412 assert!(matches!(
413 Message::fire_and_forget("BUY 100")
414 .in_sequence(sequence())
415 .validate(),
416 Err(ConfigError::SequencedMessageNeedsAProgressive)
417 ));
418 }
419
420 #[test]
421 fn test_a_max_wait_needs_a_sequence() {
422 let built = Message::new("BUY 100", NonZeroU32::MIN).with_max_wait(Duration::from_secs(1));
423 match built {
424 Ok(message) => assert!(matches!(
425 message.validate(),
426 Err(ConfigError::MaxWaitNeedsASequence)
427 )),
428 Err(error) => panic!("a one-second wait is expressible: {error}"),
429 }
430 }
431
432 #[test]
433 fn test_a_sequenced_numbered_message_with_a_wait_is_valid() {
434 let built = Message::new("BUY 100", NonZeroU32::MIN)
435 .in_sequence(sequence())
436 .with_max_wait(Duration::from_secs(1));
437 match built {
438 Ok(message) => assert!(message.validate().is_ok()),
439 Err(error) => panic!("a one-second wait is expressible: {error}"),
440 }
441 }
442
443 #[test]
444 fn test_the_plain_shapes_are_valid() {
445 assert!(Message::fire_and_forget("hello").validate().is_ok());
446 assert!(Message::new("hello", NonZeroU32::MIN).validate().is_ok());
447 assert!(
448 Message::new("hello", NonZeroU32::MIN)
449 .in_sequence(sequence())
450 .validate()
451 .is_ok()
452 );
453 }
454
455 #[test]
456 fn test_a_zero_max_wait_is_a_boundary_not_an_error() {
457 // Zero means "do not wait for the ones before it", which is a request
458 // the server can honour.
459 let built = Message::new("BUY 100", NonZeroU32::MIN)
460 .in_sequence(sequence())
461 .with_max_wait(Duration::ZERO);
462 match built {
463 Ok(message) => {
464 assert!(message.validate().is_ok());
465 assert_eq!(message.into_outgoing().max_wait_millis, Some(0));
466 }
467 Err(error) => panic!("zero is expressible in milliseconds: {error}"),
468 }
469 }
470
471 #[test]
472 fn test_outcome_distinguishes_where_a_message_failed() {
473 let delivered = MessageOutcome::new(
474 MessageSequence::Named("ORDERS".to_owned()),
475 Some(1),
476 WireResult::Done {
477 response: "ok".to_owned(),
478 },
479 );
480 assert!(delivered.is_delivered());
481 assert_eq!(delivered.sequence.as_deref(), Some("ORDERS"));
482
483 let refused = MessageOutcome::new(
484 MessageSequence::Unspecified,
485 None,
486 WireResult::Refused {
487 cause: ServerCause {
488 code: 65,
489 message: "Syntax error".to_owned(),
490 },
491 },
492 );
493 assert!(!refused.is_delivered());
494 assert_eq!(refused.sequence, None);
495 assert!(matches!(refused.result, MessageResult::Refused(cause) if cause.code() == 65));
496
497 let never_sent = MessageOutcome::new(
498 MessageSequence::Unspecified,
499 None,
500 WireResult::NotSent {
501 reason: "no session".to_owned(),
502 },
503 );
504 assert!(matches!(never_sent.result, MessageResult::NotSent { .. }));
505 }
506}