Skip to main content

mnesis_store/
decoded.rs

1//! Consumer-side typed view over the raw envelope streams (#249).
2//!
3//! [`read_stream`](crate::RawEventStore::read_stream) and
4//! [`read_all`](crate::RawEventStore::read_all) yield **raw**
5//! [`PersistedEnvelope`]s, and [`Subscription`](crate::Subscription) yields
6//! them wrapped in a [`Step`] phase marker — by design (the distributed
7//! multi-consumer contract: each consumer holds its own codec). This module
8//! adds an ergonomic layer on top — it does **not** make the core subscription
9//! typed.
10//!
11//! - [`DecodedStreamExt::decoded`] — for **owning** codecs (JSON, bincode): a
12//!   stream of carry-away [`Decoded<E>`] items.
13//! - [`DecodedStreamExt::for_each_decoded`] — for **owning and zero-copy**
14//!   codecs (rkyv, bytemuck): an internal-iteration fold that hands the borrowed
15//!   window to a closure, so no lending stream is needed.
16//! - [`StepStreamExt`] — the phase-aware surface over a `Step`-tagged
17//!   subscription stream: [`.events()`](StepStreamExt::events) drops the phase
18//!   (then the two `DecodedStreamExt` methods apply), or
19//!   [`.decoded()`](StepStreamExt::decoded) decodes while **keeping** the phase.
20
21use core::future::Future;
22
23use bytes::Bytes;
24use futures::{Stream, StreamExt};
25
26use crate::codec::{Decode, OwningCodec};
27use crate::envelope::PersistedEnvelope;
28use crate::step::Step;
29use crate::stream_id::StreamKey;
30use mnesis::Version;
31
32/// A raw envelope, un-packed: the decoded event plus its resume bookmark and
33/// metadata. `T` is the owned event (`E`) on the stream path, or the borrowed
34/// window (`Decode::Output`) inside a fold closure.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Decoded<T> {
37    /// The decoded event (owned `E`, or a borrowed window).
38    pub event: T,
39    /// The per-stream version — the per-stream resume bookmark.
40    pub version: Version,
41    /// The event's metadata, if any (cheap Arc-shared bytes).
42    pub metadata: Option<Bytes>,
43}
44
45/// A read from the raw stream failed, or the codec failed to decode the payload.
46/// The two failure domains never share a variant (CLAUDE rule 3).
47#[derive(Debug, thiserror::Error)]
48pub enum DecodeStreamError<R, D> {
49    /// The underlying raw stream yielded an error item.
50    #[error("subscription stream read failed")]
51    Read(#[source] R),
52    /// The codec rejected the payload.
53    #[error("event decode failed")]
54    Decode(#[source] D),
55}
56
57/// [`DecodedStreamExt::for_each_decoded`] failure: a raw read, a decode, or the
58/// consumer's own fold closure. Three distinct domains (CLAUDE rule 3).
59#[derive(Debug, thiserror::Error)]
60pub enum FoldDecodedError<R, D, H> {
61    /// The underlying raw stream yielded an error item.
62    #[error("subscription stream read failed")]
63    Read(#[source] R),
64    /// The codec rejected the payload.
65    #[error("event decode failed")]
66    Decode(#[source] D),
67    /// The consumer's fold closure returned an error.
68    #[error("decoded-event handler failed")]
69    Handler(#[source] H),
70}
71
72mod sealed {
73    pub trait Sealed {}
74}
75
76/// A raw stream item that carries a [`PersistedEnvelope`] and can be re-tagged
77/// with its decoded counterpart.
78///
79/// Implemented for the two raw shapes the store yields:
80/// - `PersistedEnvelope` (per-stream / `read_stream`) → typed item `Decoded<T>`;
81///   the bookmark (`version`) lives **inside** the box.
82/// - `(P, StreamKey, PersistedEnvelope)` (`$all` / `read_all`) → typed item
83///   `(P, StreamKey, Decoded<T>)`; the `$all` position bookmark and the origin
84///   stream key both stay **beside** the box, preserved through the decode.
85///
86/// The asymmetry is intentional (CLAUDE rule 4): each tag rides exactly
87/// where it lives in the raw layer. Sealed — not implementable downstream.
88pub trait RawItem: sealed::Sealed {
89    /// The typed item once the envelope is decoded to `Decoded<T>`.
90    type Typed<T>;
91    /// Borrow the carried envelope (to decode it).
92    fn envelope(&self) -> &PersistedEnvelope;
93    /// Re-attach the decoded box into this item's shape (copying any tag).
94    fn retag<T>(&self, decoded: Decoded<T>) -> Self::Typed<T>;
95}
96
97impl sealed::Sealed for PersistedEnvelope {}
98impl RawItem for PersistedEnvelope {
99    type Typed<T> = Decoded<T>;
100    fn envelope(&self) -> &PersistedEnvelope {
101        self
102    }
103    fn retag<T>(&self, decoded: Decoded<T>) -> Decoded<T> {
104        decoded
105    }
106}
107
108impl<P: Copy> sealed::Sealed for (P, StreamKey, PersistedEnvelope) {}
109impl<P: Copy> RawItem for (P, StreamKey, PersistedEnvelope) {
110    type Typed<T> = (P, StreamKey, Decoded<T>);
111    fn envelope(&self) -> &PersistedEnvelope {
112        &self.2
113    }
114    fn retag<T>(&self, decoded: Decoded<T>) -> (P, StreamKey, Decoded<T>) {
115        (self.0, self.1.clone(), decoded)
116    }
117}
118
119/// Adds a typed, codec-reusing view over any stream of raw envelope items.
120///
121/// Covers [`Subscription`](crate::Subscription),
122/// [`read_stream`](crate::RawEventStore::read_stream), and
123/// [`read_all`](crate::RawEventStore::read_all).
124pub trait DecodedStreamExt<I, R>: Stream<Item = Result<I, R>> + Sized
125where
126    I: RawItem,
127{
128    /// Decode each item with `codec`, reusing the codec configured elsewhere.
129    ///
130    /// Owning codecs only — the `for<'a> Output<'a> = E` bound is unsatisfiable
131    /// for a zero-copy codec (whose `Output` borrows the envelope), so the
132    /// compiler steers zero-copy consumers to
133    /// [`for_each_decoded`](Self::for_each_decoded).
134    /// Per-stream items become `Decoded<E>`; `$all` items become
135    /// `(AllPosition, StreamKey, Decoded<E>)` (both tags are preserved beside
136    /// the box).
137    fn decoded<E, C>(
138        self,
139        codec: C,
140    ) -> impl Stream<Item = Result<I::Typed<E>, DecodeStreamError<R, C::Error>>> + Send
141    where
142        C: OwningCodec<E>,
143        E: Send + 'static,
144        I: Send + 'static,
145        R: Send + 'static,
146        Self: Send,
147    {
148        self.map(move |res| {
149            let item = res.map_err(DecodeStreamError::Read)?;
150            let event: E = codec
151                .decode(item.envelope())
152                .map_err(DecodeStreamError::Decode)?;
153            let env = item.envelope();
154            let decoded = Decoded {
155                event,
156                version: env.version(),
157                metadata: env.metadata_bytes(),
158            };
159            Ok(item.retag(decoded))
160        })
161    }
162
163    /// Fold each decoded event by handing your closure the borrowed window —
164    /// works for **owning and zero-copy** codecs, because the window lives only
165    /// for the call and never escapes (internal iteration; no lending stream).
166    /// This is the path a zero-copy codec (rkyv, bytemuck) must take: its
167    /// `Output` borrows the envelope and so cannot be carried away by
168    /// [`decoded`](Self::decoded)'s stream.
169    ///
170    /// `f` receives a [`Decoded<Output<'a>>`] valid only for that call. On a
171    /// never-ending [`Subscription`](crate::Subscription) this runs until the
172    /// first `Err`; over a finite
173    /// [`read_stream`](crate::RawEventStore::read_stream) it runs to completion.
174    ///
175    /// The closure argument is the concrete [`Decoded<Output<'a>>`] — the event
176    /// view plus its per-stream `version` and `metadata`. It is deliberately
177    /// **not** the `I::Typed<_>` shape [`decoded`](Self::decoded) yields: a bare
178    /// closure cannot be inferred higher-ranked over a lifetime hidden behind
179    /// the `I::Typed<_>` associated-type projection (rustc "implementation of
180    /// `FnMut` is not general enough"), so a concrete outer constructor is
181    /// required for the zero-copy path to type-check. Consequently, over an
182    /// `$all` stream neither the `AllPosition` tag nor the [`StreamKey`] is
183    /// surfaced to `f` (the per-stream `Decoded::version` still is) — a
184    /// positioned or routed `$all` consumer must either use
185    /// [`decoded`](Self::decoded) (owning codecs), or fold the raw
186    /// `subscribe_all` stream directly, calling `codec.decode(&env)` per item
187    /// (zero-copy; both tags ride beside the envelope on the raw tuple).
188    fn for_each_decoded<E, C, F, H>(
189        self,
190        codec: C,
191        mut f: F,
192    ) -> impl Future<Output = Result<(), FoldDecodedError<R, C::Error, H>>>
193    where
194        E: ?Sized,
195        C: Decode<E>,
196        F: for<'a> FnMut(Decoded<<C as Decode<E>>::Output<'a>>) -> Result<(), H>,
197    {
198        async move {
199            let stream = self;
200            futures::pin_mut!(stream);
201            while let Some(res) = stream.next().await {
202                let item = res.map_err(FoldDecodedError::Read)?;
203                fold_one(&codec, &mut f, item.envelope())?;
204            }
205            Ok(())
206        }
207    }
208}
209
210/// One decode-then-fold step: decode `env` with `codec`, box it as [`Decoded`],
211/// and hand the (possibly borrowed) window to `f`. Kept a free, synchronous fn
212/// so the higher-ranked `f` call over the borrowed window's lifetime resolves
213/// in a plain fn body (see [`DecodedStreamExt::for_each_decoded`]).
214fn fold_one<R, E, C, F, H>(
215    codec: &C,
216    f: &mut F,
217    env: &PersistedEnvelope,
218) -> Result<(), FoldDecodedError<R, C::Error, H>>
219where
220    E: ?Sized,
221    C: Decode<E>,
222    F: for<'a> FnMut(Decoded<<C as Decode<E>>::Output<'a>>) -> Result<(), H>,
223{
224    let window = codec.decode(env).map_err(FoldDecodedError::Decode)?;
225    let decoded = Decoded {
226        event: window,
227        version: env.version(),
228        metadata: env.metadata_bytes(),
229    };
230    f(decoded).map_err(FoldDecodedError::Handler)
231}
232
233impl<St, I, R> DecodedStreamExt<I, R> for St
234where
235    St: Stream<Item = Result<I, R>>,
236    I: RawItem,
237{
238}
239
240/// Adds phase-aware views over a [`Step`]-tagged stream — what
241/// [`Subscription::subscribe`](crate::Subscription::subscribe) /
242/// [`subscribe_all`](crate::Subscription::subscribe_all) yield.
243///
244/// The `Step` phase marker (the catch-up→live boundary) is intrinsic to a
245/// subscription (a finite read has no such boundary), so it rides on the raw
246/// stream. This trait lets a consumer either **keep** the phase and decode
247/// (`.decoded()`), or **drop** it (`.events()`) and fall back to the plain
248/// [`DecodedStreamExt`] surface a finite read uses.
249///
250/// `Step<I>` is deliberately **not** a [`RawItem`] (the [`CaughtUp`](Step::CaughtUp)
251/// marker carries no envelope), so `.decoded()` here and `.decoded()` on
252/// [`DecodedStreamExt`] are two non-overlapping impls sharing one name.
253pub trait StepStreamExt<I, R>: Stream<Item = Result<Step<I>, R>> + Sized
254where
255    I: RawItem,
256{
257    /// Drop the phase: yield bare `I` items ([`CaughtUp`](Step::CaughtUp)
258    /// removed, [`Event`](Step::Event) unwrapped). The result is a plain raw
259    /// stream, so the full [`DecodedStreamExt`] surface (`.decoded()`,
260    /// `.for_each_decoded()`) applies to it — the path for a consumer that does
261    /// not care whether it is replaying or live.
262    fn events(self) -> impl Stream<Item = Result<I, R>> + Send
263    where
264        Self: Send,
265        I: Send,
266        R: Send,
267    {
268        self.filter_map(|res| async move {
269            match res {
270                Ok(Step::Event(item)) => Some(Ok(item)),
271                Ok(Step::CaughtUp) => None,
272                Err(e) => Some(Err(e)),
273            }
274        })
275    }
276
277    /// Decode each event with `codec`, **preserving** the phase marker: the
278    /// result is a stream of `Step<I::Typed<E>>` — replay events, then exactly
279    /// one [`CaughtUp`](Step::CaughtUp), then live events. Owning codecs only
280    /// (same `for<'a> Output<'a> = E` steer as [`DecodedStreamExt::decoded`]).
281    ///
282    /// This is the projection consumption path: it tells catch-up from live
283    /// *and* hands you typed events, reusing the codec — no magic count, no
284    /// hand-rolled timeout, mnesis-owned error.
285    #[allow(
286        clippy::type_complexity,
287        reason = "the Step<Decoded>/DecodeStreamError item is intrinsic to the contract; an \
288                  alias would hide the `impl Stream` the API depends on"
289    )]
290    fn decoded<E, C>(
291        self,
292        codec: C,
293    ) -> impl Stream<Item = Result<Step<I::Typed<E>>, DecodeStreamError<R, C::Error>>> + Send
294    where
295        C: OwningCodec<E>,
296        E: Send + 'static,
297        I: Send + 'static,
298        R: Send + 'static,
299        Self: Send,
300    {
301        self.map(move |res| {
302            let step = res.map_err(DecodeStreamError::Read)?;
303            match step {
304                Step::CaughtUp => Ok(Step::CaughtUp),
305                Step::Event(item) => {
306                    let env = item.envelope();
307                    let event: E = codec.decode(env).map_err(DecodeStreamError::Decode)?;
308                    let decoded = Decoded {
309                        event,
310                        version: env.version(),
311                        metadata: env.metadata_bytes(),
312                    };
313                    Ok(Step::Event(item.retag(decoded)))
314                }
315            }
316        })
317    }
318}
319
320impl<St, I, R> StepStreamExt<I, R> for St
321where
322    St: Stream<Item = Result<Step<I>, R>>,
323    I: RawItem,
324{
325}