Skip to main content

laser_sdk/
stream.rs

1use crate::error::LaserError;
2use crate::laser::Laser;
3use std::collections::BTreeMap;
4
5pub use iggy::prelude::{HeaderKey, HeaderValue};
6
7/// The Apache Iggy producer/consumer escape hatch: same types
8/// [`Topic::iggy_producer`]/[`Topic::iggy_consumer`]/[`Topic::iggy_consumer_group`]
9/// return, and the stored message a failed publish hands back, re-exported so
10/// reaching for them doesn't need an `iggy` dependency of your own.
11pub use iggy::prelude::{
12    BackgroundConfig, BalancedSharding, DirectConfig, IggyConsumer, IggyConsumerBuilder,
13    IggyMessage, IggyProducer, IggyProducerBuilder, OrderedSharding,
14    SendMessagesConfirmationResponse, SendMessagesResponse, Sharding,
15};
16
17pub use laser_wire::codecs::{Cbor, Codec, Decoder, Json, Msgpack};
18pub use laser_wire::content::ContentType;
19pub use laser_wire::headers::{
20    CONTENT_TYPE, IDX_PREFIX, INLINE_PAYLOAD, LOGICAL_SCHEMA_FINGERPRINT, PROJECTION_REF, SCHEMA_ID,
21};
22pub use laser_wire::limits::MAX_INDEX_ENTRIES_PER_RECORD;
23
24mod consumer_group;
25pub(crate) mod producer_statistics;
26mod publish;
27mod record;
28pub(crate) mod transport;
29
30pub(crate) use consumer_group::GroupTarget;
31pub use consumer_group::{ConsumerGroup, ConsumerGroupInfo, CreateConsumerGroup, GroupFilter};
32pub use publish::{BatchPublishRequest, PublishRequest};
33pub use record::{Record, RecordBuilder};
34pub use transport::{
35    CommitPolicy, Consumer, ConsumerBuilder, ConsumerMessage, ConsumerStart, Headers, Producer,
36    ProducerBuilder, ProducerMessage, Routing, StoredOffset,
37};
38
39impl Laser {
40    /// The stream accessor: `name` is a real Apache Iggy stream, the topology
41    /// layer grouping topics. Free and synchronous, IO happens at the verbs.
42    /// One connection drives any number of streams:
43    /// `laser.stream("users").topic("events").publish(..)`.
44    pub fn stream(&self, name: impl Into<String>) -> Stream {
45        Stream {
46            laser: self.clone(),
47            name: name.into(),
48        }
49    }
50
51    /// The topic accessor against this client's default stream (set at connect
52    /// or with [`with_default_stream`](Self::with_default_stream)), the
53    /// one-word shortcut for the single-stream app. With no default configured
54    /// every verb on the handle returns the typed
55    /// [`NoStream`](LaserError::NoStream) error naming the fix. Cross-stream
56    /// work spells the stream it means: `laser.stream(name).topic(name)`.
57    pub fn topic(&self, name: impl Into<String>) -> Topic {
58        Topic {
59            laser: self.clone(),
60            stream: self.default_stream().map(str::to_owned),
61            name: name.into(),
62        }
63    }
64}
65
66/// One Apache Iggy stream: the topology handle. Build it with
67/// [`Laser::stream`], reach the data through [`topic`](Self::topic).
68#[derive(Clone)]
69pub struct Stream {
70    laser: Laser,
71    name: String,
72}
73
74impl Stream {
75    /// The topic accessor within this stream.
76    pub fn topic(&self, name: impl Into<String>) -> Topic {
77        Topic {
78            laser: self.laser.clone(),
79            stream: Some(self.name.clone()),
80            name: name.into(),
81        }
82    }
83
84    /// Idempotently create this stream.
85    pub async fn ensure(&self) -> Result<(), LaserError> {
86        crate::laser::ensure_stream(&self.laser.client(), &self.name).await
87    }
88
89    /// Delete this stream with every topic and message in it. Returns `false` when the stream did not exist. Cached producers and agent registry state for the stream are dropped.
90    pub async fn delete(&self) -> Result<bool, LaserError> {
91        crate::laser::delete_stream(&self.laser, &self.name).await
92    }
93
94    /// This stream's name.
95    pub fn name(&self) -> &str {
96        &self.name
97    }
98}
99
100/// One topic within a stream: the Log primitive's data handle. Build it with
101/// [`Stream::topic`] or, against the default stream, [`Laser::topic`]. Verbs
102/// resolve the producer through the client's shared cache, so handles are free
103/// to construct and clone.
104#[derive(Clone)]
105pub struct Topic {
106    laser: Laser,
107    stream: Option<String>,
108    name: String,
109}
110
111impl Topic {
112    /// Open the fluent publish builder (payload via `.payload()` / `.json()` /
113    /// `.record()`, then `.send()`). The raw payload path compiles to the same
114    /// bytes as the low-level send, nothing is linked unless a typed form is
115    /// used. Feature `streaming`.
116    pub fn publish(&self) -> PublishRequest<'_> {
117        match &self.stream {
118            Some(stream) => self.laser.publish_on(stream, &self.name),
119            None => self.laser.publish(&self.name),
120        }
121    }
122
123    /// Open the fluent record-batch builder (add records, then `.send()`), the
124    /// typed sibling of [`batch`](Self::batch). Feature `streaming`.
125    pub fn publish_batch(&self) -> BatchPublishRequest<'_> {
126        match &self.stream {
127            Some(stream) => self.laser.publish_batch_on(stream, &self.name),
128            None => self.laser.publish_batch(&self.name),
129        }
130    }
131
132    /// One raw message with explicit user headers, the zero-overhead path.
133    /// Keyed partitioning preserves per-key ordering, `None` lets the producer
134    /// balance across partitions.
135    pub async fn send(
136        &self,
137        payload: impl Into<Vec<u8>>,
138        headers: BTreeMap<HeaderKey, HeaderValue>,
139        partition_key: Option<&str>,
140    ) -> Result<SendMessagesResponse, LaserError> {
141        let stream = self.stream()?.to_owned();
142        self.laser
143            .send_with_headers_on(&stream, &self.name, payload.into(), headers, partition_key)
144            .await
145    }
146
147    /// One Iggy `send_messages` call covering many pre-built messages, all
148    /// sharing the same partitioning. An empty batch is a cheap no-op.
149    pub async fn batch(
150        &self,
151        messages: Vec<IggyMessage>,
152        partition_key: Option<&str>,
153    ) -> Result<SendMessagesResponse, LaserError> {
154        let stream = self.stream()?.to_owned();
155        self.laser
156            .send_batch_on(&stream, &self.name, messages, partition_key)
157            .await
158    }
159
160    /// A size-and-time batching publisher over this topic: `.batching()`,
161    /// tune `.max_records`/`.max_bytes`/`.linger`/`.partition_key`, then
162    /// `.build()`. Opt-in: the unbatched verbs compile to exactly the same
163    /// calls as before. Feature `agent` (the linger timer rides tokio).
164    #[cfg(feature = "agent")]
165    pub fn batching(&self) -> Result<crate::batching::BatchingProducerBuilder, LaserError> {
166        let stream = self.stream()?.to_owned();
167        Ok(crate::batching::BatchingProducerBuilder::new(
168            self.laser.clone(),
169            stream,
170            self.name.clone(),
171        ))
172    }
173
174    /// The positional replay handle over this topic: a resumable [`Cursor`]
175    /// reading from explicit offsets. The bounded-read rung below the reliable
176    /// consumer.
177    ///
178    /// [`Cursor`]: crate::cursor::Cursor
179    pub fn replay(&self) -> Result<crate::cursor::Cursor, LaserError> {
180        match &self.stream {
181            Some(stream) => self.laser.reader_on(stream, &self.name),
182            None => Err(LaserError::NoStream),
183        }
184    }
185
186    /// Idempotently create this topic with `partitions`, creating the stream
187    /// first if needed.
188    pub async fn ensure(&self, partitions: u32) -> Result<(), LaserError> {
189        let stream = self.stream()?.to_owned();
190        self.laser
191            .ensure_topic_on(&stream, &self.name, partitions)
192            .await
193    }
194
195    /// Idempotently create the consumer group `name` on this topic without
196    /// joining it or giving it a filter policy, so an operator can address it
197    /// before any member connects. [`ConsumerGroup::create`] configures a
198    /// policy in the same step.
199    pub async fn ensure_consumer_group(&self, name: &str) -> Result<(), LaserError> {
200        self.consumer_group(name).create().build().await.map(|_| ())
201    }
202
203    /// The Iggy producer builder for this topic, the substrate front door:
204    /// Iggy's own options (batching, partitioning, retries, encryption) with
205    /// nothing wrapped. Independent of the fluent path's cached producer.
206    pub fn iggy_producer(&self) -> Result<IggyProducerBuilder, LaserError> {
207        self.laser.iggy_producer(self.stream()?, &self.name)
208    }
209
210    /// The Iggy consumer builder named `name` over one `partition` of this
211    /// topic. The built `IggyConsumer` implements `futures::Stream`.
212    pub fn iggy_consumer(
213        &self,
214        name: &str,
215        partition: u32,
216    ) -> Result<IggyConsumerBuilder, LaserError> {
217        self.laser
218            .iggy_consumer(name, self.stream()?, &self.name, partition)
219    }
220
221    /// The Iggy consumer-group builder over this topic: the balanced group,
222    /// offsets committed under `group`.
223    pub fn iggy_consumer_group(&self, group: &str) -> Result<IggyConsumerBuilder, LaserError> {
224        self.laser
225            .iggy_consumer_group(group, self.stream()?, &self.name)
226    }
227
228    /// Build a Laser streaming producer with direct batching, linger, retries,
229    /// routing, and optional stream/topic creation. `build().await` initializes
230    /// it before returning.
231    pub fn producer(&self) -> ProducerBuilder {
232        ProducerBuilder::new(self.clone())
233    }
234
235    /// Build a live Laser consumer for one partition. The built consumer
236    /// implements `futures::Stream` and exposes server-backed offset control.
237    pub fn consumer(&self, name: impl Into<String>, partition: u32) -> ConsumerBuilder {
238        ConsumerBuilder::partition(self.clone(), name, partition)
239    }
240
241    /// The consumer group `group` of this topic: the handle that owns the
242    /// group's filter policy and builds its consumers and readers. Free and
243    /// synchronous, IO happens at the verbs.
244    pub fn consumer_group(&self, group: impl Into<String>) -> ConsumerGroup {
245        ConsumerGroup::new(self.clone(), GroupTarget::Name(group.into()))
246    }
247
248    /// [`consumer_group`](Self::consumer_group) by the group's native numeric
249    /// id. The id names a group inside this topic incarnation only.
250    pub fn consumer_group_id(&self, id: u64) -> ConsumerGroup {
251        ConsumerGroup::new(self.clone(), GroupTarget::Id(id))
252    }
253
254    /// This topic's name.
255    pub fn name(&self) -> &str {
256        &self.name
257    }
258
259    pub(crate) fn laser(&self) -> &Laser {
260        &self.laser
261    }
262
263    pub(crate) fn stream(&self) -> Result<&str, LaserError> {
264        self.stream.as_deref().ok_or(LaserError::NoStream)
265    }
266}