1use std::task::Poll;
11
12use moq_json::{ConsumerConfig, Producer, ProducerConfig};
13use serde_json::{Value, json};
14
15fn telemetry(tick: u64) -> Value {
17 let t = tick as f64;
19 let lat = 37.7749 + (t * 0.0001).sin() * 0.01;
20 let lon = -122.4194 + (t * 0.0001).cos() * 0.01;
21
22 json!({
23 "device": {
24 "id": "veh-4417-a2",
25 "model": "Sentinel X2",
26 "firmware": "4.18.2-rc1",
27 "serial": "SNX2-0000-4417-A2C9",
28 "region": "us-west-2",
29 "fleet": "logistics-prod",
30 "tags": ["cold-chain", "long-haul", "priority"],
31 },
32 "config": {
33 "sample_hz": 1,
34 "upload_hz": 1,
35 "geofence": "bay-area",
36 "thresholds": { "temp_c": 8.0, "humidity": 85, "shock_g": 3.5, "battery_pct": 15 },
37 "contacts": ["ops@example.com", "fleet@example.com"],
38 },
39 "ts": 1_700_000_000 + tick,
40 "uptime_s": tick,
41 "location": {
42 "lat": (lat * 1e6).round() / 1e6,
43 "lon": (lon * 1e6).round() / 1e6,
44 "alt_m": 12 + (tick % 5),
45 "heading": (tick * 7) % 360,
46 "speed_kph": 40 + (tick % 25),
47 "fix": "3d",
48 "sats": 9 + (tick % 3),
49 },
50 "sensors": {
51 "temp_c": (4.0 + (t * 0.05).sin() * 1.5 * 100.0).round() / 100.0,
52 "humidity": 60 + (tick % 10),
53 "shock_g": (((t * 0.3).sin().abs()) * 100.0).round() / 100.0,
54 "door_open": tick % 30 == 0,
55 },
56 "power": {
57 "battery_pct": 100 - (tick / 6) % 100,
58 "charging": false,
59 "voltage_mv": 12_400 - (tick % 50) as i64,
60 "current_ma": 850 + (tick % 120) as i64,
61 },
62 "network": {
63 "rssi_dbm": -70 - (tick % 15) as i64,
64 "type": "lte",
65 "bytes_up": 1_024 * tick,
66 "bytes_down": 256 * tick,
67 "latency_ms": 35 + (tick % 40),
68 },
69 "counters": {
70 "events": tick,
71 "errors": tick / 50,
72 "reconnects": tick / 120,
73 },
74 })
75}
76
77fn wire_bytes(config: ProducerConfig, ticks: u64) -> usize {
79 let track = moq_net::Track::new("telemetry").produce();
80 let consumer = track.consume();
81 let mut producer = Producer::<Value>::new(track, config);
82
83 for tick in 0..ticks {
84 producer.update(&telemetry(tick)).unwrap();
85 }
86 producer.finish().unwrap();
87
88 let waiter = kio::Waiter::noop();
90 let mut total = 0;
91 let mut track = consumer;
92 while let Poll::Ready(Ok(Some(mut group))) = track.poll_next_group(&waiter) {
93 while let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) {
94 total += frame.len();
95 }
96 }
97 total
98}
99
100fn verify(producer_config: ProducerConfig, ticks: u64) {
103 let track = moq_net::Track::new("telemetry").produce();
104 let consumer = track.consume();
105 let mut producer = Producer::<Value>::new(track, producer_config.clone());
106
107 let mut consumer_config = ConsumerConfig::default();
108 consumer_config.compression = producer_config.compression;
109 let mut consumer = moq_json::Consumer::<Value>::new(consumer, consumer_config);
110 let waiter = kio::Waiter::noop();
111
112 for tick in 0..ticks {
113 let expected = telemetry(tick);
114 producer.update(&expected).unwrap();
115 match consumer.poll_next(&waiter) {
118 Poll::Ready(Ok(Some(value))) => assert_eq!(value, expected, "tick {tick} reconstruction mismatch"),
119 other => panic!("tick {tick}: expected a value, got {other:?}"),
120 }
121 }
122 producer.finish().unwrap();
123
124 assert!(
126 matches!(consumer.poll_next(&waiter), Poll::Ready(Ok(None))),
127 "stream did not end cleanly"
128 );
129}
130
131fn verify_late_joiner(producer_config: ProducerConfig, ticks: u64) -> usize {
138 let track = moq_net::Track::new("telemetry").produce();
139 let consumer = track.consume();
140 let mut producer = Producer::<Value>::new(track, producer_config.clone());
141 for tick in 0..ticks {
142 producer.update(&telemetry(tick)).unwrap();
143 }
144 producer.finish().unwrap();
145
146 let mut consumer_config = ConsumerConfig::default();
147 consumer_config.compression = producer_config.compression;
148 let mut consumer = moq_json::Consumer::<Value>::new(consumer, consumer_config);
149 let waiter = kio::Waiter::noop();
150 let mut last = None;
151 let mut yielded = 0;
152 while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
153 last = Some(value);
154 yielded += 1;
155 }
156 assert_eq!(
157 last.as_ref(),
158 Some(&telemetry(ticks - 1)),
159 "late joiner reconstruction mismatch"
160 );
161 yielded
162}
163
164fn cfg(delta_ratio: u32, compression: bool) -> ProducerConfig {
165 let mut config = ProducerConfig::default();
166 config.delta_ratio = delta_ratio;
167 config.compression = compression;
168 config
169}
170
171fn main() {
172 const TICKS: u64 = 60;
173
174 let raw: usize = (0..TICKS)
176 .map(|t| serde_json::to_vec(&telemetry(t)).unwrap().len())
177 .sum();
178 let snapshot_len = serde_json::to_vec(&telemetry(0)).unwrap().len();
179
180 let combos = [
181 ("snapshot-per-group, plaintext", cfg(0, false)),
182 ("snapshot-per-group, deflate ", cfg(0, true)),
183 ("snapshot+delta, plaintext", cfg(8, false)),
184 ("snapshot+delta, deflate ", cfg(8, true)),
185 ];
186
187 println!("Telemetry stream: {TICKS} ticks, ~{snapshot_len} bytes per snapshot\n");
188 println!("Raw JSON (one blob per tick): {raw:>8} bytes (baseline)\n");
189
190 println!("{:<32} {:>10} {:>10} {:>9}", "config", "wire", "vs raw", "saved");
191 println!("{}", "-".repeat(64));
192 for (name, config) in combos.clone() {
193 verify(config.clone(), TICKS);
194 verify_late_joiner(config.clone(), TICKS);
195 let bytes = wire_bytes(config, TICKS);
196 let pct = 100.0 * bytes as f64 / raw as f64;
197 let saved = 100.0 - pct;
198 println!("{name:<32} {bytes:>8} B {pct:>8.1}% {saved:>7.1}%");
199 }
200
201 println!("\nVerified: every tick reconstructs exactly (live + late joiner) for all 4 configs.");
202
203 println!("\nLate joiner: values surfaced to the app (was {TICKS} per-frame, now collapsed):");
206 for (name, config) in combos {
207 let yielded = verify_late_joiner(config, TICKS);
208 println!(" {name:<32} {yielded:>3} value(s)");
209 }
210}