moqtap_client/track_locations.rs
1//! How far each track's objects have reached, for the drafts that make an
2//! end-of-track object's placement a protocol error.
3//!
4//! Six drafts state it. Draft-11 Section 9.1.1.1 gives the form that drafts 12
5//! and 13 repeat word for word: Object Status 0x4 "Indicates end of Track.
6//! GroupID is either the largest group produced in this track and the ObjectID
7//! is one greater than the largest object produced in that group, or GroupID is
8//! one greater than the largest group produced in this track and the ObjectID is
9//! zero. This status also indicates the last group has ended. An object with
10//! this status that has a Group ID less than any other GroupID, or an ObjectID
11//! less than or equal to the largest in the specified group, is a protocol
12//! error, and the receiver MUST terminate the session."
13//!
14//! Drafts 08, 09 and 10 spell the same prohibition against a status 0x4 that
15//! means "end of Track and Group", and carry a second status beside it — 0x5,
16//! "end of Track" — whose condition is one notch stricter: "An object with this
17//! status that has a Group ID less than or equal to any other Group ID, or an
18//! Object ID other than zero, is a protocol error, and the receiver MUST
19//! terminate the session." Draft-11 merged the two statuses and kept the looser
20//! condition, which is why one record answers both.
21//!
22//! This module holds only the observation. What each draft does about it is its
23//! own close table's business, as it is for every other rule broken on a data
24//! stream.
25//!
26//! # Two numbers per track, and that is not a shortcut
27//!
28//! "The largest object produced in that group" reads like a record of every
29//! group a track has carried, which for a live track is unbounded. It is not
30//! needed, and the reason is the other half of the same sentence.
31//!
32//! The Group ID condition says an end-of-track object may not name a group
33//! behind any the track has carried. So by the time the Object ID condition is
34//! read, "the specified group" is either the largest group the track has
35//! carried or one beyond it — and a group beyond it has carried no objects, so
36//! there is no largest in it to be at or behind. Every group below the largest
37//! is unreachable by the question. Two numbers answer it: the largest Group ID
38//! seen, and the largest Object ID seen *in that group*, which resets whenever
39//! a larger group arrives.
40//!
41//! # Only Normal objects are produced objects
42//!
43//! An object with a status is a statement about objects rather than one of
44//! them, and two of the statuses name an ID one past the end on purpose. Status
45//! 0x3, End of Group, has an "ObjectId ... one greater that the largest object
46//! produced in the group"; status 0x4 has an Object ID one greater again. So a
47//! record that counted an End of Group object as produced would raise the
48//! largest by one and then refuse the very End of Track object the draft
49//! defines — the rule would fire on a conforming track. Only status Normal is
50//! written down.
51//!
52//! # Why the key is the track and not the alias
53//!
54//! The same reason [`crate::forwarding_preference`] gives: an alias is free
55//! again the moment its subscription ends and may then name a different track,
56//! so a record kept against the alias would measure the second track's
57//! end-of-track object against the first track's groups. The alias is resolved
58//! through the endpoint's binding table before anything is written down.
59//!
60//! # Which objects reach this
61//!
62//! Objects a subscriber *receives*, on subgroup streams and in datagrams. The
63//! sentence names the receiver — "the receiver MUST terminate the session" —
64//! which is what separates this from the forwarding-preference rule beside it:
65//! that one names the Original Publisher, so its writers are gated too, and
66//! this one does not.
67//!
68//! # A second rule, and a third number
69//!
70//! Drafts 12 through 20 carry a Malformed Track condition this record is the
71//! right place for as well. Draft-12 Section 2.5: "An Object is received on a
72//! Track whose Group and Object ID are larger than the final Object in the
73//! Track. The final Object in a Track is the Object with Status END_OF_TRACK or
74//! the last Object sent in a FETCH whose response indicated End of Track."
75//! Drafts 13 through 17 carry it word for word; drafts 18, 19 and 20 drop the
76//! two words "on a Track" and change nothing else.
77//!
78//! **It is the mirror of the rule above rather than a widening of it.** That
79//! one asks whether an end-of-track object is behind what the track has
80//! carried, and ends the session over it. This one asks whether an ordinary
81//! object is past where the track ended, and withdraws from the track. Two
82//! rules, two answers, and the two numbers above answer neither of them for the
83//! other: a third is kept, the location the end-of-track object named, written
84//! down once that object has been judged.
85//!
86//! **Larger is the drafts' own comparison and not a reading of the words.**
87//! Every draft that carries the condition carries a Location Structure section
88//! with it, and that section settles the ordering outright — draft-12 Section
89//! 1.3.1 and draft-19 Section 1.4.2 give one Location as below another in the
90//! same words: "A.Group < B.Group || (A.Group == B.Group && A.Object <
91//! B.Object)". Lexicographic, and worth naming because the field-by-field
92//! reading the sentence also admits would let an object in a later group with a
93//! smaller Object ID escape, which is the plainest case of the fault there is.
94//!
95//! **The FETCH half of the definition is out of reach on this path**, and is
96//! not quietly folded into the other half. A fetch's objects arrive on a stream
97//! that opens by naming a Request ID and never a Track Alias, so nothing
98//! measuring objects here knows which track they belong to. What is written
99//! down is the END_OF_TRACK half, and a track whose end was only ever announced
100//! by a fetch response has no final object recorded to be past.
101//!
102//! # Objects that do not exist
103//!
104//! Draft-23 Section 12.1 adds: "An Object is received after it has been
105//! recorded as not existing, or is signalled as not existing after it was
106//! received (see Section 2.1.2)." [`TrackLocations::note_exists`](crate::track_locations::TrackLocations::note_exists)
107//! and [`TrackLocations::note_absent`](crate::track_locations::TrackLocations::note_absent) keep the two records that rule needs: the
108//! Objects received, as runs of Object IDs per Group, and the ranges signalled
109//! as not existing.
110
111use std::collections::BTreeMap;
112use std::sync::{Arc, Mutex};
113
114use moqtap_codec::types::TrackNamespace;
115
116/// The Group and Object an object names.
117///
118/// Not [`moqtap_codec::types::Location`], which carries the same pair as a
119/// pair of `VarInt`s because it is a field on the wire. This one is compared
120/// and maximised rather than encoded, so it holds the numbers themselves.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct ObjectLocation {
123 /// Group ID.
124 pub group: u64,
125 /// Object ID.
126 pub object: u64,
127}
128
129/// Which of the two conditions an end-of-track status carries.
130///
131/// The distinction is a draft's, not a caller's: drafts 08 through 10 have both
132/// statuses and drafts 11 through 13 have only the first.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum EndOfTrackForm {
135 /// The status whose Group ID names the track's *last* group — 0x4 on all
136 /// six drafts. The Group ID may equal the largest group seen, and the
137 /// Object ID must be past everything produced in it.
138 LastGroup,
139 /// The status whose Group ID names the group *after* the track's last —
140 /// 0x5 on drafts 08, 09 and 10, which draft-11 folded into 0x4. The Group
141 /// ID must be past every group seen.
142 ///
143 /// Its Object-ID-must-be-zero half needs no record and is refused by the
144 /// codec on the one header that carries it.
145 PastLastGroup,
146}
147
148/// Why an end-of-track object is not where the track ended.
149///
150/// Both halves carry what they were measured against, because "this is in the
151/// wrong place" and "this is in the wrong place, and here is the place the
152/// track had already reached" are different reports and only the second can be
153/// read by whoever has to fix it.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum EndOfTrackPlacement {
156 /// The Group ID is behind a group the track has already carried.
157 GroupBehind {
158 /// The largest Group ID the track has carried.
159 largest_group: u64,
160 },
161 /// The Object ID is at or behind the largest object produced in its own
162 /// group.
163 ObjectBehind {
164 /// The largest Object ID produced in that group.
165 largest_object: u64,
166 },
167}
168
169impl std::fmt::Display for EndOfTrackPlacement {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 match self {
172 EndOfTrackPlacement::GroupBehind { largest_group } => {
173 write!(f, "the track has already carried group {largest_group}")
174 }
175 EndOfTrackPlacement::ObjectBehind { largest_object } => {
176 write!(f, "that group's largest object is {largest_object}")
177 }
178 }
179 }
180}
181
182/// What went wrong with an object measured against its track's record.
183///
184/// Two rules meet here and their answers are opposites — one ends the session,
185/// the other gives up a track and leaves the session alone — so they are told
186/// apart at the point of detection rather than at the point of reporting.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum TrackFault {
189 /// An end-of-track object is not where the track ended.
190 EndOfTrackOutOfPlace(EndOfTrackPlacement),
191 /// An object arrived past the track's final object, carrying where the
192 /// track ended so the report can name it.
193 PastFinalObject(ObjectLocation),
194}
195
196/// One track and how far its objects have reached.
197struct TrackRecord {
198 namespace: TrackNamespace,
199 name: Vec<u8>,
200 /// The largest Group ID seen, and the largest Object ID seen in that group.
201 /// `None` until the track has carried an object with status Normal.
202 reached: Option<ObjectLocation>,
203 /// Where the track ended, once an end-of-track object has said so and been
204 /// found to be in a place the track could end at.
205 ///
206 /// `None` on a track still running, and on one whose end-of-track object
207 /// was refused — an object the draft calls a protocol error settles
208 /// nothing, so nothing after it is measured against where it claimed the
209 /// track stopped.
210 final_object: Option<ObjectLocation>,
211 /// The Objects received, per Group, as inclusive runs of Object IDs. Kept
212 /// only by [`TrackLocations::note_exists`].
213 received: BTreeMap<u64, Vec<(u64, u64)>>,
214 /// The inclusive Location ranges signalled as not existing. Kept only by
215 /// [`TrackLocations::note_absent`].
216 absent: Vec<(ObjectLocation, ObjectLocation)>,
217}
218
219impl TrackRecord {
220 fn new(namespace: &TrackNamespace, name: &[u8]) -> Self {
221 Self {
222 namespace: namespace.clone(),
223 name: name.to_vec(),
224 reached: None,
225 final_object: None,
226 received: BTreeMap::new(),
227 absent: Vec::new(),
228 }
229 }
230}
231
232/// Whether `at` lies in the inclusive range `from..=to`, in Location order.
233fn within(at: ObjectLocation, from: ObjectLocation, to: ObjectLocation) -> bool {
234 (from.group, from.object) <= (at.group, at.object)
235 && (at.group, at.object) <= (to.group, to.object)
236}
237
238/// Add `object` to a Group's sorted runs, merging runs it joins.
239fn insert_run(runs: &mut Vec<(u64, u64)>, object: u64) {
240 let i = runs.partition_point(|&(_, hi)| hi.saturating_add(1) < object);
241 if let Some(run) = runs.get_mut(i) {
242 if run.0 <= object && object <= run.1 {
243 return;
244 }
245 if run.0 == object.saturating_add(1) {
246 run.0 = object;
247 } else if run.1.saturating_add(1) == object {
248 run.1 = object;
249 if let Some(&(next_lo, next_hi)) = runs.get(i + 1) {
250 if next_lo == object.saturating_add(1) {
251 runs[i].1 = next_hi;
252 runs.remove(i + 1);
253 }
254 }
255 } else {
256 runs.insert(i, (object, object));
257 }
258 return;
259 }
260 runs.push((object, object));
261}
262
263/// How far each track's objects have reached, so far.
264///
265/// A list rather than a map, and for the same reason the endpoint's own table of
266/// track aliases is one: a session holds a handful of tracks, and
267/// [`TrackNamespace`] has no `Hash`.
268#[derive(Default)]
269pub struct TrackLocations {
270 tracks: Vec<TrackRecord>,
271}
272
273impl TrackLocations {
274 /// An empty record, for a session that has carried no objects yet.
275 pub fn new() -> Self {
276 Self { tracks: Vec::new() }
277 }
278
279 /// Record an object a track produced.
280 ///
281 /// Only status Normal reaches here; see the module documentation for why an
282 /// End of Group object would raise the largest object by one and refuse the
283 /// End of Track object that follows it.
284 pub fn observe(&mut self, namespace: &TrackNamespace, name: &[u8], at: ObjectLocation) {
285 let Some(record) = self.record_mut(namespace, name) else {
286 self.tracks
287 .push(TrackRecord { reached: Some(at), ..TrackRecord::new(namespace, name) });
288 return;
289 };
290 record.reached = Some(match record.reached {
291 None => at,
292 // A larger group starts the object count again: what is kept is the
293 // largest object *in the largest group*, and this is a different
294 // group.
295 Some(seen) if at.group > seen.group => at,
296 Some(seen) if at.group == seen.group && at.object > seen.object => at,
297 // A group behind the largest can never be the one an end-of-track
298 // object names, so nothing about it is worth keeping.
299 Some(seen) => seen,
300 });
301 }
302
303 /// Judge where an end-of-track object says the track ended.
304 ///
305 /// `Ok` when the track has carried nothing yet: there is no group for the
306 /// object to be behind, and a receiver that refused it would be enforcing an
307 /// ordering against an empty record.
308 pub fn check_end_of_track(
309 &self,
310 namespace: &TrackNamespace,
311 name: &[u8],
312 at: ObjectLocation,
313 form: EndOfTrackForm,
314 ) -> Result<(), EndOfTrackPlacement> {
315 let Some(reached) = self.record(namespace, name).and_then(|record| record.reached) else {
316 return Ok(());
317 };
318 match form {
319 EndOfTrackForm::PastLastGroup if at.group <= reached.group => {
320 Err(EndOfTrackPlacement::GroupBehind { largest_group: reached.group })
321 }
322 EndOfTrackForm::PastLastGroup => Ok(()),
323 EndOfTrackForm::LastGroup if at.group < reached.group => {
324 Err(EndOfTrackPlacement::GroupBehind { largest_group: reached.group })
325 }
326 // Past the largest group: that group has produced no objects, so
327 // there is no largest in it for this one to be at or behind.
328 EndOfTrackForm::LastGroup if at.group > reached.group => Ok(()),
329 EndOfTrackForm::LastGroup if at.object <= reached.object => {
330 Err(EndOfTrackPlacement::ObjectBehind { largest_object: reached.object })
331 }
332 EndOfTrackForm::LastGroup => Ok(()),
333 }
334 }
335
336 /// Write down where an end-of-track object says the track ended.
337 ///
338 /// Called only after [`Self::check_end_of_track`] has accepted it, so a
339 /// location this record would refuse never becomes the one later objects
340 /// are measured against.
341 ///
342 /// The first one is kept. A second end-of-track object on a track that has
343 /// already ended is itself past the final object on every ordering, so
344 /// letting it move the mark would answer the fault by adopting it.
345 pub fn note_final_object(
346 &mut self,
347 namespace: &TrackNamespace,
348 name: &[u8],
349 at: ObjectLocation,
350 ) {
351 let Some(record) = self.record_mut(namespace, name) else {
352 self.tracks
353 .push(TrackRecord { final_object: Some(at), ..TrackRecord::new(namespace, name) });
354 return;
355 };
356 if record.final_object.is_none() {
357 record.final_object = Some(at);
358 }
359 }
360
361 /// Judge an ordinary object against where the track ended.
362 ///
363 /// `Ok` on a track no end-of-track object has been seen for: there is no
364 /// final object for this one to be past, and a receiver that refused it
365 /// would be giving up a track for arriving.
366 ///
367 /// The comparison is the drafts' own Location ordering — Group first, and
368 /// the Object only where the Groups are equal — so an object in a later
369 /// group is past the end whatever its Object ID.
370 pub fn check_not_past_final(
371 &self,
372 namespace: &TrackNamespace,
373 name: &[u8],
374 at: ObjectLocation,
375 ) -> Result<(), ObjectLocation> {
376 let Some(final_object) = self.record(namespace, name).and_then(|r| r.final_object) else {
377 return Ok(());
378 };
379 if (at.group, at.object) > (final_object.group, final_object.object) {
380 return Err(final_object);
381 }
382 Ok(())
383 }
384
385 /// Record that the Object at `at` was received, unless the track has
386 /// recorded it as not existing.
387 ///
388 /// Draft-23 Section 12.1 counts "An Object is received after it has been
389 /// recorded as not existing" as a Malformed Track. The `Err` is `at`.
390 pub fn note_exists(
391 &mut self,
392 namespace: &TrackNamespace,
393 name: &[u8],
394 at: ObjectLocation,
395 ) -> Result<(), ObjectLocation> {
396 let record = self.record_or_insert(namespace, name);
397 if record.absent.iter().any(|&(from, to)| within(at, from, to)) {
398 return Err(at);
399 }
400 insert_run(record.received.entry(at.group).or_default(), at.object);
401 Ok(())
402 }
403
404 /// Record that every Object from `from` through `to`, inclusive, does not
405 /// exist, unless one of them was received.
406 ///
407 /// The other half of the same condition: an Object "is signalled as not
408 /// existing after it was received". The `Err` is the first received
409 /// Object the range covers.
410 pub fn note_absent(
411 &mut self,
412 namespace: &TrackNamespace,
413 name: &[u8],
414 from: ObjectLocation,
415 to: ObjectLocation,
416 ) -> Result<(), ObjectLocation> {
417 let record = self.record_or_insert(namespace, name);
418 for (&group, runs) in record.received.range(from.group..=to.group) {
419 for &(lo, hi) in runs {
420 let start = if group == from.group { lo.max(from.object) } else { lo };
421 let end = if group == to.group { hi.min(to.object) } else { hi };
422 if start <= end {
423 return Err(ObjectLocation { group, object: start });
424 }
425 }
426 }
427 record.absent.push((from, to));
428 Ok(())
429 }
430
431 fn record_or_insert(&mut self, namespace: &TrackNamespace, name: &[u8]) -> &mut TrackRecord {
432 let i = match self.tracks.iter().position(|t| t.namespace == *namespace && t.name == name) {
433 Some(i) => i,
434 None => {
435 self.tracks.push(TrackRecord::new(namespace, name));
436 self.tracks.len() - 1
437 }
438 };
439 &mut self.tracks[i]
440 }
441
442 fn record(&self, namespace: &TrackNamespace, name: &[u8]) -> Option<&TrackRecord> {
443 self.tracks.iter().find(|t| t.namespace == *namespace && t.name == name)
444 }
445
446 fn record_mut(&mut self, namespace: &TrackNamespace, name: &[u8]) -> Option<&mut TrackRecord> {
447 self.tracks.iter_mut().find(|t| t.namespace == *namespace && t.name == name)
448 }
449}
450
451/// What an Object Status makes of an object here.
452///
453/// Three answers and not two: a status is not only "ends the track or does
454/// not". End of Group and Object Does Not Exist are statements *about* objects
455/// that neither settle where a track has reached nor test it, and reading them
456/// as either would break the rule in one direction or the other.
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub enum ObjectRole {
459 /// Status Normal: an object the track produced, and the only kind written
460 /// down.
461 Produced,
462 /// An end-of-track status: the object that settles where the track ended.
463 ///
464 /// `Some(form)` on a draft that states a rule about where one may be
465 /// placed, naming which of the two conditions to judge it by, and `None` on
466 /// a draft that states none. Drafts 08 through 13 are the first; drafts 07
467 /// and 14 through 20 are the second, and
468 /// `a_track_may_end_where_it_has_already_been.rs` asserts the acceptance on
469 /// all seven of them. Either way the object settles where the track ended,
470 /// which is the other record's business and not that rule's.
471 EndsTrack(Option<EndOfTrackForm>),
472 /// Every other status.
473 Neither,
474}
475
476/// One track's record, held by whatever is reading that track's objects.
477///
478/// # Why the stream holds this and not the endpoint
479///
480/// The objects are read one at a time off a stream handle the caller owns, and
481/// that handle has no way back to the session — the same shape trap the close
482/// table has, and the reason `Connection::close_for_data_stream` exists. A
483/// record reachable only from the endpoint would leave the rule enforceable
484/// only by a caller that remembered to ask, which for a conformance crate is
485/// not enforcement. So the endpoint hands one of these to each stream it opens
486/// for a track it can name, the stream measures every object it decodes against
487/// it, and a stream nobody handed one to behaves exactly as it did before.
488///
489/// The clone is of the handle, not of the record: every stream on a session
490/// measures against the same [`TrackLocations`], which is what makes the
491/// largest group a property of the track rather than of one stream.
492#[derive(Clone)]
493pub struct TrackObjects {
494 locations: Arc<Mutex<TrackLocations>>,
495 namespace: TrackNamespace,
496 name: Vec<u8>,
497 /// The Track Alias this handle was resolved from.
498 ///
499 /// Carried for the report only, so it can name the track the way the wire
500 /// did. Nothing is keyed on it — see the module documentation for why the
501 /// record itself is keyed on the track.
502 alias: u64,
503}
504
505impl TrackObjects {
506 /// Bind a record to the track an alias resolved to.
507 pub fn new(
508 locations: Arc<Mutex<TrackLocations>>,
509 namespace: TrackNamespace,
510 name: Vec<u8>,
511 alias: u64,
512 ) -> Self {
513 Self { locations, namespace, name, alias }
514 }
515
516 /// The Track Alias this handle was resolved from.
517 pub fn alias(&self) -> u64 {
518 self.alias
519 }
520
521 /// Record or judge one object, by what its status makes of it.
522 ///
523 /// The one entry point both data paths use, so a subgroup object and a
524 /// datagram cannot come to disagree about which statuses count.
525 pub fn note(&self, at: ObjectLocation, role: ObjectRole) -> Result<(), EndOfTrackPlacement> {
526 match role {
527 ObjectRole::Produced => {
528 self.observe(at);
529 Ok(())
530 }
531 ObjectRole::EndsTrack(form) => {
532 if let Some(form) = form {
533 self.check_end_of_track(at, form)?;
534 }
535 self.note_final_object(at);
536 Ok(())
537 }
538 ObjectRole::Neither => Ok(()),
539 }
540 }
541
542 /// [`Self::note`], and the final-object rule with it, for the drafts that
543 /// state one.
544 ///
545 /// The two rules are checked in the order that gives each the objects it is
546 /// about. Where the track ended is asked first, because an object past the
547 /// end is one this endpoint is giving up the track over and there is
548 /// nothing to be gained by writing it into the record on the way past.
549 ///
550 /// # Every object, and not only the produced ones
551 ///
552 /// The sentence names an Object without qualifying it, and this reads it
553 /// that way: a second end-of-track object naming a later place, or an End
554 /// of Group beyond where the track stopped, is as much a track that carried
555 /// on after its end as an ordinary object would be. That is the opposite of
556 /// how [`TrackLocations::observe`] treats a status, and the two are not in
557 /// tension — a status is not a *produced* object, which is what that record
558 /// counts, but it is still an object *received*, which is what this one
559 /// asks about.
560 pub fn note_with_final_object(
561 &self,
562 at: ObjectLocation,
563 role: ObjectRole,
564 ) -> Result<(), TrackFault> {
565 self.check_not_past_final(at).map_err(TrackFault::PastFinalObject)?;
566 self.note(at, role).map_err(TrackFault::EndOfTrackOutOfPlace)
567 }
568
569 /// The final-object rule on its own, for the drafts that state no rule
570 /// about where an end-of-track object may be placed.
571 ///
572 /// The same rule [`Self::note_with_final_object`] applies, and a different
573 /// set of answers: a draft that judges nothing about placement has one
574 /// fault to report and not two, so its callers are handed the one rather
575 /// than an enum with an arm they cannot reach. Which entry point a draft
576 /// uses is the whole of what it says about the rule beside this one.
577 ///
578 /// An end-of-track object settles where the track ended whatever form its
579 /// status names, because the form only says how to judge a placement and
580 /// this judges none.
581 pub fn note_past_final(
582 &self,
583 at: ObjectLocation,
584 role: ObjectRole,
585 ) -> Result<(), ObjectLocation> {
586 self.check_not_past_final(at)?;
587 match role {
588 ObjectRole::Produced => self.observe(at),
589 ObjectRole::EndsTrack(_) => self.note_final_object(at),
590 ObjectRole::Neither => {}
591 }
592 Ok(())
593 }
594
595 /// Write down where an end-of-track object says the track ended. See
596 /// [`TrackLocations::note_final_object`].
597 pub fn note_final_object(&self, at: ObjectLocation) {
598 self.locked().note_final_object(&self.namespace, &self.name, at);
599 }
600
601 /// Judge an object against where the track ended. See
602 /// [`TrackLocations::check_not_past_final`].
603 pub fn check_not_past_final(&self, at: ObjectLocation) -> Result<(), ObjectLocation> {
604 self.locked().check_not_past_final(&self.namespace, &self.name, at)
605 }
606
607 /// Record that an Object was received. See [`TrackLocations::note_exists`].
608 pub fn note_exists(&self, at: ObjectLocation) -> Result<(), ObjectLocation> {
609 self.locked().note_exists(&self.namespace, &self.name, at)
610 }
611
612 /// Record a range of Objects signalled as not existing. See
613 /// [`TrackLocations::note_absent`].
614 pub fn note_absent(
615 &self,
616 from: ObjectLocation,
617 to: ObjectLocation,
618 ) -> Result<(), ObjectLocation> {
619 self.locked().note_absent(&self.namespace, &self.name, from, to)
620 }
621
622 /// Record an object this track produced. See [`TrackLocations::observe`].
623 pub fn observe(&self, at: ObjectLocation) {
624 self.locked().observe(&self.namespace, &self.name, at);
625 }
626
627 /// Judge an end-of-track object's placement. See
628 /// [`TrackLocations::check_end_of_track`].
629 pub fn check_end_of_track(
630 &self,
631 at: ObjectLocation,
632 form: EndOfTrackForm,
633 ) -> Result<(), EndOfTrackPlacement> {
634 self.locked().check_end_of_track(&self.namespace, &self.name, at, form)
635 }
636
637 /// The record, with a poisoned lock recovered rather than propagated.
638 ///
639 /// Nothing here can leave the record in a state a later reader is misled by:
640 /// every mutation is one field of one entry, and a panic between the read
641 /// and the write leaves the entry as it was.
642 fn locked(&self) -> std::sync::MutexGuard<'_, TrackLocations> {
643 self.locations.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
644 }
645}