Skip to main content

aion_integrations/envelope_delta/
decode.rs

1//! Reconstructing a persisted delta back into the envelope it was diffed from.
2
3use std::collections::HashMap;
4use std::fmt;
5
6use aion_core::{ActivityEvent, ActivityEventKind};
7use serde_json::Value;
8
9use super::patch::{self, Patch};
10use super::slot::{RecordedBase, StreamSlot};
11use super::wire::{self, ENVELOPE_DELTA_VERSION, EnvelopeDelta};
12
13/// What a reader should use in place of one event's persisted raw value.
14#[derive(Clone, Debug, PartialEq)]
15pub enum ResolvedEnvelope {
16    /// The event carries no delta. Its persisted value stands as-is — this is every non-`Raw`
17    /// event, every pre-change full envelope, and every turn's base frame.
18    Verbatim,
19    /// A delta resolved against its base. The carried value is byte-identical to the envelope the
20    /// frame would have carried had it never been compacted.
21    Reconstructed(Value),
22    /// A delta that could not be resolved, and why. The event's persisted value is untouched and
23    /// still carries every field that actually changed, so a reader can show the delta itself
24    /// rather than nothing.
25    Unresolved(UnresolvedDelta),
26}
27
28/// A delta the reader declined to reconstruct.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct UnresolvedDelta {
31    /// The provider response identifier of the turn, when the document named one.
32    pub base: String,
33    /// The `worker_seq` of the base record, when the document named one.
34    pub base_worker_seq: Option<u64>,
35    /// Why reconstruction was declined.
36    pub reason: UnresolvedReason,
37}
38
39impl fmt::Display for UnresolvedDelta {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        write!(formatter, "envelope delta for turn {:?}", self.base)?;
42        if let Some(worker_seq) = self.base_worker_seq {
43            write!(formatter, " (base at worker_seq {worker_seq})")?;
44        }
45        write!(formatter, " could not be resolved: {}", self.reason)
46    }
47}
48
49/// Why one delta could not be turned back into an envelope.
50#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
51pub enum UnresolvedReason {
52    /// The base record is not in what this reader has seen — it fell outside the requested window,
53    /// or retention trimmed it. Nothing is wrong; the reader simply does not hold the bytes.
54    #[error(
55        "its base record was not in the range this reader has seen (trimmed by retention, or before the requested window)"
56    )]
57    BaseNotSeen,
58    /// A base for this turn was seen, but its bytes are not the bytes the delta was built against
59    /// — for instance because the base event was truncated by the server's per-event size ceiling
60    /// after the encoder read it.
61    #[error(
62        "the base record present here has digest {observed}, but the delta was built against {expected}"
63    )]
64    BaseDigestMismatch {
65        /// The digest the delta names.
66        expected: String,
67        /// The digest of the base this reader holds.
68        observed: String,
69    },
70    /// The document names a format version this reader does not implement.
71    #[error("it declares delta format version {version}, and this reader implements {supported}")]
72    UnsupportedVersion {
73        /// The version the document declares.
74        version: u64,
75        /// The version this reader implements.
76        supported: u64,
77    },
78    /// The document is not a well-formed delta.
79    #[error("the delta document is malformed: {detail}")]
80    Malformed {
81        /// What was wrong with it.
82        detail: String,
83    },
84    /// The delta did not apply cleanly to the base, which means it does not belong to it.
85    #[error("the delta did not apply to the base: {detail}")]
86    PatchFailed {
87        /// The patch error, rendered.
88        detail: String,
89    },
90}
91
92/// Reconstructs compacted provider envelopes as a transcript is read back.
93///
94/// Hold one per served stream and feed it every event **in the order the reader will present
95/// them**: a base is only available to the deltas that follow it. It remembers a single base per
96/// `(stream, agent)`, mirroring [`EnvelopeDeltaEncoder`](super::EnvelopeDeltaEncoder) exactly.
97#[derive(Debug, Default)]
98pub struct EnvelopeDeltaDecoder {
99    bases: HashMap<StreamSlot, RecordedBase>,
100}
101
102impl EnvelopeDeltaDecoder {
103    /// A decoder holding no bases.
104    #[must_use]
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Resolves one event, recording it as a base when it is one.
110    pub fn resolve(&mut self, event: &ActivityEvent) -> ResolvedEnvelope {
111        let slot = StreamSlot::of(event);
112        let ActivityEventKind::Raw { value, .. } = &event.kind else {
113            return ResolvedEnvelope::Verbatim;
114        };
115        if let Some(document) = wire::delta_document(value) {
116            return self.resolve_delta(&slot, document);
117        }
118        let Some(response_id) = wire::envelope_response_id(value) else {
119            return ResolvedEnvelope::Verbatim;
120        };
121        // An ephemeral frame is never persisted, so the encoder never recorded it as a base and a
122        // durable delta can never refer to one. Recording it here would break that symmetry: an
123        // ephemeral frame of a DIFFERENT turn would displace the base a durable delta still needs.
124        // The two halves must agree on what a base is, exactly.
125        if event.ephemeral {
126            return ResolvedEnvelope::Verbatim;
127        }
128        self.record_base(slot, response_id, event.worker_seq, value);
129        ResolvedEnvelope::Verbatim
130    }
131
132    /// Returns a copy of the event with any delta replaced by the envelope it reconstructs to.
133    ///
134    /// This is the form the server's transcript serving boundary uses: downstream consumers keep
135    /// receiving exactly the full envelopes they received before compaction existed. An
136    /// unresolvable delta is left in place — never fabricated — and reported to the caller so it
137    /// can be logged where a delta going unresolved is worth knowing about.
138    pub fn resolve_event(
139        &mut self,
140        event: &ActivityEvent,
141    ) -> (ActivityEvent, Option<UnresolvedDelta>) {
142        match self.resolve(event) {
143            ResolvedEnvelope::Verbatim => (event.clone(), None),
144            ResolvedEnvelope::Reconstructed(envelope) => {
145                let mut resolved = event.clone();
146                if let ActivityEventKind::Raw { value, .. } = &mut resolved.kind {
147                    *value = envelope;
148                }
149                (resolved, None)
150            }
151            ResolvedEnvelope::Unresolved(unresolved) => (event.clone(), Some(unresolved)),
152        }
153    }
154
155    /// Remembers a full envelope as its turn's base.
156    ///
157    /// A base already held for the same turn is kept rather than overwritten, because the encoder
158    /// diffs every one of a turn's frames against the turn's *first* frame. A frame bearing a new
159    /// turn displaces the old base, which is what bounds what a decoder retains.
160    fn record_base(&mut self, slot: StreamSlot, response_id: &str, worker_seq: u64, value: &Value) {
161        if self
162            .bases
163            .get(&slot)
164            .is_some_and(|held| held.response_id == response_id)
165        {
166            return;
167        }
168        if let Some(base) = RecordedBase::record_parts(response_id, worker_seq, value) {
169            self.bases.insert(slot, base);
170        }
171    }
172
173    /// Reconstructs one delta document, or says why it could not.
174    fn resolve_delta(&self, slot: &StreamSlot, document: &Value) -> ResolvedEnvelope {
175        let delta: EnvelopeDelta = match serde_json::from_value(document.clone()) {
176            Ok(delta) => delta,
177            Err(error) => {
178                return unresolved(
179                    document
180                        .get("base")
181                        .and_then(Value::as_str)
182                        .unwrap_or_default(),
183                    document.get("base_worker_seq").and_then(Value::as_u64),
184                    UnresolvedReason::Malformed {
185                        detail: error.to_string(),
186                    },
187                );
188            }
189        };
190        if delta.version != ENVELOPE_DELTA_VERSION {
191            return unresolved(
192                &delta.base,
193                Some(delta.base_worker_seq),
194                UnresolvedReason::UnsupportedVersion {
195                    version: delta.version,
196                    supported: ENVELOPE_DELTA_VERSION,
197                },
198            );
199        }
200        let Some(base) = self
201            .bases
202            .get(slot)
203            .filter(|held| held.response_id == delta.base)
204        else {
205            return unresolved(
206                &delta.base,
207                Some(delta.base_worker_seq),
208                UnresolvedReason::BaseNotSeen,
209            );
210        };
211        if base.digest != delta.base_digest {
212            return unresolved(
213                &delta.base,
214                Some(delta.base_worker_seq),
215                UnresolvedReason::BaseDigestMismatch {
216                    expected: delta.base_digest,
217                    observed: base.digest.clone(),
218                },
219            );
220        }
221        let reconstruction = patch::apply(
222            &base.value,
223            &Patch {
224                set: delta.set,
225                unset: delta.unset,
226            },
227        );
228        match reconstruction {
229            Ok(envelope) => ResolvedEnvelope::Reconstructed(envelope),
230            Err(error) => unresolved(
231                &delta.base,
232                Some(delta.base_worker_seq),
233                UnresolvedReason::PatchFailed {
234                    detail: error.to_string(),
235                },
236            ),
237        }
238    }
239}
240
241/// Builds the unresolved arm.
242fn unresolved(
243    base: &str,
244    base_worker_seq: Option<u64>,
245    reason: UnresolvedReason,
246) -> ResolvedEnvelope {
247    ResolvedEnvelope::Unresolved(UnresolvedDelta {
248        base: base.to_owned(),
249        base_worker_seq,
250        reason,
251    })
252}