Skip to main content

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}