Skip to main content

lightstreamer_rs/client/
updates.rs

1//! What one subscription tells you, and the stream that delivers it.
2
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6use futures_util::Stream;
7use tokio::sync::mpsc;
8
9use crate::client::events::SubscriptionId;
10use crate::client::router::RouterCommand;
11use crate::error::ServerError;
12use crate::session::{
13    CommandFields as WireCommandFields, FilteringMode, MaxFrequency as WireFrequency,
14    SubscriptionEvent as Wire,
15};
16use crate::subscription::item_update::ItemUpdate;
17
18/// Where the key and the command sit in a `COMMAND`-mode field schema
19/// [`docs/spec/04-notifications.md` §3.2].
20///
21/// Both positions are **1-based**, matching the numbering
22/// [`ItemUpdate`] uses everywhere.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24#[non_exhaustive]
25pub struct CommandFields {
26    /// Position of the field that identifies the row.
27    pub key: usize,
28    /// Position of the field that says what happened to the row — `ADD`,
29    /// `UPDATE` or `DELETE`.
30    pub command: usize,
31}
32
33#[cfg(feature = "test-util")]
34impl CommandFields {
35    /// Assembles one field by field, for [`crate::test_util`].
36    #[must_use]
37    pub(crate) const fn from_parts(key: usize, command: usize) -> Self {
38        Self { key, command }
39    }
40}
41
42impl From<WireCommandFields> for CommandFields {
43    fn from(fields: WireCommandFields) -> Self {
44        Self {
45            key: fields.key,
46            command: fields.command,
47        }
48    }
49}
50
51/// The update rate the server has settled on for a subscription
52/// [`docs/spec/04-notifications.md` §3.8].
53#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
54#[non_exhaustive]
55pub enum UpdateFrequency {
56    /// No limit is applied.
57    Unlimited,
58    /// At most this many updates per second, per item.
59    Limited {
60        /// The limit exactly as the server wrote it. Kept as text because the
61        /// protocol carries it as text and this crate never reinterprets a
62        /// server-supplied number on the caller's behalf.
63        updates_per_second: String,
64    },
65    /// **The rate could not be read**, so do not treat it as a limit.
66    ///
67    /// The server sent something that is neither `unlimited` nor a decimal
68    /// number [`docs/spec/04-notifications.md` §3.8]. It is reported rather
69    /// than dropped, and reported apart from
70    /// [`Limited`](UpdateFrequency::Limited) rather than inside it, because a
71    /// caller that reads `Limited { updates_per_second: "" }` and divides by it
72    /// is acting on a number nobody sent. Log it, or treat the subscription's
73    /// rate as unknown — but do not compute with it.
74    Unrecognized {
75        /// The value as it arrived.
76        literal: String,
77    },
78}
79
80impl From<WireFrequency> for UpdateFrequency {
81    fn from(frequency: WireFrequency) -> Self {
82        match frequency {
83            WireFrequency::Unlimited => Self::Unlimited,
84            WireFrequency::Limited { updates_per_second } => Self::Limited { updates_per_second },
85            WireFrequency::Unrecognized { literal } => Self::Unrecognized { literal },
86        }
87    }
88}
89
90/// Whether the server may drop or merge updates for a subscription to keep up
91/// [`docs/spec/04-notifications.md` §3.8].
92#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93#[non_exhaustive]
94pub enum Filtering {
95    /// The server may filter: it merges or discards updates rather than fall
96    /// behind, and you see fewer events than the adapter produced.
97    Filtered,
98    /// The server may not filter: every update is delivered. If it cannot keep
99    /// up it says so with a [`SubscriptionEvent::Overflow`] rather than
100    /// dropping silently.
101    Unfiltered,
102    /// The server named a mode this version does not know.
103    ///
104    /// Preserved rather than rejected, so a newer server cannot break an older
105    /// client over one word of an otherwise well-formed line.
106    Unrecognized {
107        /// The word as it arrived.
108        literal: String,
109    },
110}
111
112impl From<FilteringMode> for Filtering {
113    fn from(mode: FilteringMode) -> Self {
114        match mode {
115            FilteringMode::Filtered => Self::Filtered,
116            FilteringMode::Unfiltered => Self::Unfiltered,
117            FilteringMode::Unrecognized { literal } => Self::Unrecognized { literal },
118        }
119    }
120}
121
122/// Everything one subscription can tell you.
123///
124/// Delivered by [`Updates`]. `#[non_exhaustive]`, so match with a trailing `_`
125/// arm and a later version adding a variant will not break your build.
126///
127/// The ordinary shape of a subscription's life is
128/// [`Activated`](SubscriptionEvent::Activated), then a run of
129/// [`Update`](SubscriptionEvent::Update)s — snapshot first if one was asked
130/// for, real-time after — and finally either
131/// [`Unsubscribed`](SubscriptionEvent::Unsubscribed) or
132/// [`Rejected`](SubscriptionEvent::Rejected). After a session is replaced, a
133/// second `Activated` arrives and the run starts over.
134#[derive(Debug, Clone, PartialEq, Eq)]
135#[non_exhaustive]
136pub enum SubscriptionEvent {
137    /// The subscription is live on the server and its shape is now known
138    /// [`docs/spec/04-notifications.md` §3.1, §3.2].
139    ///
140    /// Arrives again whenever the subscription is re-created on a session that
141    /// replaced a lost one, in which case every item's state has been rebuilt
142    /// from nothing.
143    Activated {
144        /// How many items the server resolved the item group into.
145        item_count: usize,
146        /// How many fields the server resolved the field schema into. Every
147        /// update carries exactly this many values.
148        field_count: usize,
149        /// Present exactly for [`SubscriptionMode::Command`](crate::SubscriptionMode)
150        /// subscriptions, which the server activates differently because it
151        /// must also say where the key and command fields are.
152        command_fields: Option<CommandFields>,
153    },
154
155    /// New values for one item [`docs/spec/04-notifications.md` §2].
156    ///
157    /// The update carries the item's **complete** state, not only what
158    /// changed, so every field is readable; ask
159    /// [`ItemUpdate::changed_fields`] what actually moved.
160    Update(Box<ItemUpdate>),
161
162    /// The snapshot of one item is complete; everything after this is a live
163    /// change [`docs/spec/04-notifications.md` §3.5].
164    ///
165    /// Sent in `DISTINCT` and `COMMAND` modes. A `MERGE` snapshot is a single
166    /// update and gets no boundary marker; `RAW` has no snapshot at all.
167    EndOfSnapshot {
168        /// **1-based** index of the item whose snapshot ended.
169        item_index: usize,
170    },
171
172    /// The server cleared one item's snapshot
173    /// [`docs/spec/04-notifications.md` §3.6].
174    ///
175    /// Drop the list, history or row set you accumulated for this item. The
176    /// field values this crate tracks are deliberately **not** cleared, so
177    /// that a delta arriving straight afterwards still decodes.
178    SnapshotCleared {
179        /// **1-based** index of the item whose snapshot was cleared.
180        item_index: usize,
181    },
182
183    /// The **server** dropped updates for one item, because of its own buffer
184    /// limits [`docs/spec/04-notifications.md` §3.7].
185    ///
186    /// Those updates are gone; nothing can reconstruct them, and the item's
187    /// values now reflect only what survived. This is the protocol's own way
188    /// of surfacing loss rather than hiding it, and this crate passes it
189    /// straight through.
190    ///
191    /// It can only happen in `RAW` mode, for `ADD`/`DELETE` events in
192    /// `COMMAND` mode, and for subscriptions that asked for
193    /// [`MaxFrequency::Unfiltered`](crate::MaxFrequency).
194    Overflow {
195        /// **1-based** index of the item whose updates were dropped.
196        item_index: usize,
197        /// How many update events were dropped.
198        dropped_count: u64,
199    },
200
201    /// The subscription's frequency configuration
202    /// [`docs/spec/04-notifications.md` §3.8].
203    ///
204    /// Arrives once when the subscription starts — possibly before
205    /// [`Activated`](SubscriptionEvent::Activated) — and again after every
206    /// reconfiguration, even one that changed nothing.
207    Reconfigured {
208        /// The rate now in force.
209        max_frequency: UpdateFrequency,
210        /// Whether the server may filter.
211        filtering: Filtering,
212    },
213
214    /// The subscription request could not be sent, and will be retried when
215    /// the session next binds.
216    ///
217    /// **Not terminal, and not a refusal.** The server never saw the request —
218    /// the connection was gone, or the transport rejected it — so it has no
219    /// opinion on your subscription yet. This crate keeps the subscription in
220    /// its desired set and re-issues it on the next connection, after which
221    /// [`Activated`](SubscriptionEvent::Activated) arrives as usual.
222    ///
223    /// You do not have to do anything. It is surfaced because it means a real
224    /// delay before data starts, and a subscription that seems slow to start
225    /// should not be a mystery.
226    Deferred {
227        /// What stopped it. Diagnostic text; never a credential.
228        reason: String,
229    },
230
231    /// The server refused the subscription, with a code from **Appendix B**
232    /// [`docs/spec/05-error-codes.md` §2].
233    ///
234    /// Terminal: nothing else arrives on this stream, and the subscription is
235    /// not retried on a later session. Common causes are a bad item group
236    /// (code 21), a bad field schema (23), a mode the item does not allow
237    /// (24), and — in `COMMAND` mode — a schema with no key or no command
238    /// field (15, 16).
239    ///
240    /// Contrast [`Deferred`](SubscriptionEvent::Deferred), where the request
241    /// never reached the server and is still coming.
242    Rejected(ServerError),
243
244    /// The subscription ended [`docs/spec/04-notifications.md` §3.4].
245    ///
246    /// Terminal: no further update for these items and fields will be sent.
247    Unsubscribed,
248
249    /// A notification for this subscription could not be interpreted.
250    ///
251    /// Never fatal and never silent: the stream keeps running, and the item
252    /// state it refers to is whatever the last decodable update left. Seeing
253    /// this repeatedly means this client and the server disagree about the
254    /// subscription's shape, which is worth reporting as a bug.
255    Undecodable {
256        /// What went wrong, in terms a bug report can use.
257        detail: String,
258    },
259}
260
261impl SubscriptionEvent {
262    /// Whether nothing further will arrive on this subscription's stream.
263    #[must_use]
264    #[inline]
265    pub const fn is_terminal(&self) -> bool {
266        matches!(self, Self::Unsubscribed | Self::Rejected(_))
267    }
268
269    /// Translates the subscription layer's event into the public one.
270    pub(crate) fn from_wire(event: Wire) -> Self {
271        match event {
272            Wire::Activated {
273                item_count,
274                field_count,
275                command_fields,
276            } => Self::Activated {
277                item_count,
278                field_count,
279                command_fields: command_fields.map(Into::into),
280            },
281            Wire::Update(update) => Self::Update(Box::new(update)),
282            Wire::EndOfSnapshot { item_index } => Self::EndOfSnapshot { item_index },
283            Wire::SnapshotCleared { item_index } => Self::SnapshotCleared { item_index },
284            Wire::Overflow {
285                item_index,
286                dropped_count,
287            } => Self::Overflow {
288                item_index,
289                dropped_count,
290            },
291            Wire::Reconfigured {
292                max_frequency,
293                filtering,
294            } => Self::Reconfigured {
295                max_frequency: max_frequency.into(),
296                filtering: filtering.into(),
297            },
298            Wire::Unsubscribed => Self::Unsubscribed,
299        }
300    }
301}
302
303/// The stream of one subscription's events.
304///
305/// Returned by [`Client::subscribe`](crate::Client::subscribe). It implements
306/// [`futures_util::Stream`], so the usual combinators work and the plain shape
307/// is a `while let` loop:
308///
309/// ```no_run
310/// use futures_util::StreamExt;
311/// use lightstreamer_rs::SubscriptionEvent;
312///
313/// # async fn run(mut updates: lightstreamer_rs::Updates) {
314/// while let Some(event) = updates.next().await {
315///     if let SubscriptionEvent::Update(update) = event {
316///         println!("{}: {:?}", update.item_name(), update.changed_fields());
317///     }
318/// }
319/// # }
320/// ```
321///
322/// # Dropping this stream unsubscribes
323///
324/// There is no deregistration dance: drop the `Updates` and the client sends
325/// the unsubscription for you. The request goes out asynchronously — the drop
326/// itself does not block and cannot fail — so a `UNSUB` may still be in flight
327/// when the value is gone. If you need to observe the unsubscription
328/// completing, call [`Client::unsubscribe`](crate::Client::unsubscribe)
329/// instead and keep reading until [`SubscriptionEvent::Unsubscribed`].
330///
331/// # Backpressure: bounded, and nothing is dropped
332///
333/// The stream is fed by a **bounded** channel whose capacity is
334/// [`ConnectionOptions::with_update_capacity`](crate::ConnectionOptions::with_update_capacity)
335/// — 1024 events by default. When it fills, the client **blocks** rather than
336/// discarding anything, and that backpressure propagates through the session
337/// down to the socket. Concretely, a consumer that stops polling this stream
338/// will, once the buffer fills, stall **the whole client**: other
339/// subscriptions and the session's own liveness included.
340///
341/// That is a deliberate choice, and the alternatives are worse. Dropping
342/// updates locally would corrupt the notification count that makes recovery
343/// after a broken connection correct
344/// [`docs/spec/02-session-lifecycle.md` §5.2], and it would hide loss the
345/// protocol goes out of its way to report — TLCP has its own overflow
346/// notification precisely so that a client is *told* when data was discarded
347/// [`docs/spec/04-notifications.md` §3.7].
348///
349/// # What a long stall actually does
350///
351/// "Stalls the client" is literal, and it has a consequence worth planning
352/// for. While the client is blocked on a full stream it is not reading from
353/// the socket either, so its **liveness timers do not advance**. If you stop
354/// consuming for longer than the keepalive interval the server negotiated —
355/// reported to you as [`Connected::keepalive`](crate::Connected::keepalive),
356/// plus [`ConnectionOptions::with_keepalive_slack`](crate::ConnectionOptions::with_keepalive_slack)
357/// — then the moment you start reading again the client will conclude the
358/// connection went silent, tear it down, and recover
359/// [`docs/spec/02-session-lifecycle.md` §8.1]. You will see a
360/// [`SessionEvent::Disconnected`](crate::SessionEvent::Disconnected) carrying
361/// [`DisconnectReason::Stalled`](crate::DisconnectReason::Stalled), followed
362/// by a reconnection.
363///
364/// Nothing is lost when that happens — recovery is what the notification count
365/// exists for — but it is real work, and it is avoidable. A consumer that may
366/// pause for seconds at a time should either buffer on its own side or, better,
367/// ask the server to send less.
368///
369/// If you cannot keep up, say so to the server, which is where the protocol
370/// puts that decision: cap the rate with
371/// [`Subscription::with_max_frequency`](crate::Subscription::with_max_frequency),
372/// or bound what the server buffers with
373/// [`Subscription::with_buffer_size`](crate::Subscription::with_buffer_size).
374/// The server will then merge or drop on its side, and tell you it did with a
375/// [`SubscriptionEvent::Overflow`].
376///
377/// # The one exception: shutdown
378///
379/// "Nothing is dropped" holds **while the client is running**. It does not
380/// survive an ordered shutdown, and it deliberately does not:
381/// [`Client::disconnect`](crate::Client::disconnect) and dropping the
382/// [`Client`](crate::Client) are signalled out of band, every blocked delivery
383/// gives way to them, and whatever had not yet been delivered is discarded
384/// along with this stream.
385///
386/// Without that exception a consumer that walked away could make the client
387/// impossible to stop — no disconnect, no drop, and a task and a socket alive
388/// until the process ended. Nothing observable is lost by it: a caller still
389/// reading frees room and the delivery completes, and a caller that has asked
390/// to disconnect has said it wants no more. See
391/// `docs/adr/0003-typed-event-stream-as-delivery-surface.md`.
392#[derive(Debug)]
393pub struct Updates {
394    id: SubscriptionId,
395    events: mpsc::Receiver<SubscriptionEvent>,
396    /// Unbounded on purpose: [`Drop`] cannot await, and this channel carries
397    /// one small message per subscribe and unsubscribe — user actions, not
398    /// data volume — so it cannot grow with the update flow.
399    control: mpsc::UnboundedSender<RouterCommand>,
400}
401
402impl Updates {
403    /// Wraps the receiving half the router just registered.
404    pub(crate) const fn new(
405        id: SubscriptionId,
406        events: mpsc::Receiver<SubscriptionEvent>,
407        control: mpsc::UnboundedSender<RouterCommand>,
408    ) -> Self {
409        Self {
410            id,
411            events,
412            control,
413        }
414    }
415
416    /// The handle identifying this subscription.
417    ///
418    /// Stable for the life of the stream, including across a session that was
419    /// lost and replaced. Use it to correlate with
420    /// [`SessionEvent::Resubscribed`](crate::SessionEvent::Resubscribed).
421    #[must_use]
422    #[inline]
423    pub const fn id(&self) -> SubscriptionId {
424        self.id
425    }
426}
427
428impl Stream for Updates {
429    type Item = SubscriptionEvent;
430
431    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
432        self.events.poll_recv(cx)
433    }
434}
435
436impl Drop for Updates {
437    /// Unsubscribes.
438    ///
439    /// Best effort by necessity: a `Drop` cannot await and cannot report a
440    /// failure. The command channel is unbounded, so the send only fails when
441    /// the client has already stopped — in which case the subscription is gone
442    /// anyway.
443    fn drop(&mut self) {
444        if self
445            .control
446            .send(RouterCommand::StreamDropped { id: self.id })
447            .is_err()
448        {
449            tracing::debug!(
450                id = self.id.get(),
451                "subscription stream dropped after the client"
452            );
453        }
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    #[test]
462    fn test_events_that_end_a_subscription_are_marked_terminal() {
463        assert!(SubscriptionEvent::Unsubscribed.is_terminal());
464        assert!(SubscriptionEvent::Rejected(ServerError::new(19, "not found")).is_terminal());
465        assert!(
466            !SubscriptionEvent::EndOfSnapshot { item_index: 1 }.is_terminal(),
467            "end of snapshot is a boundary, not an ending"
468        );
469        assert!(
470            !SubscriptionEvent::Overflow {
471                item_index: 1,
472                dropped_count: 4
473            }
474            .is_terminal()
475        );
476    }
477
478    #[test]
479    fn test_wire_events_translate_without_losing_anything() {
480        let activated = SubscriptionEvent::from_wire(Wire::Activated {
481            item_count: 3,
482            field_count: 4,
483            command_fields: Some(WireCommandFields { key: 1, command: 2 }),
484        });
485        assert!(matches!(
486            activated,
487            SubscriptionEvent::Activated {
488                item_count: 3,
489                field_count: 4,
490                command_fields: Some(CommandFields { key: 1, command: 2 })
491            }
492        ));
493
494        let overflow = SubscriptionEvent::from_wire(Wire::Overflow {
495            item_index: 2,
496            dropped_count: 17,
497        });
498        assert!(matches!(
499            overflow,
500            SubscriptionEvent::Overflow {
501                item_index: 2,
502                dropped_count: 17
503            }
504        ));
505    }
506
507    #[test]
508    fn test_reconfiguration_keeps_the_servers_own_text() {
509        let event = SubscriptionEvent::from_wire(Wire::Reconfigured {
510            max_frequency: WireFrequency::Limited {
511                updates_per_second: "3.0".to_owned(),
512            },
513            filtering: FilteringMode::Unrecognized {
514                literal: "novel".to_owned(),
515            },
516        });
517        assert!(matches!(
518            event,
519            SubscriptionEvent::Reconfigured {
520                max_frequency: UpdateFrequency::Limited { updates_per_second },
521                filtering: Filtering::Unrecognized { literal },
522            } if updates_per_second == "3.0" && literal == "novel"
523        ));
524    }
525
526    #[test]
527    fn test_an_unreadable_frequency_is_not_delivered_as_a_limit() {
528        // A `CONF` whose max-frequency argument is neither `unlimited` nor a
529        // decimal number [`docs/spec/04-notifications.md` §3.8] must not reach
530        // the caller inside `Limited`, where it would look like a rate.
531        let event = SubscriptionEvent::from_wire(Wire::Reconfigured {
532            max_frequency: WireFrequency::Unrecognized {
533                literal: "banana".to_owned(),
534            },
535            filtering: FilteringMode::Filtered,
536        });
537        assert!(matches!(
538            event,
539            SubscriptionEvent::Reconfigured {
540                max_frequency: UpdateFrequency::Unrecognized { literal },
541                filtering: Filtering::Filtered,
542            } if literal == "banana"
543        ));
544    }
545}