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                source_sequence_number: -1,
1445            });
1446        }
1447        // Listener notify outside the inbox lock to avoid re-entrancy.
1448        self.notify_data_arrived();
1449        Ok(())
1450    }
1451
1452    /// Calls the `on_data_on_readers` and `on_data_available` bubble-up
1453    /// paths. Spec §2.2.4.1: for each new sample, `data_on_readers`
1454    /// (subscriber level) and `data_available` (reader level) are set as
1455    /// independent statuses; once the subscriber has consumed
1456    /// `data_on_readers`, `data_available` must *not* be suppressed — the
1457    /// two statuses are separate bits in the mask.
1458    #[cfg(feature = "std")]
1459    pub(crate) fn notify_data_arrived(&self) {
1460        let chain = self.listener_chain();
1461        let reader_handle = self.entity_state.instance_handle();
1462        crate::listener_dispatch::dispatch_data_on_readers(&chain, reader_handle);
1463        crate::listener_dispatch::dispatch_data_available(&chain, reader_handle);
1464    }
1465
1466    // ========================================================================
1467    // SampleInfo statechart + instance lifecycle.
1468    // Spec §2.2.2.5.1, §2.2.2.5.3.{5,27,28}.
1469    // ========================================================================
1470
1471    /// Returns the current [`InstanceTracker`] (shared with the internal
1472    /// bookkeeping). Mainly for tests / inspection.
1473    #[cfg(feature = "std")]
1474    #[must_use]
1475    pub fn instance_tracker(&self) -> InstanceTracker {
1476        self.instances.clone()
1477    }
1478
1479    /// This reader's instance handle (GUID-derived). Lets the application
1480    /// ignore the reader's own subscription — e.g. a durability service
1481    /// ignoring its ingest reader on the replay-writer side to avoid an echo
1482    /// loop. Mirrors [`crate::DataWriter::instance_handle`].
1483    #[must_use]
1484    pub fn instance_handle(&self) -> crate::instance_handle::InstanceHandle {
1485        self.entity_state.instance_handle()
1486    }
1487
1488    /// Returns (Runtime, EntityId) when the reader runs in live mode.
1489    /// Cross-crate hook for the async layer (dcps-async), which must
1490    /// register the waker slot directly.
1491    #[doc(hidden)]
1492    #[cfg(feature = "std")]
1493    pub fn runtime_handle(
1494        &self,
1495    ) -> Option<(alloc::sync::Arc<crate::runtime::DcpsRuntime>, EntityId)> {
1496        match (&self.runtime, self.entity_id) {
1497            (Some(rt), Some(eid)) => Some((alloc::sync::Arc::clone(rt), eid)),
1498            _ => None,
1499        }
1500    }
1501
1502    /// Spec §2.2.3.23 — hook for "writer X lost liveliness". Does two
1503    /// things:
1504    ///   1. clears the OWNERSHIP=EXCLUSIVE owner for all instances whose
1505    ///      owner was this writer (so the next sample from another writer
1506    ///      can win again via
1507    ///      `should_accept_sample_under_exclusive_ownership`);
1508    ///   2. returns the number of affected instances.
1509    ///
1510    /// Called from the WLP path once a writer lease has expired (see
1511    /// `wlp::WlpEndpoint::lost_peers`).
1512    #[must_use]
1513    pub fn notify_writer_liveliness_lost(&self, writer_guid: [u8; 16]) -> usize {
1514        self.instances.clear_owner_for_writer(writer_guid)
1515    }
1516
1517    /// Like [`Self::notify_writer_liveliness_lost`], but matches only on
1518    /// the first 12 bytes (GuidPrefix). Allows failover when only the
1519    /// participant identity is known (e.g. on SPDP lease expiry).
1520    #[must_use]
1521    pub fn notify_participant_liveliness_lost(&self, prefix: [u8; 12]) -> usize {
1522        self.instances.clear_owner_for_writer_prefix(prefix)
1523    }
1524
1525    /// Turns a sample value into its corresponding local
1526    /// [`InstanceHandle`], or [`HANDLE_NIL`] if unknown / non-keyed.
1527    /// Spec §2.2.2.5.3.26 `lookup_instance` (reader variant).
1528    #[cfg(feature = "std")]
1529    #[must_use]
1530    pub fn lookup_instance(&self, instance: &T) -> InstanceHandle {
1531        if !T::HAS_KEY {
1532            return HANDLE_NIL;
1533        }
1534        let mut holder = crate::dds_type::PlainCdr2BeKeyHolder::new();
1535        instance.encode_key_holder_be(&mut holder);
1536        let bytes = holder.as_bytes();
1537        let max = T::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
1538        let kh = crate::dds_type::compute_key_hash(bytes, max);
1539        self.instances.lookup(&kh).unwrap_or(HANDLE_NIL)
1540    }
1541
1542    /// Spec §2.2.2.5.3.25 `get_key_value`. Returns the sample value with
1543    /// only the `@key` fields filled in (reconstructed from the stored
1544    /// key holder via `T::decode`).
1545    ///
1546    /// # Errors
1547    /// `BadParameter` if `handle` is unknown; `WireError` if `T::decode`
1548    /// cannot reconstruct the key stream.
1549    #[cfg(feature = "std")]
1550    pub fn get_key_value(&self, handle: InstanceHandle) -> Result<T> {
1551        let Some(bytes) = self.instances.get_key_holder(handle) else {
1552            return Err(DdsError::BadParameter {
1553                what: "unknown instance handle",
1554            });
1555        };
1556        T::decode(&bytes).map_err(|e| DdsError::WireError {
1557            message: alloc::string::ToString::to_string(&e),
1558        })
1559    }
1560
1561    /// Drains all pending bytes from rx + inbox into the internal sample
1562    /// cache. For each sample the KeyHash is computed, the instance is
1563    /// registered (if new), and a matching [`SampleInfo`] is created.
1564    ///
1565    /// Called automatically by the `*_with_info`/`*_instance` APIs.
1566    #[cfg(feature = "std")]
1567    fn ingest_into_cache(&self) -> Result<()> {
1568        // Step 1: collect all incoming samples. `raw` carries
1569        // (bytes, writer_guid, writer_strength) so the exclusive-
1570        // ownership filter (DDS 1.4 §2.2.3.23) is applicable.
1571        //
1572        // Wave 2.1 zero-copy: `raw` now carries `SampleBytes` (refcounted
1573        // Arc<[u8]>) instead of `Vec<u8>`. Decode goes via Deref<[u8]>
1574        // directly without to_vec. Saves 2 hot-path copies per recv'd
1575        // Alive sample. Spec: docs/specs/zerodds-zero-copy-1.0.md §6 wave 2.1.
1576        let mut raw: Vec<RawIngestSample> = Vec::new();
1577        {
1578            let mut inbox = self
1579                .inbox
1580                .lock()
1581                .map_err(|_| DdsError::PreconditionNotMet {
1582                    reason: "datareader inbox poisoned",
1583                })?;
1584            for item in inbox.drain(..) {
1585                if let crate::runtime::UserSample::Alive {
1586                    payload,
1587                    writer_guid,
1588                    writer_strength,
1589                    source_timestamp,
1590                    big_endian,
1591                    representation,
1592                    ..
1593                } = item
1594                {
1595                    raw.push((
1596                        payload,
1597                        writer_guid,
1598                        writer_strength,
1599                        source_timestamp,
1600                        big_endian,
1601                        representation,
1602                    ));
1603                }
1604            }
1605        }
1606        // Live-mode channel: enqueue Alive samples into `raw`, handle
1607        // lifecycle markers directly via __push_lifecycle.
1608        let mut lifecycle_pending: Vec<(
1609            crate::instance_tracker::KeyHash,
1610            crate::sample_info::InstanceStateKind,
1611        )> = Vec::new();
1612        if let Some(rx_mu) = self.rx.as_ref() {
1613            let rx = rx_mu.lock().map_err(|_| DdsError::PreconditionNotMet {
1614                reason: "datareader rx poisoned",
1615            })?;
1616            while let Ok(item) = rx.try_recv() {
1617                match item {
1618                    crate::runtime::UserSample::Alive {
1619                        payload: bytes,
1620                        writer_guid,
1621                        writer_strength,
1622                        source_timestamp,
1623                        big_endian,
1624                        representation,
1625                        ..
1626                    } => raw.push((
1627                        bytes,
1628                        writer_guid,
1629                        writer_strength,
1630                        source_timestamp,
1631                        big_endian,
1632                        representation,
1633                    )),
1634                    crate::runtime::UserSample::Lifecycle { key_hash, kind } => {
1635                        let lc_kind = match kind {
1636                            zerodds_rtps::history_cache::ChangeKind::NotAliveDisposed
1637                            | zerodds_rtps::history_cache::ChangeKind::NotAliveDisposedUnregistered => {
1638                                crate::sample_info::InstanceStateKind::NotAliveDisposed
1639                            }
1640                            zerodds_rtps::history_cache::ChangeKind::NotAliveUnregistered => {
1641                                crate::sample_info::InstanceStateKind::NotAliveNoWriters
1642                            }
1643                            _ => crate::sample_info::InstanceStateKind::Alive,
1644                        };
1645                        lifecycle_pending.push((key_hash, lc_kind));
1646                    }
1647                }
1648            }
1649        }
1650        // Apply lifecycle markers only AFTER draining so the lock path
1651        // stays clean (__push_lifecycle takes its own locks).
1652        for (kh, lc_kind) in lifecycle_pending {
1653            let mut holder_bytes = Vec::with_capacity(16);
1654            holder_bytes.extend_from_slice(&kh);
1655            let _ = self.__push_lifecycle(kh, holder_bytes, lc_kind);
1656        }
1657        let now = get_current_time();
1658        let mut cache = self
1659            .cache
1660            .lock()
1661            .map_err(|_| DdsError::PreconditionNotMet {
1662                reason: "datareader cache poisoned",
1663            })?;
1664        if raw.is_empty() {
1665            // Even without new bytes, autopurge must run; otherwise
1666            // disposed/nowriter instances never expire outside sample inflow.
1667            self.run_reader_autopurge(now, &mut cache);
1668            return Ok(());
1669        }
1670        for (bytes, writer_guid, writer_strength, src_ts, big_endian, representation) in raw {
1671            // RTPS-F1: use the writer's INFO_TS source timestamp for the
1672            // SampleInfo + DESTINATION_ORDER; fall back to reception time
1673            // (`now`) when the writer sent none.
1674            let sample_source_ts = src_ts.map_or(now, crate::time::he_timestamp_to_time);
1675            // Decode T to (a) evaluate the filter and (b) compute the
1676            // KeyHash.
1677            let sample =
1678                decode_for_encap::<T>(&bytes, representation, big_endian).map_err(|e| {
1679                    DdsError::WireError {
1680                        message: alloc::string::ToString::to_string(&e),
1681                    }
1682                })?;
1683            if !self.sample_passes_filter(&sample) {
1684                continue;
1685            }
1686            // §2.2.3.23 exclusive-ownership filter: reject samples from
1687            // weaker writers before they enter the cache.
1688            if !self.passes_exclusive_ownership(&sample, writer_guid, writer_strength) {
1689                continue;
1690            }
1691            let info = if T::HAS_KEY {
1692                let mut holder = crate::dds_type::PlainCdr2BeKeyHolder::new();
1693                sample.encode_key_holder_be(&mut holder);
1694                let key_bytes = holder.as_bytes().to_vec();
1695                let max = T::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
1696                let kh = crate::dds_type::compute_key_hash(&key_bytes, max);
1697                // QoS filter BEFORE observe_sample so discarded samples do
1698                // not affect the sample state.
1699                let (min_sep_nanos, by_source_ts) = {
1700                    let qos = self.qos.lock().unwrap_or_else(|e| e.into_inner());
1701                    (
1702                        qos.time_based_filter.minimum_separation.to_nanos(),
1703                        qos.destination_order.kind
1704                            == zerodds_qos::DestinationOrderKind::BySourceTimestamp,
1705                    )
1706                };
1707                // Spec §2.2.3.13 TIME_BASED_FILTER: drop if less than
1708                // minimum_separation has elapsed since the last delivered
1709                // sample of this instance.
1710                if !self
1711                    .instances
1712                    .should_deliver_under_time_based_filter(&kh, now, min_sep_nanos)
1713                {
1714                    continue;
1715                }
1716                // Spec §2.2.3.18 DESTINATION_ORDER: under BY_SOURCE_TIMESTAMP
1717                // the ordering key is the writer's source timestamp (from
1718                // INFO_TS), not the reception time — only deliver samples with a
1719                // strictly greater source_ts. Under BY_RECEPTION_TIMESTAMP the
1720                // reception clock (`now`) is the key.
1721                let order_ts = if by_source_ts { sample_source_ts } else { now };
1722                if !self.instances.should_deliver_under_destination_order(
1723                    &kh,
1724                    order_ts,
1725                    by_source_ts,
1726                ) {
1727                    continue;
1728                }
1729                let (handle, _) = self.instances.observe_sample(kh, key_bytes, Some(now));
1730                self.instances.record_delivery(&kh, order_ts);
1731                let state = match self.instances.get_by_handle(handle) {
1732                    Some(s) => s,
1733                    None => continue, // should never happen — defensive
1734                };
1735                SampleInfo {
1736                    sample_state: SampleStateKind::NotRead,
1737                    view_state: if state.reader_view_new {
1738                        ViewStateKind::New
1739                    } else {
1740                        ViewStateKind::NotNew
1741                    },
1742                    instance_state: state.kind,
1743                    disposed_generation_count: state.disposed_generation_count,
1744                    no_writers_generation_count: state.no_writers_generation_count,
1745                    source_timestamp: sample_source_ts,
1746                    instance_handle: handle,
1747                    valid_data: true,
1748                    ..SampleInfo::default()
1749                }
1750            } else {
1751                // Non-keyed topics: a "pseudo handle" per sample would be
1752                // overkill — we leave it at HANDLE_NIL (spec §2.2.2.5.1.10
1753                // allows that, since the instance view for non-keyed
1754                // topics is formally "everything is one instance").
1755                SampleInfo {
1756                    sample_state: SampleStateKind::NotRead,
1757                    view_state: ViewStateKind::NotNew,
1758                    instance_handle: HANDLE_NIL,
1759                    source_timestamp: sample_source_ts,
1760                    valid_data: true,
1761                    ..SampleInfo::default()
1762                }
1763            };
1764            cache.push(CachedSample {
1765                bytes: Some(bytes),
1766                info,
1767            });
1768        }
1769        // Spec §2.2.3.22 ReaderDataLifecycle: remove instances from the
1770        // tracker and cache that have been in NotAlive-Disposed or
1771        // NotAlive-NoWriters longer than autopurge_*_samples_delay.
1772        self.run_reader_autopurge(now, &mut cache);
1773        Ok(())
1774    }
1775
1776    /// Applies `ReaderDataLifecycle.autopurge_*`: removes expired
1777    /// instances from the tracker + cache. Called by `ingest_into_cache`
1778    /// and when reading with no new bytes.
1779    #[cfg(feature = "std")]
1780    fn run_reader_autopurge(&self, now: Time, cache: &mut Vec<CachedSample>) {
1781        let (purge_disp, purge_now) = {
1782            let qos = self.qos.lock().unwrap_or_else(|e| e.into_inner());
1783            (
1784                qos.reader_data_lifecycle
1785                    .autopurge_disposed_samples_delay
1786                    .to_nanos(),
1787                qos.reader_data_lifecycle
1788                    .autopurge_nowriter_samples_delay
1789                    .to_nanos(),
1790            )
1791        };
1792        if purge_disp == u128::MAX && purge_now == u128::MAX {
1793            return;
1794        }
1795        let purged = self.instances.autopurge(now, purge_disp, purge_now);
1796        if purged > 0 {
1797            cache.retain(|s| {
1798                s.info.instance_handle.is_nil()
1799                    || self
1800                        .instances
1801                        .get_by_handle(s.info.instance_handle)
1802                        .is_some()
1803            });
1804        }
1805    }
1806
1807    /// Pushes a pure lifecycle marker (dispose / unregister) into the
1808    /// cache. Called by the runtime as soon as a writer sends
1809    /// `dispose`/`unregister_instance`.
1810    #[cfg(feature = "std")]
1811    #[doc(hidden)]
1812    pub fn __push_lifecycle(
1813        &self,
1814        keyhash: crate::instance_tracker::KeyHash,
1815        key_holder: Vec<u8>,
1816        kind: InstanceStateKind,
1817    ) -> Result<()> {
1818        let now = get_current_time();
1819        // First bring the instance into the right state in the tracker.
1820        // observe_sample re-registers it if needed and makes it alive.
1821        let (handle, _) = self
1822            .instances
1823            .observe_sample(keyhash, key_holder, Some(now));
1824        match kind {
1825            InstanceStateKind::NotAliveDisposed => {
1826                self.instances.dispose(handle, Some(now));
1827            }
1828            InstanceStateKind::NotAliveNoWriters => {
1829                self.instances.unregister(handle, Some(now));
1830            }
1831            InstanceStateKind::Alive => {}
1832        }
1833        let Some(state) = self.instances.get_by_handle(handle) else {
1834            return Ok(()); // should never happen — defensive
1835        };
1836        let info = SampleInfo {
1837            source_timestamp: now,
1838            valid_data: false,
1839            instance_handle: handle,
1840            instance_state: state.kind,
1841            disposed_generation_count: state.disposed_generation_count,
1842            no_writers_generation_count: state.no_writers_generation_count,
1843            view_state: if state.reader_view_new {
1844                ViewStateKind::New
1845            } else {
1846                ViewStateKind::NotNew
1847            },
1848            ..SampleInfo::default()
1849        };
1850        let mut cache = self
1851            .cache
1852            .lock()
1853            .map_err(|_| DdsError::PreconditionNotMet {
1854                reason: "datareader cache poisoned",
1855            })?;
1856        cache.push(CachedSample { bytes: None, info });
1857        Ok(())
1858    }
1859
1860    /// `take` with full [`SampleInfo`]. Spec §2.2.2.5.3.5 `take`.
1861    /// Consumes the samples from the cache (the `NOT_READ → READ`
1862    /// transition is moot since they are gone).
1863    ///
1864    /// # Errors
1865    /// Same as [`Self::take`].
1866    #[cfg(feature = "std")]
1867    pub fn take_with_info(&self) -> Result<Vec<Sample<T>>> {
1868        self.take_filtered(
1869            sample_state_mask::ANY,
1870            view_state_mask::ANY,
1871            instance_state_mask::ANY,
1872        )
1873    }
1874
1875    /// `read` with full [`SampleInfo`]. Does not consume — only marks
1876    /// the samples as `READ` (spec §2.2.2.5.3.4).
1877    ///
1878    /// # Errors
1879    /// Same as [`Self::read`].
1880    #[cfg(feature = "std")]
1881    pub fn read_with_info(&self) -> Result<Vec<Sample<T>>> {
1882        self.read_filtered(
1883            sample_state_mask::ANY,
1884            view_state_mask::ANY,
1885            instance_state_mask::ANY,
1886        )
1887    }
1888
1889    /// `take` with state masks (spec §2.2.2.5.3.6 `take_w_condition`).
1890    ///
1891    /// # Errors
1892    /// Same as [`Self::take`].
1893    #[cfg(feature = "std")]
1894    pub fn take_filtered(
1895        &self,
1896        sample_mask: u32,
1897        view_mask: u32,
1898        instance_mask: u32,
1899    ) -> Result<Vec<Sample<T>>> {
1900        self.ingest_into_cache()?;
1901        let mut cache = self
1902            .cache
1903            .lock()
1904            .map_err(|_| DdsError::PreconditionNotMet {
1905                reason: "datareader cache poisoned",
1906            })?;
1907        let mut out = Vec::new();
1908        let mut keep = Vec::with_capacity(cache.len());
1909        for s in cache.drain(..) {
1910            if s.info.matches_states(sample_mask, view_mask, instance_mask) {
1911                let sample = self.materialize(s)?;
1912                self.instances.mark_view_seen(sample.info.instance_handle);
1913                if sample.info.instance_handle != HANDLE_NIL {
1914                    self.instances.drain_samples(sample.info.instance_handle, 1);
1915                }
1916                out.push(sample);
1917            } else {
1918                keep.push(s);
1919            }
1920        }
1921        *cache = keep;
1922        Ok(out)
1923    }
1924
1925    /// `read` with state masks (spec §2.2.2.5.3.3 `read_w_condition`).
1926    ///
1927    /// # Errors
1928    /// Same as [`Self::read`].
1929    #[cfg(feature = "std")]
1930    pub fn read_filtered(
1931        &self,
1932        sample_mask: u32,
1933        view_mask: u32,
1934        instance_mask: u32,
1935    ) -> Result<Vec<Sample<T>>> {
1936        self.ingest_into_cache()?;
1937        let mut cache = self
1938            .cache
1939            .lock()
1940            .map_err(|_| DdsError::PreconditionNotMet {
1941                reason: "datareader cache poisoned",
1942            })?;
1943        let mut out = Vec::with_capacity(cache.len());
1944        for s in cache.iter_mut() {
1945            if !s.info.matches_states(sample_mask, view_mask, instance_mask) {
1946                continue;
1947            }
1948            // Build a snapshot (with the current sample-state view).
1949            let snapshot = Sample::new(
1950                self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?,
1951                s.info,
1952            );
1953            // Sample-state transition NOT_READ → READ (spec §2.2.2.5.3.4).
1954            s.info.sample_state = SampleStateKind::Read;
1955            self.instances.mark_view_seen(s.info.instance_handle);
1956            out.push(snapshot);
1957        }
1958        Ok(out)
1959    }
1960
1961    /// `read_w_condition` (spec §2.2.2.5.3.7) — in addition to the state
1962    /// mask, applies the QueryCondition's SQL filter per sample. Samples
1963    /// stay in the cache (sample state NOT_READ → READ).
1964    ///
1965    /// # Errors
1966    /// `PreconditionNotMet` on lock poisoning or SQL eval error.
1967    #[cfg(feature = "std")]
1968    pub fn read_w_condition(
1969        &self,
1970        condition: &Arc<crate::condition::QueryCondition>,
1971    ) -> Result<Vec<Sample<T>>> {
1972        let base = condition.base();
1973        let sample_mask = base.get_sample_state_mask();
1974        let view_mask = base.get_view_state_mask();
1975        let instance_mask = base.get_instance_state_mask();
1976
1977        self.ingest_into_cache()?;
1978        let mut cache = self
1979            .cache
1980            .lock()
1981            .map_err(|_| DdsError::PreconditionNotMet {
1982                reason: "datareader cache poisoned",
1983            })?;
1984        let mut out = Vec::with_capacity(cache.len());
1985        for s in cache.iter_mut() {
1986            if !s.info.matches_states(sample_mask, view_mask, instance_mask) {
1987                continue;
1988            }
1989            let decoded = self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?;
1990            let row = crate::dds_type::DdsTypeRow::new(&decoded);
1991            // Filter eval error -> sample is rejected (spec: "filter
1992            // expression false" semantics), but we do not propagate a
1993            // hard error upward, except for lock poisoning.
1994            if !condition.evaluate(&row).unwrap_or(false) {
1995                continue;
1996            }
1997            let snapshot = Sample::new(decoded, s.info);
1998            s.info.sample_state = SampleStateKind::Read;
1999            self.instances.mark_view_seen(s.info.instance_handle);
2000            out.push(snapshot);
2001        }
2002        Ok(out)
2003    }
2004
2005    /// `take_w_condition` (spec §2.2.2.5.3.8) — like `read_w_condition`,
2006    /// but consumes the samples (removes them from the cache).
2007    ///
2008    /// # Errors
2009    /// `PreconditionNotMet` on lock poisoning or SQL eval error.
2010    #[cfg(feature = "std")]
2011    pub fn take_w_condition(
2012        &self,
2013        condition: &Arc<crate::condition::QueryCondition>,
2014    ) -> Result<Vec<Sample<T>>> {
2015        let base = condition.base();
2016        let sample_mask = base.get_sample_state_mask();
2017        let view_mask = base.get_view_state_mask();
2018        let instance_mask = base.get_instance_state_mask();
2019
2020        self.ingest_into_cache()?;
2021        let mut cache = self
2022            .cache
2023            .lock()
2024            .map_err(|_| DdsError::PreconditionNotMet {
2025                reason: "datareader cache poisoned",
2026            })?;
2027        let mut out = Vec::new();
2028        let mut keep = Vec::with_capacity(cache.len());
2029        for s in cache.drain(..) {
2030            if !s.info.matches_states(sample_mask, view_mask, instance_mask) {
2031                keep.push(s);
2032                continue;
2033            }
2034            let decoded = self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?;
2035            let row = crate::dds_type::DdsTypeRow::new(&decoded);
2036            if !condition.evaluate(&row).unwrap_or(false) {
2037                keep.push(s);
2038                continue;
2039            }
2040            let sample = Sample::new(decoded, s.info);
2041            self.instances.mark_view_seen(sample.info.instance_handle);
2042            if sample.info.instance_handle != HANDLE_NIL {
2043                self.instances.drain_samples(sample.info.instance_handle, 1);
2044            }
2045            out.push(sample);
2046        }
2047        *cache = keep;
2048        Ok(out)
2049    }
2050
2051    /// `read_instance` (spec §2.2.2.5.3.27). Returns only samples of the
2052    /// given instance.
2053    ///
2054    /// # Errors
2055    /// `BadParameter` if `handle == HANDLE_NIL`.
2056    #[cfg(feature = "std")]
2057    pub fn read_instance(&self, handle: InstanceHandle) -> Result<Vec<Sample<T>>> {
2058        if handle.is_nil() {
2059            return Err(DdsError::BadParameter {
2060                what: "read_instance with HANDLE_NIL",
2061            });
2062        }
2063        self.ingest_into_cache()?;
2064        let mut cache = self
2065            .cache
2066            .lock()
2067            .map_err(|_| DdsError::PreconditionNotMet {
2068                reason: "datareader cache poisoned",
2069            })?;
2070        let mut out = Vec::new();
2071        for s in cache.iter_mut() {
2072            if s.info.instance_handle != handle {
2073                continue;
2074            }
2075            let snap = Sample::new(
2076                self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?,
2077                s.info,
2078            );
2079            s.info.sample_state = SampleStateKind::Read;
2080            self.instances.mark_view_seen(handle);
2081            out.push(snap);
2082        }
2083        Ok(out)
2084    }
2085
2086    /// `take_instance` (spec §2.2.2.5.3.27, take variant). Consumes.
2087    ///
2088    /// # Errors
2089    /// `BadParameter` if `handle == HANDLE_NIL`.
2090    #[cfg(feature = "std")]
2091    pub fn take_instance(&self, handle: InstanceHandle) -> Result<Vec<Sample<T>>> {
2092        if handle.is_nil() {
2093            return Err(DdsError::BadParameter {
2094                what: "take_instance with HANDLE_NIL",
2095            });
2096        }
2097        self.ingest_into_cache()?;
2098        let mut cache = self
2099            .cache
2100            .lock()
2101            .map_err(|_| DdsError::PreconditionNotMet {
2102                reason: "datareader cache poisoned",
2103            })?;
2104        let mut out = Vec::new();
2105        let mut keep = Vec::with_capacity(cache.len());
2106        for s in cache.drain(..) {
2107            if s.info.instance_handle == handle {
2108                out.push(self.materialize(s)?);
2109            } else {
2110                keep.push(s);
2111            }
2112        }
2113        *cache = keep;
2114        if !out.is_empty() {
2115            self.instances.mark_view_seen(handle);
2116            self.instances.drain_samples(handle, out.len() as u32);
2117        }
2118        Ok(out)
2119    }
2120
2121    /// `read_next_instance` (spec §2.2.2.5.3.28). Returns the samples of
2122    /// the **next** instance (in sort order) after `previous`.
2123    ///
2124    /// `previous == HANDLE_NIL` starts at the first handle.
2125    ///
2126    /// # Errors
2127    /// Same as `read`.
2128    #[cfg(feature = "std")]
2129    pub fn read_next_instance(&self, previous: InstanceHandle) -> Result<Vec<Sample<T>>> {
2130        let Some(next) = self.instances.next_handle_after(previous) else {
2131            return Ok(Vec::new());
2132        };
2133        self.read_instance(next)
2134    }
2135
2136    /// `take_next_instance` (spec §2.2.2.5.3.28). Take variant.
2137    ///
2138    /// # Errors
2139    /// Same as `take`.
2140    #[cfg(feature = "std")]
2141    pub fn take_next_instance(&self, previous: InstanceHandle) -> Result<Vec<Sample<T>>> {
2142        let Some(next) = self.instances.next_handle_after(previous) else {
2143            return Ok(Vec::new());
2144        };
2145        self.take_instance(next)
2146    }
2147
2148    /// Helper: turns a CachedSample into a `Sample<T>`. For lifecycle
2149    /// markers (`bytes == None`), `T` is reconstructed from the stored
2150    /// key holder (spec §2.2.2.5.1.13: `data` then contains only the key
2151    /// portion).
2152    #[cfg(feature = "std")]
2153    fn materialize(&self, s: CachedSample) -> Result<Sample<T>> {
2154        let data = self.decode_or_keyholder(s.bytes.as_deref(), s.info.instance_handle)?;
2155        #[cfg(feature = "metrics")]
2156        crate::metrics::add_samples_read(self.topic.name(), 1);
2157        Ok(Sample::new(data, s.info))
2158    }
2159
2160    /// Decode helper: for `Some(bytes)` via `T::decode`, for `None`
2161    /// (lifecycle marker) via the instance's key holder; if that is also
2162    /// unavailable, falls back to `T::decode(&[])`.
2163    #[cfg(feature = "std")]
2164    fn decode_or_keyholder(&self, bytes: Option<&[u8]>, handle: InstanceHandle) -> Result<T> {
2165        if let Some(b) = bytes {
2166            return T::decode(b).map_err(|e| DdsError::WireError {
2167                message: alloc::string::ToString::to_string(&e),
2168            });
2169        }
2170        if let Some(holder) = self.instances.get_key_holder(handle) {
2171            return T::decode(&holder).map_err(|e| DdsError::WireError {
2172                message: alloc::string::ToString::to_string(&e),
2173            });
2174        }
2175        T::decode(&[]).map_err(|e| DdsError::WireError {
2176            message: alloc::string::ToString::to_string(&e),
2177        })
2178    }
2179}
2180
2181#[cfg(feature = "std")]
2182impl<T: DdsType> crate::entity::Entity for DataReader<T> {
2183    type Qos = DataReaderQos;
2184
2185    fn get_qos(&self) -> Self::Qos {
2186        self.qos.lock().map(|q| q.clone()).unwrap_or_default()
2187    }
2188
2189    /// Spec §2.2.3 / §2.2.2.5.3: DURABILITY, RELIABILITY, HISTORY,
2190    /// RESOURCE_LIMITS, OWNERSHIP are Changeable=NO post-enable.
2191    fn set_qos(&self, qos: Self::Qos) -> Result<()> {
2192        let enabled = self.entity_state.is_enabled();
2193        if let Ok(mut current) = self.qos.lock() {
2194            if enabled {
2195                if current.durability != qos.durability {
2196                    return Err(crate::entity::immutable_if_enabled("DURABILITY"));
2197                }
2198                if current.reliability != qos.reliability {
2199                    return Err(crate::entity::immutable_if_enabled("RELIABILITY"));
2200                }
2201                if current.history != qos.history {
2202                    return Err(crate::entity::immutable_if_enabled("HISTORY"));
2203                }
2204                if current.resource_limits != qos.resource_limits {
2205                    return Err(crate::entity::immutable_if_enabled("RESOURCE_LIMITS"));
2206                }
2207                if current.ownership != qos.ownership {
2208                    return Err(crate::entity::immutable_if_enabled("OWNERSHIP"));
2209                }
2210                if current.liveliness != qos.liveliness {
2211                    return Err(crate::entity::immutable_if_enabled("LIVELINESS"));
2212                }
2213            }
2214            *current = qos;
2215        }
2216        Ok(())
2217    }
2218
2219    fn enable(&self) -> Result<()> {
2220        self.entity_state.enable();
2221        Ok(())
2222    }
2223
2224    fn entity_state(&self) -> Arc<crate::entity::EntityState> {
2225        Arc::clone(&self.entity_state)
2226    }
2227}
2228
2229// ---- Boxed type-mapped variant for heterogeneous reader lists ----
2230#[allow(dead_code)]
2231pub(crate) trait AnyDataReader: Send + Sync + core::fmt::Debug {
2232    fn topic_name(&self) -> &str;
2233    fn type_name(&self) -> &'static str;
2234}
2235
2236impl<T: DdsType + Send + 'static> AnyDataReader for DataReader<T>
2237where
2238    T: Send + Sync,
2239{
2240    fn topic_name(&self) -> &str {
2241        self.topic.name()
2242    }
2243    fn type_name(&self) -> &'static str {
2244        T::TYPE_NAME
2245    }
2246}
2247
2248#[allow(dead_code)]
2249pub(crate) fn boxed_any_reader<T: DdsType + Send + Sync + 'static>(
2250    r: DataReader<T>,
2251) -> Box<dyn AnyDataReader> {
2252    Box::new(r)
2253}
2254
2255#[cfg(test)]
2256#[allow(clippy::expect_used, clippy::unwrap_used)]
2257mod tests {
2258    use super::*;
2259    use crate::dds_type::RawBytes;
2260    use crate::factory::DomainParticipantFactory;
2261    use crate::qos::{DomainParticipantQos, TopicQos};
2262
2263    fn mk_topic() -> Topic<RawBytes> {
2264        let p = DomainParticipantFactory::instance()
2265            .create_participant_offline(0, DomainParticipantQos::default());
2266        Topic::new("Chatter".into(), TopicQos::default(), p)
2267    }
2268
2269    /// `decode_for_encap` must pick the decoder matching the sample's
2270    /// encapsulation — XCDR version *and* byte order — so an XCDR1 writer
2271    /// (CycloneDDS / legacy RTI default for `@final`) is decoded with the
2272    /// classic-CDR rule, not the XCDR2 rule. Regression for Bug DR1.
2273    #[test]
2274    fn decode_for_encap_dispatches_on_representation_and_endianness() {
2275        use crate::dds_type::{DecodeError, EncodeError};
2276
2277        /// Marker whose four decode variants return a distinct value, so the
2278        /// test can prove which one `decode_for_encap` selected.
2279        #[derive(Debug, PartialEq)]
2280        struct Probe(u8);
2281        impl DdsType for Probe {
2282            const TYPE_NAME: &'static str = "test::Probe";
2283            fn encode(&self, _out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
2284                Ok(())
2285            }
2286            fn decode(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2287                Ok(Probe(1)) // XCDR2-LE
2288            }
2289            fn decode_be(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2290                Ok(Probe(2)) // XCDR2-BE
2291            }
2292            fn decode_xcdr1(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2293                Ok(Probe(10)) // XCDR1-LE
2294            }
2295            fn decode_xcdr1_be(_b: &[u8]) -> core::result::Result<Self, DecodeError> {
2296                Ok(Probe(20)) // XCDR1-BE
2297            }
2298        }
2299
2300        // (representation, big_endian) -> expected marker.
2301        // representation: 1 = XCDR2, 0 = XCDR1.
2302        assert_eq!(decode_for_encap::<Probe>(&[], 1, false).unwrap(), Probe(1));
2303        assert_eq!(decode_for_encap::<Probe>(&[], 1, true).unwrap(), Probe(2));
2304        assert_eq!(decode_for_encap::<Probe>(&[], 0, false).unwrap(), Probe(10));
2305        assert_eq!(decode_for_encap::<Probe>(&[], 0, true).unwrap(), Probe(20));
2306    }
2307
2308    #[test]
2309    fn subscriber_creates_datareader_for_matching_type() {
2310        let s = Subscriber::new(SubscriberQos::default(), None);
2311        let r = s
2312            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2313            .unwrap();
2314        assert_eq!(r.topic().name(), "Chatter");
2315    }
2316
2317    #[test]
2318    fn datareader_take_returns_decoded_samples() {
2319        let s = Subscriber::new(SubscriberQos::default(), None);
2320        let r = s
2321            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2322            .unwrap();
2323        r.__push_raw(vec![1, 2, 3]).unwrap();
2324        r.__push_raw(vec![4, 5]).unwrap();
2325        let samples = r.take().unwrap();
2326        assert_eq!(samples.len(), 2);
2327        assert_eq!(samples[0].data, vec![1, 2, 3]);
2328        assert_eq!(samples[1].data, vec![4, 5]);
2329        // The inbox is now empty.
2330        let again = r.take().unwrap();
2331        assert!(again.is_empty());
2332    }
2333
2334    #[test]
2335    fn datareader_read_preserves_samples() {
2336        let s = Subscriber::new(SubscriberQos::default(), None);
2337        let r = s
2338            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2339            .unwrap();
2340        r.__push_raw(vec![0xAA]).unwrap();
2341        let first = r.read().unwrap();
2342        let second = r.read().unwrap();
2343        assert_eq!(first.len(), 1);
2344        assert_eq!(second.len(), 1);
2345    }
2346
2347    // poll_subscription_matched + listener-slot API.
2348
2349    use core::sync::atomic::{AtomicU32, Ordering};
2350
2351    #[test]
2352    fn datareader_set_listener_stores_arc_and_mask() {
2353        struct L;
2354        impl crate::listener::DataReaderListener for L {}
2355        let s = Subscriber::new(SubscriberQos::default(), None);
2356        let r = s
2357            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2358            .unwrap();
2359        assert!(r.get_listener().is_none());
2360        r.set_listener(Some(Arc::new(L)), crate::psm_constants::status::ANY);
2361        assert!(r.get_listener().is_some());
2362        assert_eq!(
2363            r.entity_state.listener_mask(),
2364            crate::psm_constants::status::ANY
2365        );
2366    }
2367
2368    #[test]
2369    fn poll_subscription_matched_fires_on_count_increase() {
2370        struct Cnt(AtomicU32);
2371        impl crate::listener::DataReaderListener for Cnt {
2372            fn on_subscription_matched(
2373                &self,
2374                _r: crate::InstanceHandle,
2375                _s: crate::status::SubscriptionMatchedStatus,
2376            ) {
2377                self.0.fetch_add(1, Ordering::Relaxed);
2378            }
2379        }
2380        let s = Subscriber::new(SubscriberQos::default(), None);
2381        let r = s
2382            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2383            .unwrap();
2384        let cnt = Arc::new(Cnt(AtomicU32::new(0)));
2385        r.set_listener(Some(cnt.clone()), crate::psm_constants::status::ANY);
2386
2387        r.poll_subscription_matched(0);
2388        assert_eq!(cnt.0.load(Ordering::Relaxed), 1);
2389        r.poll_subscription_matched(1);
2390        assert_eq!(cnt.0.load(Ordering::Relaxed), 2);
2391        r.poll_subscription_matched(1);
2392        assert_eq!(cnt.0.load(Ordering::Relaxed), 2);
2393        r.poll_subscription_matched(0);
2394        assert_eq!(cnt.0.load(Ordering::Relaxed), 3);
2395    }
2396
2397    #[test]
2398    fn poll_subscription_matched_with_no_listener_is_noop() {
2399        let s = Subscriber::new(SubscriberQos::default(), None);
2400        let r = s
2401            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2402            .unwrap();
2403        r.poll_subscription_matched(0);
2404        r.poll_subscription_matched(3);
2405    }
2406
2407    #[test]
2408    fn notify_data_arrived_fires_data_available_and_data_on_readers() {
2409        struct ReadCnt(AtomicU32, AtomicU32);
2410        impl crate::listener::DataReaderListener for ReadCnt {
2411            fn on_data_available(&self, _r: crate::InstanceHandle) {
2412                self.0.fetch_add(1, Ordering::Relaxed);
2413            }
2414            fn on_subscription_matched(
2415                &self,
2416                _r: crate::InstanceHandle,
2417                _s: crate::status::SubscriptionMatchedStatus,
2418            ) {
2419                self.1.fetch_add(1, Ordering::Relaxed);
2420            }
2421        }
2422        let s = Subscriber::new(SubscriberQos::default(), None);
2423        let r = s
2424            .create_datareader::<RawBytes>(&mk_topic(), DataReaderQos::default())
2425            .unwrap();
2426        let rc = Arc::new(ReadCnt(AtomicU32::new(0), AtomicU32::new(0)));
2427        r.set_listener(Some(rc.clone()), crate::psm_constants::status::ANY);
2428        r.notify_data_arrived();
2429        assert_eq!(rc.0.load(Ordering::Relaxed), 1);
2430        // sub_matched counter unchanged (different status bit).
2431        assert_eq!(rc.1.load(Ordering::Relaxed), 0);
2432    }
2433
2434    // ---- §2.2.2.5.2.8/.9 begin/end_access ----
2435
2436    #[test]
2437    fn subscriber_begin_end_access_roundtrip() {
2438        let s = Subscriber::new(SubscriberQos::default(), None);
2439        assert!(!s.is_access_open());
2440        s.begin_access();
2441        assert!(s.is_access_open());
2442        s.end_access().unwrap();
2443        assert!(!s.is_access_open());
2444    }
2445
2446    #[test]
2447    fn subscriber_end_access_without_begin_returns_precondition_not_met() {
2448        // Spec §2.2.2.5.2.9 — end without begin is a spec violation.
2449        let s = Subscriber::new(SubscriberQos::default(), None);
2450        let res = s.end_access();
2451        assert!(matches!(
2452            res,
2453            Err(crate::error::DdsError::PreconditionNotMet { .. })
2454        ));
2455    }
2456
2457    #[test]
2458    fn subscriber_begin_access_is_nestable() {
2459        // Spec §2.2.2.5.2.8 — nesting allowed; each begin needs its own
2460        // end.
2461        let s = Subscriber::new(SubscriberQos::default(), None);
2462        s.begin_access();
2463        s.begin_access();
2464        assert!(s.is_access_open());
2465        s.end_access().unwrap();
2466        // Still open after the first end (recursive nesting).
2467        assert!(s.is_access_open());
2468        s.end_access().unwrap();
2469        // Only after the second end is the scope closed again.
2470        assert!(!s.is_access_open());
2471    }
2472
2473    #[test]
2474    fn subscriber_too_many_ends_after_balanced_returns_error() {
2475        // Negative: after a balanced begin/end, the next end is an
2476        // underflow → PreconditionNotMet.
2477        let s = Subscriber::new(SubscriberQos::default(), None);
2478        s.begin_access();
2479        s.end_access().unwrap();
2480        let res = s.end_access();
2481        assert!(matches!(
2482            res,
2483            Err(crate::error::DdsError::PreconditionNotMet { .. })
2484        ));
2485    }
2486}