aion_integrations/envelope_delta/encode.rs
1//! Compacting a repeated provider envelope into a delta against its turn's base.
2
3use std::collections::HashMap;
4
5use aion_core::{ActivityEvent, ActivityEventKind};
6use serde_json::{Value, json};
7
8use super::patch;
9use super::slot::{RecordedBase, StreamSlot};
10use super::wire::{self, ENVELOPE_DELTA_KEY, ENVELOPE_DELTA_VERSION};
11
12/// What the encoder did with one event, so the caller can record it.
13///
14/// Every arm is reported, including the ones where nothing was saved. A compaction pass that
15/// quietly declined to compact would be indistinguishable from one that was never reached.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum Compaction {
18 /// The event is not a provider response envelope (or is ephemeral, and so is never
19 /// persisted): untouched.
20 NotApplicable,
21 /// The first frame of a turn. Persisted in full and remembered as that turn's base.
22 BaseRecorded {
23 /// The provider response identifier the base was filed under.
24 base: String,
25 },
26 /// A later frame of a turn, persisted as a delta against the base.
27 Compacted {
28 /// The provider response identifier the delta refers to.
29 base: String,
30 /// Serialized bytes the frame would have cost persisted in full.
31 full_bytes: usize,
32 /// Serialized bytes the delta document costs instead.
33 delta_bytes: usize,
34 },
35 /// A later frame of a turn that was persisted in full anyway, and why.
36 ///
37 /// This is the safe outcome, not the failure outcome: the stream is exactly what it would have
38 /// been without this module.
39 FullRetained {
40 /// The provider response identifier the frame belongs to.
41 base: String,
42 /// Why the delta was declined.
43 reason: FullRetainedReason,
44 },
45}
46
47/// Why an eligible frame was persisted in full rather than as a delta.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum FullRetainedReason {
50 /// Applying the delta back to the base did not reproduce the frame byte for byte. The encoder
51 /// refuses to persist a delta it cannot prove reversible, whatever the saving would have been.
52 NotByteReversible,
53 /// The delta document is not smaller than the frame it would replace, so compacting it would
54 /// cost bytes rather than save them.
55 NotSmaller {
56 /// Serialized bytes of the frame.
57 full_bytes: usize,
58 /// Serialized bytes the delta document would have cost.
59 delta_bytes: usize,
60 },
61 /// The frame or its base could not be serialized, so neither the digest nor the reversibility
62 /// check could be performed.
63 NotSerializable,
64}
65
66/// Compacts a harness adapter's outgoing provider envelopes, one stream's turns at a time.
67///
68/// Hold one per session. It remembers a single base envelope per `(stream, agent)`; see
69/// [`RecordedBase`] for why that is both sufficient and bounded.
70#[derive(Debug, Default)]
71pub struct EnvelopeDeltaEncoder {
72 bases: HashMap<StreamSlot, RecordedBase>,
73}
74
75impl EnvelopeDeltaEncoder {
76 /// A encoder holding no bases.
77 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 /// Rewrites `event`'s raw value in place when it is a compactible provider envelope frame.
83 ///
84 /// The returned [`Compaction`] says what happened; the event is modified only in the
85 /// [`Compaction::Compacted`] case.
86 pub fn compact(&mut self, event: &mut ActivityEvent) -> Compaction {
87 // Ephemeral events are forwarded live and never persisted, so compacting one would trade
88 // nothing for the risk of a reader meeting a delta whose base was never durable.
89 if event.ephemeral {
90 return Compaction::NotApplicable;
91 }
92 let slot = StreamSlot::of(event);
93 let worker_seq = event.worker_seq;
94 let ActivityEventKind::Raw { value, .. } = &mut event.kind else {
95 return Compaction::NotApplicable;
96 };
97 // A value that is already a delta document is not re-compacted; the encoder is the only
98 // writer of them, so this can only mean the same event was passed twice.
99 if wire::delta_document(value).is_some() {
100 return Compaction::NotApplicable;
101 }
102 let Some(response_id) = wire::envelope_response_id(value) else {
103 return Compaction::NotApplicable;
104 };
105 let response_id = response_id.to_owned();
106
107 match self.bases.get(&slot) {
108 Some(base) if base.response_id == response_id => match build_delta(base, value) {
109 Ok(delta) => {
110 *value = delta.document;
111 Compaction::Compacted {
112 base: response_id,
113 full_bytes: delta.full_bytes,
114 delta_bytes: delta.delta_bytes,
115 }
116 }
117 Err(reason) => Compaction::FullRetained {
118 base: response_id,
119 reason,
120 },
121 },
122 // Either the first frame this stream has produced, or the first frame of a new turn:
123 // the previous turn's base is dropped by the same insert.
124 _ => match RecordedBase::record_parts(&response_id, worker_seq, value) {
125 Some(base) => {
126 self.bases.insert(slot, base);
127 Compaction::BaseRecorded { base: response_id }
128 }
129 None => Compaction::FullRetained {
130 base: response_id,
131 reason: FullRetainedReason::NotSerializable,
132 },
133 },
134 }
135 }
136}
137
138/// A delta the encoder has proved reversible, with the byte counts that justify it.
139struct BuiltDelta {
140 document: Value,
141 full_bytes: usize,
142 delta_bytes: usize,
143}
144
145/// Builds the delta document for `frame` against `base`, refusing anything it cannot prove.
146fn build_delta(base: &RecordedBase, frame: &Value) -> Result<BuiltDelta, FullRetainedReason> {
147 let (Value::Object(base_object), Value::Object(frame_object)) = (&base.value, frame) else {
148 return Err(FullRetainedReason::NotByteReversible);
149 };
150 let diff = patch::diff(base_object, frame_object);
151 let document = json!({
152 ENVELOPE_DELTA_KEY: {
153 "v": ENVELOPE_DELTA_VERSION,
154 "base": base.response_id,
155 "base_worker_seq": base.worker_seq,
156 "base_digest": base.digest,
157 "set": Value::Object(diff.set.clone()),
158 "unset": diff.unset.clone(),
159 }
160 });
161
162 // The reversibility proof. `Value` equality ignores object key order, so this compares the
163 // serialized bytes — the thing a reader will actually render — and not merely the structures.
164 let reconstructed =
165 patch::apply(&base.value, &diff).map_err(|_| FullRetainedReason::NotByteReversible)?;
166 let (Ok(frame_bytes), Ok(reconstructed_bytes)) = (
167 serde_json::to_vec(frame),
168 serde_json::to_vec(&reconstructed),
169 ) else {
170 return Err(FullRetainedReason::NotSerializable);
171 };
172 if frame_bytes != reconstructed_bytes {
173 return Err(FullRetainedReason::NotByteReversible);
174 }
175
176 let Ok(document_bytes) = serde_json::to_vec(&document) else {
177 return Err(FullRetainedReason::NotSerializable);
178 };
179 let full_bytes = frame_bytes.len();
180 let delta_bytes = document_bytes.len();
181 if delta_bytes >= full_bytes {
182 return Err(FullRetainedReason::NotSmaller {
183 full_bytes,
184 delta_bytes,
185 });
186 }
187 Ok(BuiltDelta {
188 document,
189 full_bytes,
190 delta_bytes,
191 })
192}