Skip to main content

moq_net/model/
group.rs

1//! A group is a stream of frames, split into a [Producer] and [Consumer] handle.
2//!
3//! A [Producer] writes an ordered stream of frames.
4//! Frames can be written all at once ([Producer::write_frame]), or in chunks
5//! ([Producer::create_frame]).
6//!
7//! A [Consumer] reads an ordered stream of frames.
8//! The reader can be cloned, in which case each reader receives a copy of each frame. (fanout)
9//!
10//! The stream is closed with [Error] when all writers or readers are dropped.
11use crate::cache;
12use crate::frame::{self, Frame, FrameBuf};
13use crate::{Timescale, stats, track};
14use std::collections::VecDeque;
15use std::mem::MaybeUninit;
16use std::sync::Arc;
17use std::task::{Poll, ready};
18
19use crate::{Error, IntoBytes, Result, Timestamp};
20
21/// Maximum total size of frames cached in a group before old frames are evicted.
22///
23/// Doubles as the per-frame size cap: a single frame can be at most this large (a
24/// larger declared size is refused before allocating), so one maximum-size frame can
25/// fill a group's cache.
26const MAX_GROUP_CACHE: u64 = 32 * 1024 * 1024; // 32 MB
27
28/// A group contains a sequence number because they can arrive out of order.
29///
30/// You can use [track::Producer::append_group] if you just want to +1 the sequence number.
31#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
32pub struct Info {
33	/// Per-track sequence number used to detect ordering and gaps. Higher numbers
34	/// supersede lower ones; consumers may skip late arrivals.
35	pub sequence: u64,
36}
37
38impl Info {
39	/// Create an untimed producer for this group.
40	///
41	/// Test-only: real groups are created via [`track::Producer`], which
42	/// supplies the parent track's [`track::Info`]. This helper exists for in-crate
43	/// tests that don't exercise timestamps.
44	#[cfg(test)]
45	pub(crate) fn produce(self) -> Producer {
46		Producer::new(self, track::Info::default(), Default::default())
47	}
48}
49
50impl From<usize> for Info {
51	fn from(sequence: usize) -> Self {
52		Self {
53			sequence: sequence as u64,
54		}
55	}
56}
57
58impl From<u64> for Info {
59	fn from(sequence: u64) -> Self {
60		Self { sequence }
61	}
62}
63
64impl From<u32> for Info {
65	fn from(sequence: u32) -> Self {
66		Self {
67			sequence: sequence as u64,
68		}
69	}
70}
71
72impl From<u16> for Info {
73	fn from(sequence: u16) -> Self {
74		Self {
75			sequence: sequence as u64,
76		}
77	}
78}
79
80/// The in-flight (tail) frame being written. At most one exists at a time, since a
81/// group is a single ordered stream.
82pub(crate) struct Partial {
83	timestamp: Timestamp,
84	buf: FrameBuf,
85}
86
87/// Shared group state. `pub(crate)` so [`frame`] handles can observe the abort flag
88/// while streaming a partial frame.
89#[derive(Default)]
90pub(crate) struct GroupState {
91	// Completed frames, each a contiguous payload. Evicted frames are popped from the
92	// front; `offset` tracks how many.
93	pub(crate) frames: VecDeque<Frame>,
94
95	// The single in-flight frame, if one is open.
96	pub(crate) partial: Option<Partial>,
97
98	// The number of frames evicted from the front of the group.
99	pub(crate) offset: usize,
100
101	// The total size (in bytes) of all cached frames plus any in-flight frame.
102	pub(crate) cache: u64,
103
104	// Mirrors `cache` into the track's shared cache pool, so the group's bytes count
105	// against the byte budget tracks evict toward.
106	charge: cache::Charge,
107
108	// Once finalized, the total number of frames the group will ever contain. Recorded
109	// at finish so the count outlives an abort that clears the cache.
110	pub(crate) fin: Option<usize>,
111
112	// The error that caused the group to be aborted, if any.
113	pub(crate) abort: Option<Error>,
114}
115
116impl GroupState {
117	/// Resolve the source for the frame at `index`: a completed frame (whole) or the
118	/// in-flight tail (streamed). Used by [`Consumer::poll_next_frame`].
119	fn poll_frame_source(&self, index: usize) -> Poll<Result<Option<(frame::Info, frame::Source)>>> {
120		if index < self.offset {
121			return Poll::Ready(Err(Error::Lagged));
122		}
123		let local = index - self.offset;
124		if let Some(f) = self.frames.get(local) {
125			let info = frame::Info {
126				size: f.payload.len() as u64,
127				timestamp: f.timestamp,
128			};
129			return Poll::Ready(Ok(Some((info, frame::Source::Complete(f.payload.clone())))));
130		}
131		if local == self.frames.len()
132			&& let Some(p) = &self.partial
133		{
134			let info = frame::Info {
135				size: p.buf.capacity() as u64,
136				timestamp: p.timestamp,
137			};
138			return Poll::Ready(Ok(Some((info, frame::Source::Partial(p.buf.clone())))));
139		}
140		ready!(self.poll_terminal(index))?;
141		Poll::Ready(Ok(None))
142	}
143
144	/// Resolve the group's terminal state for a reader positioned at `index`.
145	///
146	/// A finished group is still aborted once its frames are released to free memory
147	/// (aged out of the track's latency window, or evicted by the cache pool). A reader
148	/// that already consumed every frame is missing nothing, so it gets the clean end of
149	/// group; one that fell short sees the abort rather than a silently truncated stream.
150	fn poll_terminal(&self, index: usize) -> Poll<Result<()>> {
151		match (self.fin, &self.abort) {
152			(Some(total), Some(err)) if index < total => Poll::Ready(Err(err.clone())),
153			(Some(_), _) => Poll::Ready(Ok(())),
154			(None, Some(err)) => Poll::Ready(Err(err.clone())),
155			(None, None) => Poll::Pending,
156		}
157	}
158
159	fn poll_finished(&self) -> Poll<Result<u64>> {
160		// The count is recorded at finish, so a later abort that cleared the cache
161		// doesn't turn a complete group into an error.
162		if let Some(total) = self.fin {
163			Poll::Ready(Ok(total as u64))
164		} else if let Some(err) = &self.abort {
165			Poll::Ready(Err(err.clone()))
166		} else {
167			Poll::Pending
168		}
169	}
170
171	/// Evict completed frames from the front until within the byte budget.
172	fn evict(&mut self) {
173		while self.cache > MAX_GROUP_CACHE {
174			let Some(frame) = self.frames.pop_front() else {
175				break;
176			};
177			let size = frame.payload.len() as u64;
178			self.cache -= size;
179			self.charge.sub(size);
180			self.offset += 1;
181		}
182	}
183
184	/// Drop the cached frames (and any in-flight tail) and release their pool charge.
185	fn release(&mut self) {
186		self.frames.clear();
187		self.partial = None;
188		self.cache = 0;
189		self.charge.clear();
190	}
191}
192
193fn modify(state: &kio::Producer<GroupState>) -> Result<kio::Mut<'_, GroupState>> {
194	state.write().map_err(|r| r.abort.clone().unwrap_or(Error::Dropped))
195}
196
197/// Writes frames to a group in order.
198///
199/// Each group is delivered independently over a QUIC stream.
200/// Use [Self::write_frame] for simple single-buffer frames,
201/// or [Self::create_frame] for multi-chunk streaming writes.
202pub struct Producer {
203	// Mutable stream state.
204	state: kio::Producer<GroupState>,
205
206	// The group header containing the sequence number. A small `Copy` value,
207	// inherited by each frame (see [`Self::create_frame`]).
208	info: Info,
209
210	// The parent track's properties, inherited rather than passed piecemeal. Its
211	// `timescale` is used by [`Self::create_frame`] to normalize every frame's
212	// timestamp into the track scale before it enters the stream. Threaded down by
213	// value from [`track::Producer::create_group`] / `append_group`.
214	track: track::Info,
215
216	// The parent track's account against the shared cache pool. Held here as well as
217	// in the group's `cache::Charge` so a frame write can settle the track's eviction
218	// debt with the group lock released.
219	cache: Arc<cache::Track>,
220
221	// Ingress payload meter, set by a tagged [`track::Producer`] via
222	// [`Self::with_meter`]. Empty (no-op) for an untagged group.
223	stats: stats::Meter,
224
225	// Shared by every clone: its `Drop` is the abrupt-teardown, running exactly once
226	// when the last of them goes.
227	alive: Arc<Alive>,
228}
229
230/// Ends the group when the last [`Producer`] clone drops, including the clone the
231/// parent track holds in its cache.
232///
233/// A refcount rather than a "am I the last one?" check inside `Drop`: that answer is
234/// a snapshot, and acting on it is exactly what can invalidate it. Holding a producer
235/// of its own also keeps the state writable until the teardown has run, whatever order
236/// the last owner's fields drop in.
237struct Alive {
238	info: Info,
239	state: kio::Producer<GroupState>,
240}
241
242impl Drop for Alive {
243	fn drop(&mut self) {
244		// See track::Alive: the last producer dropping without a clean finish releases
245		// the cached frames so a stale consumer can't pin their buffers forever. A
246		// finished group keeps its cache so consumers can drain.
247		//
248		// Check Ok and Err: Ok is unreachable after a deliberate close.
249		match self.state.write() {
250			Ok(mut state) => {
251				if state.fin.is_some() || state.abort.is_some() {
252					return;
253				}
254				tracing::warn!(
255					sequence = self.info.sequence,
256					"group::Producer dropped without finish() or abort()"
257				);
258				state.release();
259			}
260			Err(state) => {
261				if state.fin.is_some() || state.abort.is_some() {
262					return;
263				}
264				tracing::warn!(
265					sequence = self.info.sequence,
266					"group::Producer dropped without finish() or abort()"
267				);
268			}
269		}
270	}
271}
272
273impl std::ops::Deref for Producer {
274	type Target = Info;
275
276	fn deref(&self) -> &Self::Target {
277		&self.info
278	}
279}
280
281impl Producer {
282	/// Create a group producer bound to its parent track's [`track::Info`] and cache
283	/// account.
284	///
285	/// Crate-private: groups are only constructed via [`track::Producer`], which
286	/// threads both down so properties like the timescale are inherited rather than
287	/// passed in. Every frame added to this group is normalized to the track's
288	/// timescale by [`Self::create_frame`].
289	///
290	/// Charges the group into `cache`, so its cached bytes count against the budget the
291	/// track evicts toward under memory pressure.
292	pub(crate) fn new(info: Info, track: track::Info, cache: Arc<cache::Track>) -> Self {
293		let state = kio::Producer::<GroupState>::default();
294		state.write().ok().expect("a new group is open").charge = cache.charge();
295		let alive = Arc::new(Alive {
296			info,
297			state: state.clone(),
298		});
299		Self {
300			info,
301			state,
302			track,
303			cache,
304			stats: stats::Meter::default(),
305			alive,
306		}
307	}
308
309	/// Attach an ingress payload meter, counting this as one delivered group.
310	/// Called by a tagged [`track::Producer`] when it creates the group.
311	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
312		meter.group();
313		self.stats = meter;
314		self
315	}
316
317	/// The group header.
318	pub(crate) fn info(&self) -> Info {
319		self.info
320	}
321
322	/// The parent track's timescale.
323	pub fn timescale(&self) -> Timescale {
324		self.track.timescale
325	}
326
327	/// A helper method to write a frame from a single byte buffer.
328	///
329	/// If you want to write multiple chunks, use [Self::create_frame] to get a frame producer.
330	/// But an upfront size is required.
331	///
332	/// `timestamp` is converted into the parent track's timescale. For data without
333	/// a presentation time, pass [`Timestamp::now`] explicitly.
334	pub fn write_frame<B: IntoBytes>(&mut self, timestamp: Timestamp, data: B) -> Result<()> {
335		let timestamp = timestamp
336			.convert(self.track.timescale)
337			.map_err(|_| Error::TimestampMismatch)?;
338		let payload = data.into_bytes();
339		if payload.len() as u64 > MAX_GROUP_CACHE {
340			return Err(Error::FrameTooLarge);
341		}
342
343		let mut state = modify(&self.state)?;
344		if state.fin.is_some() {
345			return Err(Error::Closed);
346		}
347		debug_assert!(state.partial.is_none(), "a frame is already open");
348		let size = payload.len() as u64;
349		state.cache += size;
350		state.charge.add(size);
351		state.frames.push_back(Frame { timestamp, payload });
352		state.evict();
353		drop(state);
354
355		// With the group lock released (lock order is track then group), settle
356		// eviction debt if enough has been written since the track last paid.
357		self.cache.settle();
358
359		// Ingress payload: one whole frame written.
360		self.stats.frames(1);
361		self.stats.bytes(size);
362		Ok(())
363	}
364
365	/// Create a frame with an upfront size and presentation timestamp, streamed in
366	/// chunks. Borrows the group exclusively until the returned [`frame::Producer`]
367	/// is finished or dropped, so only one frame is open at a time.
368	///
369	/// The `timestamp` is converted into the parent track's timescale, so the scale you
370	/// build it with doesn't have to match the track. Returns [`Error::FrameTooLarge`]
371	/// if the declared size exceeds the group's byte budget (refused before allocating)
372	/// or [`Error::TimestampMismatch`] if the timestamp can't be converted (overflow).
373	pub fn create_frame(&mut self, frame: frame::Info) -> Result<frame::Producer<'_>> {
374		let timestamp = frame
375			.timestamp
376			.convert(self.track.timescale)
377			.map_err(|_| Error::TimestampMismatch)?;
378		if frame.size > MAX_GROUP_CACHE {
379			return Err(Error::FrameTooLarge);
380		}
381		let buf = FrameBuf::new(frame.size as usize);
382
383		let mut state = modify(&self.state)?;
384		if state.fin.is_some() {
385			return Err(Error::Closed);
386		}
387		debug_assert!(state.partial.is_none(), "a frame is already open");
388		state.cache += frame.size;
389		state.charge.add(frame.size);
390		state.partial = Some(Partial {
391			timestamp,
392			buf: buf.clone(),
393		});
394		state.evict();
395		drop(state);
396
397		// With the group lock released (lock order is track then group), settle
398		// eviction debt if enough has been written since the track last paid.
399		self.cache.settle();
400
401		// Ingress payload: one frame opened; its bytes are counted per chunk as the
402		// frame::Producer writes them.
403		self.stats.frames(1);
404		let meter = self.stats.clone();
405
406		let info = frame::Info {
407			size: frame.size,
408			timestamp,
409		};
410		Ok(frame::Producer::new(self, buf, info).with_meter(meter))
411	}
412
413	/// Wake consumers parked on the group channel (called after a partial write).
414	pub(crate) fn frame_notify(&self) {
415		// Taking the write lock and dropping it triggers kio's notify.
416		let _ = self.state.write();
417	}
418
419	/// Commit the in-flight frame as a completed frame (called by [`frame::Producer::finish`]).
420	pub(crate) fn frame_commit(&mut self, frame: Frame) -> Result<()> {
421		let mut state = modify(&self.state)?;
422		// Bytes were already counted against the cache (and the pool charge) when the
423		// frame was created; committing just moves the tail into the completed set.
424		state.partial = None;
425		state.frames.push_back(frame);
426		Ok(())
427	}
428
429	/// Fail the group because an in-flight frame couldn't complete (called by
430	/// [`frame::Producer::abort`] / its drop).
431	pub(crate) fn frame_abort(&mut self, err: Error) {
432		let _ = self.clone().abort(err);
433	}
434
435	/// Return the number of frames written so far (completed plus any in-flight).
436	pub fn frame_count(&self) -> usize {
437		let state = self.state.read();
438		state.offset + state.frames.len() + state.partial.is_some() as usize
439	}
440
441	/// Mark the group as complete; no more frames will be written.
442	///
443	/// Borrows rather than consumes, so a later failure can still be reported through
444	/// [`abort`](Self::abort). The handle also keeps the cached frames readable.
445	pub fn finish(&mut self) -> Result<()> {
446		let mut state = modify(&self.state)?;
447		state.fin = Some(state.offset + state.frames.len());
448		Ok(())
449	}
450
451	/// Abort the group with the given error.
452	///
453	/// Consumes the handle. Drops the cached frames so a stale [`Consumer`] can't pin
454	/// their buffers in memory forever; consumers that haven't drained yet surface the
455	/// abort error instead of the leftover cache.
456	pub fn abort(self, err: Error) -> Result<()> {
457		let mut guard = modify(&self.state)?;
458		guard.abort = Some(err);
459		guard.release();
460		guard.close();
461		Ok(())
462	}
463
464	/// Whether the group has been aborted (including pool eviction). The track's
465	/// read paths treat an aborted cached group as absent.
466	pub(crate) fn is_aborted(&self) -> bool {
467		self.state.read().abort.is_some()
468	}
469
470	/// The group's full cached footprint (payload plus fixed overhead), used by the
471	/// track to size this group as an eviction victim.
472	pub(crate) fn cache_size(&self) -> u64 {
473		self.state.read().charge.size()
474	}
475
476	/// Tick of the group's last cache access, driving eviction protection and age
477	/// expiry (see [`cache::Pool::average`]).
478	pub(crate) fn cache_accessed(&self) -> u64 {
479		self.state.read().charge.accessed()
480	}
481
482	/// Enter the group into the evictable population: demoted from the live edge,
483	/// or inserted behind it. Idempotent; a no-op once the group is closed.
484	pub(crate) fn cache_demote(&self) {
485		if let Ok(mut state) = self.state.write() {
486			state.charge.demote();
487		}
488	}
489
490	/// Record a cache access (a FETCH hit, or a fetched backfill's birth),
491	/// protecting the group from eviction and restarting its expiry clock. A no-op
492	/// once the group is closed.
493	pub(crate) fn cache_refresh(&self) {
494		if let Ok(mut state) = self.state.write() {
495			state.charge.refresh();
496		}
497	}
498
499	/// Create a new consumer for the group.
500	pub fn consume(&self) -> Consumer {
501		Consumer {
502			info: self.info,
503			state: self.state.consume(),
504			track: self.track.clone(),
505			index: 0,
506			prefetch: Prefetch::default(),
507			// Untagged: a tagged track attaches the egress meter via `with_meter`
508			// when it hands the consumer to a subscriber/fetch.
509			stats: stats::Meter::default(),
510		}
511	}
512
513	/// Block until the group is closed or aborted.
514	pub async fn closed(&self) -> Error {
515		kio::wait(|waiter| self.poll_closed(waiter)).await
516	}
517
518	/// Poll until the group is closed or aborted; ready with the cause.
519	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
520		self.state.poll_closed(waiter).map(|()| self.abort_reason())
521	}
522
523	/// Block until there are no active consumers.
524	pub async fn unused(&self) -> Result<()> {
525		self.state.unused().await.map_err(|_| self.abort_reason())
526	}
527
528	/// The recorded abort reason, or [`Error::Dropped`] if the group closed without one.
529	fn abort_reason(&self) -> Error {
530		self.state.read().abort.clone().unwrap_or(Error::Dropped)
531	}
532}
533
534impl Clone for Producer {
535	fn clone(&self) -> Self {
536		Self {
537			info: self.info,
538			state: self.state.clone(),
539			track: self.track.clone(),
540			cache: self.cache.clone(),
541			stats: self.stats.clone(),
542			alive: self.alive.clone(),
543		}
544	}
545}
546
547/// A small inline batch of completed frames, drained from the shared group state
548/// under one lock and then handed out without re-locking.
549///
550/// Each [`Consumer::read_frame`] otherwise takes the group mutex and allocates a
551/// waker just to clone one `Bytes`; draining a batch amortizes both across `CAP`
552/// frames. Storage is inline and uninitialized (no heap), so a consumer that never
553/// reads whole frames, or drains through a higher-level buffer, pays nothing.
554struct Prefetch {
555	// Initialized, not-yet-taken frames are `frames[pos..len]`; the rest are uninitialized.
556	frames: [MaybeUninit<Frame>; Self::CAP],
557	pos: usize,
558	len: usize,
559}
560
561impl Prefetch {
562	const CAP: usize = 8;
563
564	/// Take the next buffered frame, or `None` if the batch is drained.
565	fn pop(&mut self) -> Option<Frame> {
566		if self.pos == self.len {
567			return None;
568		}
569		// SAFETY: `pos < len`, so this slot was written by `fill` and not yet taken.
570		let frame = unsafe { self.frames[self.pos].assume_init_read() };
571		self.pos += 1;
572		Some(frame)
573	}
574
575	/// Refill with up to `CAP` frames. Must be drained first (`pop` returned `None`).
576	fn fill(&mut self, frames: impl Iterator<Item = Frame>) {
577		debug_assert_eq!(self.pos, self.len, "fill on a non-empty batch would leak frames");
578		self.pos = 0;
579		self.len = 0;
580		for frame in frames.take(Self::CAP) {
581			self.frames[self.len].write(frame);
582			self.len += 1;
583		}
584	}
585
586	/// `(frame count, total payload bytes)` of the buffered, not-yet-taken frames.
587	/// Read once per fill to bump the egress payload counters for the whole batch.
588	fn buffered(&self) -> (u64, u64) {
589		let mut bytes = 0u64;
590		for slot in &self.frames[self.pos..self.len] {
591			// SAFETY: slots in `pos..len` are initialized (written by `fill`, not yet popped).
592			bytes += unsafe { slot.assume_init_ref() }.payload.len() as u64;
593		}
594		((self.len - self.pos) as u64, bytes)
595	}
596}
597
598impl Default for Prefetch {
599	fn default() -> Self {
600		Self {
601			frames: [const { MaybeUninit::uninit() }; Self::CAP],
602			pos: 0,
603			len: 0,
604		}
605	}
606}
607
608impl Drop for Prefetch {
609	fn drop(&mut self) {
610		for slot in &mut self.frames[self.pos..self.len] {
611			// SAFETY: slots in `pos..len` are initialized and were never taken.
612			unsafe { slot.assume_init_drop() };
613		}
614	}
615}
616
617/// Consume a group, frame-by-frame.
618pub struct Consumer {
619	// Shared state with the producer.
620	state: kio::Consumer<GroupState>,
621
622	// Immutable stream state.
623	info: Info,
624
625	// The parent track's info, inherited from the producer. Its `timescale` lets the
626	// wire publisher emit per-frame timestamps at the right scale for a fetched group.
627	track: track::Info,
628
629	// The number of frames we've read.
630	// NOTE: Cloned readers inherit this offset, but then run in parallel.
631	index: usize,
632
633	// A batch of completed frames drained ahead under one lock (whole-frame reads only).
634	prefetch: Prefetch,
635
636	// Egress payload meter, set by a tagged track via [`Self::with_meter`]. Empty
637	// (no-op) for an untagged group.
638	stats: stats::Meter,
639}
640
641impl Clone for Consumer {
642	fn clone(&self) -> Self {
643		// A clone shares the channel and inherits `index`, but starts with an empty
644		// prefetch: it re-reads its batch from the shared state, in parallel.
645		Self {
646			state: self.state.clone(),
647			info: self.info,
648			track: self.track.clone(),
649			index: self.index,
650			prefetch: Prefetch::default(),
651			// Inherit the meter without re-counting the group: the original already
652			// counted it when the track handed it out.
653			stats: self.stats.clone(),
654		}
655	}
656}
657
658impl std::ops::Deref for Consumer {
659	type Target = Info;
660
661	fn deref(&self) -> &Self::Target {
662		&self.info
663	}
664}
665
666impl Consumer {
667	/// Attach an egress payload meter, counting this as one delivered group.
668	/// Called by a tagged track when it hands the consumer to a subscriber or fetch.
669	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
670		meter.group();
671		self.stats = meter;
672		self
673	}
674
675	/// Whether the group has been aborted (including pool eviction); the abort
676	/// dropped the cached frames, so a held consumer has nothing left to read.
677	pub(crate) fn is_aborted(&self) -> bool {
678		self.state.read().abort.is_some()
679	}
680
681	/// Park `waiter` until the group closes (finish, abort, or eviction). Spliced
682	/// subscribers register on parked groups so an eviction wakes them; a group
683	/// that already closed cleanly can never abort, so no waiter is needed.
684	pub(crate) fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<()> {
685		self.state.poll_closed(waiter)
686	}
687
688	/// The parent track's timescale.
689	pub fn timescale(&self) -> Timescale {
690		self.track.timescale
691	}
692
693	// A helper to automatically apply Dropped if the state is closed without an error.
694	fn poll<F, R>(&self, waiter: &kio::Waiter, f: F) -> Poll<Result<R>>
695	where
696		F: Fn(&kio::Ref<'_, GroupState>) -> Poll<Result<R>>,
697	{
698		Poll::Ready(match ready!(self.state.poll(waiter, f)) {
699			Ok(res) => res,
700			// We try to clone abort just in case the function forgot to check for terminal state.
701			Err(state) => Err(state.abort.clone().unwrap_or(Error::Dropped)),
702		})
703	}
704
705	/// Return a consumer for the next frame for chunked reading.
706	pub async fn next_frame(&mut self) -> Result<Option<frame::Consumer>> {
707		kio::wait(|waiter| self.poll_next_frame(waiter)).await
708	}
709
710	/// Poll for the next frame, without blocking.
711	///
712	/// Returns None if the group is finished and the index is out of range.
713	pub fn poll_next_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Consumer>>> {
714		// Hand out any frames a prior read_frame prefetched before touching the tail.
715		// Their bytes were already counted at the batch fill, so the frame::Consumer
716		// carries no meter.
717		if let Some(frame) = self.prefetch.pop() {
718			self.index += 1;
719			let info = frame::Info {
720				size: frame.payload.len() as u64,
721				timestamp: frame.timestamp,
722			};
723			let source = frame::Source::Complete(frame.payload);
724			return Poll::Ready(Ok(Some(frame::Consumer::new(self.state.clone(), info, source))));
725		}
726
727		let index = self.index;
728		let Some((info, source)) = ready!(self.poll(waiter, |state| state.poll_frame_source(index))?) else {
729			return Poll::Ready(Ok(None));
730		};
731
732		self.index += 1;
733		// A direct read (not prefetched): count the frame here; the frame::Consumer
734		// counts its bytes per chunk as they're read out.
735		self.stats.frames(1);
736		Poll::Ready(Ok(Some(
737			frame::Consumer::new(self.state.clone(), info, source).with_meter(self.stats.clone()),
738		)))
739	}
740
741	/// Read the next frame (timestamp and payload) all at once, without blocking.
742	pub fn poll_read_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Frame>>> {
743		// Fast path: serve from the prefetched batch without locking or allocating a waker.
744		if let Some(frame) = self.prefetch.pop() {
745			self.index += 1;
746			return Poll::Ready(Ok(Some(frame)));
747		}
748
749		// The batch is drained: refill it under a single lock, registering the waiter if
750		// nothing is ready. Borrow the two fields disjointly so the closure can fill.
751		let index = self.index;
752		let prefetch = &mut self.prefetch;
753		let res = self.state.poll(waiter, |state| {
754			if index < state.offset {
755				return Poll::Ready(Err(Error::Lagged));
756			}
757			// `local` can run past the buffered count when frames were cleared or evicted out
758			// from under us (abort, unfinished drop, an eviction gap); clamp so `range` never
759			// panics on an out-of-bounds start. `fill` always resets the batch, so an empty
760			// range leaves `len == 0` and the terminal checks below resolve abort/fin/pending.
761			let local = (index - state.offset).min(state.frames.len());
762			prefetch.fill(state.frames.range(local..).cloned());
763			if prefetch.len > 0 {
764				return Poll::Ready(Ok(()));
765			}
766			// Nothing completed at `index`: an in-flight tail waits, otherwise resolve
767			// the terminal state (whole-frame reads never stream the partial).
768			state.poll_terminal(index)
769		});
770
771		match ready!(res) {
772			Ok(Ok(())) => {}
773			Ok(Err(err)) => return Poll::Ready(Err(err)),
774			Err(state) => return Poll::Ready(Err(state.abort.clone().unwrap_or(Error::Dropped))),
775		}
776
777		// A fresh batch was just filled (empty only on a clean end). Count the whole
778		// batch once here, under no lock, so the drained pops that follow stay free.
779		let (frames, bytes) = self.prefetch.buffered();
780		self.stats.frames(frames);
781		self.stats.bytes(bytes);
782
783		Poll::Ready(Ok(self.prefetch.pop().inspect(|_| {
784			self.index += 1;
785		})))
786	}
787
788	/// Read the next frame (timestamp and payload) all at once.
789	pub async fn read_frame(&mut self) -> Result<Option<frame::Frame>> {
790		// Serve from the prefetched batch without building a future or allocating a waker.
791		if let Some(frame) = self.prefetch.pop() {
792			self.index += 1;
793			return Ok(Some(frame));
794		}
795		kio::wait(|waiter| self.poll_read_frame(waiter)).await
796	}
797
798	/// Poll for the final number of frames in the group.
799	pub fn poll_finished(&mut self, waiter: &kio::Waiter) -> Poll<Result<u64>> {
800		self.poll(waiter, |state| state.poll_finished())
801	}
802
803	/// Block until the group is finished, returning the number of frames in the group.
804	pub async fn finished(&mut self) -> Result<u64> {
805		kio::wait(|waiter| self.poll_finished(waiter)).await
806	}
807}
808
809/// Options for a one-shot [`track::Consumer::fetch_group`] of a past group.
810#[derive(Clone, Debug, Default)]
811#[non_exhaustive]
812pub struct Fetch {
813	/// Delivery priority for the fetched group's stream. Defaults to 0.
814	pub priority: u8,
815}
816
817impl Fetch {
818	/// Set the delivery priority, returning `self` for chaining.
819	pub fn with_priority(mut self, priority: u8) -> Self {
820		self.priority = priority;
821		self
822	}
823}
824
825#[cfg(test)]
826mod test {
827	use super::*;
828	use crate::model::test_tracing::count_drop_warnings;
829	use bytes::Bytes;
830	use futures::FutureExt;
831
832	#[test]
833	fn basic_frame_reading() {
834		let mut producer = Info { sequence: 0 }.produce();
835		producer
836			.write_frame(Timestamp::ZERO, Bytes::from_static(b"frame0"))
837			.unwrap();
838		producer
839			.write_frame(Timestamp::ZERO, Bytes::from_static(b"frame1"))
840			.unwrap();
841		producer.finish().unwrap();
842
843		let mut consumer = producer.consume();
844		let f0 = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
845		assert_eq!(f0.size, 6);
846		let f1 = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
847		assert_eq!(f1.size, 6);
848		let end = consumer.next_frame().now_or_never().unwrap().unwrap();
849		assert!(end.is_none());
850	}
851
852	#[test]
853	fn read_frame_all_at_once() {
854		let mut producer = Info { sequence: 0 }.produce();
855		producer
856			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
857			.unwrap();
858		producer.finish().unwrap();
859
860		let mut consumer = producer.consume();
861		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
862		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
863	}
864
865	#[test]
866	fn read_frame_preserves_timestamp() {
867		let mut producer = Info { sequence: 0 }.produce();
868		let timestamp = Timestamp::from_micros(20_000).unwrap();
869		producer.write_frame(timestamp, Bytes::from_static(b"hello")).unwrap();
870		producer.finish().unwrap();
871
872		let mut consumer = producer.consume();
873		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
874		assert_eq!(frame.timestamp.as_micros(), 20_000);
875		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
876	}
877
878	#[test]
879	fn chunked_frame_reads_whole() {
880		let mut producer = Info { sequence: 0 }.produce();
881		{
882			let mut frame = producer
883				.create_frame(frame::Info {
884					size: 10,
885					timestamp: Timestamp::ZERO,
886				})
887				.unwrap();
888			frame.write(Bytes::from_static(b"hello")).unwrap();
889			frame.write(Bytes::from_static(b"world")).unwrap();
890			frame.finish().unwrap();
891		}
892		producer.finish().unwrap();
893
894		// Frame data is held in a single per-frame buffer; a whole-frame read returns
895		// the full contents in one slice.
896		let mut consumer = producer.consume();
897		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
898		assert_eq!(frame.payload, Bytes::from_static(b"helloworld"));
899	}
900
901	#[test]
902	fn chunked_frame_streams_partial() {
903		let mut producer = Info { sequence: 0 }.produce();
904		let mut consumer = producer.consume();
905
906		let mut frame = producer
907			.create_frame(frame::Info {
908				size: 6,
909				timestamp: Timestamp::ZERO,
910			})
911			.unwrap();
912		frame.write(Bytes::from_static(b"foo")).unwrap();
913
914		// A consumer can stream the in-flight tail before it's finished.
915		let mut f = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
916		let c1 = f.read_chunk().now_or_never().unwrap().unwrap();
917		assert_eq!(c1, Some(Bytes::from_static(b"foo")));
918		assert!(f.read_chunk().now_or_never().is_none());
919
920		frame.write(Bytes::from_static(b"bar")).unwrap();
921		frame.finish().unwrap();
922
923		let c2 = f.read_chunk().now_or_never().unwrap().unwrap();
924		assert_eq!(c2, Some(Bytes::from_static(b"bar")));
925		let c3 = f.read_chunk().now_or_never().unwrap().unwrap();
926		assert_eq!(c3, None);
927	}
928
929	#[test]
930	fn group_finish_returns_none() {
931		let mut producer = Info { sequence: 0 }.produce();
932		producer.finish().unwrap();
933
934		let mut consumer = producer.consume();
935		let end = consumer.next_frame().now_or_never().unwrap().unwrap();
936		assert!(end.is_none());
937	}
938
939	#[test]
940	fn abort_propagates() {
941		let producer = Info { sequence: 0 }.produce();
942		let mut consumer = producer.consume();
943		producer.abort(crate::Error::Cancel).unwrap();
944
945		let result = consumer.next_frame().now_or_never().unwrap();
946		assert!(matches!(result, Err(crate::Error::Cancel)));
947	}
948
949	#[test]
950	fn abort_clears_cached_frames() {
951		let mut producer = Info { sequence: 0 }.produce();
952		producer
953			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
954			.unwrap();
955
956		// A stale consumer that never reads must not pin the cached frames.
957		let _consumer = producer.consume();
958		assert_eq!(producer.state.read().frames.len(), 1);
959
960		producer.clone().abort(crate::Error::Cancel).unwrap();
961
962		let state = producer.state.read();
963		assert!(state.frames.is_empty(), "cached frames should be dropped on abort");
964		assert_eq!(state.cache, 0);
965	}
966
967	#[test]
968	fn drop_unfinished_clears_cached_frames() {
969		let producer = Info { sequence: 0 }.produce();
970		let mut writer = producer.clone();
971		writer
972			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
973			.unwrap();
974
975		// A stale consumer keeps the channel (and thus the cache) alive.
976		let mut consumer = producer.consume();
977		assert_eq!(producer.state.read().frames.len(), 1);
978
979		// Drop every producer without finishing: the cache is released.
980		drop(writer);
981		drop(producer);
982
983		let result = consumer.next_frame().now_or_never().unwrap();
984		assert!(matches!(result, Err(crate::Error::Dropped)));
985	}
986
987	#[test]
988	fn drop_after_abort_does_not_warn() {
989		let warns = count_drop_warnings("group::Producer dropped without finish", || {
990			let producer = Info { sequence: 0 }.produce();
991			let keep = producer.clone();
992			let mut writer = producer.clone();
993			writer
994				.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
995				.unwrap();
996			let _consumer = producer.consume();
997			writer.abort(crate::Error::Cancel).unwrap();
998			drop(keep);
999		});
1000		assert_eq!(warns, 0, "abort-then-drop must not emit unfinished-producer WARN");
1001	}
1002
1003	#[test]
1004	fn drop_unfinished_warns() {
1005		let warns = count_drop_warnings("group::Producer dropped without finish", || {
1006			let producer = Info { sequence: 0 }.produce();
1007			let mut writer = producer.clone();
1008			writer
1009				.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1010				.unwrap();
1011			let _consumer = producer.consume();
1012			drop(writer);
1013			drop(producer);
1014		});
1015		assert!(warns >= 1, "unfinished drop must emit unfinished-producer WARN");
1016	}
1017
1018	#[test]
1019	fn drop_finished_keeps_cached_frames() {
1020		let mut producer = Info { sequence: 0 }.produce();
1021		producer
1022			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1023			.unwrap();
1024		producer.finish().unwrap();
1025
1026		let mut consumer = producer.consume();
1027		drop(producer);
1028
1029		// A cleanly finished group keeps its cache so the consumer can still drain.
1030		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1031		assert_eq!(frame.payload, Bytes::from_static(b"data"));
1032	}
1033
1034	#[tokio::test]
1035	async fn pending_then_ready() {
1036		let mut producer = Info { sequence: 0 }.produce();
1037		let mut consumer = producer.consume();
1038
1039		// Consumer blocks because no frames yet.
1040		assert!(consumer.next_frame().now_or_never().is_none());
1041
1042		producer
1043			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1044			.unwrap();
1045		producer.finish().unwrap();
1046
1047		let frame = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
1048		assert_eq!(frame.size, 4);
1049	}
1050
1051	#[test]
1052	fn eviction_drops_old_frames() {
1053		let mut producer = Info { sequence: 0 }.produce();
1054
1055		// Write frames that total more than MAX_GROUP_CACHE.
1056		let big = Bytes::from(vec![0u8; MAX_GROUP_CACHE as usize]);
1057		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
1058		producer.write_frame(Timestamp::ZERO, big).unwrap();
1059
1060		// The first frame should have been evicted (tombstoned via offset).
1061		let state = producer.state.read();
1062		assert_eq!(state.offset, 1);
1063		assert_eq!(state.frames.len(), 1);
1064		assert_eq!(state.frames[0].payload.len(), MAX_GROUP_CACHE as usize);
1065	}
1066
1067	#[test]
1068	fn next_frame_returns_cache_full_on_tombstone() {
1069		let mut producer = Info { sequence: 0 }.produce();
1070
1071		let big = Bytes::from(vec![0u8; MAX_GROUP_CACHE as usize]);
1072		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
1073		producer.write_frame(Timestamp::ZERO, big).unwrap();
1074
1075		let mut consumer = producer.consume();
1076		// First frame was evicted, next_frame should return Lagged.
1077		let result = consumer.next_frame().now_or_never().unwrap();
1078		assert!(matches!(result, Err(crate::Error::Lagged)));
1079	}
1080
1081	#[test]
1082	fn no_eviction_under_budget() {
1083		let mut producer = Info { sequence: 0 }.produce();
1084		// Many small frames stay cached: there is no frame-count cap, only a byte budget.
1085		for _ in 0..100_000 {
1086			producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"x")).unwrap();
1087		}
1088		producer.finish().unwrap();
1089
1090		let state = producer.state.read();
1091		assert_eq!(state.offset, 0);
1092		assert_eq!(state.frames.len(), 100_000);
1093	}
1094
1095	#[test]
1096	fn clone_consumer_independent() {
1097		let mut producer = Info { sequence: 0 }.produce();
1098		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1099
1100		let mut c1 = producer.consume();
1101		// Read one frame from c1
1102		let _ = c1.next_frame().now_or_never().unwrap().unwrap().unwrap();
1103
1104		// Clone c1, inheriting its index (past first frame).
1105		let mut c2 = c1.clone();
1106
1107		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1108		producer.finish().unwrap();
1109
1110		// c2 should get the second frame (inherited index)
1111		let f = c2.next_frame().now_or_never().unwrap().unwrap().unwrap();
1112		assert_eq!(f.size, 1); // "b"
1113
1114		let end = c2.next_frame().now_or_never().unwrap().unwrap();
1115		assert!(end.is_none());
1116	}
1117
1118	/// Reading more than one prefetch batch drains every frame in order across the
1119	/// batch boundary (the refill starts exactly where the previous batch ended).
1120	#[test]
1121	fn read_frame_crosses_prefetch_batches() {
1122		let n = Prefetch::CAP * 3 + 5;
1123		let mut producer = Info { sequence: 0 }.produce();
1124		for i in 0..n {
1125			producer
1126				.write_frame(Timestamp::ZERO, Bytes::from(vec![i as u8; 4]))
1127				.unwrap();
1128		}
1129		producer.finish().unwrap();
1130
1131		let mut consumer = producer.consume();
1132		for i in 0..n {
1133			let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1134			assert_eq!(frame.payload, Bytes::from(vec![i as u8; 4]));
1135		}
1136		assert!(consumer.read_frame().now_or_never().unwrap().unwrap().is_none());
1137	}
1138
1139	/// A finished group is still aborted once its frames are released to free memory (the
1140	/// track's latency window, or the cache pool). A reader that already drained every frame
1141	/// is missing nothing, so it must see the clean end of group rather than the abort.
1142	#[test]
1143	fn abort_after_finish_keeps_the_clean_end_for_a_drained_reader() {
1144		let mut producer = Info { sequence: 0 }.produce();
1145		producer
1146			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
1147			.unwrap();
1148		producer.finish().unwrap();
1149
1150		let mut drained = producer.consume();
1151		let mut behind = producer.consume();
1152		let frame = drained.read_frame().now_or_never().unwrap().unwrap().unwrap();
1153		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
1154
1155		producer.abort(Error::Old).unwrap();
1156
1157		// Drained everything before the abort: nothing is missing.
1158		assert!(drained.read_frame().now_or_never().unwrap().unwrap().is_none());
1159		assert!(drained.next_frame().now_or_never().unwrap().unwrap().is_none());
1160
1161		// Never read the frame, and its bytes are gone: a truncated stream, not a clean end.
1162		assert!(matches!(behind.read_frame().now_or_never().unwrap(), Err(Error::Old)));
1163	}
1164
1165	/// The frame count is fixed at finish, so an abort that clears the cache can't turn a
1166	/// complete group into an error.
1167	#[test]
1168	fn finished_survives_a_later_abort() {
1169		let mut producer = Info { sequence: 0 }.produce();
1170		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1171		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1172		producer.finish().unwrap();
1173
1174		let mut consumer = producer.consume();
1175		producer.abort(Error::Old).unwrap();
1176
1177		assert_eq!(consumer.finished().now_or_never().unwrap().unwrap(), 2);
1178	}
1179
1180	/// `next_frame` drains frames a prior `read_frame` prefetched, preserving order.
1181	#[test]
1182	fn interleave_read_and_next_frame() {
1183		let mut producer = Info { sequence: 0 }.produce();
1184		for i in 0..5u8 {
1185			producer.write_frame(Timestamp::ZERO, Bytes::from(vec![i; 1])).unwrap();
1186		}
1187		producer.finish().unwrap();
1188
1189		let mut consumer = producer.consume();
1190		// The first whole-frame read prefetches all five frames into the batch.
1191		let f0 = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1192		assert_eq!(f0.payload, Bytes::from(vec![0u8; 1]));
1193
1194		// next_frame must continue from the batch, not skip ahead or repeat.
1195		for i in 1..5u8 {
1196			let mut f = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
1197			let data = f.read_all().now_or_never().unwrap().unwrap();
1198			assert_eq!(data, Bytes::from(vec![i; 1]));
1199		}
1200		assert!(consumer.next_frame().now_or_never().unwrap().unwrap().is_none());
1201	}
1202
1203	/// A `read_frame` whose index sits past the buffered frames (cleared by an abort, or an
1204	/// eviction gap) must surface the error, not panic on an out-of-range `range(local..)`.
1205	#[test]
1206	fn read_frame_past_cleared_frames_does_not_panic() {
1207		let mut producer = Info { sequence: 0 }.produce();
1208		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1209		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1210
1211		let mut consumer = producer.consume();
1212		consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1213		consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1214
1215		// Abort clears the cached frames but leaves the consumer's index (2) past them, so the
1216		// refill's `local` (2) exceeds `frames.len()` (0).
1217		producer.abort(Error::Cancel).unwrap();
1218
1219		let result = consumer.read_frame().now_or_never().unwrap();
1220		assert!(matches!(result, Err(Error::Cancel)), "expected Cancel, got {result:?}");
1221	}
1222
1223	/// Dropping a consumer mid-batch must drop the buffered-but-untaken frames
1224	/// (exercises the `MaybeUninit` Drop path; run under miri to catch leaks/UB).
1225	#[test]
1226	fn drop_with_partial_batch() {
1227		let mut producer = Info { sequence: 0 }.produce();
1228		for _ in 0..Prefetch::CAP {
1229			producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"x")).unwrap();
1230		}
1231		producer.finish().unwrap();
1232
1233		let mut consumer = producer.consume();
1234		// Take one frame so the batch is filled but only partially drained.
1235		let _ = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1236		drop(consumer);
1237	}
1238
1239	/// A frame whose timestamp is at a different scale is converted to the group's
1240	/// scale by `create_frame`.
1241	#[test]
1242	fn create_frame_converts_mismatched_scale() {
1243		use crate::{Timescale, Timestamp};
1244
1245		let mut producer = Producer::new(
1246			Info { sequence: 0 },
1247			track::Info::default().with_timescale(Timescale::MICRO),
1248			Default::default(),
1249		);
1250		let frame = frame::Info {
1251			size: 3,
1252			timestamp: Timestamp::from_millis(1).unwrap(), // 1ms -> 1000µs
1253		};
1254		let writer = producer.create_frame(frame).unwrap();
1255		assert_eq!(writer.timestamp.scale(), Timescale::MICRO);
1256		assert_eq!(writer.timestamp.value(), 1000);
1257	}
1258
1259	/// An explicit current timestamp is converted to the group's scale.
1260	#[tokio::test]
1261	async fn create_frame_converts_current_timestamp() {
1262		use crate::Timescale;
1263
1264		let mut producer = Producer::new(
1265			Info { sequence: 0 },
1266			track::Info::default().with_timescale(Timescale::MICRO),
1267			Default::default(),
1268		);
1269		let writer = producer
1270			.create_frame(frame::Info {
1271				size: 3,
1272				timestamp: Timestamp::now(),
1273			})
1274			.unwrap();
1275		assert_eq!(writer.timestamp.scale(), Timescale::MICRO);
1276		assert!(!writer.timestamp.is_zero(), "local clock should be non-zero");
1277	}
1278
1279	/// The per-frame size cap (the group byte budget) is enforced before allocating.
1280	#[test]
1281	fn create_frame_rejects_oversized() {
1282		let mut producer = Info { sequence: 0 }.produce();
1283		let result = producer.create_frame(frame::Info {
1284			size: MAX_GROUP_CACHE + 1,
1285			timestamp: Timestamp::ZERO,
1286		});
1287		assert!(matches!(result, Err(Error::FrameTooLarge)));
1288	}
1289}