aion_integrations/envelope_delta/
decode.rs1use 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#[derive(Clone, Debug, PartialEq)]
15pub enum ResolvedEnvelope {
16 Verbatim,
19 Reconstructed(Value),
22 Unresolved(UnresolvedDelta),
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct UnresolvedDelta {
31 pub base: String,
33 pub base_worker_seq: Option<u64>,
35 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#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
51pub enum UnresolvedReason {
52 #[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 #[error(
62 "the base record present here has digest {observed}, but the delta was built against {expected}"
63 )]
64 BaseDigestMismatch {
65 expected: String,
67 observed: String,
69 },
70 #[error("it declares delta format version {version}, and this reader implements {supported}")]
72 UnsupportedVersion {
73 version: u64,
75 supported: u64,
77 },
78 #[error("the delta document is malformed: {detail}")]
80 Malformed {
81 detail: String,
83 },
84 #[error("the delta did not apply to the base: {detail}")]
86 PatchFailed {
87 detail: String,
89 },
90}
91
92#[derive(Debug, Default)]
98pub struct EnvelopeDeltaDecoder {
99 bases: HashMap<StreamSlot, RecordedBase>,
100}
101
102impl EnvelopeDeltaDecoder {
103 #[must_use]
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 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 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 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 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 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
241fn 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}