1use crate::book::{OrderBookManager, ParsedBookLevel};
12use crate::errors::{PolyfillError, Result};
13use crate::types::{Price, Qty, Side, MAX_PRICE_TICKS, MAX_QTY, MIN_PRICE_TICKS, SCALE_FACTOR};
14use simd_json::prelude::*;
15
16#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
18pub struct WsBookApplyStats {
19 pub book_messages: usize,
20 pub book_levels_applied: usize,
21}
22
23pub struct WsBookUpdateProcessor {
28 buffers: simd_json::Buffers,
29 tape: Option<simd_json::Tape<'static>>,
30 parsed_levels: Vec<ParsedBookLevel>,
31}
32
33impl WsBookUpdateProcessor {
34 pub fn new(input_len_hint: usize) -> Self {
38 Self {
39 buffers: simd_json::Buffers::new(input_len_hint),
40 tape: Some(simd_json::Tape::null().reset()),
42 parsed_levels: Vec::with_capacity((input_len_hint / 32).max(8)),
43 }
44 }
45
46 pub fn process_bytes(
48 &mut self,
49 bytes: &mut [u8],
50 books: &OrderBookManager,
51 ) -> Result<WsBookApplyStats> {
52 let mut tape = self
53 .tape
54 .take()
55 .expect("WsBookUpdateProcessor tape must be present")
56 .reset();
57
58 let result = match simd_json::fill_tape(bytes, &mut self.buffers, &mut tape) {
59 Ok(()) => {
60 let root = tape.as_value();
61 process_root_value(root, books, &mut self.parsed_levels)
62 },
63 Err(e) => Err(PolyfillError::parse(
64 "Failed to parse WebSocket JSON",
65 Some(Box::new(e)),
66 )),
67 };
68
69 self.tape = Some(tape.reset());
71 result
72 }
73
74 pub fn process_text(
76 &mut self,
77 text: String,
78 books: &OrderBookManager,
79 ) -> Result<WsBookApplyStats> {
80 let mut bytes = text.into_bytes();
81 self.process_bytes(bytes.as_mut_slice(), books)
82 }
83}
84
85fn process_root_value<'tape, 'input>(
86 value: simd_json::tape::Value<'tape, 'input>,
87 books: &OrderBookManager,
88 parsed_levels: &mut Vec<ParsedBookLevel>,
89) -> Result<WsBookApplyStats> {
90 if let Some(obj) = value.as_object() {
91 return process_stream_object(obj, books, parsed_levels);
92 }
93
94 let Some(arr) = value.as_array() else {
95 return Ok(WsBookApplyStats::default());
96 };
97
98 let mut total = WsBookApplyStats::default();
99 for elem in arr.iter() {
100 let Some(obj) = elem.as_object() else {
101 continue;
102 };
103 let stats = process_stream_object(obj, books, parsed_levels)?;
104 total.book_messages += stats.book_messages;
105 total.book_levels_applied += stats.book_levels_applied;
106 }
107
108 Ok(total)
109}
110
111fn process_stream_object<'tape, 'input>(
112 obj: simd_json::tape::Object<'tape, 'input>,
113 books: &OrderBookManager,
114 parsed_levels: &mut Vec<ParsedBookLevel>,
115) -> Result<WsBookApplyStats> {
116 let Some(event_type) = obj.get("event_type").and_then(|v| v.into_string()) else {
117 return Ok(WsBookApplyStats::default());
118 };
119
120 if event_type != "book" {
121 return Ok(WsBookApplyStats::default());
122 }
123
124 let asset_id = obj
125 .get("asset_id")
126 .and_then(|v| v.into_string())
127 .ok_or_else(|| PolyfillError::parse("Missing asset_id", None))?;
128
129 let timestamp_value = obj
130 .get("timestamp")
131 .ok_or_else(|| PolyfillError::parse("Missing timestamp", None))?;
132 let timestamp = parse_u64(timestamp_value)
133 .ok_or_else(|| PolyfillError::parse("Invalid timestamp", None))?;
134 let hash = obj.get("hash").and_then(|v| v.into_string());
135
136 let bids = obj
137 .get("bids")
138 .ok_or_else(|| PolyfillError::parse("Missing bids", None))?
139 .as_array()
140 .ok_or_else(|| PolyfillError::parse("Invalid bids", None))?;
141 let asks = obj
142 .get("asks")
143 .ok_or_else(|| PolyfillError::parse("Missing asks", None))?
144 .as_array()
145 .ok_or_else(|| PolyfillError::parse("Invalid asks", None))?;
146
147 parsed_levels.clear();
148
149 let should_parse = books.with_book_mut(asset_id, |book| {
150 book.should_apply_ws_book_update(asset_id, timestamp, hash)
151 })?;
152 if !should_parse {
153 return Ok(WsBookApplyStats {
154 book_messages: 1,
155 book_levels_applied: 0,
156 });
157 }
158
159 let parsed_count = match (|| {
160 collect_levels(Side::BUY, bids, parsed_levels)?;
161 collect_levels(Side::SELL, asks, parsed_levels)?;
162 Ok(parsed_levels.len())
163 })() {
164 Ok(parsed_count) => parsed_count,
165 Err(error) => {
166 parsed_levels.clear();
167 return Err(error);
168 },
169 };
170
171 let result = books.with_book_mut(asset_id, |book| {
172 if book.apply_ws_book_snapshot_fast(asset_id, timestamp, hash, parsed_levels)? {
173 Ok(parsed_count)
174 } else {
175 Ok(0)
176 }
177 });
178 parsed_levels.clear();
179 let levels_applied = result?;
180
181 Ok(WsBookApplyStats {
182 book_messages: 1,
183 book_levels_applied: levels_applied,
184 })
185}
186
187fn parse_u64<'tape, 'input>(value: simd_json::tape::Value<'tape, 'input>) -> Option<u64> {
188 value
189 .as_u64()
190 .or_else(|| value.into_string().and_then(|s| s.parse::<u64>().ok()))
191}
192
193fn collect_levels<'tape, 'input>(
194 side: Side,
195 levels: simd_json::tape::Array<'tape, 'input>,
196 parsed_levels: &mut Vec<ParsedBookLevel>,
197) -> Result<usize> {
198 let mut applied = 0usize;
199 for level in levels.iter() {
200 let Some(obj) = level.as_object() else {
201 continue;
202 };
203
204 let price_str = obj
205 .get("price")
206 .and_then(|v| v.into_string())
207 .ok_or_else(|| PolyfillError::parse("Missing price", None))?;
208 let size_str = obj
209 .get("size")
210 .and_then(|v| v.into_string())
211 .ok_or_else(|| PolyfillError::parse("Missing size", None))?;
212
213 let price_ticks = parse_price_ticks_4dp(price_str)?;
214 let size_units = parse_qty_scaled_4dp(size_str)?;
215
216 parsed_levels.push(ParsedBookLevel {
217 side,
218 price_ticks,
219 size_units,
220 });
221 applied += 1;
222 }
223
224 Ok(applied)
225}
226
227#[inline]
228fn parse_price_ticks_4dp(value: &str) -> Result<Price> {
229 let scaled = parse_scaled_4_u64(value)?;
230 if scaled < MIN_PRICE_TICKS as u64 {
231 return Err(PolyfillError::validation("Invalid price"));
232 }
233 if scaled > MAX_PRICE_TICKS as u64 {
234 return Err(PolyfillError::validation("Invalid price"));
235 }
236
237 Ok(scaled as Price)
238}
239
240#[inline]
241fn parse_qty_scaled_4dp(value: &str) -> Result<Qty> {
242 let scaled = parse_scaled_4_u64(value)?;
243 if scaled > MAX_QTY as u64 {
244 return Err(PolyfillError::validation("Invalid size"));
245 }
246
247 Ok(scaled as Qty)
248}
249
250#[inline]
251fn parse_scaled_4_u64(value: &str) -> Result<u64> {
252 if value.is_empty() {
253 return Err(PolyfillError::parse("invalid decimal", None));
254 }
255
256 let mut whole = 0u64;
257 let mut frac = 0u64;
258 let mut frac_digits = 0u8;
259 let mut seen_dot = false;
260 let mut seen_digit = false;
261
262 for &byte in value.as_bytes() {
263 match byte {
264 b'0'..=b'9' => {
265 seen_digit = true;
266 let digit = (byte - b'0') as u64;
267 if seen_dot {
268 if frac_digits >= 4 {
269 if digit != 0 {
270 return Err(PolyfillError::parse("too many decimal places", None));
271 }
272 } else {
273 frac = frac
274 .checked_mul(10)
275 .and_then(|x| x.checked_add(digit))
276 .ok_or_else(|| PolyfillError::parse("scaled value overflow", None))?;
277 frac_digits += 1;
278 }
279 } else {
280 whole = whole
281 .checked_mul(10)
282 .and_then(|x| x.checked_add(digit))
283 .ok_or_else(|| PolyfillError::parse("scaled value overflow", None))?;
284 }
285 },
286 b'.' if !seen_dot => {
287 seen_dot = true;
288 },
289 _ => return Err(PolyfillError::parse("invalid decimal", None)),
290 }
291 }
292
293 if !seen_digit {
294 return Err(PolyfillError::parse("invalid decimal", None));
295 }
296
297 while frac_digits < 4 {
298 frac *= 10;
299 frac_digits += 1;
300 }
301
302 whole
303 .checked_mul(SCALE_FACTOR as u64)
304 .and_then(|x| x.checked_add(frac))
305 .ok_or_else(|| PolyfillError::parse("scaled value overflow", None))
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::types::{BookUpdate, OrderSummary};
312 use rust_decimal_macros::dec;
313
314 #[test]
315 fn fixed_point_parser_matches_expected_price_ticks() {
316 assert_eq!(parse_price_ticks_4dp("0.6543").unwrap(), 6543);
317 assert_eq!(parse_price_ticks_4dp("1.0000").unwrap(), 10_000);
318 assert_eq!(parse_price_ticks_4dp("1.000000").unwrap(), 10_000);
319 assert!(parse_price_ticks_4dp("0.00005").is_err());
320 assert!(parse_price_ticks_4dp("0").is_err());
321 assert!(parse_price_ticks_4dp("-0.1").is_err());
322 }
323
324 #[test]
325 fn fixed_point_parser_matches_expected_qty_units() {
326 assert_eq!(parse_qty_scaled_4dp("100.0").unwrap(), 1_000_000);
327 assert_eq!(parse_qty_scaled_4dp("0.0000").unwrap(), 0);
328 assert_eq!(parse_qty_scaled_4dp("1.234500").unwrap(), 12_345);
329 assert!(parse_qty_scaled_4dp("-50.5").is_err());
330 assert!(parse_qty_scaled_4dp("0.00004").is_err());
331 assert!(parse_qty_scaled_4dp("0.00005").is_err());
332 }
333
334 #[test]
335 fn processor_recovers_after_parse_and_validation_errors() {
336 let books = OrderBookManager::new(10);
337 books.get_or_create_book("test_asset_id").unwrap();
338 let mut processor = WsBookUpdateProcessor::new(1024);
339
340 let mut malformed_json = br#"{"event_type":"#.to_vec();
341 assert!(processor
342 .process_bytes(malformed_json.as_mut_slice(), &books)
343 .is_err());
344
345 let mut invalid_level = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1000,"bids":[{"price":"0.75001","size":"1.0000"}],"asks":[]}"#.to_vec();
346 assert!(processor
347 .process_bytes(invalid_level.as_mut_slice(), &books)
348 .is_err());
349
350 let mut valid_update = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1001,"bids":[{"price":"0.7500","size":"1.0000"}],"asks":[]}"#.to_vec();
351 let stats = processor
352 .process_bytes(valid_update.as_mut_slice(), &books)
353 .unwrap();
354
355 assert_eq!(stats.book_messages, 1);
356 assert_eq!(stats.book_levels_applied, 1);
357 }
358
359 #[test]
360 fn processor_error_keeps_existing_snapshot() {
361 let books = OrderBookManager::new(10);
362 books.get_or_create_book("test_asset_id").unwrap();
363 books
364 .apply_book_update(&BookUpdate {
365 asset_id: "test_asset_id".to_string(),
366 market: "0xabc".to_string(),
367 timestamp: 1000,
368 bids: vec![OrderSummary {
369 price: dec!(0.50),
370 size: dec!(10),
371 }],
372 asks: vec![OrderSummary {
373 price: dec!(0.60),
374 size: dec!(20),
375 }],
376 hash: None,
377 })
378 .unwrap();
379
380 let mut processor = WsBookUpdateProcessor::new(1024);
381 let mut invalid_snapshot = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1001,"bids":[{"price":"0.5100","size":"11.0000"},{"price":"0.51001","size":"12.0000"}],"asks":[{"price":"0.6100","size":"21.0000"}]}"#.to_vec();
382 assert!(processor
383 .process_bytes(invalid_snapshot.as_mut_slice(), &books)
384 .is_err());
385
386 let snapshot = books.get_book("test_asset_id").unwrap();
387 assert_eq!(snapshot.sequence, 0);
388 assert_eq!(snapshot.last_delta_sequence, 0);
389 assert_eq!(snapshot.last_snapshot_timestamp_ms, 1000);
390 assert_eq!(snapshot.timestamp.timestamp_millis(), 1000);
391 assert_eq!(snapshot.bids.len(), 1);
392 assert_eq!(snapshot.asks.len(), 1);
393 assert_eq!(snapshot.bids[0].price, dec!(0.50));
394 assert_eq!(snapshot.bids[0].size, dec!(10));
395 assert_eq!(snapshot.asks[0].price, dec!(0.60));
396 assert_eq!(snapshot.asks[0].size, dec!(20));
397 }
398
399 #[test]
400 fn processor_missing_side_keeps_existing_snapshot() {
401 let books = OrderBookManager::new(10);
402 books.get_or_create_book("test_asset_id").unwrap();
403 books
404 .apply_book_update(&BookUpdate {
405 asset_id: "test_asset_id".to_string(),
406 market: "0xabc".to_string(),
407 timestamp: 1000,
408 bids: vec![OrderSummary {
409 price: dec!(0.50),
410 size: dec!(10),
411 }],
412 asks: vec![OrderSummary {
413 price: dec!(0.60),
414 size: dec!(20),
415 }],
416 hash: None,
417 })
418 .unwrap();
419
420 let mut processor = WsBookUpdateProcessor::new(1024);
421 let mut missing_asks = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1001,"bids":[{"price":"0.5100","size":"11.0000"}]}"#.to_vec();
422 assert!(processor
423 .process_bytes(missing_asks.as_mut_slice(), &books)
424 .is_err());
425
426 let snapshot = books.get_book("test_asset_id").unwrap();
427 assert_eq!(snapshot.sequence, 0);
428 assert_eq!(snapshot.last_delta_sequence, 0);
429 assert_eq!(snapshot.last_snapshot_timestamp_ms, 1000);
430 assert_eq!(snapshot.timestamp.timestamp_millis(), 1000);
431 assert_eq!(snapshot.bids.len(), 1);
432 assert_eq!(snapshot.asks.len(), 1);
433 assert_eq!(snapshot.bids[0].price, dec!(0.50));
434 assert_eq!(snapshot.asks[0].price, dec!(0.60));
435 }
436
437 #[test]
438 fn processor_allows_same_timestamp_with_different_hash() {
439 let books = OrderBookManager::new(10);
440 books.get_or_create_book("test_asset_id").unwrap();
441 let mut processor = WsBookUpdateProcessor::new(1024);
442
443 let mut first = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1000,"hash":"hash_a","bids":[{"price":"0.5000","size":"10.0000"}],"asks":[{"price":"0.6000","size":"20.0000"}]}"#.to_vec();
444 let first_stats = processor
445 .process_bytes(first.as_mut_slice(), &books)
446 .unwrap();
447 assert_eq!(first_stats.book_levels_applied, 2);
448
449 let mut second = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1000,"hash":"hash_b","bids":[{"price":"0.5100","size":"11.0000"}],"asks":[{"price":"0.6100","size":"21.0000"}]}"#.to_vec();
450 let second_stats = processor
451 .process_bytes(second.as_mut_slice(), &books)
452 .unwrap();
453 assert_eq!(second_stats.book_levels_applied, 2);
454
455 let snapshot = books.get_book("test_asset_id").unwrap();
456 assert_eq!(snapshot.sequence, 0);
457 assert_eq!(snapshot.last_delta_sequence, 0);
458 assert_eq!(snapshot.last_snapshot_timestamp_ms, 1000);
459 assert_eq!(snapshot.timestamp.timestamp_millis(), 1000);
460 assert_eq!(snapshot.bids[0].price, dec!(0.51));
461 assert_eq!(snapshot.bids[0].size, dec!(11));
462 assert_eq!(snapshot.asks[0].price, dec!(0.61));
463 assert_eq!(snapshot.asks[0].size, dec!(21));
464
465 let mut duplicate = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1000,"hash":"hash_b","bids":[{"price":"0.5200","size":"12.0000"}],"asks":[{"price":"0.6200","size":"22.0000"}]}"#.to_vec();
466 let duplicate_stats = processor
467 .process_bytes(duplicate.as_mut_slice(), &books)
468 .unwrap();
469 assert_eq!(duplicate_stats.book_levels_applied, 0);
470
471 let snapshot = books.get_book("test_asset_id").unwrap();
472 assert_eq!(snapshot.bids[0].price, dec!(0.51));
473 assert_eq!(snapshot.asks[0].price, dec!(0.61));
474 }
475}