Skip to main content

rings_node/
registration.rs

1//! Node-layer DHT registration tasks.
2//!
3//! A registration task is a periodic node-side publisher. The task decides what
4//! value to publish; [`DhtRegistrationPublisher`] owns the common DHT
5//! touch/tombstone mechanics so new registries do not reimplement that state.
6
7use std::collections::BTreeSet;
8use std::sync::Arc;
9use std::sync::Mutex;
10use std::time::Duration;
11
12use async_trait::async_trait;
13use futures::lock::Mutex as AsyncMutex;
14use rings_core::dht::entry;
15use rings_core::dht::Did;
16use rings_core::ecc::VerificationPublicKey;
17use rings_core::lifecycle::StopToken;
18use rings_core::message::Encoded;
19use rings_core::message::Encoder;
20use rings_core::session::SessionSk;
21use rings_core::utils::get_epoch_ms;
22
23use crate::error::Error;
24use crate::error::Result;
25use crate::extension::ext::MaybeSend;
26use crate::online::OnlineNodeDescriptor;
27use crate::online::OnlineNodeDescriptorBody;
28use crate::online::OnlineNodeType;
29use crate::online::ONLINE_NODES_TOPIC;
30use crate::online::ONLINE_NODE_CAPABILITY_STORAGE;
31use crate::processor::Processor;
32
33const DEFAULT_ONLINE_NODE_HEARTBEAT_INTERVAL_SECS: u64 = 30;
34const DEFAULT_ONLINE_NODE_TTL_SECS: u64 = 90;
35
36/// Default online-node registry heartbeat interval in seconds.
37pub(crate) const fn default_online_node_heartbeat_interval_secs() -> u64 {
38    DEFAULT_ONLINE_NODE_HEARTBEAT_INTERVAL_SECS
39}
40
41/// Default online-node registry descriptor TTL in seconds.
42pub(crate) const fn default_online_node_ttl_secs() -> u64 {
43    DEFAULT_ONLINE_NODE_TTL_SECS
44}
45
46/// Default runtime family advertised in the online-node registry.
47pub(crate) fn default_online_node_type() -> OnlineNodeType {
48    #[cfg(feature = "ffi")]
49    {
50        OnlineNodeType::Ffi
51    }
52    #[cfg(all(not(feature = "ffi"), feature = "browser", target_family = "wasm"))]
53    {
54        OnlineNodeType::Browser
55    }
56    #[cfg(all(
57        not(feature = "ffi"),
58        not(all(feature = "browser", target_family = "wasm"))
59    ))]
60    {
61        OnlineNodeType::Native
62    }
63}
64
65/// Default node presence advertisement enablement.
66pub(crate) const fn default_advertise_presence() -> bool {
67    true
68}
69
70/// Validate online-node registration scheduling.
71pub(crate) fn validate_online_node_registration_timing(
72    advertise_presence: bool,
73    heartbeat_interval: Duration,
74    ttl: Duration,
75) -> Result<()> {
76    if advertise_presence && heartbeat_interval >= ttl {
77        return Err(Error::InvalidConfig(format!(
78            "online_node_heartbeat_interval ({heartbeat_interval:?}) must be less than online_node_ttl ({ttl:?}) when advertise_presence is enabled"
79        )));
80    }
81    Ok(())
82}
83
84#[cfg(not(all(feature = "browser", target_family = "wasm")))]
85pub(crate) async fn sleep_registration_interval(interval: Duration) -> Result<()> {
86    // Native timers are infallible; the Result keeps the daemon shape shared
87    // with the wasm arm, where browser timer setup can fail.
88    futures_timer::Delay::new(interval).await;
89    Ok(())
90}
91
92#[cfg(all(feature = "browser", target_family = "wasm"))]
93pub(crate) async fn sleep_registration_interval(interval: Duration) -> Result<()> {
94    let interval_ms = i32::try_from(interval.as_millis()).unwrap_or(i32::MAX);
95    rings_core::utils::js_utils::window_sleep(interval_ms)
96        .await
97        .map_err(|error| Error::JsError(format!("{error:?}")))?;
98    Ok(())
99}
100
101/// Capability passed to registration tasks.
102///
103/// The context exposes only the node facts and DHT publication operation that a
104/// registry needs. The task does not own the processor.
105pub struct RegistrationContext<'a> {
106    processor: &'a Processor,
107    stop: StopToken,
108}
109
110impl<'a> RegistrationContext<'a> {
111    pub(crate) fn new(processor: &'a Processor) -> Self {
112        Self::new_with_stop(processor, StopToken::never())
113    }
114
115    pub(crate) const fn new_with_stop(processor: &'a Processor, stop: StopToken) -> Self {
116        Self { processor, stop }
117    }
118
119    /// Return whether the owning registration daemon has requested shutdown.
120    pub fn should_stop(&self) -> bool {
121        self.stop.should_stop()
122    }
123
124    pub(crate) fn ensure_running(&self) -> Result<()> {
125        if self.should_stop() {
126            return Err(Error::RegistrationStopped);
127        }
128        Ok(())
129    }
130
131    /// Return the local node DID.
132    pub fn did(&self) -> Did {
133        self.processor.did()
134    }
135
136    /// Return the local network id.
137    pub fn network_id(&self) -> u32 {
138        self.processor.swarm.network_id()
139    }
140
141    /// Return storage redundancy for the local DHT protocol mode.
142    pub fn storage_redundancy(&self) -> u16 {
143        self.processor.swarm.storage_redundancy()
144    }
145
146    /// Return storage virtual-node positions for the local DHT protocol mode.
147    pub fn dht_virtual_nodes(&self) -> u16 {
148        self.processor.swarm.dht_virtual_nodes()
149    }
150
151    /// Return the account verification public key.
152    pub fn account_verification_pubkey(&self) -> Result<VerificationPublicKey> {
153        self.processor
154            .swarm
155            .account_verification_pubkey()
156            .map_err(Error::CoreError)
157    }
158
159    /// Return the local session signing key.
160    pub fn session_sk(&self) -> &SessionSk {
161        self.processor.session_sk()
162    }
163
164    pub(crate) async fn fetch_storage_entry(&self, entry_key: Did) -> Result<Option<entry::Entry>> {
165        self.processor
166            .fetch_storage_entry_with_stop(entry_key, &self.stop)
167            .await
168    }
169}
170
171/// Common publisher for DHT-backed registries.
172#[derive(Clone, Debug)]
173pub struct DhtRegistrationPublisher {
174    topic: String,
175    publish_gate: Arc<AsyncMutex<()>>,
176    published_values: Arc<Mutex<BTreeSet<Encoded>>>,
177}
178
179impl DhtRegistrationPublisher {
180    /// Create a publisher for `topic`.
181    pub fn new(topic: impl Into<String>) -> Self {
182        Self {
183            topic: topic.into(),
184            publish_gate: Arc::new(AsyncMutex::new(())),
185            published_values: Arc::new(Mutex::new(BTreeSet::new())),
186        }
187    }
188
189    /// Return the DHT topic used by this publisher.
190    pub fn topic(&self) -> &str {
191        &self.topic
192    }
193
194    /// Publish `value`, tombstoning older values previously published by this publisher.
195    pub async fn publish(&self, context: &RegistrationContext<'_>, value: Encoded) -> Result<()> {
196        self.publish_many(context, std::iter::once(value)).await
197    }
198
199    /// Publish the current value set, tombstoning older values previously published by this publisher.
200    ///
201    /// Invariant: after a successful call, DHT data previously emitted by this publisher is exactly
202    /// `values`. Preservation: every stale local value is tombstoned after every current value is
203    /// touched under the same publisher serialization lock.
204    pub async fn publish_many(
205        &self,
206        context: &RegistrationContext<'_>,
207        values: impl IntoIterator<Item = Encoded>,
208    ) -> Result<()> {
209        self.publish_many_with_replacement(context, values, false, |_| false)
210            .await
211    }
212
213    /// Publish the current value set, tombstoning older observed values with the same registry key.
214    ///
215    /// Invariant: registry topics are keyed presence sets, not append-only heartbeat logs.
216    /// Preservation: every observed value replaced by the current publish is tombstoned after the
217    /// replacement value has been touched, while unrelated publisher keys stay joinable.
218    pub async fn publish_many_replacing(
219        &self,
220        context: &RegistrationContext<'_>,
221        values: impl IntoIterator<Item = Encoded>,
222        replaces_observed_value: impl Fn(&Encoded) -> bool,
223    ) -> Result<()> {
224        self.publish_many_with_replacement(context, values, true, replaces_observed_value)
225            .await
226    }
227
228    /// Publish values, tombstone stale observed registry values, and compact at the owner.
229    ///
230    /// This never sends a replacement value set computed from an observed client
231    /// snapshot. Compaction is requested with only the removable payloads, so the
232    /// storage owner computes the final live set from its current local entry and
233    /// preserves concurrent live writes.
234    pub async fn publish_many_replacing_and_compacting(
235        &self,
236        context: &RegistrationContext<'_>,
237        values: impl IntoIterator<Item = Encoded>,
238        replaces_observed_value: impl Fn(&Encoded) -> bool,
239        preserves_observed_value: impl Fn(&Encoded) -> bool,
240    ) -> Result<()> {
241        let current_values = values.into_iter().collect::<BTreeSet<_>>();
242        let _publish_turn = self.publish_gate.lock().await;
243        context.ensure_running()?;
244        let observed_entry = self.observed_registry_entry(context).await?;
245        let observed_values = observed_entry
246            .as_ref()
247            .map(|entry| entry.data.clone())
248            .unwrap_or_default();
249        let should_compact_metadata = observed_entry
250            .as_ref()
251            .is_some_and(registry_entry_has_compactable_metadata);
252        let stale_values = {
253            let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
254            begin_registration_publish(
255                &mut published_values,
256                &current_values,
257                observed_values,
258                |observed| {
259                    should_prune_observed_registry_value(
260                        observed,
261                        &replaces_observed_value,
262                        &preserves_observed_value,
263                    )
264                },
265            )
266        };
267        let should_compact = should_compact_metadata || !stale_values.is_empty();
268        let removals = stale_values.clone();
269
270        for value in &current_values {
271            context.ensure_running()?;
272            context
273                .processor
274                .storage_touch_data(&self.topic, value.clone())
275                .await?;
276        }
277        for stale_value in stale_values {
278            context.ensure_running()?;
279            context
280                .processor
281                .storage_tombstone_data(&self.topic, stale_value.clone())
282                .await?;
283            self.published_values
284                .lock()
285                .map_err(|_| Error::Lock)?
286                .remove(&stale_value);
287        }
288        if should_compact {
289            context.ensure_running()?;
290            context
291                .processor
292                .storage_compact_data(&self.topic, removals)
293                .await?;
294        }
295        {
296            let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
297            finish_registration_publish(&mut published_values, current_values);
298        }
299        Ok(())
300    }
301
302    async fn publish_many_with_replacement(
303        &self,
304        context: &RegistrationContext<'_>,
305        values: impl IntoIterator<Item = Encoded>,
306        load_observed_values: bool,
307        replaces_observed_value: impl Fn(&Encoded) -> bool,
308    ) -> Result<()> {
309        let current_values = values.into_iter().collect::<BTreeSet<_>>();
310        // This capability serializes the effect trace. The published-value state has a separate
311        // synchronous mutex whose guard is never carried across an external await.
312        let _publish_turn = self.publish_gate.lock().await;
313        context.ensure_running()?;
314        let observed_values = if load_observed_values {
315            self.observed_registry_values(context).await?
316        } else {
317            vec![]
318        };
319        let stale_values = {
320            let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
321            begin_registration_publish(
322                &mut published_values,
323                &current_values,
324                observed_values,
325                replaces_observed_value,
326            )
327        };
328
329        for value in &current_values {
330            context.ensure_running()?;
331            context
332                .processor
333                .storage_touch_data(&self.topic, value.clone())
334                .await?;
335        }
336        for stale_value in stale_values {
337            context.ensure_running()?;
338            context
339                .processor
340                .storage_tombstone_data(&self.topic, stale_value.clone())
341                .await?;
342            self.published_values
343                .lock()
344                .map_err(|_| Error::Lock)?
345                .remove(&stale_value);
346        }
347        {
348            let mut published_values = self.published_values.lock().map_err(|_| Error::Lock)?;
349            finish_registration_publish(&mut published_values, current_values);
350        }
351        Ok(())
352    }
353
354    async fn observed_registry_values(
355        &self,
356        context: &RegistrationContext<'_>,
357    ) -> Result<Vec<Encoded>> {
358        Ok(self
359            .observed_registry_entry(context)
360            .await?
361            .map(|entry| entry.data)
362            .unwrap_or_default())
363    }
364
365    async fn observed_registry_entry(
366        &self,
367        context: &RegistrationContext<'_>,
368    ) -> Result<Option<entry::Entry>> {
369        let entry_key = entry::Entry::gen_did(&self.topic)?;
370        context.fetch_storage_entry(entry_key).await
371    }
372}
373
374fn registry_entry_has_compactable_metadata(entry: &entry::Entry) -> bool {
375    !entry.crdt.tombstones.is_empty()
376}
377
378fn should_prune_observed_registry_value(
379    observed: &Encoded,
380    replaces_observed_value: &impl Fn(&Encoded) -> bool,
381    preserves_observed_value: &impl Fn(&Encoded) -> bool,
382) -> bool {
383    replaces_observed_value(observed) || !preserves_observed_value(observed)
384}
385
386fn begin_registration_publish(
387    published_values: &mut BTreeSet<Encoded>,
388    current_values: &BTreeSet<Encoded>,
389    observed_values: Vec<Encoded>,
390    replaces_observed_value: impl Fn(&Encoded) -> bool,
391) -> Vec<Encoded> {
392    let mut stale_values = published_values
393        .iter()
394        .filter(|published| !current_values.contains(*published))
395        .cloned()
396        .collect::<BTreeSet<_>>();
397    stale_values.extend(
398        observed_values
399            .into_iter()
400            .filter(|observed| !current_values.contains(observed))
401            .filter(replaces_observed_value),
402    );
403    // Invariant: every value whose touch may have reached storage is remembered
404    // before the first await. If the publish future is later cancelled by an
405    // attempt timeout, the next attempt can still tombstone the value.
406    published_values.extend(current_values.iter().cloned());
407    stale_values.into_iter().collect()
408}
409
410fn finish_registration_publish(
411    published_values: &mut BTreeSet<Encoded>,
412    current_values: BTreeSet<Encoded>,
413) {
414    *published_values = current_values;
415}
416
417/// Periodic node-layer registration.
418#[cfg_attr(all(feature = "browser", target_family = "wasm"), async_trait(?Send))]
419#[cfg_attr(not(all(feature = "browser", target_family = "wasm")), async_trait)]
420pub trait RegistrationTask: MaybeSend {
421    /// Stable name used in logs.
422    fn name(&self) -> &'static str;
423
424    /// Time between registration attempts.
425    fn interval(&self) -> Duration;
426
427    /// Publish one registration heartbeat.
428    async fn register_once(&self, context: &RegistrationContext<'_>) -> Result<()>;
429}
430
431/// Shared online-node capability labels.
432#[derive(Clone, Debug)]
433pub struct OnlineNodeCapabilities {
434    labels: Arc<Mutex<Vec<String>>>,
435}
436
437impl OnlineNodeCapabilities {
438    fn new(additional_capabilities: Vec<String>) -> Self {
439        let mut labels = Self::default_labels();
440        Self::append_unique_many(&mut labels, additional_capabilities);
441        Self {
442            labels: Arc::new(Mutex::new(labels)),
443        }
444    }
445
446    fn default_labels() -> Vec<String> {
447        vec![ONLINE_NODE_CAPABILITY_STORAGE.to_string()]
448    }
449
450    fn append_unique_many<I, S>(labels: &mut Vec<String>, capabilities: I)
451    where
452        I: IntoIterator<Item = S>,
453        S: Into<String>,
454    {
455        for capability in capabilities {
456            let capability = capability.into();
457            if !labels.iter().any(|known| known == &capability) {
458                labels.push(capability);
459            }
460        }
461    }
462
463    /// Add labels declared by registered extensions.
464    pub fn add_many<I>(&self, capabilities: I) -> Result<()>
465    where I: IntoIterator<Item = &'static str> {
466        let mut labels = self.labels.lock().map_err(|_| Error::Lock)?;
467        Self::append_unique_many(&mut labels, capabilities);
468        Ok(())
469    }
470
471    /// Current descriptor labels.
472    pub fn labels(&self) -> Result<Vec<String>> {
473        self.labels
474            .lock()
475            .map(|labels| labels.clone())
476            .map_err(|_| Error::Lock)
477    }
478}
479
480/// Online-node registry task.
481#[derive(Clone, Debug)]
482pub struct OnlineNodeRegistration {
483    heartbeat_interval: Duration,
484    ttl: Duration,
485    node_type: OnlineNodeType,
486    started_at_ms: u128,
487    endpoint_hint: Option<String>,
488    capabilities: OnlineNodeCapabilities,
489    publisher: DhtRegistrationPublisher,
490}
491
492impl OnlineNodeRegistration {
493    /// Create an online-node registration task.
494    pub fn new(
495        heartbeat_interval: Duration,
496        ttl: Duration,
497        node_type: OnlineNodeType,
498        endpoint_hint: Option<String>,
499        additional_capabilities: Vec<String>,
500    ) -> Self {
501        Self {
502            heartbeat_interval,
503            ttl,
504            node_type,
505            started_at_ms: get_epoch_ms(),
506            endpoint_hint,
507            capabilities: OnlineNodeCapabilities::new(additional_capabilities),
508            publisher: DhtRegistrationPublisher::new(ONLINE_NODES_TOPIC),
509        }
510    }
511
512    /// Validate this registration's periodic schedule when it is enabled.
513    pub fn validate_enabled_schedule(&self) -> Result<()> {
514        validate_online_node_registration_timing(true, self.heartbeat_interval, self.ttl)
515    }
516
517    /// Return capability labels advertised by online-node descriptors.
518    pub fn default_capabilities() -> Vec<String> {
519        OnlineNodeCapabilities::default_labels()
520    }
521
522    /// Add extension-declared capability labels.
523    pub fn add_capabilities<I>(&self, capabilities: I) -> Result<()>
524    where I: IntoIterator<Item = &'static str> {
525        self.capabilities.add_many(capabilities)
526    }
527
528    /// Return capability labels advertised by this registration.
529    pub fn capabilities(&self) -> Result<Vec<String>> {
530        self.capabilities.labels()
531    }
532
533    /// Build this node's signed descriptor at `now_ms`.
534    pub fn descriptor_at(
535        &self,
536        context: &RegistrationContext<'_>,
537        now_ms: u128,
538    ) -> Result<OnlineNodeDescriptor> {
539        OnlineNodeDescriptor::new_signed(
540            OnlineNodeDescriptorBody {
541                did: context.did(),
542                public_key: context.account_verification_pubkey()?,
543                session_public_key: context.session_sk().session_public_key(),
544                node_type: self.node_type.clone(),
545                network_id: context.network_id(),
546                storage_redundancy: context.storage_redundancy(),
547                dht_virtual_nodes: context.dht_virtual_nodes(),
548                capabilities: self.capabilities()?,
549                endpoint_hint: self.endpoint_hint.clone(),
550                started_at_ms: self.started_at_ms,
551                heartbeat_at_ms: now_ms,
552                expires_at_ms: now_ms + self.ttl.as_millis(),
553                version: crate::util::build_version(),
554            },
555            context.session_sk(),
556        )
557        .map_err(Error::CoreError)
558    }
559
560    /// Publish this node's signed online descriptor.
561    pub async fn publish_descriptor(
562        &self,
563        context: &RegistrationContext<'_>,
564    ) -> Result<OnlineNodeDescriptor> {
565        let now_ms = get_epoch_ms();
566        let descriptor = self.descriptor_at(context, now_ms)?;
567        let encoded = descriptor.encode().map_err(Error::CoreError)?;
568        self.publisher
569            .publish_many_replacing_and_compacting(
570                context,
571                std::iter::once(encoded),
572                |observed| {
573                    observed
574                        .decode::<OnlineNodeDescriptor>()
575                        .is_ok_and(|descriptor| {
576                            descriptor.did == context.did()
577                                || (descriptor.verify_signature()
578                                    && descriptor.is_expired_at(now_ms))
579                        })
580                },
581                |observed| {
582                    observed
583                        .decode::<OnlineNodeDescriptor>()
584                        .is_ok_and(|descriptor| {
585                            descriptor.verify_signature() && !descriptor.is_expired_at(now_ms)
586                        })
587                },
588            )
589            .await?;
590        Ok(descriptor)
591    }
592
593    /// Decode online-node descriptors from a DHT entry.
594    pub fn descriptors_from_entry(
595        entry: &rings_core::dht::entry::Entry,
596    ) -> Vec<OnlineNodeDescriptor> {
597        entry
598            .data
599            .iter()
600            .filter_map(|value| value.decode::<OnlineNodeDescriptor>().ok())
601            .collect()
602    }
603}
604
605#[cfg_attr(all(feature = "browser", target_family = "wasm"), async_trait(?Send))]
606#[cfg_attr(not(all(feature = "browser", target_family = "wasm")), async_trait)]
607impl RegistrationTask for OnlineNodeRegistration {
608    fn name(&self) -> &'static str {
609        "online-node"
610    }
611
612    fn interval(&self) -> Duration {
613        self.heartbeat_interval
614    }
615
616    async fn register_once(&self, context: &RegistrationContext<'_>) -> Result<()> {
617        self.publish_descriptor(context).await.map(|_| ())
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use std::collections::BTreeSet;
624
625    use rings_core::message::Encoded;
626
627    use super::*;
628
629    fn encoded(value: &str) -> Encoded {
630        value.into()
631    }
632
633    fn encoded_subset(mask: u8) -> BTreeSet<Encoded> {
634        ["a", "b", "c"]
635            .into_iter()
636            .enumerate()
637            .filter(|(bit, _value)| mask & (1 << bit) != 0)
638            .map(|(_bit, value)| encoded(value))
639            .collect()
640    }
641
642    #[test]
643    fn test_registration_publish_remembers_attempted_values_before_effects() {
644        let old = encoded("old");
645        let attempted = encoded("attempted");
646        let current = BTreeSet::from([attempted.clone()]);
647        let mut known = BTreeSet::from([old.clone()]);
648
649        let stale = begin_registration_publish(&mut known, &current, vec![], |_| false);
650
651        assert_eq!(stale, vec![old.clone()]);
652        assert_eq!(known, BTreeSet::from([old, attempted]));
653    }
654
655    #[test]
656    fn test_registration_publish_retry_tombstones_values_from_cancelled_attempts() {
657        let old = encoded("old");
658        let cancelled = encoded("cancelled");
659        let replacement = encoded("replacement");
660        let mut known = BTreeSet::from([old.clone()]);
661
662        let cancelled_current = BTreeSet::from([cancelled.clone()]);
663        let _ = begin_registration_publish(&mut known, &cancelled_current, vec![], |_| false);
664        let replacement_current = BTreeSet::from([replacement.clone()]);
665        let stale = begin_registration_publish(&mut known, &replacement_current, vec![], |_| false);
666
667        assert_eq!(
668            stale.into_iter().collect::<BTreeSet<_>>(),
669            BTreeSet::from([old, cancelled])
670        );
671        assert!(known.contains(&replacement));
672        finish_registration_publish(&mut known, replacement_current.clone());
673        assert_eq!(known, replacement_current);
674    }
675
676    #[test]
677    fn test_registration_publish_begin_finish_preserve_known_set_law() {
678        for old_mask in 0..8 {
679            for current_mask in 0..8 {
680                for replacement_mask in 0..8 {
681                    let old = encoded_subset(old_mask);
682                    let current = encoded_subset(current_mask);
683                    let replacement = encoded_subset(replacement_mask);
684                    let mut known = old.clone();
685
686                    let _ = begin_registration_publish(&mut known, &current, vec![], |_| false);
687                    let attempted = old.union(&current).cloned().collect::<BTreeSet<_>>();
688                    assert_eq!(known, attempted);
689
690                    let stale =
691                        begin_registration_publish(&mut known, &replacement, vec![], |_| false)
692                            .into_iter()
693                            .collect::<BTreeSet<_>>();
694                    let expected_stale = attempted
695                        .difference(&replacement)
696                        .cloned()
697                        .collect::<BTreeSet<_>>();
698                    assert_eq!(stale, expected_stale);
699
700                    finish_registration_publish(&mut known, replacement.clone());
701                    assert_eq!(known, replacement);
702                }
703            }
704        }
705    }
706
707    #[test]
708    fn test_registration_publish_tombstones_matching_observed_values() {
709        let current = BTreeSet::from([encoded("self-new")]);
710        let observed_self_old = encoded("self-old");
711        let observed_other = encoded("other");
712        let mut known = BTreeSet::new();
713
714        let stale = begin_registration_publish(
715            &mut known,
716            &current,
717            vec![observed_self_old.clone(), observed_other],
718            |observed| observed == &observed_self_old,
719        );
720
721        assert_eq!(stale, vec![observed_self_old]);
722        assert_eq!(known, current);
723    }
724
725    #[test]
726    fn test_registration_pruning_removes_replaced_or_unpreserved_observed_values() {
727        let observed_self_old = encoded("self-old");
728        let observed_live = encoded("other-live");
729        let observed_invalid = encoded("invalid");
730
731        let should_prune = |observed: &Encoded| {
732            should_prune_observed_registry_value(
733                observed,
734                &|value| value == &observed_self_old,
735                &|value| value == &observed_live,
736            )
737        };
738
739        assert!(should_prune(&observed_self_old));
740        assert!(!should_prune(&observed_live));
741        assert!(should_prune(&observed_invalid));
742    }
743
744    #[test]
745    fn test_registration_publish_tombstones_unpreserved_observed_values() {
746        let current = BTreeSet::from([encoded("self-new")]);
747        let observed_self_old = encoded("self-old");
748        let observed_live = encoded("other-live");
749        let observed_invalid = encoded("invalid");
750        let mut known = BTreeSet::new();
751
752        let stale = begin_registration_publish(
753            &mut known,
754            &current,
755            vec![
756                observed_self_old.clone(),
757                observed_live,
758                observed_invalid.clone(),
759            ],
760            |observed| observed == &observed_self_old || observed == &observed_invalid,
761        );
762
763        assert_eq!(
764            stale.into_iter().collect::<BTreeSet<_>>(),
765            BTreeSet::from([observed_invalid, observed_self_old])
766        );
767        assert_eq!(known, current);
768    }
769}