Skip to main content

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}