Skip to main content

zerodds_dcps/
subscriber.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Subscriber + DataReader — the receive end of the DCPS API.
4//!
5//! Spec reference: OMG DDS 1.4 §2.2.2.5 `Subscriber`, §2.2.2.5.2
6//! `DataReader`.
7//!
8//! # Scope v1.2
9//!
10//! - `Subscriber::create_datareader<T>(topic, qos)` → `DataReader<T>`.
11//! - `DataReader::take()` removes all cached samples.
12//! - `DataReader::read()` peeks without removing (offline: identical to
13//!   take, no state change — spec §2.2.2.5.3.4 sample-state is
14//!   implemented in live mode).
15//! - Listener / WaitSet: live mode.
16
17extern crate alloc;
18use alloc::boxed::Box;
19use alloc::string::ToString;
20use alloc::sync::Arc;
21use alloc::vec::Vec;
22use core::marker::PhantomData;
23
24#[cfg(feature = "std")]
25use std::sync::Mutex;
26#[cfg(feature = "std")]
27use std::sync::mpsc;
28
29use crate::dds_type::DdsType;
30use crate::entity::StatusMask;
31use crate::error::{DdsError, Result};
32#[cfg(feature = "std")]
33use crate::instance_handle::{HANDLE_NIL, InstanceHandle};
34#[cfg(feature = "std")]
35use crate::instance_tracker::InstanceTracker;
36use crate::listener::{ArcDataReaderListener, ArcSubscriberListener};
37use crate::qos::{DataReaderQos, SubscriberQos};
38#[cfg(feature = "std")]
39use crate::sample::Sample;
40#[cfg(feature = "std")]
41use crate::sample_info::{
42    InstanceStateKind, SampleInfo, SampleStateKind, ViewStateKind, instance_state_mask,
43    sample_state_mask, view_state_mask,
44};
45#[cfg(feature = "std")]
46use crate::time::{Time, get_current_time};
47use crate::topic::Topic;
48
49#[cfg(feature = "std")]
50use crate::runtime::DcpsRuntime;
51#[cfg(feature = "std")]
52use zerodds_qos::ReliabilityKind;
53#[cfg(feature = "std")]
54use zerodds_rtps::wire_types::EntityId;
55
56/// Decodes a received sample body with the encoder that matches its
57/// **encapsulation** — both the XCDR version (`representation`: `0` = XCDR1 /
58/// classic CDR, `1` = XCDR2) and the byte order (`big_endian`), as extracted
59/// from the RTPS encapsulation header (RTPS 2.5 §10.5) when the sample was
60/// staged (`UserSample::Alive`).
61///
62/// This is essential for cross-vendor interop: CycloneDDS (and legacy RTI /
63/// OpenDDS) default their writers to **XCDR1** for `@final` types, so a ZeroDDS
64/// reader must decode XCDR1 with the classic-CDR alignment rule (max-align 8, no
65/// DHEADER) rather than the XCDR2 rule (max-align 4) — otherwise every body with
66/// an 8-byte member or an `@appendable` DHEADER would be mis-aligned. The
67/// per-representation framing lives in `DdsType::{decode, decode_be,
68/// decode_xcdr1, decode_xcdr1_be}` (XTypes 1.3 §7.4.3.4.2).
69#[inline]
70fn decode_for_encap<T: DdsType>(
71    bytes: &[u8],
72    representation: u8,
73    big_endian: bool,
74) -> core::result::Result<T, crate::dds_type::DecodeError> {
75    match (representation, big_endian) {
76        (0, false) => T::decode_xcdr1(bytes),
77        (0, true) => T::decode_xcdr1_be(bytes),
78        (_, false) => T::decode(bytes),
79        (_, true) => T::decode_be(bytes),
80    }
81}
82
83/// Subscriber — entity group for DataReaders.
84#[derive(Debug)]
85pub struct Subscriber {
86    pub(crate) inner: Arc<SubscriberInner>,
87}
88
89pub(crate) struct SubscriberInner {
90    #[cfg(feature = "std")]
91    pub(crate) qos: std::sync::Mutex<SubscriberQos>,
92    #[cfg(not(feature = "std"))]
93    #[allow(dead_code)]
94    pub(crate) qos: SubscriberQos,
95    pub(crate) entity_state: alloc::sync::Arc<crate::entity::EntityState>,
96    #[cfg(feature = "std")]
97    pub(crate) runtime: Option<Arc<DcpsRuntime>>,
98    /// Optional `SubscriberListener` + StatusMask.
99    /// Bubble-up target for reader events.
100    #[cfg(feature = "std")]
101    pub(crate) listener: std::sync::Mutex<Option<(ArcSubscriberListener, StatusMask)>>,
102    /// Weak back-pointer to the participant (bubble-up, cycle avoidance
103    /// via Weak).
104    #[cfg(feature = "std")]
105    pub(crate) participant:
106        std::sync::Mutex<Option<alloc::sync::Weak<crate::participant::ParticipantInner>>>,
107    /// Group access scope for §2.2.2.5.2.8/.9 begin/end_access.
108    /// Counter-based (recursively nestable per spec).
109    pub(crate) access_scope: Arc<crate::coherent_set::GroupAccessScope>,
110    /// DataReader handles (tracked per `create_datareader`) for recursive
111    /// `DomainParticipant::contains_entity` (spec §2.2.2.2.1.10).
112    #[cfg(feature = "std")]
113    pub(crate) datareaders:
114        std::sync::Mutex<alloc::vec::Vec<crate::instance_handle::InstanceHandle>>,
115}
116
117impl core::fmt::Debug for SubscriberInner {
118    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119        let listener_present = self.listener.lock().map(|s| s.is_some()).unwrap_or(false);
120        f.debug_struct("SubscriberInner")
121            .field("entity_state", &self.entity_state)
122            .field("listener_present", &listener_present)
123            .finish_non_exhaustive()
124    }
125}
126
127impl Subscriber {
128    #[cfg(feature = "std")]
129    pub(crate) fn new(qos: SubscriberQos, runtime: Option<Arc<DcpsRuntime>>) -> Self {
130        Self {
131            inner: Arc::new(SubscriberInner {
132                qos: std::sync::Mutex::new(qos),
133                entity_state: crate::entity::EntityState::new(),
134                runtime,
135                listener: std::sync::Mutex::new(None),
136                participant: std::sync::Mutex::new(None),
137                access_scope: crate::coherent_set::GroupAccessScope::new(),
138                datareaders: std::sync::Mutex::new(alloc::vec::Vec::new()),
139            }),
140        }
141    }
142
143    /// Spec §2.2.2.2.1.10 — `true` if `handle` is a DataReader created
144    /// via this Subscriber.
145    #[cfg(feature = "std")]
146    #[must_use]
147    pub fn contains_reader(&self, handle: crate::instance_handle::InstanceHandle) -> bool {
148        self.inner
149            .datareaders
150            .lock()
151            .map(|v| v.contains(&handle))
152            .unwrap_or(false)
153    }
154
155    #[cfg(feature = "std")]
156    fn track_reader(&self, handle: crate::instance_handle::InstanceHandle) {
157        if let Ok(mut list) = self.inner.datareaders.lock() {
158            list.push(handle);
159        }
160        // Propagate to the participant for recursive contains_entity.
161        if let Ok(slot) = self.inner.participant.lock() {
162            if let Some(weak) = slot.as_ref() {
163                if let Some(p_inner) = weak.upgrade() {
164                    if let Ok(mut drs) = p_inner.datareaders.lock() {
165                        drs.push(handle);
166                    }
167                }
168            }
169        }
170    }
171    #[cfg(not(feature = "std"))]
172    pub(crate) fn new(qos: SubscriberQos) -> Self {
173        Self {
174            inner: Arc::new(SubscriberInner {
175                qos,
176                entity_state: crate::entity::EntityState::new(),
177                access_scope: crate::coherent_set::GroupAccessScope::new(),
178            }),
179        }
180    }
181
182    /// Spec §2.2.2.5.2.8 `begin_access` — marks the start of a coherent
183    /// read set. Nesting is allowed; each call increments an internal
184    /// counter, each `end_access` decrements it.
185    pub fn begin_access(&self) {
186        self.inner.access_scope.begin();
187    }
188
189    /// Spec §2.2.2.5.2.9 `end_access` — counterpart to `begin_access`.
190    ///
191    /// # Errors
192    /// `DdsError::PreconditionNotMet` if `end_access` is called without a
193    /// preceding `begin_access`.
194    pub fn end_access(&self) -> Result<()> {
195        self.inner.access_scope.end()
196    }
197
198    /// `true` if a group access is currently open.
199    #[must_use]
200    pub fn is_access_open(&self) -> bool {
201        self.inner.access_scope.is_active()
202    }
203
204    /// Sets the `SubscriberListener` + StatusMask. `None` clears the
205    /// slot. Spec §2.2.2.5.6.x set_listener.
206    #[cfg(feature = "std")]
207    pub fn set_listener(&self, listener: Option<ArcSubscriberListener>, mask: StatusMask) {
208        if let Ok(mut slot) = self.inner.listener.lock() {
209            *slot = listener.map(|l| (l, mask));
210        }
211        self.inner.entity_state.set_listener_mask(mask);
212    }
213
214    /// Current listener clone.
215    #[cfg(feature = "std")]
216    #[must_use]
217    pub fn get_listener(&self) -> Option<ArcSubscriberListener> {
218        self.inner
219            .listener
220            .lock()
221            .ok()
222            .and_then(|s| s.as_ref().map(|(l, _)| Arc::clone(l)))
223    }
224
225    /// Sets the weak back-pointer to the participant.
226    #[cfg(feature = "std")]
227    pub(crate) fn attach_participant(
228        &self,
229        participant: alloc::sync::Weak<crate::participant::ParticipantInner>,
230    ) {
231        if let Ok(mut slot) = self.inner.participant.lock() {
232            *slot = Some(participant);
233        }
234    }
235
236    /// Snapshot of the reader bubble-up chain: the given
237    /// `reader_listener` tuple + subscriber stage + participant stage.
238    #[cfg(feature = "std")]
239    #[must_use]
240    pub(crate) fn snapshot_reader_chain(
241        &self,
242        reader_listener: Option<(ArcDataReaderListener, StatusMask)>,
243    ) -> crate::listener_dispatch::ReaderListenerChain {
244        let subscriber = self
245            .inner
246            .listener
247            .lock()
248            .ok()
249            .and_then(|s| s.as_ref().map(|(l, m)| (Arc::clone(l), *m)));
250        let participant = {
251            let weak = self.inner.participant.lock().ok().and_then(|s| s.clone());
252            weak.and_then(|w| w.upgrade()).and_then(|inner| {
253                inner
254                    .listener
255                    .lock()
256                    .ok()
257                    .and_then(|s| s.as_ref().map(|(l, m)| (Arc::clone(l), *m)))
258            })
259        };
260        crate::listener_dispatch::ReaderListenerChain {
261            reader: reader_listener,
262            subscriber,
263            participant,
264        }
265    }
266
267    /// Creates a typed `DataReader<T>`.
268    ///
269    /// # Errors
270    /// `BadParameter` on a type-name mismatch.
271    pub fn create_datareader<T: DdsType + Send + 'static>(
272        &self,
273        topic: &Topic<T>,
274        qos: DataReaderQos,
275    ) -> Result<DataReader<T>> {
276        if topic.type_name() != T::TYPE_NAME {
277            return Err(DdsError::BadParameter {
278                what: "topic.type_name mismatch",
279            });
280        }
281        #[cfg(feature = "std")]
282        if let Some(rt) = self.inner.runtime.as_ref() {
283            let reliable = qos.reliability.kind == ReliabilityKind::Reliable;
284            // Derive entityKind from the type's keyedness (spec §9.3.1.2:
285            // 0x04=NoKey / 0x07=WithKey). A keyless type (`HAS_KEY=false`)
286            // MUST produce a NoKey reader; otherwise cross-vendor writers
287            // (CycloneDDS/ROS 2) reject the endpoint match due to an
288            // entityKind mismatch (keyed vs no-key) — silently, with no log.
289            let (eid, rx) = rt.register_user_reader_kind(
290                crate::runtime::UserReaderConfig {
291                    topic_name: topic.name().into(),
292                    type_name: T::TYPE_NAME.into(),
293                    reliable,
294                    durability: qos.durability.kind,
295                    deadline: qos.deadline,
296                    liveliness: qos.liveliness,
297                    ownership: qos.ownership.kind,
298                    partition: qos.partition.names.clone(),
299                    user_data: qos.user_data.value.clone(),
300                    topic_data: qos.topic_data.value.clone(),
301                    group_data: qos.group_data.value.clone(),
302                    // F-TYPES-3: pass through the topic type identifier + TCE QoS.
303                    type_identifier: T::TYPE_IDENTIFIER.clone(),
304                    type_consistency: zerodds_types::qos::TypeConsistencyEnforcement::default(),
305                    // Per-reader DataRepresentation override from the QoS
306                    // (`None` = runtime default). XTypes 1.3 §7.6.3.1.2.
307                    data_representation_offer: qos.data_representation.clone(),
308                },
309                T::HAS_KEY,
310            )?;
311            let dr = DataReader::new_live(
312                topic.clone(),
313                qos,
314                self.inner.clone(),
315                Arc::clone(rt),
316                eid,
317                rx,
318            );
319            self.track_reader(dr.entity_state.instance_handle());
320            return Ok(dr);
321        }
322        let dr = DataReader::new_offline(topic.clone(), qos, self.inner.clone());
323        #[cfg(feature = "std")]
324        self.track_reader(dr.entity_state.instance_handle());
325        Ok(dr)
326    }
327}
328
329// ============================================================================
330// Entity-Trait (DCPS §2.2.2.1) —
331// ============================================================================
332
333#[cfg(feature = "std")]
334impl crate::entity::Entity for Subscriber {
335    type Qos = SubscriberQos;
336
337    fn get_qos(&self) -> Self::Qos {
338        self.inner.qos.lock().map(|q| q.clone()).unwrap_or_default()
339    }
340
341    fn set_qos(&self, qos: Self::Qos) -> Result<()> {
342        // SubscriberQos: Partition / GroupData / Presentation are all
343        // Changeable=YES per spec §2.2.3 — no immutable check needed.
344        if let Ok(mut current) = self.inner.qos.lock() {
345            *current = qos;
346        }
347        Ok(())
348    }
349
350    fn enable(&self) -> Result<()> {
351        self.inner.entity_state.enable();
352        Ok(())
353    }
354
355    fn entity_state(&self) -> alloc::sync::Arc<crate::entity::EntityState> {
356        alloc::sync::Arc::clone(&self.inner.entity_state)
357    }
358}
359
360/// Typed DataReader — removes samples that the RTPS reader has received
361/// for the topic.
362///
363/// Live mode: `rx: Some` delivers samples from the runtime mpsc.
364/// Offline mode: in-memory `inbox` for unit tests.
365pub struct DataReader<T: DdsType> {
366    topic: Topic<T>,
367    qos: Mutex<DataReaderQos>,
368    /// Entity lifecycle (DCPS §2.2.2.1).
369    entity_state: Arc<crate::entity::EntityState>,
370    /// Parent subscriber — for bubble-up to the subscriber and
371    /// participant listeners.
372    subscriber: Arc<SubscriberInner>,
373    /// Optional `DataReaderListener` + StatusMask.
374    #[cfg(feature = "std")]
375    listener: Mutex<Option<(ArcDataReaderListener, StatusMask)>>,
376    /// Last seen number of matched writers (for delta detection in
377    /// poll_subscription_matched).
378    #[cfg(feature = "std")]
379    last_match_count: std::sync::atomic::AtomicI64,
380    /// Last seen requested_deadline_missed counter.
381    #[cfg(feature = "std")]
382    last_requested_deadline_missed: std::sync::atomic::AtomicU64,
383    /// Last seen (alive_count, not_alive_count).
384    #[cfg(feature = "std")]
385    last_liveliness_alive: std::sync::atomic::AtomicI64,
386    /// Last seen not_alive counter.
387    #[cfg(feature = "std")]
388    last_liveliness_not_alive: std::sync::atomic::AtomicI64,
389    /// Last seen requested_incompatible_qos.total_count.
390    #[cfg(feature = "std")]
391    last_requested_incompatible_qos: std::sync::atomic::AtomicI64,
392    /// Last seen sample_lost counter.
393    #[cfg(feature = "std")]
394    last_sample_lost: std::sync::atomic::AtomicU64,
395    /// Last seen sample_rejected.total_count.
396    #[cfg(feature = "std")]
397    last_sample_rejected: std::sync::atomic::AtomicI64,
398    /// Offline fallback inbox. Stores full [`UserSample`] values
399    /// (including writer_guid + writer_strength for Alive) so that
400    /// `take()`/`read()` can apply the exclusive-ownership filter in a
401    /// spec-compliant way.
402    inbox: Arc<Mutex<Vec<crate::runtime::UserSample>>>,
403    #[cfg(feature = "std")]
404    #[allow(dead_code)]
405    runtime: Option<Arc<DcpsRuntime>>,
406    #[cfg(feature = "std")]
407    #[allow(dead_code)]
408    entity_id: Option<EntityId>,
409    /// Runtime channel for incoming samples (live mode).
410    #[cfg(feature = "std")]
411    rx: Option<Mutex<mpsc::Receiver<crate::runtime::UserSample>>>,
412    /// Optional content-filter closure. Applied to each sample in
413    /// `take()` after decoding; returns `true` → sample is delivered,
414    /// `false` → discarded.
415    ///
416    /// Spec reference: OMG DDS 1.4 §2.2.2.5.4 `ContentFilteredTopic`.
417    /// This Rust closure variant is more idiomatic than the spec's SQL
418    /// expression syntax and is sufficient for all in-process use cases.
419    /// SQL parser + cross-vendor SEDP propagation follow later.
420    #[allow(clippy::type_complexity)]
421    filter: Option<Arc<dyn Fn(&T) -> bool + Send + Sync>>,
422    /// Instance bookkeeping (spec §2.2.2.5.1).
423    #[cfg(feature = "std")]
424    instances: InstanceTracker,
425    /// Sample cache with resolved [`SampleInfo`]. The cache is filled on
426    /// arrival via `ingest_bytes`; `take`/`read`/`take_with_info`/
427    /// `read_with_info` read from it.
428    #[cfg(feature = "std")]
429    cache: Arc<Mutex<Vec<CachedSample>>>,
430    /// Optionally configured Flatdata SlotBackend for the same-host
431    /// zero-copy read path (`zerodds-flatdata-1.0` §4.1 + §9.1). Set via
432    /// `set_flat_backend`; `read_flat()` falls back to classic `take()`
433    /// when `None`.
434    #[cfg(all(feature = "std", feature = "flatdata-integration"))]
435    #[allow(clippy::type_complexity)]
436    pub(crate) flat_backend: Mutex<
437        Option<(
438            Arc<dyn zerodds_flatdata::SlotBackend>,
439            u8, // reader_index (0..31)
440            std::sync::atomic::AtomicU32,
441        )>,
442    >,
443    _t: PhantomData<fn() -> T>,
444}
445
446/// Internal: a decoded sample in the reader cache.
447///
448/// We carry the bytes (instead of `T`) so the reader cache is not bound
449/// to `T: Clone` and so `T::decode` can happen lazily. Lifecycle markers
450/// (dispose/unregister) have `bytes == None`.
451#[cfg(feature = "std")]
452#[derive(Debug)]
453pub(crate) struct CachedSample {
454    /// Zero-copy container: SampleBytes holds an Arc<[u8]> slice onto the
455    /// RTPS wire datagram. None for lifecycle markers (spec §2.2.2.5.1.13).
456    pub bytes: Option<crate::sample_bytes::SampleBytes>,
457    pub info: SampleInfo,
458}
459
460impl<T: DdsType> core::fmt::Debug for DataReader<T> {
461    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
462        f.debug_struct("DataReader")
463            .field("topic", &self.topic.name())
464            .field("type", &T::TYPE_NAME)
465            .field("qos", &self.qos)
466            .finish_non_exhaustive()
467    }
468}
469
470/// One drained inbox entry awaiting decode in `ingest_into_cache`:
471/// `(bytes, writer_guid, writer_strength, source_timestamp, key_only, flags)`.
472/// `bytes` is the refcounted zero-copy `SampleBytes` (Wave 2.1).
473#[cfg(feature = "std")]
474type RawIngestSample = (
475    crate::sample_bytes::SampleBytes,
476    [u8; 16],
477    i32,
478    Option<zerodds_rtps::header_extension::HeTimestamp>,
479    bool,
480    u8,
481);
482
483/// RAII teardown (Spec §2.2.2.5.1.2 — deleting a `DataReader`). Dropping the
484/// user's handle deregisters the reader from the runtime: it removes the slot,
485/// rebuilds the intra-runtime route, and sends an SEDP dispose so remote peers
486/// drop the matched reader at once. Offline readers (no runtime) are no-ops.
487#[cfg(feature = "std")]
488impl<T: DdsType> Drop for DataReader<T> {
489    fn drop(&mut self) {
490        if let (Some(rt), Some(eid)) = (self.runtime.as_ref(), self.entity_id) {
491            rt.remove_user_reader(eid);
492        }
493    }
494}
495
496impl<T: DdsType> DataReader<T> {
497    #[cfg(feature = "std")]
498    fn new_offline(topic: Topic<T>, qos: DataReaderQos, subscriber: Arc<SubscriberInner>) -> Self {
499        Self {
500            topic,
501            qos: Mutex::new(qos),
502            entity_state: crate::entity::EntityState::new(),
503            subscriber,
504            listener: Mutex::new(None),
505            last_match_count: std::sync::atomic::AtomicI64::new(-1),
506            last_requested_deadline_missed: std::sync::atomic::AtomicU64::new(0),
507            last_liveliness_alive: std::sync::atomic::AtomicI64::new(-1),
508            last_liveliness_not_alive: std::sync::atomic::AtomicI64::new(-1),
509            last_requested_incompatible_qos: std::sync::atomic::AtomicI64::new(-1),
510            last_sample_lost: std::sync::atomic::AtomicU64::new(0),
511            last_sample_rejected: std::sync::atomic::AtomicI64::new(-1),
512            inbox: Arc::new(Mutex::new(Vec::new())),
513            runtime: None,
514            entity_id: None,
515            rx: None,
516            filter: None,
517            instances: InstanceTracker::new(),
518            cache: Arc::new(Mutex::new(Vec::new())),
519            #[cfg(feature = "flatdata-integration")]
520            flat_backend: Mutex::new(None),
521            _t: PhantomData,
522        }
523    }
524
525    #[cfg(feature = "std")]
526    fn new_live(
527        topic: Topic<T>,
528        qos: DataReaderQos,
529        subscriber: Arc<SubscriberInner>,
530        runtime: Arc<DcpsRuntime>,
531        entity_id: EntityId,
532        rx: mpsc::Receiver<crate::runtime::UserSample>,
533    ) -> Self {
534        Self {
535            topic,
536            qos: Mutex::new(qos),
537            entity_state: crate::entity::EntityState::new(),
538            subscriber,
539            listener: Mutex::new(None),
540            last_match_count: std::sync::atomic::AtomicI64::new(-1),
541            last_requested_deadline_missed: std::sync::atomic::AtomicU64::new(0),
542            last_liveliness_alive: std::sync::atomic::AtomicI64::new(-1),
543            last_liveliness_not_alive: std::sync::atomic::AtomicI64::new(-1),
544            last_requested_incompatible_qos: std::sync::atomic::AtomicI64::new(-1),
545            last_sample_lost: std::sync::atomic::AtomicU64::new(0),
546            last_sample_rejected: std::sync::atomic::AtomicI64::new(-1),
547            inbox: Arc::new(Mutex::new(Vec::new())),
548            runtime: Some(runtime),
549            entity_id: Some(entity_id),
550            rx: Some(Mutex::new(rx)),
551            filter: None,
552            instances: InstanceTracker::new(),
553            cache: Arc::new(Mutex::new(Vec::new())),
554            #[cfg(feature = "flatdata-integration")]
555            flat_backend: Mutex::new(None),
556            _t: PhantomData,
557        }
558    }
559
560    #[cfg(not(feature = "std"))]
561    fn new(topic: Topic<T>, qos: DataReaderQos, subscriber: Arc<SubscriberInner>) -> Self {
562        Self {
563            topic,
564            qos,
565            subscriber,
566            inbox: Arc::new(Mutex::new(Vec::new())),
567            filter: None,
568            _t: PhantomData,
569        }
570    }
571
572    /// Constructor for builtin-topic readers.
573    ///
574    /// Unlike `new_offline`, this reader shares the inbox with the
575    /// `DcpsRuntime` discovery hook: SPDP/SEDP receive pushes an encoded
576    /// sample through the same `Arc<Mutex<Vec<crate::runtime::UserSample>>>`,
577    /// which is read here via `take()`/`read()`.
578    #[cfg(feature = "std")]
579    pub(crate) fn new_builtin(
580        topic: Topic<T>,
581        qos: DataReaderQos,
582        subscriber: Arc<SubscriberInner>,
583        inbox: Arc<Mutex<Vec<crate::runtime::UserSample>>>,
584    ) -> Self {
585        Self {
586            topic,
587            qos: Mutex::new(qos),
588            entity_state: crate::entity::EntityState::new(),
589            subscriber,
590            listener: Mutex::new(None),
591            last_match_count: std::sync::atomic::AtomicI64::new(-1),
592            last_requested_deadline_missed: std::sync::atomic::AtomicU64::new(0),
593            last_liveliness_alive: std::sync::atomic::AtomicI64::new(-1),
594            last_liveliness_not_alive: std::sync::atomic::AtomicI64::new(-1),
595            last_requested_incompatible_qos: std::sync::atomic::AtomicI64::new(-1),
596            last_sample_lost: std::sync::atomic::AtomicU64::new(0),
597            last_sample_rejected: std::sync::atomic::AtomicI64::new(-1),
598            inbox,
599            runtime: None,
600            entity_id: None,
601            rx: None,
602            filter: None,
603            instances: InstanceTracker::new(),
604            cache: Arc::new(Mutex::new(Vec::new())),
605            #[cfg(feature = "flatdata-integration")]
606            flat_backend: Mutex::new(None),
607            _t: PhantomData,
608        }
609    }
610
611    /// Sets a content filter that is evaluated on every sample in the
612    /// `take()` path. Returning `false` discards the sample.
613    ///
614    /// Builder style: `reader.with_filter(|s| s.value > 0)`.
615    ///
616    /// .7a — SQL expression syntax via `set_filter_expression` follows
617    /// later.
618    #[must_use]
619    pub fn with_filter<F>(mut self, filter: F) -> Self
620    where
621        F: Fn(&T) -> bool + Send + Sync + 'static,
622    {
623        self.filter = Some(Arc::new(filter));
624        self
625    }
626
627    /// The topic being read from.
628    #[must_use]
629    pub fn topic(&self) -> &Topic<T> {
630        &self.topic
631    }
632
633    /// Spec §2.2.2.5.3.6 / §2.2.2.1.1 — `InstanceHandle` of this
634    /// DataReader. A stable identity for
635    /// `DomainParticipant::contains_entity`.
636    #[must_use]
637    pub fn subscription_handle(&self) -> crate::instance_handle::InstanceHandle {
638        self.entity_state.instance_handle()
639    }
640
641    /// Sets the `DataReaderListener` + StatusMask. `None` clears the
642    /// slot. Spec §2.2.2.5.7.x set_listener.
643    #[cfg(feature = "std")]
644    pub fn set_listener(&self, listener: Option<ArcDataReaderListener>, mask: StatusMask) {
645        if let Ok(mut slot) = self.listener.lock() {
646            *slot = listener.map(|l| (l, mask));
647        }
648        self.entity_state.set_listener_mask(mask);
649    }
650
651    /// Current listener clone, if any.
652    #[cfg(feature = "std")]
653    #[must_use]
654    pub fn get_listener(&self) -> Option<ArcDataReaderListener> {
655        self.listener
656            .lock()
657            .ok()
658            .and_then(|s| s.as_ref().map(|(l, _)| Arc::clone(l)))
659    }
660
661    /// Snapshot of the bubble-up chain (Reader → Subscriber → Participant)
662    /// for hot-path listener dispatch.
663    #[cfg(feature = "std")]
664    #[must_use]
665    pub(crate) fn listener_chain(&self) -> crate::listener_dispatch::ReaderListenerChain {
666        let reader = self
667            .listener
668            .lock()
669            .ok()
670            .and_then(|s| s.as_ref().map(|(l, m)| (Arc::clone(l), *m)));
671        let sub_handle = Subscriber {
672            inner: Arc::clone(&self.subscriber),
673        };
674        sub_handle.snapshot_reader_chain(reader)
675    }
676
677    /// Current QoS (cloned, .1).
678    #[must_use]
679    pub fn qos(&self) -> DataReaderQos {
680        self.qos.lock().map(|q| q.clone()).unwrap_or_default()
681    }
682
683    /// Takes all cached samples and removes them from the inbox. Returns
684    /// an empty Vec if there is nothing.
685    ///
686    /// # Errors
687    /// - `WireError` if a stored payload can no longer be decoded
688    ///   (type-eval mismatch).
689    pub fn take(&self) -> Result<Vec<T>> {
690        // Spec §2.2.3.22 ReaderDataLifecycle.autopurge — on every read/take,
691        // check whether expired instances must be removed from the tracker.
692        #[cfg(feature = "std")]
693        {
694            let now = get_current_time();
695            let mut empty: Vec<CachedSample> = Vec::new();
696            self.run_reader_autopurge(now, &mut empty);
697        }
698        // Cross-vendor same-host zero-copy: if this reader was
699        // `enable_cyclone_iox`-ed, return Cyclone-published samples drained from
700        // iceoryx (classic-CDR-decoded) when any are queued.
701        #[cfg(all(feature = "std", feature = "cyclone-iox", target_os = "linux"))]
702        {
703            let iox = crate::cyclone_iox_integration::take::<T>(self.topic.name());
704            if !iox.is_empty() {
705                return Ok(iox);
706            }
707        }
708        // Live mode: first drain the staging inbox (filled by
709        // wait_for_data), then pull all not-yet-polled samples from mpsc.
710        #[cfg(feature = "std")]
711        if let Some(rx_mu) = self.rx.as_ref() {
712            let mut out = Vec::new();
713            // Read TimeBasedFilter (spec §2.2.3.13) min_separation from QoS
714            // so live mode applies the same filtering as ingest_into_cache.
715            let min_sep_nanos = {
716                let qos = self.qos.lock().unwrap_or_else(|e| e.into_inner());
717                qos.time_based_filter.minimum_separation.to_nanos()
718            };
719            let staged = {
720                let mut inbox = self
721                    .inbox
722                    .lock()
723                    .map_err(|_| DdsError::PreconditionNotMet {
724                        reason: "datareader inbox poisoned",
725                    })?;
726                core::mem::take(&mut *inbox)
727            };
728            for staged_item in staged {
729                match staged_item {
730                    crate::runtime::UserSample::Alive {
731                        payload: bytes,
732                        writer_guid,
733                        writer_strength,
734                        representation,
735                        big_endian,
736                        ..
737                    } => {
738                        let sample = decode_for_encap::<T>(&bytes, representation, big_endian)
739                            .map_err(|e| DdsError::WireError {
740                                message: e.to_string(),
741                            })?;
742                        if !self.sample_passes_filter(&sample) {
743                            continue;
744                        }
745                        if !self.live_mode_time_based_filter_pass(&sample, min_sep_nanos) {
746                            continue;
747                        }
748                        // §2.2.3.23 exclusive-ownership filter.
749                        if !self.passes_exclusive_ownership(&sample, writer_guid, writer_strength) {
750                            continue;
751                        }
752                        out.push(sample);
753                    }
754                    crate::runtime::UserSample::Lifecycle { .. } => {
755                        // Lifecycle in the staging inbox: in the
756                        // live-mode take() loop it is handled
757                        // immediately below via __push_lifecycle — just
758                        // skip it here; it comes around next round.
759                    }
760                }
761            }
762            let rx = rx_mu.lock().map_err(|_| DdsError::PreconditionNotMet {
763                reason: "datareader rx poisoned",
764            })?;
765            while let Ok(item) = rx.try_recv() {
766                match item {
767                    crate::runtime::UserSample::Alive {
768                        payload: bytes,
769                        writer_guid,
770                        writer_strength,
771                        representation,
772                        big_endian,
773                        ..
774                    } => {
775                        let sample = decode_for_encap::<T>(&bytes, representation, big_endian)
776                            .map_err(|e| DdsError::WireError {
777                                message: e.to_string(),
778                            })?;
779                        if !self.sample_passes_filter(&sample) {
780                            continue;
781                        }
782                        if !self.live_mode_time_based_filter_pass(&sample, min_sep_nanos) {
783                            continue;
784                        }
785                        // §2.2.3.23 exclusive-ownership filter.
786                        if !self.passes_exclusive_ownership(&sample, writer_guid, writer_strength) {
787                            continue;
788                        }
789                        out.push(sample);
790                    }
791                    crate::runtime::UserSample::Lifecycle { key_hash, kind } => {
792                        // Feed lifecycle markers into the tracker via
793                        // __push_lifecycle (spec §8.2.1.2).
794                        let mut holder_bytes = Vec::with_capacity(16);
795                        holder_bytes.extend_from_slice(&key_hash);
796                        let lc_kind = match kind {
797                            zerodds_rtps::history_cache::ChangeKind::NotAliveDisposed
798                            | zerodds_rtps::history_cache::ChangeKind::NotAliveDisposedUnregistered => {
799                                crate::sample_info::InstanceStateKind::NotAliveDisposed
800                            }
801                            zerodds_rtps::history_cache::ChangeKind::NotAliveUnregistered => {
802                                crate::sample_info::InstanceStateKind::NotAliveNoWriters
803                            }
804                            _ => crate::sample_info::InstanceStateKind::Alive,
805                        };
806                        let _ = self.__push_lifecycle(key_hash, holder_bytes, lc_kind);
807                    }
808                }
809            }
810            return Ok(out);
811        }
812        // Offline fallback.
813        let raw = {
814            let mut inbox = self
815                .inbox
816                .lock()
817                .map_err(|_| DdsError::PreconditionNotMet {
818                    reason: "datareader inbox poisoned",
819                })?;
820            core::mem::take(&mut *inbox)
821        };
822        let mut out = Vec::with_capacity(raw.len());
823        for staged_item in raw {
824            let crate::runtime::UserSample::Alive {
825                payload: bytes,
826                writer_guid,
827                writer_strength,
828                representation,
829                big_endian,
830                ..
831            } = staged_item
832            else {
833                continue;
834            };
835            let sample =
836                decode_for_encap::<T>(&bytes, representation, big_endian).map_err(|e| {
837                    DdsError::WireError {
838                        message: e.to_string(),
839                    }
840                })?;
841            if !self.sample_passes_filter(&sample) {
842                continue;
843            }
844            // §2.2.3.23 exclusive-ownership filter (also in the offline
845            // fallback). The builtin-inject path uses writer_guid=[0;16]
846            // with a shared-ownership default; passes_exclusive_ownership
847            // then always returns `true`.
848            if !self.passes_exclusive_ownership(&sample, writer_guid, writer_strength) {
849                continue;
850            }
851            out.push(sample);
852        }
853        Ok(out)
854    }
855
856    /// Helper — evaluates the content filter if set.
857    fn sample_passes_filter(&self, sample: &T) -> bool {
858        match &self.filter {
859            Some(f) => f(sample),
860            None => true,
861        }
862    }
863
864    /// Spec §2.2.3.23 / §2.2.2.5.5 — exclusive-ownership filter.
865    ///
866    /// Returns `true` if the sample may be delivered:
867    /// - Reader ownership QoS = Shared → always `true` (no filter).
868    /// - Keyless topic → always `true` (no per-instance owner state).
869    /// - Otherwise: computes the KeyHash and consults
870    ///   [`instance_tracker::InstanceTracker::should_accept_sample_under_exclusive_ownership`],
871    ///   which holds the (writer_guid, writer_strength) of the
872    ///   currently-winning source per instance and rejects samples from
873    ///   weaker writers.
874    #[cfg(feature = "std")]
875    fn passes_exclusive_ownership(
876        &self,
877        sample: &T,
878        writer_guid: [u8; 16],
879        writer_strength: i32,
880    ) -> bool {
881        let kind = {
882            let qos = self.qos.lock().unwrap_or_else(|e| e.into_inner());
883            qos.ownership.kind
884        };
885        if kind != zerodds_qos::OwnershipKind::Exclusive {
886            return true;
887        }
888        // Spec §2.2.3.23: ownership resolution applies per instance; for
889        // keyless topics we treat the topic as a single instance with a
890        // synthetic all-zero KeyHash.
891        let (kh, key_bytes) = if T::HAS_KEY {
892            let mut holder = crate::dds_type::PlainCdr2BeKeyHolder::new();
893            sample.encode_key_holder_be(&mut holder);
894            let kb = holder.as_bytes().to_vec();
895            let max = T::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
896            (crate::dds_type::compute_key_hash(&kb, max), kb)
897        } else {
898            ([0u8; 16], Vec::new())
899        };
900        // The instance must be registered so the owner tracker can
901        // create the slot (`should_accept` otherwise returns `true` for
902        // an unknown instance, which bypasses the filtering).
903        let _ = self.instances.observe_sample(kh, key_bytes, None);
904        self.instances
905            .should_accept_sample_under_exclusive_ownership(&kh, writer_guid, writer_strength)
906    }
907
908    /// Spec §2.2.3.13 TIME_BASED_FILTER for the live-mode path.
909    /// Returns `true` if the sample may be delivered.
910    /// For keyless types or min_separation=0, always `true`.
911    /// For keyed types: compute the keyhash via `encode_key_holder_be`,
912    /// check it against instance_tracker, and on `true` call
913    /// `record_delivery` directly so subsequent samples of the same
914    /// instance are filtered correctly.
915    #[cfg(feature = "std")]
916    fn live_mode_time_based_filter_pass(&self, sample: &T, min_sep_nanos: u128) -> bool {
917        if min_sep_nanos == 0 || !T::HAS_KEY {
918            return true;
919        }
920        let mut holder = crate::dds_type::PlainCdr2BeKeyHolder::new();
921        sample.encode_key_holder_be(&mut holder);
922        let key_bytes = holder.as_bytes().to_vec();
923        let max = T::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
924        let kh = crate::dds_type::compute_key_hash(&key_bytes, max);
925        let now = get_current_time();
926        if !self
927            .instances
928            .should_deliver_under_time_based_filter(&kh, now, min_sep_nanos)
929        {
930            return false;
931        }
932        let _ = self.instances.observe_sample(kh, key_bytes, Some(now));
933        self.instances.record_delivery(&kh, now);
934        true
935    }
936
937    /// Reads all samples without removing them. Currently identical to
938    /// `take` minus the removal. Sample state (`ReadCondition`
939    /// §2.2.2.5.8) follows during wire-up.
940    ///
941    /// # Errors
942    /// Same as `take`.
943    pub fn read(&self) -> Result<Vec<T>> {
944        let raw = {
945            let inbox = self
946                .inbox
947                .lock()
948                .map_err(|_| DdsError::PreconditionNotMet {
949                    reason: "datareader inbox poisoned",
950                })?;
951            inbox.clone()
952        };
953        let mut out = Vec::with_capacity(raw.len());
954        for staged_item in raw {
955            let crate::runtime::UserSample::Alive {
956                payload: bytes,
957                writer_guid,
958                writer_strength,
959                representation,
960                big_endian,
961                ..
962            } = staged_item
963            else {
964                continue;
965            };
966            let sample =
967                decode_for_encap::<T>(&bytes, representation, big_endian).map_err(|e| {
968                    DdsError::WireError {
969                        message: e.to_string(),
970                    }
971                })?;
972            if !self.sample_passes_filter(&sample) {
973                continue;
974            }
975            // §2.2.3.23 exclusive-ownership filter (also in the offline
976            // fallback). The builtin-inject path uses writer_guid=[0;16]
977            // with a shared-ownership default; passes_exclusive_ownership
978            // then always returns `true`.
979            if !self.passes_exclusive_ownership(&sample, writer_guid, writer_strength) {
980                continue;
981            }
982            out.push(sample);
983        }
984        Ok(out)
985    }
986
987    /// Number of matched remote writers. Always 0 in offline mode.
988    ///
989    /// Spec: OMG DDS 1.4 §2.2.2.5.3.15 `get_matched_publications`.
990    ///
991    /// Side effect — when the matched count changes versus the last
992    /// call, `on_subscription_matched` is fired via the bubble-up chain
993    /// (spec §2.2.4.2.6.7).
994    #[must_use]
995    pub fn matched_publication_count(&self) -> usize {
996        #[cfg(feature = "std")]
997        if let (Some(rt), Some(eid)) = (&self.runtime, self.entity_id) {
998            let n = rt.user_reader_matched_count(eid);
999            self.poll_subscription_matched(n);
1000            return n;
1001        }
1002        0
1003    }
1004
1005    /// Delta-detect helper for `on_subscription_matched`.
1006    #[cfg(feature = "std")]
1007    pub(crate) fn poll_subscription_matched(&self, current: usize) {
1008        let curr = current as i64;
1009        let prev = self
1010            .last_match_count
1011            .swap(curr, std::sync::atomic::Ordering::AcqRel);
1012        if prev == curr {
1013            return;
1014        }
1015        let total = if curr > prev.max(0) {
1016            curr
1017        } else {
1018            prev.max(0)
1019        };
1020        let delta = curr - prev.max(0);
1021        let status = crate::status::SubscriptionMatchedStatus {
1022            total_count: total as i32,
1023            total_count_change: delta.max(0) as i32,
1024            current_count: curr as i32,
1025            current_count_change: delta as i32,
1026            last_publication_handle: crate::instance_handle::HANDLE_NIL,
1027        };
1028        let chain = self.listener_chain();
1029        crate::listener_dispatch::dispatch_subscription_matched(
1030            &chain,
1031            self.entity_state.instance_handle(),
1032            status,
1033        );
1034    }
1035
1036    /// Delta-detect for `on_requested_deadline_missed`.
1037    /// Spec §2.2.4.2.6.4.
1038    #[cfg(feature = "std")]
1039    pub(crate) fn poll_requested_deadline_missed(&self, current: u64) {
1040        let prev = self
1041            .last_requested_deadline_missed
1042            .swap(current, std::sync::atomic::Ordering::AcqRel);
1043        if current == prev {
1044            return;
1045        }
1046        let total_change = current.saturating_sub(prev);
1047        let status = crate::status::RequestedDeadlineMissedStatus {
1048            total_count: current as i32,
1049            total_count_change: total_change as i32,
1050            last_instance_handle: crate::instance_handle::HANDLE_NIL,
1051        };
1052        let chain = self.listener_chain();
1053        crate::listener_dispatch::dispatch_requested_deadline_missed(
1054            &chain,
1055            self.entity_state.instance_handle(),
1056            status,
1057        );
1058    }
1059
1060    /// Delta-detect for `on_liveliness_changed`. Spec §2.2.4.2.6.6.
1061    /// Considers both counters (alive + not_alive); each change triggers
1062    /// exactly once.
1063    #[cfg(feature = "std")]
1064    pub(crate) fn poll_liveliness_changed(&self, alive_count: u64, not_alive_count: u64) {
1065        let curr_alive = alive_count as i64;
1066        let curr_not = not_alive_count as i64;
1067        let prev_alive = self
1068            .last_liveliness_alive
1069            .swap(curr_alive, std::sync::atomic::Ordering::AcqRel);
1070        let prev_not = self
1071            .last_liveliness_not_alive
1072            .swap(curr_not, std::sync::atomic::Ordering::AcqRel);
1073        // First observation (prev == -1) only counts if the counter is
1074        // nonzero; otherwise no trigger.
1075        let alive_changed = if prev_alive < 0 {
1076            curr_alive != 0
1077        } else {
1078            prev_alive != curr_alive
1079        };
1080        let not_changed = if prev_not < 0 {
1081            curr_not != 0
1082        } else {
1083            prev_not != curr_not
1084        };
1085        if !alive_changed && !not_changed {
1086            return;
1087        }
1088        let alive_delta = if prev_alive < 0 {
1089            curr_alive
1090        } else {
1091            curr_alive - prev_alive
1092        };
1093        let not_delta = if prev_not < 0 {
1094            curr_not
1095        } else {
1096            curr_not - prev_not
1097        };
1098        let status = crate::status::LivelinessChangedStatus {
1099            alive_count: curr_alive as i32,
1100            not_alive_count: curr_not as i32,
1101            alive_count_change: alive_delta as i32,
1102            not_alive_count_change: not_delta as i32,
1103            last_publication_handle: crate::instance_handle::HANDLE_NIL,
1104        };
1105        let chain = self.listener_chain();
1106        crate::listener_dispatch::dispatch_liveliness_changed(
1107            &chain,
1108            self.entity_state.instance_handle(),
1109            status,
1110        );
1111    }
1112
1113    /// Delta-detect for `on_requested_incompatible_qos`.
1114    /// Spec §2.2.4.2.6.5.
1115    #[cfg(feature = "std")]
1116    pub(crate) fn poll_requested_incompatible_qos(
1117        &self,
1118        snapshot: crate::status::RequestedIncompatibleQosStatus,
1119    ) {
1120        let curr = i64::from(snapshot.total_count);
1121        let prev = self
1122            .last_requested_incompatible_qos
1123            .swap(curr, std::sync::atomic::Ordering::AcqRel);
1124        if curr == prev {
1125            return;
1126        }
1127        let delta = curr - prev.max(0);
1128        let status = crate::status::RequestedIncompatibleQosStatus {
1129            total_count: curr as i32,
1130            total_count_change: delta.max(0) as i32,
1131            last_policy_id: snapshot.last_policy_id,
1132            policies: snapshot.policies,
1133        };
1134        let chain = self.listener_chain();
1135        crate::listener_dispatch::dispatch_requested_incompatible_qos(
1136            &chain,
1137            self.entity_state.instance_handle(),
1138            status,
1139        );
1140    }
1141
1142    /// Delta-detect for `on_sample_lost`. Spec §2.2.4.2.6.2.
1143    #[cfg(feature = "std")]
1144    pub(crate) fn poll_sample_lost(&self, current: u64) {
1145        let prev = self
1146            .last_sample_lost
1147            .swap(current, std::sync::atomic::Ordering::AcqRel);
1148        if current == prev {
1149            return;
1150        }
1151        let delta = current.saturating_sub(prev);
1152        let status = crate::status::SampleLostStatus {
1153            total_count: current as i32,
1154            total_count_change: delta as i32,
1155        };
1156        let chain = self.listener_chain();
1157        crate::listener_dispatch::dispatch_sample_lost(
1158            &chain,
1159            self.entity_state.instance_handle(),
1160            status,
1161        );
1162    }
1163
1164    /// Delta-detect for `on_sample_rejected`. Spec §2.2.4.2.6.3.
1165    #[cfg(feature = "std")]
1166    pub(crate) fn poll_sample_rejected(&self, snapshot: crate::status::SampleRejectedStatus) {
1167        let curr = i64::from(snapshot.total_count);
1168        let prev = self
1169            .last_sample_rejected
1170            .swap(curr, std::sync::atomic::Ordering::AcqRel);
1171        if curr == prev {
1172            return;
1173        }
1174        let delta = curr - prev.max(0);
1175        let status = crate::status::SampleRejectedStatus {
1176            total_count: curr as i32,
1177            total_count_change: delta.max(0) as i32,
1178            last_reason: snapshot.last_reason,
1179            last_instance_handle: snapshot.last_instance_handle,
1180        };
1181        let chain = self.listener_chain();
1182        crate::listener_dispatch::dispatch_sample_rejected(
1183            &chain,
1184            self.entity_state.instance_handle(),
1185            status,
1186        );
1187    }
1188
1189    /// Blocks until at least `min_count` remote writers are matched or
1190    /// `timeout` elapses. Event-driven via the runtime condvar
1191    /// (D.5e phase 1) — wakeup directly when SEDP propagates a match, no
1192    /// more 20-ms polling.
1193    ///
1194    /// # Errors
1195    /// [`DdsError::Timeout`] if `min_count` is not reached within the
1196    /// time window.
1197    #[cfg(feature = "std")]
1198    pub fn wait_for_matched_publication(
1199        &self,
1200        min_count: usize,
1201        timeout: core::time::Duration,
1202    ) -> Result<()> {
1203        let deadline = std::time::Instant::now() + timeout;
1204        loop {
1205            if self.matched_publication_count() >= min_count {
1206                return Ok(());
1207            }
1208            let now = std::time::Instant::now();
1209            if now >= deadline {
1210                return Err(DdsError::Timeout);
1211            }
1212            // Live mode: park on the runtime match event. Spurious
1213            // wakeups are fine — we check the count on the next iteration.
1214            if let Some(rt) = self.runtime.as_ref() {
1215                let _ = rt.wait_match_event(deadline - now);
1216            } else {
1217                // Offline mode: no match events, sleep fallback.
1218                std::thread::sleep(core::time::Duration::from_millis(20));
1219            }
1220        }
1221    }
1222
1223    /// Counter for requested-deadline violations (spec §2.2.4.2.11
1224    /// `REQUESTED_DEADLINE_MISSED_STATUS`). Monotonically increasing;
1225    /// rises by 1 per expired deadline window without a received sample.
1226    /// Offline / INFINITE → 0.
1227    ///
1228    /// May fire `on_requested_deadline_missed`.
1229    #[must_use]
1230    pub fn requested_deadline_missed_count(&self) -> u64 {
1231        #[cfg(feature = "std")]
1232        if let (Some(rt), Some(eid)) = (&self.runtime, self.entity_id) {
1233            let n = rt.user_reader_requested_deadline_missed(eid);
1234            self.poll_requested_deadline_missed(n);
1235            return n;
1236        }
1237        0
1238    }
1239
1240    /// Current `RequestedIncompatibleQosStatus`. Spec §2.2.4.2.6.5.
1241    /// May trigger `on_requested_incompatible_qos`.
1242    #[must_use]
1243    pub fn requested_incompatible_qos_status(
1244        &self,
1245    ) -> crate::status::RequestedIncompatibleQosStatus {
1246        #[cfg(feature = "std")]
1247        if let (Some(rt), Some(eid)) = (&self.runtime, self.entity_id) {
1248            let s = rt.user_reader_requested_incompatible_qos(eid);
1249            self.poll_requested_incompatible_qos(s.clone());
1250            return s;
1251        }
1252        crate::status::RequestedIncompatibleQosStatus::default()
1253    }
1254
1255    /// SampleLost counter. Spec §2.2.4.2.6.2.
1256    #[must_use]
1257    pub fn sample_lost_count(&self) -> u64 {
1258        #[cfg(feature = "std")]
1259        if let (Some(rt), Some(eid)) = (&self.runtime, self.entity_id) {
1260            let n = rt.user_reader_sample_lost(eid);
1261            self.poll_sample_lost(n);
1262            return n;
1263        }
1264        0
1265    }
1266
1267    /// SampleRejected status. Spec §2.2.4.2.6.3.
1268    #[must_use]
1269    pub fn sample_rejected_status(&self) -> crate::status::SampleRejectedStatus {
1270        #[cfg(feature = "std")]
1271        if let (Some(rt), Some(eid)) = (&self.runtime, self.entity_id) {
1272            let s = rt.user_reader_sample_rejected(eid);
1273            self.poll_sample_rejected(s);
1274            return s;
1275        }
1276        crate::status::SampleRejectedStatus::default()
1277    }
1278
1279    /// Polls all reader statuses once and fires pending listeners.
1280    /// Convenience helper for tests + periodic tick callers.
1281    #[cfg(feature = "std")]
1282    pub fn drive_listeners(&self) {
1283        let _ = self.matched_publication_count();
1284        let _ = self.requested_deadline_missed_count();
1285        let (_, alive, not_alive) = self.liveliness_changed_status();
1286        self.poll_liveliness_changed(alive, not_alive);
1287        let _ = self.requested_incompatible_qos_status();
1288        let _ = self.sample_lost_count();
1289        let _ = self.sample_rejected_status();
1290    }
1291
1292    /// Liveliness status of the matched writer (spec §2.2.4.2.14
1293    /// `LIVELINESS_CHANGED_STATUS`): `(alive, alive_count, not_alive_count)`.
1294    ///
1295    /// * `alive`: current state (true = writer delivered a sample within
1296    ///   its lease duration).
1297    /// * `alive_count`: counter of "not_alive → alive" transitions.
1298    /// * `not_alive_count`: counter of "alive → not_alive" transitions.
1299    ///
1300    /// Offline / INFINITE lease → `(false, 0, 0)` / `(true, 0, 0)`
1301    /// depending on init. For v1.3 only `LivelinessKind::Automatic` is
1302    /// monitored.
1303    #[must_use]
1304    pub fn liveliness_changed_status(&self) -> (bool, u64, u64) {
1305        #[cfg(feature = "std")]
1306        if let (Some(rt), Some(eid)) = (&self.runtime, self.entity_id) {
1307            let triple = rt.user_reader_liveliness_status(eid);
1308            // Listener trigger via delta detection.
1309            self.poll_liveliness_changed(triple.1, triple.2);
1310            return triple;
1311        }
1312        (false, 0, 0)
1313    }
1314
1315    /// Blocks until at least one sample is available or the timeout has
1316    /// elapsed. The sample is not removed in the process — it is placed
1317    /// in a staging buffer that the next `take()` reads. This keeps
1318    /// `wait_for_data` + `take()` the canonical subscriber loop instead
1319    /// of busy-polling in application code.
1320    ///
1321    /// Spec analog: OMG DDS 1.4 §2.2.2.5.8 `ReadCondition` + `WaitSet`.
1322    /// This API provides the most important semantics (wake-on-data)
1323    /// without the full WaitSet/Condition infrastructure.
1324    ///
1325    /// # Errors
1326    /// [`DdsError::Timeout`] if nothing arrives within the time window.
1327    #[cfg(feature = "std")]
1328    pub fn wait_for_data(&self, timeout: core::time::Duration) -> Result<()> {
1329        let Some(rx_mu) = self.rx.as_ref() else {
1330            // Offline mode: if the inbox already has something, OK,
1331            // otherwise timeout.
1332            let inbox_has = self.inbox.lock().map(|i| !i.is_empty()).unwrap_or(false);
1333            if inbox_has {
1334                return Ok(());
1335            }
1336            return Err(DdsError::Timeout);
1337        };
1338
1339        // Anything already in the staging inbox?
1340        {
1341            let inbox = self
1342                .inbox
1343                .lock()
1344                .map_err(|_| DdsError::PreconditionNotMet {
1345                    reason: "datareader inbox poisoned",
1346                })?;
1347            if !inbox.is_empty() {
1348                return Ok(());
1349            }
1350        }
1351
1352        let rx = rx_mu.lock().map_err(|_| DdsError::PreconditionNotMet {
1353            reason: "datareader rx poisoned",
1354        })?;
1355        let result = match rx.recv_timeout(timeout) {
1356            Ok(item) => {
1357                match item {
1358                    sample @ crate::runtime::UserSample::Alive { .. } => {
1359                        let mut inbox =
1360                            self.inbox
1361                                .lock()
1362                                .map_err(|_| DdsError::PreconditionNotMet {
1363                                    reason: "datareader inbox poisoned",
1364                                })?;
1365                        inbox.push(sample);
1366                    }
1367                    crate::runtime::UserSample::Lifecycle { key_hash, kind } => {
1368                        let lc_kind = match kind {
1369                            zerodds_rtps::history_cache::ChangeKind::NotAliveDisposed
1370                            | zerodds_rtps::history_cache::ChangeKind::NotAliveDisposedUnregistered => {
1371                                crate::sample_info::InstanceStateKind::NotAliveDisposed
1372                            }
1373                            zerodds_rtps::history_cache::ChangeKind::NotAliveUnregistered => {
1374                                crate::sample_info::InstanceStateKind::NotAliveNoWriters
1375                            }
1376                            _ => crate::sample_info::InstanceStateKind::Alive,
1377                        };
1378                        let mut holder_bytes = Vec::with_capacity(16);
1379                        holder_bytes.extend_from_slice(&key_hash);
1380                        let _ = self.__push_lifecycle(key_hash, holder_bytes, lc_kind);
1381                    }
1382                }
1383                Ok(())
1384            }
1385            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(DdsError::Timeout),
1386            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
1387                Err(DdsError::PreconditionNotMet {
1388                    reason: "datareader rx disconnected",
1389                })
1390            }
1391        };
1392        // Release the lock first, then fire listeners (lock discipline).
1393        drop(rx);
1394        if result.is_ok() {
1395            self.notify_data_arrived();
1396        }
1397        result
1398    }
1399
1400    /// Builtin-topic helper: returns the Arc to the shared inbox (reader
1401    /// clones share the same buffer).
1402    #[doc(hidden)]
1403    #[cfg(feature = "std")]
1404    pub fn __inbox_handle(&self) -> Arc<Mutex<Vec<crate::runtime::UserSample>>> {
1405        Arc::clone(&self.inbox)
1406    }
1407
1408    /// Test helper: inserts an encoded payload into the inbox.
1409    /// At runtime this is replaced by the ReliableReader delivery path.
1410    ///
1411    /// Triggers the listener bubble-up chain `on_data_on_readers`
1412    /// (subscriber stage) and `on_data_available` (reader stage). Spec
1413    /// §2.2.4.2.7.1 / §2.2.4.2.6.1.
1414    #[doc(hidden)]
1415    pub fn __push_raw(&self, bytes: Vec<u8>) -> Result<()> {
1416        self.__push_raw_with_writer(bytes, [0u8; 16], 0)
1417    }
1418
1419    /// Test hook: pushes a sample with an explicit writer GUID and
1420    /// `ownership_strength` into the inbox. Used by the Cyclone interop
1421    /// harness and the exclusive-ownership tests.
1422    #[doc(hidden)]
1423    pub fn __push_raw_with_writer(
1424        &self,
1425        bytes: Vec<u8>,
1426        writer_guid: [u8; 16],
1427        writer_strength: i32,
1428    ) -> Result<()> {
1429        {
1430            let mut inbox = self
1431                .inbox
1432                .lock()
1433                .map_err(|_| DdsError::PreconditionNotMet {
1434                    reason: "datareader inbox poisoned",
1435                })?;
1436            inbox.push(crate::runtime::UserSample::Alive {
1437                payload: crate::sample_bytes::SampleBytes::from_vec(bytes),
1438                writer_guid,
1439                writer_strength,
1440                // Test hook: raw-bytes injection, XCDR1 baseline, little-endian.
1441                representation: 0,
1442                big_endian: false,
1443                source_timestamp: None,
1444            });
1445        }
1446        // Listener notify outside the inbox lock to avoid re-entrancy.
1447        self.notify_data_arrived();
1448        Ok(())
1449    }
1450
1451    /// Calls the `on_data_on_readers` and `on_data_available` bubble-up
1452    /// paths. Spec §2.2.4.1: for each new sample, `data_on_readers`
1453    /// (subscriber level) and `data_available` (reader level) are set as
1454    /// independent statuses; once the subscriber has consumed
1455    /// `data_on_readers`, `data_available` must *not* be suppressed — the
1456    /// two statuses are separate bits in the mask.
1457    #[cfg(feature = "std")]
1458    pub(crate) fn notify_data_arrived(&self) {
1459        let chain = self.listener_chain();
1460        let reader_handle = self.entity_state.instance_handle();
1461        crate::listener_dispatch::dispatch_data_on_readers(&chain, reader_handle);
1462        crate::listener_dispatch::dispatch_data_available(&chain, reader_handle);
1463    }
1464
1465    // ========================================================================
1466    // SampleInfo statechart + instance lifecycle.
1467    // Spec §2.2.2.5.1, §2.2.2.5.3.{5,27,28}.
1468    // ========================================================================
1469
1470    /// Returns the current [`InstanceTracker`] (shared with the internal
1471    /// bookkeeping). Mainly for tests / inspection.
1472    #[cfg(feature = "std")]
1473    #[must_use]
1474    pub fn instance_tracker(&self) -> InstanceTracker {
1475        self.instances.clone()
1476    }
1477
1478    /// This reader's instance handle (GUID-derived). Lets the application
1479    /// ignore the reader's own subscription — e.g. a durability service
1480    /// ignoring its ingest reader on the replay-writer side to avoid an echo
1481    /// loop. Mirrors [`crate::DataWriter::instance_handle`].
1482    #[must_use]
1483    pub fn instance_handle(&self) -> crate::instance_handle::InstanceHandle {
1484        self.entity_state.instance_handle()
1485    }
1486
1487    /// Returns (Runtime, EntityId) when the reader runs in live mode.
1488    /// Cross-crate hook for the async layer (dcps-async), which must
1489    /// register the waker slot directly.
1490    #[doc(hidden)]
1491    #[cfg(feature = "std")]
1492    pub fn runtime_handle(
1493        &self,
1494    ) -> Option<(alloc::sync::Arc<crate::runtime::DcpsRuntime>, EntityId)> {
1495        match (&self.runtime, self.entity_id) {
1496            (Some(rt), Some(eid)) => Some((alloc::sync::Arc::clone(rt), eid)),
1497            _ => None,
1498        }
1499    }
1500
1501    /// Spec §2.2.3.23 — hook for "writer X lost liveliness". Does two
1502    /// things:
1503    ///   1. clears the OWNERSHIP=EXCLUSIVE owner for all instances whose
1504    ///      owner was this writer (so the next sample from another writer
1505    ///      can win again via
1506    ///      `should_accept_sample_under_exclusive_ownership`);
1507    ///   2. returns the number of affected instances.
1508    ///
1509    /// Called from the WLP path once a writer lease has expired (see
1510    /// `wlp::WlpEndpoint::lost_peers`).
1511    #[must_use]
1512    pub fn notify_writer_liveliness_lost(&self, writer_guid: [u8; 16]) -> usize {
1513        self.instances.clear_owner_for_writer(writer_guid)
1514    }
1515
1516    /// Like [`Self::notify_writer_liveliness_lost`], but matches only on
1517    /// the first 12 bytes (GuidPrefix). Allows failover when only the
1518    /// participant identity is known (e.g. on SPDP lease expiry).
1519    #[must_use]
1520    pub fn notify_participant_liveliness_lost(&self, prefix: [u8; 12]) -> usize {
1521        self.instances.clear_owner_for_writer_prefix(prefix)
1522    }
1523
1524    /// Turns a sample value into its corresponding local
1525    /// [`InstanceHandle`], or [`HANDLE_NIL`] if unknown / non-keyed.
1526    /// Spec §2.2.2.5.3.26 `lookup_instance` (reader variant).
1527    #[cfg(feature = "std")]
1528    #[must_use]
1529    pub fn lookup_instance(&self, instance: &T) -> InstanceHandle {
1530        if !T::HAS_KEY {
1531            return HANDLE_NIL;
1532        }
1533        let mut holder = crate::dds_type::PlainCdr2BeKeyHolder::new();
1534        instance.encode_key_holder_be(&mut holder);
1535        let bytes = holder.as_bytes();
1536        let max = T::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
1537        let kh = crate::dds_type::compute_key_hash(bytes, max);
1538        self.instances.lookup(&kh).unwrap_or(HANDLE_NIL)
1539    }
1540
1541    /// Spec §2.2.2.5.3.25 `get_key_value`. Returns the sample value with
1542    /// only the `@key` fields filled in (reconstructed from the stored
1543    /// key holder via `T::decode`).
1544    ///
1545    /// # Errors
1546    /// `BadParameter` if `handle` is unknown; `WireError` if `T::decode`
1547    /// cannot reconstruct the key stream.
1548    #[cfg(feature = "std")]
1549    pub fn get_key_value(&self, handle: InstanceHandle) -> Result<T> {
1550        let Some(bytes) = self.instances.get_key_holder(handle) else {
1551            return Err(DdsError::BadParameter {
1552                what: "unknown instance handle",
1553            });
1554        };
1555        T::decode(&bytes).map_err(|e| DdsError::WireError {
1556            message: alloc::string::ToString::to_string(&e),
1557        })
1558    }
1559
1560    /// Drains all pending bytes from rx + inbox into the internal sample
1561    /// cache. For each sample the KeyHash is computed, the instance is
1562    /// registered (if new), and a matching [`SampleInfo`] is created.
1563    ///
1564    /// Called automatically by the `*_with_info`/`*_instance` APIs.
1565    #[cfg(feature = "std")]
1566    fn ingest_into_cache(&self) -> Result<()> {
1567        // Step 1: collect all incoming samples. `raw` carries
1568        // (bytes, writer_guid, writer_strength) so the exclusive-
1569        // ownership filter (DDS 1.4 §2.2.3.23) is applicable.
1570        //
1571        // Wave 2.1 zero-copy: `raw` now carries `SampleBytes` (refcounted
1572        // Arc<[u8]>) instead of `Vec<u8>`. Decode goes via Deref<[u8]>
1573        // directly without to_vec. Saves 2 hot-path copies per recv'd
1574        // Alive sample. Spec: docs/specs/zerodds-zero-copy-1.0.md §6 wave 2.1.
1575        let mut raw: Vec<RawIngestSample> = Vec::new();
1576        {
1577            let mut inbox = self
1578                .inbox
1579                .lock()
1580                .map_err(|_| DdsError::PreconditionNotMet {
1581                    reason: "datareader inbox poisoned",
1582                })?;
1583            for item in inbox.drain(..) {
1584                if let crate::runtime::UserSample::Alive {
1585                    payload,
1586                    writer_guid,
1587                    writer_strength,
1588                    source_timestamp,
1589                    big_endian,
1590                    representation,
1591                    ..
1592                } = item
1593                {
1594                    raw.push((
1595                        payload,
1596                        writer_guid,
1597                        writer_strength,
1598                        source_timestamp,
1599                        big_endian,
1600                        representation,
1601                    ));
1602                }
1603            }
1604        }
1605        // Live-mode channel: enqueue Alive samples into `raw`, handle
1606        // lifecycle markers directly via __push_lifecycle.
1607        let mut lifecycle_pending: Vec<(
1608            crate::instance_tracker::KeyHash,
1609            crate::sample_info::InstanceStateKind,
1610        )> = Vec::new();
1611        if let Some(rx_mu) = self.rx.as_ref() {
1612            let rx = rx_mu.lock().map_err(|_| DdsError::PreconditionNotMet {
1613                reason: "datareader rx poisoned",
1614            })?;
1615            while let Ok(item) = rx.try_recv() {
1616                match item {
1617                    crate::runtime::UserSample::Alive {
1618                        payload: bytes,
1619                        writer_guid,
1620                        writer_strength,
1621                        source_timestamp,
1622                        big_endian,
1623                        representation,
1624                        ..
1625                    } => raw.push((
1626                        bytes,
1627                        writer_guid,
1628                        writer_strength,
1629                        source_timestamp,
1630                        big_endian,
1631                        representation,
1632                    )),
1633                    crate::runtime::UserSample::Lifecycle { key_hash, kind } => {
1634                        let lc_kind = match kind {
1635                            zerodds_rtps::history_cache::ChangeKind::NotAliveDisposed
1636                            | zerodds_rtps::history_cache::ChangeKind::NotAliveDisposedUnregistered => {
1637                                crate::sample_info::InstanceStateKind::NotAliveDisposed
1638                            }
1639                            zerodds_rtps::history_cache::ChangeKind::NotAliveUnregistered => {
1640                                crate::sample_info::InstanceStateKind::NotAliveNoWriters
1641                            }
1642                            _ => crate::sample_info::InstanceStateKind::Alive,
1643                        };
1644                        lifecycle_pending.push((key_hash, lc_kind));
1645                    }
1646                }
1647            }
1648        }
1649        // Apply lifecycle markers only AFTER draining so the lock path
1650        // stays clean (__push_lifecycle takes its own locks).
1651        for (kh, lc_kind) in lifecycle_pending {
1652            let mut holder_bytes = Vec::with_capacity(16);
1653            holder_bytes.extend_from_slice(&kh);
1654            let _ = self.__push_lifecycle(kh, holder_bytes, lc_kind);
1655        }
1656        let now = get_current_time();
1657        let mut cache = self
1658            .cache
1659            .lock()
1660            .map_err(|_| DdsError::PreconditionNotMet {
1661                reason: "datareader cache poisoned",
1662            })?;
1663        if raw.is_empty() {
1664            // Even without new bytes, autopurge must run; otherwise
1665            // disposed/nowriter instances never expire outside sample inflow.
1666            self.run_reader_autopurge(now, &mut cache);
1667            return Ok(());
1668        }
1669        for (bytes, writer_guid, writer_strength, src_ts, big_endian, representation) in raw {
1670            // RTPS-F1: use the writer's INFO_TS source timestamp for the
1671            // SampleInfo + DESTINATION_ORDER; fall back to reception time
1672            // (`now`) when the writer sent none.
1673            let sample_source_ts = src_ts.map_or(now, crate::time::he_timestamp_to_time);
1674            // Decode T to (a) evaluate the filter and (b) compute the
1675            // KeyHash.
1676            let sample =
1677                decode_for_encap::<T>(&bytes, representation, big_endian).map_err(|e| {
1678                    DdsError::WireError {
1679                        message: alloc::string::ToString::to_string(&e),
1680                    }
1681                })?;
1682            if !self.sample_passes_filter(&sample) {
1683                continue;
1684            }
1685            // §2.2.3.23 exclusive-ownership filter: reject samples from
1686            // weaker writers before they enter the cache.
1687            if !self.passes_exclusive_ownership(&sample, writer_guid, writer_strength) {
1688                continue;
1689            }
1690            let info = if T::HAS_KEY {
1691                let mut holder = crate::dds_type::PlainCdr2BeKeyHolder::new();
1692                sample.encode_key_holder_be(&mut holder);
1693                let key_bytes = holder.as_bytes().to_vec();
1694                let max = T::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
1695                let kh = crate::dds_type::compute_key_hash(&key_bytes, max);
1696                // QoS filter BEFORE observe_sample so discarded samples do
1697                // not affect the sample state.
1698                let (min_sep_nanos, by_source_ts) = {
1699                    let qos = self.qos.lock().unwrap_or_else(|e| e.into_inner());
1700                    (
1701                        qos.time_based_filter.minimum_separation.to_nanos(),
1702                        qos.destination_order.kind
1703                            == zerodds_qos::DestinationOrderKind::BySourceTimestamp,
1704                    )
1705                };
1706                // Spec §2.2.3.13 TIME_BASED_FILTER: drop if less than
1707                // minimum_separation has elapsed since the last delivered
1708                // sample of this instance.
1709                if !self
1710                    .instances
1711                    .should_deliver_under_time_based_filter(&kh, now, min_sep_nanos)
1712                {
1713                    continue;
1714                }
1715                // Spec §2.2.3.18 DESTINATION_ORDER: under BY_SOURCE_TIMESTAMP
1716                // the ordering key is the writer's source timestamp (from
1717                // INFO_TS), not the reception time — only deliver samples with a
1718                // strictly greater source_ts. Under BY_RECEPTION_TIMESTAMP the
1719                // reception clock (`now`) is the key.
1720                let order_ts = if by_source_ts { sample_source_ts } else { now };
1721                if !self.instances.should_deliver_under_destination_order(
1722                    &kh,
1723                    order_ts,
1724                    by_source_ts,
1725                ) {
1726                    continue;
1727                }
1728                let (handle, _) = self.instances.observe_sample(kh, key_bytes, Some(now));
1729                self.instances.record_delivery(&kh, order_ts);
1730                let state = match self.instances.get_by_handle(handle) {
1731                    Some(s) => s,
1732                    None => continue, // should never happen — defensive
1733                };
1734                SampleInfo {
1735                    sample_state: SampleStateKind::NotRead,
1736                    view_state: if state.reader_view_new {
1737                        ViewStateKind::New
1738                    } else {
1739                        ViewStateKind::NotNew
1740                    },
1741                    instance_state: state.kind,
1742                    disposed_generation_count: state.disposed_generation_count,
1743                    no_writers_generation_count: state.no_writers_generation_count,
1744                    source_timestamp: sample_source_ts,
1745                    instance_handle: handle,
1746                    valid_data: true,
1747                    ..SampleInfo::default()
1748                }
1749            } else {
1750                // Non-keyed topics: a "pseudo handle" per sample would be
1751                // overkill — we leave it at HANDLE_NIL (spec §2.2.2.5.1.10
1752                // allows that, since the instance view for non-keyed
1753                // topics is formally "everything is one instance").
1754                SampleInfo {
1755                    sample_state: SampleStateKind::NotRead,
1756                    view_state: ViewStateKind::NotNew,
1757                    instance_handle: HANDLE_NIL,
1758                    source_timestamp: sample_source_ts,
1759                    valid_data: true,
1760                    ..SampleInfo::default()
1761                }
1762            };
1763            cache.push(CachedSample {
1764                bytes: Some(bytes),
1765                info,
1766            });
1767        }
1768        // Spec §2.2.3.22 ReaderDataLifecycle: remove instances from the
1769        // tracker and cache that have been in NotAlive-Disposed or
1770        // NotAlive-NoWriters longer than autopurge_*_samples_delay.
1771        self.run_reader_autopurge(now, &mut cache);
1772        Ok(())
1773    }
1774
1775    /// Applies `ReaderDataLifecycle.autopurge_*`: removes expired
1776    /// instances from the tracker + cache. Called by `ingest_into_cache`
1777    /// and when reading with no new bytes.
1778    #[cfg(feature = "std")]
1779    fn run_reader_autopurge(&self, now: Time, cache: &mut Vec<CachedSample>) {
1780        let (purge_disp, purge_now) = {
1781            let qos = self.qos.lock().unwrap_or_else(|e| e.into_inner());
1782            (
1783                qos.reader_data_lifecycle
1784                    .autopurge_disposed_samples_delay
1785                    .to_nanos(),
1786                qos.reader_data_lifecycle
1787                    .autopurge_nowriter_samples_delay
1788                    .to_nanos(),
1789            )
1790        };
1791        if purge_disp == u128::MAX && purge_now == u128::MAX {
1792            return;
1793        }
1794        let purged = self.instances.autopurge(now, purge_disp, purge_now);
1795        if purged > 0 {
1796            cache.retain(|s| {
1797                s.info.instance_handle.is_nil()
1798                    || self
1799                        .instances
1800                        .get_by_handle(s.info.instance_handle)
1801                        .is_some()
1802            });
1803        }
1804    }
1805
1806    /// Pushes a pure lifecycle marker (dispose / unregister) into the
1807    /// cache. Called by the runtime as soon as a writer sends
1808    /// `dispose`/`unregister_instance`.
1809    #[cfg(feature = "std")]
1810    #[doc(hidden)]
1811    pub fn __push_lifecycle(
1812        &self,
1813        keyhash: crate::instance_tracker::KeyHash,
1814        key_holder: Vec<u8>,
1815        kind: InstanceStateKind,
1816    ) -> Result<()> {
1817        let now = get_current_time();
1818        // First bring the instance into the right state in the tracker.
1819        // observe_sample re-registers it if needed and makes it alive.
1820        let (handle, _) = self
1821            .instances
1822            .observe_sample(keyhash, key_holder, Some(now));
1823        match kind {
1824            InstanceStateKind::NotAliveDisposed => {
1825                self.instances.dispose(handle, Some(now));
1826            }
1827            InstanceStateKind::NotAliveNoWriters => {
1828                self.instances.unregister(handle, Some(now));
1829            }
1830            InstanceStateKind::Alive => {}
1831        }
1832        let Some(state) = self.instances.get_by_handle(handle) else {
1833            return Ok(()); // should never happen — defensive
1834        };
1835        let info = SampleInfo {
1836            source_timestamp: now,
1837            valid_data: false,
1838            instance_handle: handle,
1839            instance_state: state.kind,
1840            disposed_generation_count: state.disposed_generation_count,
1841            no_writers_generation_count: state.no_writers_generation_count,
1842            view_state: if state.reader_view_new {
1843                ViewStateKind::New
1844            } else {
1845                ViewStateKind::NotNew
1846            },
1847            ..SampleInfo::default()
1848        };
1849        let mut cache = self
1850            .cache
1851            .lock()
1852            .map_err(|_| DdsError::PreconditionNotMet {
1853                reason: "datareader cache poisoned",
1854            })?;
1855        cache.push(CachedSample { bytes: None, info });
1856        Ok(())
1857    }
1858
1859    /// `take` with full [`SampleInfo`]. Spec §2.2.2.5.3.5 `take`.
1860    /// Consumes the samples from the cache (the `NOT_READ → READ`
1861    /// transition is moot since they are gone).
1862    ///
1863    /// # Errors
1864    /// Same as [`Self::take`].
1865    #[cfg(feature = "std")]
1866    pub fn take_with_info(&self) -> Result<Vec<Sample<T>>> {
1867        self.take_filtered(
1868            sample_state_mask::ANY,
1869            view_state_mask::ANY,
1870            instance_state_mask::ANY,
1871        )
1872    }
1873
1874    /// `read` with full [`SampleInfo`]. Does not consume — only marks
1875    /// the samples as `READ` (spec §2.2.2.5.3.4).
1876    ///
1877    /// # Errors
1878    /// Same as [`Self::read`].
1879    #[cfg(feature = "std")]
1880    pub fn read_with_info(&self) -> Result<Vec<Sample<T>>> {
1881        self.read_filtered(
1882            sample_state_mask::ANY,
1883            view_state_mask::ANY,
1884            instance_state_mask::ANY,
1885        )
1886    }
1887
1888    /// `take` with state masks (spec §2.2.2.5.3.6 `take_w_condition`).
1889    ///
1890    /// # Errors
1891    /// Same as [`Self::take`].
1892    #[cfg(feature = "std")]
1893    pub fn take_filtered(
1894        &self,
1895        sample_mask: u32,
1896        view_mask: u32,
1897        instance_mask: u32,
1898    ) -> Result<Vec<Sample<T>>> {
1899        self.ingest_into_cache()?;
1900        let mut cache = self
1901            .cache
1902            .lock()
1903            .map_err(|_| DdsError::PreconditionNotMet {
1904                reason: "datareader cache poisoned",
1905            })?;
1906        let mut out = Vec::new();
1907        let mut keep = Vec::with_capacity(cache.len());
1908        for s in cache.drain(..) {
1909            if s.info.matches_states(sample_mask, view_mask, instance_mask) {
1910                let sample = self.materialize(s)?;
1911                self.instances.mark_view_seen(sample.info.instance_handle);
1912                if sample.info.instance_handle != HANDLE_NIL {
1913                    self.instances.drain_samples(sample.info.instance_handle, 1);
1914                }
1915                out.push(sample);
1916            } else {
1917                keep.push(s);
1918            }
1919        }
1920        *cache = keep;
1921        Ok(out)
1922    }
1923
1924    /// `read` with state masks (spec §2.2.2.5.3.3 `read_w_condition`).
1925    ///
1926    /// # Errors
1927    /// Same as [`Self::read`].
1928    #[cfg(feature = "std")]
1929    pub fn read_filtered(
1930        &self,
1931        sample_mask: u32,
1932        view_mask: u32,
1933        instance_mask: u32,
1934    ) -> Result<Vec<Sample<T>>> {
1935        self.ingest_into_cache()?;
1936        let mut cache = self
1937            .cache
1938            .lock()
1939            .map_err(|_| DdsError::PreconditionNotMet {
1940                reason: "datareader cache poisoned",
1941            })?;
1942        let mut out = Vec::with_capacity(cache.len());
1943        for s in cache.iter_mut() {
1944            if !s.info.matches_states(sample_mask, view_mask, instance_mask) {
1945                continue;
1946            }
1947            // Build a snapshot (with the current sample-state view).
1948            let snapshot = Sample::new(
1949                self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?,
1950                s.info,
1951            );
1952            // Sample-state transition NOT_READ → READ (spec §2.2.2.5.3.4).
1953            s.info.sample_state = SampleStateKind::Read;
1954            self.instances.mark_view_seen(s.info.instance_handle);
1955            out.push(snapshot);
1956        }
1957        Ok(out)
1958    }
1959
1960    /// `read_w_condition` (spec §2.2.2.5.3.7) — in addition to the state
1961    /// mask, applies the QueryCondition's SQL filter per sample. Samples
1962    /// stay in the cache (sample state NOT_READ → READ).
1963    ///
1964    /// # Errors
1965    /// `PreconditionNotMet` on lock poisoning or SQL eval error.
1966    #[cfg(feature = "std")]
1967    pub fn read_w_condition(
1968        &self,
1969        condition: &Arc<crate::condition::QueryCondition>,
1970    ) -> Result<Vec<Sample<T>>> {
1971        let base = condition.base();
1972        let sample_mask = base.get_sample_state_mask();
1973        let view_mask = base.get_view_state_mask();
1974        let instance_mask = base.get_instance_state_mask();
1975
1976        self.ingest_into_cache()?;
1977        let mut cache = self
1978            .cache
1979            .lock()
1980            .map_err(|_| DdsError::PreconditionNotMet {
1981                reason: "datareader cache poisoned",
1982            })?;
1983        let mut out = Vec::with_capacity(cache.len());
1984        for s in cache.iter_mut() {
1985            if !s.info.matches_states(sample_mask, view_mask, instance_mask) {
1986                continue;
1987            }
1988            let decoded = self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?;
1989            let row = crate::dds_type::DdsTypeRow::new(&decoded);
1990            // Filter eval error -> sample is rejected (spec: "filter
1991            // expression false" semantics), but we do not propagate a
1992            // hard error upward, except for lock poisoning.
1993            if !condition.evaluate(&row).unwrap_or(false) {
1994                continue;
1995            }
1996            let snapshot = Sample::new(decoded, s.info);
1997            s.info.sample_state = SampleStateKind::Read;
1998            self.instances.mark_view_seen(s.info.instance_handle);
1999            out.push(snapshot);
2000        }
2001        Ok(out)
2002    }
2003
2004    /// `take_w_condition` (spec §2.2.2.5.3.8) — like `read_w_condition`,
2005    /// but consumes the samples (removes them from the cache).
2006    ///
2007    /// # Errors
2008    /// `PreconditionNotMet` on lock poisoning or SQL eval error.
2009    #[cfg(feature = "std")]
2010    pub fn take_w_condition(
2011        &self,
2012        condition: &Arc<crate::condition::QueryCondition>,
2013    ) -> Result<Vec<Sample<T>>> {
2014        let base = condition.base();
2015        let sample_mask = base.get_sample_state_mask();
2016        let view_mask = base.get_view_state_mask();
2017        let instance_mask = base.get_instance_state_mask();
2018
2019        self.ingest_into_cache()?;
2020        let mut cache = self
2021            .cache
2022            .lock()
2023            .map_err(|_| DdsError::PreconditionNotMet {
2024                reason: "datareader cache poisoned",
2025            })?;
2026        let mut out = Vec::new();
2027        let mut keep = Vec::with_capacity(cache.len());
2028        for s in cache.drain(..) {
2029            if !s.info.matches_states(sample_mask, view_mask, instance_mask) {
2030                keep.push(s);
2031                continue;
2032            }
2033            let decoded = self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?;
2034            let row = crate::dds_type::DdsTypeRow::new(&decoded);
2035            if !condition.evaluate(&row).unwrap_or(false) {
2036                keep.push(s);
2037                continue;
2038            }
2039            let sample = Sample::new(decoded, s.info);
2040            self.instances.mark_view_seen(sample.info.instance_handle);
2041            if sample.info.instance_handle != HANDLE_NIL {
2042                self.instances.drain_samples(sample.info.instance_handle, 1);
2043            }
2044            out.push(sample);
2045        }
2046        *cache = keep;
2047        Ok(out)
2048    }
2049
2050    /// `read_instance` (spec §2.2.2.5.3.27). Returns only samples of the
2051    /// given instance.
2052    ///
2053    /// # Errors
2054    /// `BadParameter` if `handle == HANDLE_NIL`.
2055    #[cfg(feature = "std")]
2056    pub fn read_instance(&self, handle: InstanceHandle) -> Result<Vec<Sample<T>>> {
2057        if handle.is_nil() {
2058            return Err(DdsError::BadParameter {
2059                what: "read_instance with HANDLE_NIL",
2060            });
2061        }
2062        self.ingest_into_cache()?;
2063        let mut cache = self
2064            .cache
2065            .lock()
2066            .map_err(|_| DdsError::PreconditionNotMet {
2067                reason: "datareader cache poisoned",
2068            })?;
2069        let mut out = Vec::new();
2070        for s in cache.iter_mut() {
2071            if s.info.instance_handle != handle {
2072                continue;
2073            }
2074            let snap = Sample::new(
2075                self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?,
2076                s.info,
2077            );
2078            s.info.sample_state = SampleStateKind::Read;
2079            self.instances.mark_view_seen(handle);
2080            out.push(snap);
2081        }
2082        Ok(out)
2083    }
2084
2085    /// `take_instance` (spec §2.2.2.5.3.27, take variant). Consumes.
2086    ///
2087    /// # Errors
2088    /// `BadParameter` if `handle == HANDLE_NIL`.
2089    #[cfg(feature = "std")]
2090    pub fn take_instance(&self, handle: InstanceHandle) -> Result<Vec<Sample<T>>> {
2091        if handle.is_nil() {
2092            return Err(DdsError::BadParameter {
2093                what: "take_instance with HANDLE_NIL",
2094            });
2095        }
2096        self.ingest_into_cache()?;
2097        let mut cache = self
2098            .cache
2099            .lock()
2100            .map_err(|_| DdsError::PreconditionNotMet {
2101                reason: "datareader cache poisoned",
2102            })?;
2103        let mut out = Vec::new();
2104        let mut keep = Vec::with_capacity(cache.len());
2105        for s in cache.drain(..) {
2106            if s.info.instance_handle == handle {
2107                out.push(self.materialize(s)?);
2108            } else {
2109                keep.push(s);
2110            }
2111        }
2112        *cache = keep;
2113        if !out.is_empty() {
2114            self.instances.mark_view_seen(handle);
2115            self.instances.drain_samples(handle, out.len() as u32);
2116        }
2117        Ok(out)
2118    }
2119
2120    /// `read_next_instance` (spec §2.2.2.5.3.28). Returns the samples of
2121    /// the **next** instance (in sort order) after `previous`.
2122    ///
2123    /// `previous == HANDLE_NIL` starts at the first handle.
2124    ///
2125    /// # Errors
2126    /// Same as `read`.
2127    #[cfg(feature = "std")]
2128    pub fn read_next_instance(&self, previous: InstanceHandle) -> Result<Vec<Sample<T>>> {
2129        let Some(next) = self.instances.next_handle_after(previous) else {
2130            return Ok(Vec::new());
2131        };
2132        self.read_instance(next)
2133    }
2134
2135    /// `take_next_instance` (spec §2.2.2.5.3.28). Take variant.
2136    ///
2137    /// # Errors
2138    /// Same as `take`.
2139    #[cfg(feature = "std")]
2140    pub fn take_next_instance(&self, previous: InstanceHandle) -> Result<Vec<Sample<T>>> {
2141        let Some(next) = self.instances.next_handle_after(previous) else {
2142            return Ok(Vec::new());
2143        };
2144        self.take_instance(next)
2145    }
2146
2147    /// Helper: turns a CachedSample into a `Sample<T>`. For lifecycle
2148    /// markers (`bytes == None`), `T` is reconstructed from the stored
2149    /// key holder (spec §2.2.2.5.1.13: `data` then contains only the key
2150    /// portion).
2151    #[cfg(feature = "std")]
2152    fn materialize(&self, s: CachedSample) -> Result<Sample<T>> {
2153        let data = self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?;
2154        #[cfg(feature = "metrics")]
2155        crate::metrics::add_samples_read(self.topic.name(), 1);
2156        Ok(Sample::new(data, s.info))
2157    }
2158
2159    /// Decode helper: for `Some(bytes)` via `T::decode`, for `None`
2160    /// (lifecycle marker) via the instance's key holder; if that is also
2161    /// unavailable, falls back to `T::decode(&[])`.
2162    #[cfg(feature = "std")]
2163    fn decode_or_keyholder(&self, bytes: Option<&[u8]>, handle: InstanceHandle) -> Result<T> {
2164        if let Some(b) = bytes {
2165            return T::decode(b).map_err(|e| DdsError::WireError {
2166                message: alloc::string::ToString::to_string(&e),
2167            });
2168        }
2169        if let Some(holder) = self.instances.get_key_holder(handle) {
2170            return T::decode(&holder).map_err(|e| DdsError::WireError {
2171                message: alloc::string::ToString::to_string(&e),
2172            });
2173        }
2174        T::decode(&[]).map_err(|e| DdsError::WireError {
2175            message: alloc::string::ToString::to_string(&e),
2176        })
2177    }
2178}
2179
2180#[cfg(feature = "std")]
2181impl<T: DdsType> crate::entity::Entity for DataReader<T> {
2182    type Qos = DataReaderQos;
2183
2184    fn get_qos(&self) -> Self::Qos {
2185        self.qos.lock().map(|q| q.clone()).unwrap_or_default()
2186    }
2187
2188    /// Spec §2.2.3 / §2.2.2.5.3: DURABILITY, RELIABILITY, HISTORY,
2189    /// RESOURCE_LIMITS, OWNERSHIP are Changeable=NO post-enable.
2190    fn set_qos(&self, qos: Self::Qos) -> Result<()> {
2191        let enabled = self.entity_state.is_enabled();
2192        if let Ok(mut current) = self.qos.lock() {
2193            if enabled {
2194                if current.durability != qos.durability {
2195                    return Err(crate::entity::immutable_if_enabled("DURABILITY"));
2196                }
2197                if current.reliability != qos.reliability {
2198                    return Err(crate::entity::immutable_if_enabled("RELIABILITY"));
2199                }
2200                if current.history != qos.history {
2201                    return Err(crate::entity::immutable_if_enabled("HISTORY"));
2202                }
2203                if current.resource_limits != qos.resource_limits {
2204                    return Err(crate::entity::immutable_if_enabled("RESOURCE_LIMITS"));
2205                }
2206                if current.ownership != qos.ownership {
2207                    return Err(crate::entity::immutable_if_enabled("OWNERSHIP"));
2208                }
2209                if current.liveliness != qos.liveliness {
2210                    return Err(crate::entity::immutable_if_enabled("LIVELINESS"));
2211                }
2212            }
2213            *current = qos;
2214        }
2215        Ok(())
2216    }
2217
2218    fn enable(&self) -> Result<()> {
2219        self.entity_state.enable();
2220        Ok(())
2221    }
2222
2223    fn entity_state(&self) -> Arc<crate::entity::EntityState> {
2224        Arc::clone(&self.entity_state)
2225    }
2226}
2227
2228// ---- Boxed type-mapped variant for heterogeneous reader lists ----
2229#[allow(dead_code)]
2230pub(crate) trait AnyDataReader: Send + Sync + core::fmt::Debug {
2231    fn topic_name(&self) -> &str;
2232    fn type_name(&self) -> &'static str;
2233}
2234
2235impl<T: DdsType + Send + 'static> AnyDataReader for DataReader<T>
2236where
2237    T: Send + Sync,
2238{
2239    fn topic_name(&self) -> &str {
2240        self.topic.name()
2241    }
2242    fn type_name(&self) -> &'static str {
2243        T::TYPE_NAME
2244    }
2245}
2246
2247#[allow(dead_code)]
2248pub(crate) fn boxed_any_reader<T: DdsType + Send + Sync + 'static>(
2249    r: DataReader<T>,
2250) -> Box<dyn AnyDataReader> {
2251    Box::new(r)
2252}
2253
2254#[cfg(test)]
2255#[allow(clippy::expect_used, clippy::unwrap_used)]
2256mod tests {
2257    use super::*;
2258    use crate::dds_type::RawBytes;
2259    use crate::factory::DomainParticipantFactory;
2260    use crate::qos::{DomainParticipantQos, TopicQos};
2261
2262    fn mk_topic() -> Topic<RawBytes> {
2263        let p = DomainParticipantFactory::instance()
2264            .create_participant_offline(0, DomainParticipantQos::default());
2265        Topic::new("Chatter".into(), TopicQos::default(), p)
2266    }
2267
2268    /// `decode_for_encap` must pick the decoder matching the sample's
2269    /// encapsulation — XCDR version *and* byte order — so an XCDR1 writer
2270    /// (CycloneDDS / legacy RTI default for `@final`) is decoded with the
2271    /// classic-CDR rule, not the XCDR2 rule. Regression for Bug DR1.
2272    #[test]
2273    fn decode_for_encap_dispatches_on_representation_and_endianness() {
2274        use crate::dds_type::{DecodeError, EncodeError};
2275
2276        /// Marker whose four decode variants return a distinct value, so the
2277        /// test can prove which one `decode_for_encap` selected.
2278        #[derive(Debug, PartialEq)]
2279        struct Probe(u8);
2280        impl DdsType for Probe {
2281            const TYPE_NAME: &'static str = "test::Probe";
2282            fn encode(&self, _out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
2283                Ok(())
2284            }
2285            fn decode(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2286                Ok(Probe(1)) // XCDR2-LE
2287            }
2288            fn decode_be(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2289                Ok(Probe(2)) // XCDR2-BE
2290            }
2291            fn decode_xcdr1(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2292                Ok(Probe(10)) // XCDR1-LE
2293            }
2294            fn decode_xcdr1_be(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2295                Ok(Probe(20)) // XCDR1-BE
2296            }
2297        }
2298
2299        // (representation, big_endian) -> expected marker.
2300        // representation: 1 = XCDR2, 0 = XCDR1.
2301        assert_eq!(decode_for_encap::<Probe>(&[], 1, false).unwrap(), Probe(1));
2302        assert_eq!(decode_for_encap::<Probe>(&[], 1, true).unwrap(), Probe(2));
2303        assert_eq!(decode_for_encap::<Probe>(&[], 0, false).unwrap(), Probe(10));
2304        assert_eq!(decode_for_encap::<Probe>(&[], 0, true).unwrap(), Probe(20));
2305    }
2306
2307    #[test]
2308    fn subscriber_creates_datareader_for_matching_type() {
2309        let s = Subscriber::new(SubscriberQos::default(), None);
2310        let r = s
2311            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2312            .unwrap();
2313        assert_eq!(r.topic().name(), "Chatter");
2314    }
2315
2316    #[test]
2317    fn datareader_take_returns_decoded_samples() {
2318        let s = Subscriber::new(SubscriberQos::default(), None);
2319        let r = s
2320            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2321            .unwrap();
2322        r.__push_raw(vec![1, 2, 3]).unwrap();
2323        r.__push_raw(vec![4, 5]).unwrap();
2324        let samples = r.take().unwrap();
2325        assert_eq!(samples.len(), 2);
2326        assert_eq!(samples[0].data, vec![1, 2, 3]);
2327        assert_eq!(samples[1].data, vec![4, 5]);
2328        // The inbox is now empty.
2329        let again = r.take().unwrap();
2330        assert!(again.is_empty());
2331    }
2332
2333    #[test]
2334    fn datareader_read_preserves_samples() {
2335        let s = Subscriber::new(SubscriberQos::default(), None);
2336        let r = s
2337            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2338            .unwrap();
2339        r.__push_raw(vec![0xAA]).unwrap();
2340        let first = r.read().unwrap();
2341        let second = r.read().unwrap();
2342        assert_eq!(first.len(), 1);
2343        assert_eq!(second.len(), 1);
2344    }
2345
2346    // poll_subscription_matched + listener-slot API.
2347
2348    use core::sync::atomic::{AtomicU32, Ordering};
2349
2350    #[test]
2351    fn datareader_set_listener_stores_arc_and_mask() {
2352        struct L;
2353        impl crate::listener::DataReaderListener for L {}
2354        let s = Subscriber::new(SubscriberQos::default(), None);
2355        let r = s
2356            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2357            .unwrap();
2358        assert!(r.get_listener().is_none());
2359        r.set_listener(Some(Arc::new(L)), crate::psm_constants::status::ANY);
2360        assert!(r.get_listener().is_some());
2361        assert_eq!(
2362            r.entity_state.listener_mask(),
2363            crate::psm_constants::status::ANY
2364        );
2365    }
2366
2367    #[test]
2368    fn poll_subscription_matched_fires_on_count_increase() {
2369        struct Cnt(AtomicU32);
2370        impl crate::listener::DataReaderListener for Cnt {
2371            fn on_subscription_matched(
2372                &self,
2373                _r: crate::InstanceHandle,
2374                _s: crate::status::SubscriptionMatchedStatus,
2375            ) {
2376                self.0.fetch_add(1, Ordering::Relaxed);
2377            }
2378        }
2379        let s = Subscriber::new(SubscriberQos::default(), None);
2380        let r = s
2381            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2382            .unwrap();
2383        let cnt = Arc::new(Cnt(AtomicU32::new(0)));
2384        r.set_listener(Some(cnt.clone()), crate::psm_constants::status::ANY);
2385
2386        r.poll_subscription_matched(0);
2387        assert_eq!(cnt.0.load(Ordering::Relaxed), 1);
2388        r.poll_subscription_matched(1);
2389        assert_eq!(cnt.0.load(Ordering::Relaxed), 2);
2390        r.poll_subscription_matched(1);
2391        assert_eq!(cnt.0.load(Ordering::Relaxed), 2);
2392        r.poll_subscription_matched(0);
2393        assert_eq!(cnt.0.load(Ordering::Relaxed), 3);
2394    }
2395
2396    #[test]
2397    fn poll_subscription_matched_with_no_listener_is_noop() {
2398        let s = Subscriber::new(SubscriberQos::default(), None);
2399        let r = s
2400            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2401            .unwrap();
2402        r.poll_subscription_matched(0);
2403        r.poll_subscription_matched(3);
2404    }
2405
2406    #[test]
2407    fn notify_data_arrived_fires_data_available_and_data_on_readers() {
2408        struct ReadCnt(AtomicU32, AtomicU32);
2409        impl crate::listener::DataReaderListener for ReadCnt {
2410            fn on_data_available(&self, _r: crate::InstanceHandle) {
2411                self.0.fetch_add(1, Ordering::Relaxed);
2412            }
2413            fn on_subscription_matched(
2414                &self,
2415                _r: crate::InstanceHandle,
2416                _s: crate::status::SubscriptionMatchedStatus,
2417            ) {
2418                self.1.fetch_add(1, Ordering::Relaxed);
2419            }
2420        }
2421        let s = Subscriber::new(SubscriberQos::default(), None);
2422        let r = s
2423            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2424            .unwrap();
2425        let rc = Arc::new(ReadCnt(AtomicU32::new(0), AtomicU32::new(0)));
2426        r.set_listener(Some(rc.clone()), crate::psm_constants::status::ANY);
2427        r.notify_data_arrived();
2428        assert_eq!(rc.0.load(Ordering::Relaxed), 1);
2429        // sub_matched counter unchanged (different status bit).
2430        assert_eq!(rc.1.load(Ordering::Relaxed), 0);
2431    }
2432
2433    // ---- §2.2.2.5.2.8/.9 begin/end_access ----
2434
2435    #[test]
2436    fn subscriber_begin_end_access_roundtrip() {
2437        let s = Subscriber::new(SubscriberQos::default(), None);
2438        assert!(!s.is_access_open());
2439        s.begin_access();
2440        assert!(s.is_access_open());
2441        s.end_access().unwrap();
2442        assert!(!s.is_access_open());
2443    }
2444
2445    #[test]
2446    fn subscriber_end_access_without_begin_returns_precondition_not_met() {
2447        // Spec §2.2.2.5.2.9 — end without begin is a spec violation.
2448        let s = Subscriber::new(SubscriberQos::default(), None);
2449        let res = s.end_access();
2450        assert!(matches!(
2451            res,
2452            Err(crate::error::DdsError::PreconditionNotMet { .. })
2453        ));
2454    }
2455
2456    #[test]
2457    fn subscriber_begin_access_is_nestable() {
2458        // Spec §2.2.2.5.2.8 — nesting allowed; each begin needs its own
2459        // end.
2460        let s = Subscriber::new(SubscriberQos::default(), None);
2461        s.begin_access();
2462        s.begin_access();
2463        assert!(s.is_access_open());
2464        s.end_access().unwrap();
2465        // Still open after the first end (recursive nesting).
2466        assert!(s.is_access_open());
2467        s.end_access().unwrap();
2468        // Only after the second end is the scope closed again.
2469        assert!(!s.is_access_open());
2470    }
2471
2472    #[test]
2473    fn subscriber_too_many_ends_after_balanced_returns_error() {
2474        // Negative: after a balanced begin/end, the next end is an
2475        // underflow → PreconditionNotMet.
2476        let s = Subscriber::new(SubscriberQos::default(), None);
2477        s.begin_access();
2478        s.end_access().unwrap();
2479        let res = s.end_access();
2480        assert!(matches!(
2481            res,
2482            Err(crate::error::DdsError::PreconditionNotMet { .. })
2483        ));
2484    }
2485}