1use serde::{Deserialize, Serialize};
13use serde_json::{json, Value};
14
15use crate::log::Seq;
16use beck_core::diff::Op;
17use beck_core::html::Html;
18
19pub type SubId = String;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct PatchFrame {
24 pub seq: Seq,
25 pub ops: Vec<Op>,
26}
27
28impl PatchFrame {
29 pub fn new(seq: Seq, ops: Vec<Op>) -> Self {
30 Self { seq, ops }
31 }
32
33 pub fn is_empty(&self) -> bool {
34 self.ops.is_empty()
35 }
36
37 pub fn to_json(&self) -> Value {
39 json!({
40 "t": "p",
41 "q": self.seq,
42 "o": self.ops.iter().map(Op::to_wire).collect::<Vec<_>>(),
43 })
44 }
45}
46
47#[derive(Clone, Debug, PartialEq)]
55pub enum DataFrame {
56 Whole {
58 seq: Seq,
59 state: beck_core::repr::Repr,
60 },
61 Ops {
63 seq: Seq,
64 ops: Vec<beck_core::delta::Op>,
65 },
66}
67
68impl DataFrame {
69 pub fn whole(seq: Seq, state: &beck_core::Value) -> Option<DataFrame> {
72 beck_core::repr::Repr::of(state)
73 .ok()
74 .map(|state| DataFrame::Whole { seq, state })
75 }
76
77 pub fn seq(&self) -> Seq {
78 match self {
79 DataFrame::Whole { seq, .. } | DataFrame::Ops { seq, .. } => *seq,
80 }
81 }
82
83 pub fn is_empty(&self) -> bool {
84 matches!(self, DataFrame::Ops { ops, .. } if ops.is_empty())
85 }
86
87 pub fn to_json(&self) -> Value {
88 match self {
89 DataFrame::Whole { seq, state } => json!({"t": "s", "q": seq, "v": state}),
90 DataFrame::Ops { seq, ops } => json!({"t": "d", "q": seq, "o": ops}),
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum Codec {
97 Json,
99 Postcard,
101}
102
103impl Codec {
104 pub fn encode(self, frame: &PatchFrame) -> Vec<u8> {
105 match self {
106 Codec::Json => serde_json::to_vec(&frame.to_json()).expect("frame is serialisable"),
107 Codec::Postcard => {
108 postcard::to_allocvec(&WireFrame::from(frame)).expect("frame is serialisable")
109 }
110 }
111 }
112}
113
114#[derive(Serialize, Deserialize)]
116struct WireFrame {
117 seq: Seq,
118 ops: Vec<WireOp>,
119}
120
121#[derive(Serialize, Deserialize)]
122enum WireOp {
123 Replace(Vec<u32>, WireHtml),
124 SetText(Vec<u32>, String),
125 SetAttr(Vec<u32>, String, String),
126 RemoveAttr(Vec<u32>, String),
127 Insert(Vec<u32>, u32, WireHtml),
128 Remove(Vec<u32>, u32),
129 Move(Vec<u32>, u32, u32),
130}
131
132#[derive(Serialize, Deserialize)]
133enum WireHtml {
134 Text(String),
135 El {
136 tag: String,
137 attrs: Vec<(String, String)>,
138 key: Option<String>,
139 children: Vec<WireHtml>,
140 },
141}
142
143impl From<&PatchFrame> for WireFrame {
144 fn from(frame: &PatchFrame) -> Self {
145 WireFrame {
146 seq: frame.seq,
147 ops: frame.ops.iter().map(WireOp::from).collect(),
148 }
149 }
150}
151
152impl From<&Op> for WireOp {
153 fn from(op: &Op) -> Self {
154 match op {
155 Op::Replace { path, html } => WireOp::Replace(path.clone(), html.into()),
156 Op::SetText { path, text } => WireOp::SetText(path.clone(), text.clone()),
157 Op::SetAttr { path, name, value } => {
158 WireOp::SetAttr(path.clone(), name.clone(), value.clone())
159 }
160 Op::RemoveAttr { path, name } => WireOp::RemoveAttr(path.clone(), name.clone()),
161 Op::Insert { path, index, html } => WireOp::Insert(path.clone(), *index, html.into()),
162 Op::Remove { path, index } => WireOp::Remove(path.clone(), *index),
163 Op::Move { path, from, to } => WireOp::Move(path.clone(), *from, *to),
164 }
165 }
166}
167
168impl From<&Html> for WireHtml {
169 fn from(html: &Html) -> Self {
170 match html {
171 Html::Text { text, .. } => WireHtml::Text(text.clone()),
172 Html::Element {
173 tag,
174 attrs,
175 key,
176 children,
177 ..
178 } => WireHtml::El {
179 tag: tag.clone(),
180 attrs: attrs.clone(),
181 key: key.clone(),
182 children: children.iter().map(|c| WireHtml::from(&**c)).collect(),
183 },
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use beck_core::diff::diff;
192
193 fn list(n: usize, done: Option<usize>) -> Html {
195 Html::el("ul").children((0..n).map(|i| {
196 let row = Html::el("li").key(i.to_string());
197 let row = if done == Some(i) {
198 row.attr("class", "done")
199 } else {
200 row
201 };
202 row.child(Html::text(format!("todo {i}")))
203 }))
204 }
205
206 #[test]
207 fn a_single_toggle_is_a_small_frame_in_both_encodings() {
208 let ops = diff(&list(50, None), &list(50, Some(7)));
209 let frame = PatchFrame::new(51, ops);
210 let json = Codec::Json.encode(&frame);
211 let binary = Codec::Postcard.encode(&frame);
212
213 assert!(json.len() < 100, "json frame was {} bytes", json.len());
216 assert!(binary.len() < json.len());
217 }
218
219 #[test]
220 fn the_patch_is_essentially_constant_in_the_size_of_the_list() {
221 let small = Codec::Json.encode(&PatchFrame::new(
222 1,
223 diff(&list(10, None), &list(10, Some(3))),
224 ));
225 let large = Codec::Json.encode(&PatchFrame::new(
226 1,
227 diff(&list(1000, None), &list(1000, Some(3))),
228 ));
229 assert!(
230 large.len() <= small.len() + 8,
231 "small {} vs large {}",
232 small.len(),
233 large.len()
234 );
235 }
236}