plaza_client_utils 0.7.1

Client-side prediction, server reconciliation, and entity smoothing for real-time applications. No async runtime and no server crates, so it builds for wasm.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! Sliding-window acknowledgement: telling the other side exactly what arrived,
//! in sixteen bytes, however badly the link is behaving.
//!
//! Any protocol where one side must know what the other received faces the same
//! problem. Sending back the newest sequence number alone is cheap and says
//! nothing about the gaps behind it, so the sender is left choosing between
//! resending everything (bandwidth it usually does not need) and resending
//! nothing (a stall whenever a packet drops). Sending an explicit list of what
//! arrived is precise but grows with the loss rate, which is when there is
//! least room for it.
//!
//! [`AckWindow`] is the standard third answer: one sequence number plus a bitmask
//! of the 64 before it. It is fixed size, so a link losing half its packets
//! costs the same sixteen bytes as a perfect one. It is also precise enough that
//! a sender can resend exactly the gaps.
//!
//! It is pure sequence arithmetic: it does not know what a packet is, does not
//! allocate and never touches a socket. The receiver records arrivals, the
//! window is put on the wire as a pair of integers and the sender reconstructs
//! it to ask what is missing.
//!
//! ```
//! use plaza_client_utils::ack::AckWindow;
//!
//! // Receiver: 7 went missing.
//! let mut window = AckWindow::new();
//! for seq in [4, 5, 6, 8, 9] {
//!   window.observe(seq);
//! }
//!
//! // On the wire: two integers.
//! let (newest, mask) = window.encode().unwrap();
//!
//! // Sender: rebuild and ask what to resend.
//! let peer = AckWindow::from_encoded(newest, mask);
//! assert_eq!(peer.missing_since(4).collect::<Vec<_>>(), vec![7]);
//! ```

/// How many sequence numbers behind the newest the mask covers.
pub const WINDOW: u64 = 64;

/// A record of which recent sequence numbers arrived: the newest, plus a bitmask
/// of the [`WINDOW`] before it.
///
/// Bit `i` of the mask stands for `newest - 1 - i`, so bit 0 is the immediate
/// predecessor. Anything older than the window falls out and is reported as
/// neither received nor missing, because past the window a sender should give
/// up rather than resend forever.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AckWindow {
  newest: u64,
  mask: u64,
  started: bool,
}

impl AckWindow {
  pub fn new() -> Self {
    Self::default()
  }

  /// Rebuilds a window from a received `(newest, mask)` pair.
  pub fn from_encoded(newest: u64, mask: u64) -> Self {
    Self { newest, mask, started: true }
  }

  /// The pair to put on the wire, or `None` if nothing has arrived yet.
  pub fn encode(&self) -> Option<(u64, u64)> {
    self.started.then_some((self.newest, self.mask))
  }

  /// Records an arrival. Returns whether it was new: a duplicate, or one that
  /// has already fallen out of the window, returns `false`.
  ///
  /// Handles reordering, so a straggler arriving after a newer packet is still
  /// recorded in its correct slot rather than being taken for the new newest.
  pub fn observe(&mut self, seq: u64) -> bool {
    if !self.started {
      self.started = true;
      self.newest = seq;
      self.mask = 0;
      return true;
    }
    if seq > self.newest {
      // Shift the window forward. The old newest becomes a set bit, and anything
      // that scrolls past the end is forgotten.
      //
      // A shift of exactly `WINDOW` needs care: every old bit falls out, but
      // the old newest lands in the last slot and must be kept. Using the same
      // threshold for the shift and the "too far" test loses it and only a test
      // at exactly this boundary shows the loss.
      let shift = seq - self.newest;
      let shifted = if shift >= 64 { 0 } else { self.mask << shift };
      self.mask = if shift <= WINDOW { shifted | (1u64 << (shift - 1)) } else { 0 };
      self.newest = seq;
      true
    } else if seq == self.newest {
      false
    } else {
      let back = self.newest - seq;
      if back > WINDOW {
        return false;
      }
      let bit = 1u64 << (back - 1);
      let was_set = self.mask & bit != 0;
      self.mask |= bit;
      !was_set
    }
  }

  /// The highest sequence number recorded, if any.
  pub fn newest(&self) -> Option<u64> {
    self.started.then_some(self.newest)
  }

  /// The mask as it would go on the wire. Bit `i` is `newest - 1 - i`.
  pub fn mask(&self) -> u64 {
    self.mask
  }

  /// Whether `seq` is recorded as arrived. Anything outside the window is
  /// `false`, including sequence numbers newer than the newest seen.
  pub fn contains(&self, seq: u64) -> bool {
    if !self.started || seq > self.newest {
      return false;
    }
    if seq == self.newest {
      return true;
    }
    let back = self.newest - seq;
    back <= WINDOW && self.mask & (1u64 << (back - 1)) != 0
  }

  /// The gaps from `oldest` up to the newest, ascending.
  ///
  /// This is what a sender resends. Clamped to the window, so a peer that has
  /// fallen far behind asks for a bounded amount of work rather than the whole
  /// history. Past the window the data cannot be recovered anyway and the
  /// caller should resynchronise instead of backfilling.
  pub fn missing_since(&self, oldest: u64) -> impl Iterator<Item = u64> + '_ {
    let floor = self.newest.saturating_sub(WINDOW).max(oldest);
    let end = if self.started { self.newest } else { 0 };
    (floor..end).filter(move |seq| self.started && !self.contains(*seq))
  }

  /// The newest sequence such that **everything** from `first` up to it arrived,
  /// with no gap anywhere in between.
  ///
  /// A delta-compressing protocol wants this rather than
  /// [`newest`](Self::newest). A protocol that *retransmits* needs to know which
  /// packets are missing, which is the mask. A protocol that *re-derives* needs a
  /// state the peer provably reached. Receiving packet N+1 after losing N does
  /// not put a peer in the state N+1 implies: whatever N announced and N+1 had
  /// no reason to repeat is gone. Taking the newest set bit as the baseline
  /// gives the sender a state that never existed. The bug is hard to see,
  /// because the traffic looks correct while the divergence is permanent. When
  /// measured, it made loss recovery statistically indistinguishable from no
  /// recovery at every loss rate.
  ///
  /// `first` is the oldest sequence not yet accounted for: one past the baseline
  /// a sender last settled on, or the oldest packet it still holds if it has
  /// settled on none. Expressed as the first sequence to *check* rather than the
  /// last one known, because a protocol numbering from zero has no representable
  /// "one before the first".
  ///
  /// Returns `None` when the run is empty. That covers two cases a caller
  /// treats the same way: `first` did not arrive or it is older than the window
  /// can describe after a reconnect or a long stall. In both the frontier
  /// cannot advance and it should not move *backwards* either. A naive version
  /// sticks at an old base forever instead of treating the state as unreachable
  /// and resynchronising. Use [`missing_since`](Self::missing_since) to tell
  /// the two apart when it matters.
  ///
  /// Costs at most [`WINDOW`] steps, never a scan of history.
  ///
  /// ```
  /// # use plaza_client_utils::ack::AckWindow;
  /// let mut window = AckWindow::new();
  /// for seq in [1u64, 2, 3, 5] {
  ///   window.observe(seq);
  /// }
  /// // 5 arrived, but 4 did not, so the newest state the peer provably reached
  /// // is the one after 3.
  /// assert_eq!(window.newest(), Some(5));
  /// assert_eq!(window.contiguous_base(1), Some(3));
  /// ```
  pub fn contiguous_base(&self, first: u64) -> Option<u64> {
    if !self.contains(first) {
      return None;
    }
    let mut base = first;
    while base < self.newest && self.contains(base + 1) {
      base += 1;
    }
    Some(base)
  }

  /// How many of the window's slots are filled, the newest included. A cheap
  /// delivery-rate readout: `received_in_window` over the span it covers.
  pub fn received_in_window(&self) -> u32 {
    if !self.started {
      return 0;
    }
    1 + self.mask.count_ones()
  }

  /// Forgets everything.
  pub fn reset(&mut self) {
    *self = Self::new();
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn a_clean_run_records_everything() {
    let mut w = AckWindow::new();
    for seq in 0..40 {
      assert!(w.observe(seq), "seq {seq} was new");
    }
    assert_eq!(w.newest(), Some(39));
    for seq in 0..40 {
      assert!(w.contains(seq), "seq {seq} should be recorded");
    }
    assert_eq!(w.missing_since(0).count(), 0);
  }

  #[test]
  fn gaps_are_reported_and_nothing_else_is() {
    let mut w = AckWindow::new();
    for seq in [10u64, 11, 13, 14, 17] {
      w.observe(seq);
    }
    assert_eq!(w.missing_since(10).collect::<Vec<_>>(), vec![12, 15, 16]);
    assert!(!w.contains(12));
    assert!(w.contains(13));
  }

  #[test]
  fn a_straggler_lands_in_its_own_slot() {
    // Reordering is normal under jitter. A packet arriving after a newer one must
    // not be mistaken for the new newest, or the window would scroll backwards
    // and lose everything between.
    let mut w = AckWindow::new();
    w.observe(20);
    w.observe(25);
    assert!(w.observe(22), "the straggler is new");
    assert!(!w.observe(22), "and only new once");
    assert_eq!(w.newest(), Some(25));
    assert!(w.contains(22));
    assert_eq!(w.missing_since(20).collect::<Vec<_>>(), vec![21, 23, 24]);
  }

  #[test]
  fn a_big_jump_forgets_the_old_window_rather_than_lying_about_it() {
    // Past the window, "missing" and "never heard of" are the same to a sender,
    // and the right response to both is to stop backfilling.
    let mut w = AckWindow::new();
    for seq in 0..10 {
      w.observe(seq);
    }
    w.observe(500);
    assert_eq!(w.newest(), Some(500));
    assert!(!w.contains(9));
    assert_eq!(w.mask(), 0);
    // The ask stays bounded no matter how far back the caller points it.
    assert_eq!(w.missing_since(0).count(), WINDOW as usize);
  }

  #[test]
  fn the_encoding_round_trips() {
    let mut w = AckWindow::new();
    for seq in [100u64, 101, 104, 107, 108] {
      w.observe(seq);
    }
    let (newest, mask) = w.encode().unwrap();
    let peer = AckWindow::from_encoded(newest, mask);
    assert_eq!(peer, w);
    assert_eq!(peer.missing_since(100).collect::<Vec<_>>(), w.missing_since(100).collect::<Vec<_>>());
  }

  #[test]
  fn an_empty_window_encodes_to_nothing() {
    let w = AckWindow::new();
    assert_eq!(w.encode(), None);
    assert_eq!(w.newest(), None);
    assert!(!w.contains(0));
    assert_eq!(w.missing_since(0).count(), 0);
  }

  #[test]
  fn the_cost_does_not_grow_with_the_loss_rate() {
    // Unlike an explicit list, a link dropping most of its packets reports in
    // the same sixteen bytes as a perfect one.
    let mut clean = AckWindow::new();
    let mut awful = AckWindow::new();
    for seq in 0..64u64 {
      clean.observe(seq);
      if seq % 5 == 0 {
        awful.observe(seq);
      }
    }
    assert!(clean.encode().is_some() && awful.encode().is_some());
    assert_eq!(clean.received_in_window(), 64);
    assert_eq!(awful.received_in_window(), 13);
    assert_eq!(core::mem::size_of_val(&clean.encode().unwrap()), core::mem::size_of_val(&awful.encode().unwrap()));
  }

  #[test]
  fn the_contiguous_base_stops_at_the_first_gap_not_the_newest_bit() {
    // A retransmitting protocol wants the mask. A re-deriving one wants a state
    // the peer provably reached and 5 arriving after 4 was lost does not put
    // the peer in state 5.
    let mut w = AckWindow::new();
    for seq in [1u64, 2, 3, 5, 6, 7] {
      w.observe(seq);
    }
    assert_eq!(w.newest(), Some(7));
    assert_eq!(w.contiguous_base(1), Some(3), "took a state the peer never reached");
  }

  #[test]
  fn a_run_that_reaches_the_newest_reports_it() {
    let mut w = AckWindow::new();
    for seq in 1..=10u64 {
      w.observe(seq);
    }
    assert_eq!(w.contiguous_base(1), Some(10));
    assert_eq!(w.contiguous_base(10), Some(10), "a run of one is still a run");
    assert_eq!(w.contiguous_base(11), None, "nothing beyond the newest has arrived");
  }

  #[test]
  fn a_protocol_numbering_from_zero_is_representable() {
    // This takes the first sequence to check rather than the last one known
    // because a `u64` has no "one before sequence zero". A caller forced to
    // invent one lands on zero, which reads as "zero already arrived" and skips
    // the first packet.
    let mut w = AckWindow::new();
    for seq in [0u64, 1, 2] {
      w.observe(seq);
    }
    assert_eq!(w.contiguous_base(0), Some(2), "the very first packet is part of the run");
  }

  #[test]
  fn a_filled_gap_lets_the_frontier_jump_past_it() {
    // Reordering rather than loss: once the straggler arrives the frontier
    // should cover everything it was blocking.
    let mut w = AckWindow::new();
    for seq in [1u64, 2, 4, 5, 6] {
      w.observe(seq);
    }
    assert_eq!(w.contiguous_base(1), Some(2));
    w.observe(3);
    assert_eq!(w.contiguous_base(1), Some(6), "the whole run unblocked at once");
  }

  #[test]
  fn a_base_older_than_the_window_is_reported_as_unknowable() {
    // A naive version sticks at an old base forever after a reconnect or a
    // long stall. Contiguity cannot be established here, so the caller
    // resynchronises instead of backfilling.
    let mut w = AckWindow::new();
    w.observe(5);
    w.observe(1000);
    assert_eq!(w.contiguous_base(6), None, "the window cannot speak about 6..936");
    // Still answerable inside the window.
    assert_eq!(w.contiguous_base(1000), Some(1000));
  }

  #[test]
  fn an_empty_window_has_no_run_at_all() {
    let w = AckWindow::new();
    assert_eq!(w.contiguous_base(0), None);
    assert_eq!(w.contiguous_base(7), None);
  }

  #[test]
  fn the_frontier_costs_at_most_a_window_regardless_of_the_gap() {
    // `first` sitting exactly at the edge of the window is the worst case and
    // it is still bounded.
    let mut w = AckWindow::new();
    for seq in 0..=WINDOW {
      w.observe(seq);
    }
    assert_eq!(w.contiguous_base(0), Some(WINDOW));
    assert_eq!(w.contiguous_base(WINDOW - 1), Some(WINDOW));
    assert_eq!(w.contiguous_base(WINDOW), Some(WINDOW));
  }

  #[test]
  fn shifting_exactly_the_window_width_is_not_off_by_one() {
    // The boundary the shift arithmetic is easiest to get wrong at.
    let mut w = AckWindow::new();
    w.observe(0);
    w.observe(WINDOW);
    assert!(w.contains(0), "the oldest slot is still inside the window");
    w.observe(WINDOW + 1);
    assert!(!w.contains(0), "and one more step pushes it out");
  }
}