1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! A Rust client for **TLCP**, the Text Lightstreamer Client Protocol.
//!
//! Lightstreamer is a server that pushes real-time data to clients over an
//! ordinary web connection. TLCP is the text protocol it speaks. This crate
//! implements TLCP 2.5.0 and nothing else: it opens a session, keeps it alive
//! through the network's bad behaviour, delivers updates as a typed stream,
//! and gets out of the way. There is no market-data model here, no cache, no
//! reconnect-and-hope layer — just the protocol, done properly.
//!
//! # A complete program
//!
//! This connects to the public demo server the specification's own transcripts
//! use, subscribes to two items, and prints what changes.
//!
//! ```no_run
//! use futures_util::StreamExt;
//! use lightstreamer_rs::{
//! AdapterSet, Client, ClientConfig, FieldSchema, ItemGroup, ServerAddress, Snapshot,
//! Subscription, SubscriptionEvent, SubscriptionMode,
//! };
//!
//! #[tokio::main]
//! async fn main() -> lightstreamer_rs::Result<()> {
//! let config = ClientConfig::builder(ServerAddress::try_new("https://push.lightstreamer.com")?)
//! .with_adapter_set(AdapterSet::try_new("DEMO")?)
//! .build()?;
//!
//! // This example is only interested in the data. Opting out of the
//! // session events is a `drop`, not an underscore: naming a receiver
//! // `_session_events` would *keep* it, and a stream that is held but
//! // never read stalls the client once it fills. See `SessionEvents`.
//! let (client, session_events) = Client::connect(config).await?;
//! drop(session_events);
//!
//! let mut updates = client
//! .subscribe(
//! Subscription::new(
//! SubscriptionMode::Merge,
//! ItemGroup::from_items(["item1", "item2"])?,
//! FieldSchema::from_fields(["last_price", "time", "pct_change"])?,
//! )
//! .with_snapshot(Snapshot::On),
//! )
//! .await?;
//!
//! while let Some(event) = updates.next().await {
//! match event {
//! SubscriptionEvent::Update(update) => {
//! println!("{}: {:?}", update.item_name(), update.changed_fields());
//! }
//! SubscriptionEvent::Rejected(error) => {
//! eprintln!("the server refused the subscription: {error}");
//! break;
//! }
//! _ => {}
//! }
//! }
//!
//! client.disconnect().await
//! }
//! ```
//!
//! # The three things worth knowing before you start
//!
//! **Delivery is a `Stream`, not a callback.** Subscribing gives you an
//! [`Updates`] stream; the client gives you a [`SessionEvents`] stream. Both
//! are ordinary [`futures_util::Stream`]s carrying closed, matchable enums, so
//! the borrow checker never lands in the middle of your update handler
//! (`docs/adr/0003-typed-event-stream-as-delivery-surface.md`).
//!
//! **Reconnection is automatic; its consequences are not hidden.** When the
//! connection breaks, this crate reconnects. What it will not do is pretend
//! nothing happened: [`SessionEvent::Connected`] carries a [`Continuity`] that
//! says whether the *same* session came back — in which case whatever you
//! computed from it is still valid — or whether a **new** one replaced it, in
//! which case it is not
//! (`docs/adr/0005-recovery-is-visible-in-the-event-stream.md`). An
//! application holding an order book or a portfolio needs that distinction,
//! and TLCP is one of the few protocols that actually provides it.
//!
//! **Streams are bounded and lossless.** Every stream this crate hands you has
//! a fixed capacity and, when it fills, the client blocks rather than
//! discarding anything — so a consumer that stops reading stalls the client
//! instead of silently losing data. The full reasoning, and what to do if you
//! genuinely cannot keep up, is on [`Updates`].
//!
//! # Two things that catch people out
//!
//! **Null and empty are different values.** TLCP distinguishes a field with no
//! value from a field holding the empty string, and so does
//! [`FieldValue`]: `Null` and `Text("")` are not equal and do not mean the
//! same thing. A quote adapter with no bid to report sends the first; one
//! whose status line is deliberately blank sends the second. Collapsing them
//! is a decision you make explicitly, with
//! [`FieldValue::text`] or [`FieldValue::text_or`] — this crate will not make
//! it for you. Note also the two levels of absence:
//! [`ItemUpdate::field`] returns `None` when the schema has no such field at
//! all, and `Some(FieldValue::Null)` when the field exists and is null.
//!
//! **Item and field names come from you, not from the server.** TLCP transmits
//! counts, never names [`docs/spec/04-notifications.md` §3.1]. So
//! [`ItemUpdate::item_name`] can only report a name you supplied — which you
//! do by spelling out the list with [`ItemGroup::from_items`] and
//! [`FieldSchema::from_fields`]. If you subscribe by naming a server-side
//! group with [`ItemGroup::try_new`] instead, the server resolves it and this
//! crate never learns what the items are called, so names fall back to the
//! 1-based position rendered in decimal. That stand-in is deliberately not a
//! lookup key: [`ItemUpdate::field_by_name`] with `"3"` returns `None` rather
//! than the third field, so a placeholder can never be mistaken for a real
//! name. Use [`ItemUpdate::declared_item_name`] when you need to know which
//! you got.
//!
//! # Shutdown
//!
//! Dropping the [`Client`] stops everything: the background tasks, the socket,
//! and every stream still outstanding. Dropping an [`Updates`] stream
//! unsubscribes. Nothing needs to be deregistered and nothing needs
//! `std::process::exit`.
//!
//! # What is deliberately not here
//!
//! The transports, the wire parser and the session state machine are private.
//! They are the crate's job, not yours, and keeping them private is what lets
//! them change without breaking you. What [`lib.rs`](self) re-exports is the
//! whole public surface and the whole semantic-versioning promise.
//!
//! # Testing your own code against this crate
//!
//! An [`ItemUpdate`] cannot be constructed from outside, because forging one
//! that a session never produced is not something an application should be
//! able to do by accident. Your own parsing layer still deserves unit tests,
//! so the off-by-default `test-util` feature adds one thing and nothing else:
//! `test_util::ItemUpdateBuilder`, which assembles an update with no session
//! behind it. Enable it under `[dev-dependencies]` and it never reaches your
//! release build.
//!
//! # Errors
//!
//! Everything fallible returns [`Result`], and [`Error`] has one variant per
//! condition worth reacting to. Codes the server supplied are preserved as
//! numbers in [`ServerError`], never flattened into a message — and which of
//! the protocol's two code catalogs a number belongs to is decided by the
//! variant carrying it, because the two overlap. See [`error`].
//!
//! # Provenance
//!
//! Version 1.0.0 onwards is an independently authored implementation licensed
//! under MIT. Versions up to and including 0.3.3 were GPL-3.0-only and share
//! no code with this one; they remain published, under that licence, as
//! historical versions.
//!
//! Every wire behaviour in this crate is derived from the official
//! specification and cites the chapter of `docs/spec/` it comes from. See
//! `docs/adr/0001-mit-relicensing-clean-room.md`.
/// What every redacting [`Debug`] renders in place of the value it is hiding.
///
/// One spelling, in one place, so that a test can look for it and so that no
/// type invents its own. The policy it stands for is narrow and absolute:
/// a value that authenticates or identifies — a password, a user name, a
/// session identifier — crosses this crate's **API** boundary, where the
/// caller asked for it, and never its **logging** boundary, where nobody did.
pub const REDACTED: &str = "<redacted>";
// --- The client -------------------------------------------------------------
pub use Client;
// --- Configuration ----------------------------------------------------------
pub use ;
// --- Describing a subscription ----------------------------------------------
pub use ;
// --- Consuming a subscription -----------------------------------------------
pub use ;
pub use ;
// --- Sending a message ------------------------------------------------------
pub use ;
// --- Consuming the session --------------------------------------------------
pub use ;
// --- Errors -----------------------------------------------------------------
pub use ;