webui_handler/plugin/
fast_v2.rs1use super::HandlerPlugin;
18use crate::{HandlerError, ResponseWriter, Result};
19use serde_json::Value;
20use std::fmt::Write;
21use webui_protocol::FastElementData;
22
23const V2_BINDING_START_PREFIX: &str = "<!--fe-b$$start$$";
25const V2_BINDING_END_PREFIX: &str = "<!--fe-b$$end$$";
26const V2_BINDING_SUFFIX: &str = "$$fe-b-->";
27const V2_SEPARATOR: &str = "$$";
28const V2_REPEAT_START_PREFIX: &str = "<!--fe-repeat$$start$$";
29const V2_REPEAT_END_PREFIX: &str = "<!--fe-repeat$$end$$";
30const V2_REPEAT_SUFFIX: &str = "$$fe-repeat-->";
31const V2_ATTR_SINGLE_PREFIX: &str = " data-fe-b-";
32const V2_ATTR_MULTI_PREFIX: &str = " data-fe-c-";
33
34pub struct FastV2HydrationPlugin {
40 scopes: Vec<usize>,
43 binding_stack: Vec<usize>,
45 buffer: String,
47}
48
49impl FastV2HydrationPlugin {
50 #[must_use]
53 pub fn new() -> Self {
54 Self {
55 scopes: vec![0],
57 binding_stack: Vec::with_capacity(8),
58 buffer: String::with_capacity(64),
59 }
60 }
61
62 fn is_active(&self) -> bool {
64 self.scopes.len() > 1
65 }
66
67 fn next_index(&mut self) -> usize {
69 if let Some(counter) = self.scopes.last_mut() {
70 let index = *counter;
71 *counter += 1;
72 index
73 } else {
74 0
75 }
76 }
77
78 fn next_index_n(&mut self, count: u32) -> usize {
80 if let Some(counter) = self.scopes.last_mut() {
81 let index = *counter;
82 *counter += count as usize;
83 index
84 } else {
85 0
86 }
87 }
88
89 fn build_binding_comment(&mut self, prefix: &str, index: usize, name: &str) {
91 self.buffer.clear();
92 self.buffer.push_str(prefix);
93 let _ = write!(self.buffer, "{}", index);
94 self.buffer.push_str(V2_SEPARATOR);
95 self.buffer.push_str(name);
96 self.buffer.push_str(V2_BINDING_SUFFIX);
97 }
98
99 fn build_repeat_comment(&mut self, prefix: &str, index: usize) {
101 self.buffer.clear();
102 self.buffer.push_str(prefix);
103 let _ = write!(self.buffer, "{}", index);
104 self.buffer.push_str(V2_REPEAT_SUFFIX);
105 }
106
107 fn build_attribute_marker(&mut self, binding_index: usize, count: u32) {
109 self.buffer.clear();
110 if count == 1 {
111 self.buffer.push_str(V2_ATTR_SINGLE_PREFIX);
112 let _ = write!(self.buffer, "{}", binding_index);
113 } else {
114 self.buffer.push_str(V2_ATTR_MULTI_PREFIX);
115 let _ = write!(self.buffer, "{}-{}", binding_index, count);
116 }
117 }
118}
119
120impl Default for FastV2HydrationPlugin {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl HandlerPlugin for FastV2HydrationPlugin {
127 fn push_scope(&mut self) {
128 self.scopes.push(0);
129 }
130
131 fn pop_scope(&mut self) {
132 self.scopes.pop();
133 }
134
135 fn on_binding_start(
136 &mut self,
137 name: &str,
138 _raw: bool,
139 writer: &mut dyn ResponseWriter,
140 ) -> Result<()> {
141 if !self.is_active() {
142 return Ok(());
143 }
144 let index = self.next_index();
145 self.binding_stack.push(index);
146 self.build_binding_comment(V2_BINDING_START_PREFIX, index, name);
147 writer.write(&self.buffer)
148 }
149
150 fn on_binding_end(
151 &mut self,
152 name: &str,
153 _raw: bool,
154 writer: &mut dyn ResponseWriter,
155 ) -> Result<()> {
156 if !self.is_active() {
157 return Ok(());
158 }
159 let index = self.binding_stack.pop().unwrap_or(0);
160 self.build_binding_comment(V2_BINDING_END_PREFIX, index, name);
161 writer.write(&self.buffer)
162 }
163
164 fn on_repeat_item_start(
165 &mut self,
166 index: usize,
167 writer: &mut dyn ResponseWriter,
168 ) -> Result<()> {
169 if !self.is_active() {
170 return Ok(());
171 }
172 self.build_repeat_comment(V2_REPEAT_START_PREFIX, index);
173 writer.write(&self.buffer)
174 }
175
176 fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()> {
177 if !self.is_active() {
178 return Ok(());
179 }
180 self.build_repeat_comment(V2_REPEAT_END_PREFIX, index);
181 writer.write(&self.buffer)
182 }
183
184 fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()> {
185 if !self.is_active() {
186 return Ok(());
187 }
188 let decoded = FastElementData::decode(data).map_err(|error| {
189 HandlerError::PluginData(format!(
190 "FAST v2 hydration plugin expected 4 bytes of element data: {error}"
191 ))
192 })?;
193 if decoded.binding_count > 0 {
194 let binding_index = self.next_index_n(decoded.binding_count);
195 self.build_attribute_marker(binding_index, decoded.binding_count);
196 writer.write(&self.buffer)?;
197 }
198 Ok(())
199 }
200
201 fn write_route_component_state(
204 &self,
205 state: &Value,
206 writer: &mut dyn ResponseWriter,
207 ) -> Result<()> {
208 super::fast::write_route_component_state(state, writer)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 #![allow(clippy::disallowed_methods)]
215
216 use super::*;
217
218 struct TestWriter {
219 output: String,
220 }
221
222 impl TestWriter {
223 fn new() -> Self {
224 Self {
225 output: String::new(),
226 }
227 }
228 }
229
230 impl ResponseWriter for TestWriter {
231 fn write(&mut self, content: &str) -> Result<()> {
232 self.output.push_str(content);
233 Ok(())
234 }
235 fn end(&mut self) -> Result<()> {
236 Ok(())
237 }
238 }
239
240 #[test]
241 fn test_fast_v2_binding_marker_format() {
242 let mut plugin = FastV2HydrationPlugin::new();
243 plugin.push_scope();
244 let mut writer = TestWriter::new();
245 plugin
246 .on_binding_start("userName", false, &mut writer)
247 .unwrap();
248 plugin
249 .on_binding_end("userName", false, &mut writer)
250 .unwrap();
251 assert_eq!(
252 writer.output,
253 "<!--fe-b$$start$$0$$userName$$fe-b--><!--fe-b$$end$$0$$userName$$fe-b-->"
254 );
255 }
256
257 #[test]
258 fn test_fast_v2_binding_sequence_uses_indexes() {
259 let mut plugin = FastV2HydrationPlugin::new();
260 plugin.push_scope();
261 let mut writer = TestWriter::new();
262 plugin.on_binding_start("a", false, &mut writer).unwrap();
263 plugin.on_binding_end("a", false, &mut writer).unwrap();
264 writer.output.clear();
265 plugin.on_binding_start("b", false, &mut writer).unwrap();
266 assert_eq!(writer.output, "<!--fe-b$$start$$1$$b$$fe-b-->");
267 }
268
269 #[test]
270 fn test_fast_v2_repeat_marker_format() {
271 let mut plugin = FastV2HydrationPlugin::new();
272 plugin.push_scope();
273 let mut writer = TestWriter::new();
274 plugin.on_repeat_item_start(2, &mut writer).unwrap();
275 plugin.on_repeat_item_end(2, &mut writer).unwrap();
276 assert_eq!(
277 writer.output,
278 "<!--fe-repeat$$start$$2$$fe-repeat--><!--fe-repeat$$end$$2$$fe-repeat-->"
279 );
280 }
281
282 #[test]
283 fn test_fast_v2_attribute_marker_formats() {
284 let mut single = FastV2HydrationPlugin::new();
285 single.push_scope();
286 let mut writer = TestWriter::new();
287 let one = 1u32.to_le_bytes();
288 single.on_element_data(&one, &mut writer).unwrap();
289 assert_eq!(writer.output, " data-fe-b-0");
290
291 let mut multi = FastV2HydrationPlugin::new();
292 multi.push_scope();
293 writer.output.clear();
294 let three = 3u32.to_le_bytes();
295 multi.on_element_data(&three, &mut writer).unwrap();
296 assert_eq!(writer.output, " data-fe-c-0-3");
297 }
298
299 #[test]
300 fn test_fast_v2_attribute_count_advances_binding_index() {
301 let mut plugin = FastV2HydrationPlugin::new();
302 plugin.push_scope();
303 let mut writer = TestWriter::new();
304 let three = 3u32.to_le_bytes();
305 plugin.on_element_data(&three, &mut writer).unwrap();
306
307 writer.output.clear();
308 plugin.on_binding_start("next", false, &mut writer).unwrap();
309 assert_eq!(writer.output, "<!--fe-b$$start$$3$$next$$fe-b-->");
310 }
311
312 #[test]
313 fn test_fast_v2_root_scope_disabled() {
314 let mut plugin = FastV2HydrationPlugin::new();
315 let mut writer = TestWriter::new();
316 plugin.on_binding_start("x", false, &mut writer).unwrap();
317 plugin.on_binding_end("x", false, &mut writer).unwrap();
318 plugin.on_repeat_item_start(0, &mut writer).unwrap();
319 plugin.on_repeat_item_end(0, &mut writer).unwrap();
320 let data = 3u32.to_le_bytes();
321 plugin.on_element_data(&data, &mut writer).unwrap();
322 assert_eq!(writer.output, "");
323 }
324}