1use crate::message::{
15 BackendInfo, EffectRequest, ServerMessage, ShutdownReason, ViewDelta, ViewSnapshot,
16};
17use crate::BaseStamp;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ResyncReason {
22 DeltaBaseMismatch { expected: u64, found: u64 },
25 IncarnationChanged { current: u64 },
27 LinkLost,
29}
30
31#[derive(Debug, Clone, PartialEq)]
33pub enum ClientEvent {
34 View { generation: u64 },
36 ResyncRequired { reason: ResyncReason },
38 Effect { id: u64, effect: EffectRequest },
40 Closed { reason: ShutdownReason },
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
46pub enum ClientError {
47 #[error("the client is poisoned; resync before acting")]
48 Poisoned,
49 #[error("no view has been published yet")]
50 NoView,
51}
52
53#[derive(Debug)]
55pub struct Client {
56 incarnation: u64,
57 view: Option<ViewSnapshot>,
58 poisoned: Option<ResyncReason>,
59}
60
61impl Client {
62 pub fn new(backend: &BackendInfo) -> Self {
64 Self {
65 incarnation: backend.incarnation,
66 view: None,
67 poisoned: None,
68 }
69 }
70
71 pub fn generation(&self) -> u64 {
73 self.view.as_ref().map_or(0, |view| view.generation)
74 }
75
76 pub fn view(&self) -> Option<&ViewSnapshot> {
78 self.view.as_ref()
79 }
80
81 pub fn poisoned(&self) -> Option<ResyncReason> {
83 self.poisoned
84 }
85
86 pub fn base_stamp(&self) -> Result<BaseStamp, ClientError> {
90 if self.poisoned.is_some() {
91 return Err(ClientError::Poisoned);
92 }
93 let view = self.view.as_ref().ok_or(ClientError::NoView)?;
94 Ok(BaseStamp {
95 incarnation: self.incarnation,
96 generation: view.generation,
97 })
98 }
99
100 pub fn apply(&mut self, message: &ServerMessage) -> Vec<ClientEvent> {
104 match message {
105 ServerMessage::Snapshot { incarnation, view } => {
106 let mut events = Vec::new();
107 if *incarnation != self.incarnation {
108 events.push(ClientEvent::ResyncRequired {
111 reason: ResyncReason::IncarnationChanged {
112 current: *incarnation,
113 },
114 });
115 self.incarnation = *incarnation;
116 }
117 self.poisoned = None;
118 let generation = view.generation;
119 self.view = Some(view.clone());
120 events.push(ClientEvent::View { generation });
121 events
122 }
123 ServerMessage::Delta { incarnation, delta } => self.apply_delta(*incarnation, delta),
124 ServerMessage::Effect { id, effect } => vec![ClientEvent::Effect {
125 id: *id,
126 effect: effect.clone(),
127 }],
128 ServerMessage::Bye { reason } => {
129 self.poisoned = Some(ResyncReason::LinkLost);
130 vec![ClientEvent::Closed { reason: *reason }]
131 }
132 ServerMessage::Welcome { backend, .. } => {
133 if backend.incarnation != self.incarnation {
134 self.incarnation = backend.incarnation;
135 self.view = None;
136 self.poisoned = None;
137 vec![ClientEvent::ResyncRequired {
138 reason: ResyncReason::IncarnationChanged {
139 current: backend.incarnation,
140 },
141 }]
142 } else {
143 Vec::new()
144 }
145 }
146 ServerMessage::Ack { .. } | ServerMessage::Error { .. } => Vec::new(),
147 }
148 }
149
150 pub fn link_lost(&mut self) {
153 self.poisoned = Some(ResyncReason::LinkLost);
154 }
155
156 fn apply_delta(&mut self, incarnation: u64, delta: &ViewDelta) -> Vec<ClientEvent> {
157 let poison = |client: &mut Self, reason| {
158 client.poisoned = Some(reason);
159 vec![ClientEvent::ResyncRequired { reason }]
160 };
161 if incarnation != self.incarnation {
162 return poison(
163 self,
164 ResyncReason::IncarnationChanged {
165 current: incarnation,
166 },
167 );
168 }
169 if self.poisoned.is_some() {
170 return Vec::new();
172 }
173 let Some((generation, pane_count)) = self
176 .view
177 .as_ref()
178 .map(|view| (view.generation, view.panes.len()))
179 else {
180 return poison(
181 self,
182 ResyncReason::DeltaBaseMismatch {
183 expected: 0,
184 found: delta.base,
185 },
186 );
187 };
188 if delta.base != generation || delta.panes.len() != pane_count {
189 return poison(
192 self,
193 ResyncReason::DeltaBaseMismatch {
194 expected: generation,
195 found: delta.base,
196 },
197 );
198 }
199 let Some(view) = self.view.as_mut() else {
200 unreachable!("the view was present for validation above");
201 };
202 for (pane, change) in view.panes.iter_mut().zip(&delta.panes) {
203 if let crate::message::PaneDelta::Changed(changed) = change {
204 *pane = changed.clone();
205 }
206 }
207 if let Some(geometry) = delta.geometry {
208 view.geometry = geometry;
209 }
210 if let Some(active_pane) = delta.active_pane {
211 view.active_pane = active_pane;
212 }
213 if let Some(state) = &delta.state {
214 view.state = state.clone();
215 }
216 view.generation = delta.generation;
217 vec![ClientEvent::View {
218 generation: delta.generation,
219 }]
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::message::{Geometry, PaneDelta, PaneSnapshot, ViewBounds};
227
228 fn backend(incarnation: u64) -> BackendInfo {
229 BackendInfo {
230 name: "strop".into(),
231 version: "0.0.0".into(),
232 build: None,
233 incarnation,
234 }
235 }
236
237 fn pane(lines: &[&str]) -> PaneSnapshot {
238 PaneSnapshot {
239 document: serde_json::from_value(serde_json::json!({"slot":0,"generation":0})).unwrap(),
240 revision: strop_core::id::BufferRevision::new(0),
241 bounds: ViewBounds::Complete,
242 cursor: 0,
243 view_top: 0,
244 hscroll: 0,
245 terminal_input: false,
246 overlays: false,
247 rect: Default::default(),
248 budget: Default::default(),
249 window_top: 0,
250 lines: lines.iter().map(|line| line.to_string()).collect(),
251 }
252 }
253
254 fn snapshot(generation: u64, lines: &[&str]) -> ViewSnapshot {
255 ViewSnapshot {
256 generation,
257 geometry: Geometry {
258 columns: 80,
259 rows: 24,
260 },
261 active_pane: 0,
262 panes: vec![pane(lines)],
263 state: serde_json::json!({"mode":"NORMAL"}),
264 }
265 }
266
267 fn delta(base: u64, generation: u64, lines: &[&str]) -> ViewDelta {
268 ViewDelta {
269 base,
270 generation,
271 geometry: None,
272 active_pane: None,
273 panes: vec![PaneDelta::Changed(pane(lines))],
274 state: None,
275 }
276 }
277
278 #[test]
279 fn snapshot_then_deltas_apply_in_order() {
280 let mut client = Client::new(&backend(1));
281 client.apply(&ServerMessage::Snapshot {
282 incarnation: 1,
283 view: snapshot(1, &["a"]),
284 });
285 assert_eq!(client.generation(), 1);
286 client.apply(&ServerMessage::Delta {
287 incarnation: 1,
288 delta: delta(1, 2, &["ab"]),
289 });
290 assert_eq!(client.generation(), 2);
291 assert_eq!(client.view().unwrap().panes[0].lines, ["ab"]);
292 assert!(client.poisoned().is_none());
293 assert_eq!(
294 client.base_stamp().unwrap(),
295 BaseStamp {
296 incarnation: 1,
297 generation: 2
298 }
299 );
300 }
301
302 #[test]
303 fn a_dropped_delta_poisons_until_resync() {
304 let mut client = Client::new(&backend(1));
305 client.apply(&ServerMessage::Snapshot {
306 incarnation: 1,
307 view: snapshot(1, &["a"]),
308 });
309 let events = client.apply(&ServerMessage::Delta {
311 incarnation: 1,
312 delta: delta(2, 3, &["abc"]),
313 });
314 assert_eq!(
315 events,
316 vec![ClientEvent::ResyncRequired {
317 reason: ResyncReason::DeltaBaseMismatch {
318 expected: 1,
319 found: 2
320 }
321 }]
322 );
323 assert_eq!(client.generation(), 1, "never applied onto a wrong base");
324 assert_eq!(
325 client.base_stamp().unwrap_err(),
326 ClientError::Poisoned,
327 "actions stop after the drop"
328 );
329 client.apply(&ServerMessage::Delta {
331 incarnation: 1,
332 delta: delta(3, 4, &["abcd"]),
333 });
334 assert!(client.poisoned().is_some());
335 client.apply(&ServerMessage::Snapshot {
336 incarnation: 1,
337 view: snapshot(4, &["abcd"]),
338 });
339 assert!(client.poisoned().is_none());
340 assert_eq!(client.view().unwrap().panes[0].lines, ["abcd"]);
341 }
342
343 #[test]
344 fn a_foreign_incarnation_poisons_deltas() {
345 let mut client = Client::new(&backend(1));
346 client.apply(&ServerMessage::Snapshot {
347 incarnation: 1,
348 view: snapshot(1, &["a"]),
349 });
350 let events = client.apply(&ServerMessage::Delta {
351 incarnation: 2,
352 delta: delta(1, 2, &["ab"]),
353 });
354 assert_eq!(
355 events,
356 vec![ClientEvent::ResyncRequired {
357 reason: ResyncReason::IncarnationChanged { current: 2 }
358 }]
359 );
360 assert_eq!(client.generation(), 1);
361 }
362
363 #[test]
364 fn a_pane_set_mismatch_poisons() {
365 let mut client = Client::new(&backend(1));
366 client.apply(&ServerMessage::Snapshot {
367 incarnation: 1,
368 view: snapshot(1, &["a"]),
369 });
370 let mut two_panes = delta(1, 2, &["ab"]);
371 two_panes.panes.push(PaneDelta::Unchanged);
372 client.apply(&ServerMessage::Delta {
373 incarnation: 1,
374 delta: two_panes,
375 });
376 assert!(client.poisoned().is_some());
377 assert_eq!(client.generation(), 1);
378 }
379
380 #[test]
381 fn bye_stops_actions_but_retains_the_known_view() {
382 let mut client = Client::new(&backend(1));
383 client.apply(&ServerMessage::Snapshot {
384 incarnation: 1,
385 view: snapshot(1, &["a"]),
386 });
387 client.apply(&ServerMessage::Bye {
388 reason: ShutdownReason::Requested,
389 });
390 assert_eq!(client.poisoned(), Some(ResyncReason::LinkLost));
391 assert_eq!(
392 client.view().unwrap().panes[0].lines,
393 ["a"],
394 "known outcomes are retained"
395 );
396 assert_eq!(client.base_stamp().unwrap_err(), ClientError::Poisoned);
397 }
398
399 #[test]
405 fn random_publication_sequences_never_diverge() {
406 struct Lcg(u64);
408 impl Lcg {
409 fn next(&mut self) -> u64 {
410 self.0 = self
411 .0
412 .wrapping_mul(6364136223846793005)
413 .wrapping_add(1442695040888963407);
414 self.0 >> 33
415 }
416 fn below(&mut self, n: u64) -> u64 {
417 self.next() % n
418 }
419 }
420
421 for seed in 0..64u64 {
422 let mut rand = Lcg(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1);
423 let mut client = Client::new(&backend(7));
424 let mut truth: Option<(u64, Vec<String>)> = None;
426 let mut poisoned_reference = true; for step in 0..40u64 {
428 let generation = step / 2 + 1;
429 let text = format!("s{seed}g{generation}");
430 let message = if step % 2 == 0 || truth.is_none() {
431 truth = Some((generation, vec![text.clone()]));
432 poisoned_reference = false;
433 ServerMessage::Snapshot {
434 incarnation: 7,
435 view: snapshot(generation, &[&text]),
436 }
437 } else {
438 let (base, _) = truth.clone().unwrap();
439 truth = Some((generation, vec![text.clone()]));
440 if rand.below(3) == 0 {
443 poisoned_reference = true;
444 continue; }
446 ServerMessage::Delta {
447 incarnation: 7,
448 delta: delta(base, generation, &[&text]),
449 }
450 };
451 client.apply(&message);
452 match (&client.view().cloned(), &truth) {
453 (Some(view), Some((_, lines))) if !poisoned_reference => {
454 assert_eq!(&view.panes[0].lines, lines, "seed {seed} step {step}");
455 assert!(client.poisoned().is_none());
456 }
457 _ => {
458 if poisoned_reference {
459 assert!(
460 client.poisoned().is_some() || client.view().is_none(),
461 "seed {seed} step {step}: a gap must poison"
462 );
463 }
464 }
465 }
466 if let Some(view) = client.view() {
470 assert!(view.generation <= generation);
471 if client.poisoned().is_none() {
472 assert_eq!(Some(view.generation), truth.as_ref().map(|t| t.0));
473 }
474 }
475 }
476 }
477 }
478}