//! Read-side state snapshots published from the node's natural mutators so //! pure-snapshot `show_*` queries render off the rx_loop hot path. //! //! [`StatsSnapshot`] is the R2 reference implementation of the canonical //! snapshot pattern (see `design/fast-path-refactoring-r0-read-handle.md`): a //! read-only data bundle published via `ArcSwap` from the tick after //! `StatsHistory::tick()`. It carries //! //! - the `stats_history` read-side rings (the "dual-ring": the live mutable //! ring stays on the tick; this is the cloned read copy), and //! - the cheap scalar gauges `show_status` needs (`estimated_mesh_size`, //! node `state`, `tun_state`, `tun_name`, `effective_ipv6_mtu`, and the //! peer / session / link / connection / transport counts), plus //! `peer_aliases` (effectively immutable after construction). //! //! The snapshot holds *data*, not rendered `Response` envelopes (Q1-d): //! rendering happens in the control task off the rx_loop. Staleness is bounded //! by the tick interval and is never staler than the underlying data, which //! also advances only on the tick (Q1-b). use std::collections::HashMap; use std::sync::Arc; use crate::identity::NodeAddr; use crate::node::NodeState; use crate::node::acl::PeerAclStatus; use crate::node::stats_history::StatsHistory; use crate::upper::tun::TunState; /// Read-only snapshot of the stats-history rings plus the scalar gauges and /// counts `show_status` reports. Published from the tick (Q1-b). #[derive(Clone)] pub(crate) struct StatsSnapshot { /// Cloned read copy of the history rings (the dual-ring read side). pub history: Arc, /// Cached estimated mesh size, or `None` when no estimate is available. pub estimated_mesh_size: Option, /// Node operational state. pub state: NodeState, /// TUN device state. pub tun_state: TunState, /// TUN interface name, if active. pub tun_name: Option, /// Effective IPv6 MTU over the mesh. pub effective_ipv6_mtu: u16, /// Number of pending connections (handshake in progress). pub connection_count: usize, /// Number of authenticated peers. pub peer_count: usize, /// Number of active links. pub link_count: usize, /// Number of active transports. pub transport_count: usize, /// Number of active sessions. pub session_count: usize, /// Current spanning-tree root `NodeAddr` (rendered as hex by `show_status`). pub root: NodeAddr, /// Whether this node is the spanning-tree root. pub is_root: bool, /// Per-configured-transport-type count of peers whose active link rides that /// transport type. Configured-but-idle types appear with a zero count. /// Keyed by the transport type name (`"udp"`, `"tcp"`, `"tor"`, ...). pub transport_peer_counts: std::collections::BTreeMap, /// Configured peer aliases, keyed by `NodeAddr`. Effectively immutable /// after construction; shared to avoid a per-tick map clone. pub peer_aliases: Arc>, /// Loaded peer-ACL status (`show_acl`). The ACL itself is an /// `arc_swap::ArcSwap` mutated only by the tick's `reload_peer_acl`; /// the human-readable status is a cheap projection of it (R5). pub acl_status: PeerAclStatus, /// Per-stats-history-peer metadata resolved against the live peer/session /// tables and host map at publish time (`show_stats_peers` / /// `show_stats_history_all_peers`), keyed by `NodeAddr`. The lifecycle /// timestamps and per-peer metric rings stay in `history`; this map carries /// only the cross-subsystem fields a renderer can't derive from the rings /// alone (`is_active`, resolved `npub`, resolved `display_name`) (R5). pub peer_meta: Arc>, } /// Cross-subsystem metadata for one peer tracked in the stats-history rings, /// resolved at publish time. Joined against `StatsSnapshot::history`'s rings /// (lifecycle timestamps + metric series) by the off-loop `show_stats_peers` / /// `show_stats_history_all_peers` renderers. #[derive(Clone)] pub(crate) struct StatsPeerMeta { /// Whether this peer is currently in the live authenticated-peer table. pub is_active: bool, /// Resolved npub (live peer npub, or `node_addr` hex when not a live peer), /// matching the on-loop `show_stats_peers` fallback. pub npub: String, /// Display name resolved via `Node::peer_display_name` at publish time. pub display_name: String, } impl StatsSnapshot { /// Build an empty snapshot for seeding the `ArcSwap` cell at construction, /// before the first tick has published real state. pub(crate) fn empty() -> Self { Self { history: Arc::new(StatsHistory::new()), estimated_mesh_size: None, state: NodeState::Created, tun_state: TunState::Disabled, tun_name: None, effective_ipv6_mtu: 0, connection_count: 0, peer_count: 0, link_count: 0, transport_count: 0, session_count: 0, root: zero_addr(), is_root: false, transport_peer_counts: std::collections::BTreeMap::new(), peer_aliases: Arc::new(HashMap::new()), acl_status: empty_acl_status(), peer_meta: Arc::new(HashMap::new()), } } } /// An empty/default [`PeerAclStatus`] for seeding the snapshot before the first /// tick publishes the real ACL status. `PeerAclStatus` does not derive /// `Default`, so this spells out the inert "no ACL loaded" shape. fn empty_acl_status() -> PeerAclStatus { PeerAclStatus { allow_file: String::new(), deny_file: String::new(), enforcement_active: false, effective_mode: String::new(), default_decision: String::new(), allow_all: false, deny_all: false, allow_file_entries: Vec::new(), deny_file_entries: Vec::new(), allow_entries: Vec::new(), deny_entries: Vec::new(), } } // ===================================================================== // RoutingSnapshot (R3 — Category-D derived/routing/cache read view) // ===================================================================== /// Read-only snapshot of the Category-D derived/routing/cache subsystems that /// the pure-snapshot `show_tree` / `show_bloom` / `show_cache` / `show_routing` /// / `show_identity_cache` queries render. Published via `ArcSwap`. /// /// The R0 stub (`design/fast-path-refactoring-r0-read-handle.md`) names a /// single combined `ArcSwap` for R3. This is that cell: one /// cohesive routing view holding the four subsystems (tree / bloom / coord /// cache / identity cache) plus the F-queue summary scalars. /// /// **Publisher placement (Q1).** The four subsystems mutate at many scattered /// handler sites (28 `coord_cache_mut` call sites, 16 `tree_state_mut`, ~32 /// identity-cache touches), and every projected row needs a *display name* /// resolved against the live peer/session tables and host map — Category-E /// state reachable only with `&Node`. Wiring an on-change `publish_*` at each /// mutation site would be large, error-prone surgery, and each call would still /// need `&Node` to resolve names across subsystem boundaries. So this snapshot /// is published from the **tick** (Q1-b acceptable-at-mutator / the documented /// interim the spec permits, mirroring R2's stats publish): the tick is the one /// site with coherent `&Node` access to resolve all display names together. A /// single combined cell is the natural shape because there is exactly one /// publisher — the multi-mutator "rebuild the whole snapshot N times" hazard /// that Q1-c warns against does not arise. /// /// The snapshot holds *data* (typed rows + scalars), not rendered `Response` /// envelopes (Q1-d); rendering happens off the rx_loop in the control task. The /// counter-family `stats` blocks the queries also emit come from the /// `MetricsRegistry` (already `Arc`-shared in the handle) at render time, not /// from this snapshot. /// /// Time-relative fields (`age_ms`, `idle_ms`) are derived at render time from /// the captured absolute timestamps, so the rendered age stays fresh relative /// to the read, exactly as the on-loop queries computed it. /// /// Forward-compat: when step 5 structurally extracts the Category-D subsystems /// into typed types, these projections become thin views over them without /// changing the read-handle interface or this publisher placement. #[derive(Clone)] pub(crate) struct RoutingSnapshot { /// Spanning-tree read view (`show_tree`). pub tree: TreeView, /// Bloom-filter read view (`show_bloom`). pub bloom: BloomView, /// Coordinate-cache read view (`show_cache`, `show_routing`). pub cache: CacheView, /// F-queue / discovery routing scalars + rows (`show_routing`). pub routing: RoutingView, /// Identity-cache read view (`show_identity_cache`, `show_routing`). pub identity: IdentityView, } impl RoutingSnapshot { /// Build an empty snapshot for seeding the `ArcSwap` cell at construction, /// before the first tick has published real state. pub(crate) fn empty() -> Self { Self { tree: TreeView::default(), bloom: BloomView::default(), cache: CacheView::default(), routing: RoutingView::default(), identity: IdentityView::default(), } } } /// Zero `NodeAddr` for empty/seed views (all-zero 16 bytes). fn zero_addr() -> NodeAddr { NodeAddr::from_bytes([0u8; 16]) } /// Spanning-tree read view for `show_tree`. #[derive(Clone)] pub(crate) struct TreeView { pub my_node_addr: NodeAddr, pub root: NodeAddr, /// Resolved npub of the root node, when discoverable (self when root, a live /// peer's attested npub, or an identity-cache hit); `None` otherwise. pub root_npub: Option, pub is_root: bool, pub depth: usize, /// `my_coords` entries as `NodeAddr`s (rendered as hex). pub my_coords: Vec, pub parent: NodeAddr, pub parent_display_name: String, pub declaration_sequence: u64, pub declaration_signed: bool, pub peer_tree_count: usize, pub peers: Vec, } impl Default for TreeView { fn default() -> Self { Self { my_node_addr: zero_addr(), root: zero_addr(), root_npub: None, is_root: false, depth: 0, my_coords: Vec::new(), parent: zero_addr(), parent_display_name: String::new(), declaration_sequence: 0, declaration_signed: false, peer_tree_count: 0, peers: Vec::new(), } } } /// One peer's tree position in `show_tree`. #[derive(Clone)] pub(crate) struct TreePeerRow { pub node_addr: NodeAddr, pub display_name: String, /// Present only when the peer's coordinates are known. pub coords: Option, } /// Coordinate detail for a tree peer (present only when known). #[derive(Clone)] pub(crate) struct TreePeerCoords { pub depth: usize, pub root: NodeAddr, pub coord_path: Vec, pub distance_to_us: usize, } /// Bloom-filter read view for `show_bloom`. #[derive(Clone)] pub(crate) struct BloomView { pub own_node_addr: NodeAddr, pub is_leaf_only: bool, pub sequence: u64, pub leaf_dependents: Vec, pub peer_filters: Vec, /// Fill ratio of the last filter actually sent uptree (to the tree parent). /// `None` for a root node (nothing sent uptree) or before the first /// announce has been sent. pub uptree_fill_ratio: Option, /// Estimated cardinality of the last filter sent uptree — this node's whole /// subtree (self + tree-descendants, parent excluded), since bloom is /// split-horizon. `None` for root, pre-first-announce, or when the estimate /// is undefined for the saturation. pub uptree_estimated_count: Option, } impl Default for BloomView { fn default() -> Self { Self { own_node_addr: zero_addr(), is_leaf_only: false, sequence: 0, leaf_dependents: Vec::new(), peer_filters: Vec::new(), uptree_fill_ratio: None, uptree_estimated_count: None, } } } /// One peer's bloom-filter state in `show_bloom`. #[derive(Clone)] pub(crate) struct BloomPeerRow { pub peer: NodeAddr, pub display_name: String, pub has_filter: bool, pub filter_sequence: u64, /// Present only when the peer has supplied an inbound filter. pub filter: Option, } /// Inbound-filter statistics for a bloom peer (present only when known). #[derive(Clone)] pub(crate) struct BloomPeerFilter { /// Estimated cardinality (`None` when undefined for the saturation), /// matching `BloomFilter::estimated_count`'s `Option`. pub estimated_count: Option, pub set_bits: usize, pub fill_ratio: f64, } /// Coordinate-cache read view for `show_cache` (and the cache scalars in /// `show_routing`). #[derive(Clone, Default)] pub(crate) struct CacheView { pub count: usize, pub max_entries: usize, pub fill_ratio: f64, pub default_ttl_ms: u64, pub expired: usize, pub avg_age_ms: u64, pub entries: Vec, } /// One coordinate-cache entry in `show_cache`. #[derive(Clone)] pub(crate) struct CacheEntryRow { pub node_addr: NodeAddr, pub display_name: String, pub depth: usize, pub coord_path: Vec, /// Absolute creation time (Unix ms); `age_ms` derived at render time. pub created_at: u64, pub last_used_ms: u64, pub path_mtu: Option, } /// F-queue / discovery routing read view for `show_routing`. #[derive(Clone, Default)] pub(crate) struct RoutingView { pub pending_lookups: Vec, pub pending_tun_destinations: usize, pub pending_tun_packets: usize, pub recent_requests: usize, pub retries: Vec, } /// One in-flight discovery lookup in `show_routing`. #[derive(Clone)] pub(crate) struct PendingLookupRow { pub target: NodeAddr, pub display_name: String, /// Absolute initiation time (Unix ms); `age_ms` derived at render time. pub initiated_ms: u64, pub last_sent_ms: u64, pub attempt: u8, } /// One connection-retry entry in `show_routing`. #[derive(Clone)] pub(crate) struct RetryRow { pub node_addr: NodeAddr, pub display_name: String, pub retry_count: u32, pub retry_after_ms: u64, pub auto_reconnect: bool, } /// Identity-cache read view for `show_identity_cache` (and the /// `identity_cache_entries` scalar in `show_routing`). #[derive(Clone, Default)] pub(crate) struct IdentityView { pub entries: Vec, pub max_entries: usize, } /// One identity-cache entry in `show_identity_cache`. #[derive(Clone)] pub(crate) struct IdentityRow { pub node_addr: NodeAddr, pub npub: String, pub display_name: String, pub ipv6_addr: String, pub last_seen_ms: u64, } // ===================================================================== // EntitySnapshot (R4 — Category-E per-entity table read views) // ===================================================================== /// Read-only snapshot of the Category-E per-entity tables that the /// pure-snapshot `show_peers` / `show_sessions` / `show_links` / /// `show_connections` / `show_transports` / `show_mmp` queries render. /// Published via `ArcSwap`. /// /// The R0 stub (`design/fast-path-refactoring-r0-read-handle.md`) pre-scopes /// R4 as `entities — ArcSwap: peers / sessions / links / /// connections / transports, published per-entity with `Vec>` /// structural sharing`. This is that cell. /// /// **Structural sharing (the umbrella mandate).** Every entity table is a /// `Vec>`, so a republish in which only one row changed re-allocates /// only that one `Arc` — the unchanged rows are reused by pointer from the /// previous snapshot (`Arc::ptr_eq`-stable). The publisher diffs each freshly /// projected row against the prior published row by value (`PartialEq`) and /// keeps the old `Arc` when they are equal. A clone of the snapshot for each /// accepted control connection is then a vector of cheap pointer clones, not a /// deep table copy. This is what keeps the per-tick publish cost off the hot /// path at scale, as the umbrella requires for R4. /// /// **Publisher placement (Q1).** Like R3, this is published from the **tick**, /// not per-mutator. Two reasons, both stronger than for R3: /// /// 1. Every projected row needs a *display name* resolved against the live /// peer/session tables and host map (`&Node`), and `show_peers` additionally /// needs the live tree state to derive `is_parent` / `is_child` and the /// Nostr-discovery failure-state map — cross-subsystem reads available only /// with `&Node`. /// 2. Most of the projected fields (link/session traffic counters, MMP /// metrics, `last_seen`, noise counters, replay/decrypt counters) are /// mutated continuously on the **data plane / rx_loop**, not at the discrete /// peer/session/link lifecycle mutators. Per-lifecycle-mutator publication /// (Q1-a) would therefore not even capture freshness for those fields; the /// tick is the natural cadence at which this read view advances. /// /// The diff-and-reuse therefore satisfies the structural-sharing goal the /// umbrella mandates (only changed rows re-allocate) while keeping a single /// coherent `&Node` publisher — the "no monolithic per-tick *re-allocation* of /// every row" warning is honored because unchanged rows are reused, not rebuilt. /// This is the documented acceptable interim (the spec's tick-publish-with- /// Arc-reuse fallback), consistent with R3. /// /// The snapshot holds typed rows (Q1-d data, not rendered `Response` /// envelopes). Time-relative fields (`idle_ms`) are derived at render time from /// captured absolute timestamps, so the rendered age stays fresh relative to /// the read, exactly as the on-loop queries computed it. /// /// Forward-compat: step 10 later extracts the session table into a typed /// `(transport_id, our_index)`-indexed type; these projections then become thin /// views over it without changing the read-handle interface or this publisher /// placement. #[derive(Clone)] pub(crate) struct EntitySnapshot { /// `show_peers` rows. pub peers: Vec>, /// `show_sessions` rows. pub sessions: Vec>, /// `show_links` rows. pub links: Vec>, /// `show_connections` rows. pub connections: Vec>, /// `show_transports` rows. pub transports: Vec>, /// `show_mmp` link-layer rows (peers with an MMP instance). pub mmp_peers: Vec>, /// `show_mmp` session-layer rows (sessions with an MMP instance). pub mmp_sessions: Vec>, } impl EntitySnapshot { /// Build an empty snapshot for seeding the `ArcSwap` cell at construction, /// before the first tick has published real state. pub(crate) fn empty() -> Self { Self { peers: Vec::new(), sessions: Vec::new(), links: Vec::new(), connections: Vec::new(), transports: Vec::new(), mmp_peers: Vec::new(), mmp_sessions: Vec::new(), } } } /// Per-peer link/transport/connectivity fields for `show_peers` derived from a /// peer's resolved link (present only when the link is found). #[derive(Clone, PartialEq)] pub(crate) struct PeerLinkInfo { pub direction: String, /// Transport type name, present only when the transport handle is found. pub transport_type: Option, } /// Nostr-traversal failure-state for a peer's npub in `show_peers`. Always /// emitted (the on-loop query emits a default object even when absent); the /// `present` flag distinguishes "seen by Nostr discovery" from the default. #[derive(Clone, PartialEq)] pub(crate) struct PeerNostrState { pub consecutive_failures: u32, pub cooldown_until_ms: Option, pub last_observed_skew_ms: Option, } /// Noise session counters surfaced in `show_peers` (present when the peer has a /// Noise session). #[derive(Clone, PartialEq)] pub(crate) struct PeerNoiseCounters { pub send_counter: u64, pub highest_recv_counter: u64, } /// Link/session MMP metrics surfaced inline in `show_peers` (and the /// per-session block in `show_sessions`). Fields mirror the on-loop projection; /// `Option` fields are emitted only when present. #[derive(Clone, PartialEq)] pub(crate) struct EntityMmp { pub mode: String, pub srtt_ms: Option, pub loss_rate: f64, pub etx: f64, pub goodput_bps: f64, pub delivery_ratio_forward: f64, pub delivery_ratio_reverse: f64, pub smoothed_loss: Option, pub smoothed_etx: Option, /// `lqi` (peers) / `sqi` (sessions): present only when both `srtt_ms` and /// `smoothed_etx` are present. Precomputed so the render is a plain emit. pub quality_index: Option, /// Session-only: path MTU (`show_sessions`). `None` for peer rows. pub path_mtu: Option, } /// Link-layer stat counters for a peer in `show_peers`. #[derive(Clone, PartialEq)] pub(crate) struct PeerLinkStats { pub packets_sent: u64, pub packets_recv: u64, pub bytes_sent: u64, pub bytes_recv: u64, } /// One authenticated peer in `show_peers`. Holds every field the on-loop /// `show_peers` emits; `Option` fields gate the conditionally-emitted keys. #[derive(Clone, PartialEq)] pub(crate) struct PeerRow { pub node_addr: NodeAddr, pub npub: String, pub display_name: String, pub ipv6_addr: String, pub connectivity: String, pub link_id: u64, pub authenticated_at_ms: u64, pub last_seen_ms: u64, pub has_tree_position: bool, pub has_bloom_filter: bool, pub filter_sequence: u64, pub is_parent: bool, pub is_child: bool, pub transport_addr: Option, pub link_info: Option, pub tree_depth: Option, /// `effective_depth = tree_depth + link_cost` — the same quantity /// `evaluate_parent` ranks parent candidates on. `None` when the peer is /// unmeasured (no SRTT while other peers have it) or has no coords, /// mirroring the candidacy/cold-start rules. Pre-computed daemon-side so /// fipstop never recomputes it. pub effective_depth: Option, pub stats: PeerLinkStats, pub replay_suppressed: u32, pub consecutive_decrypt_failures: u32, pub nostr_traversal: PeerNostrState, pub noise: Option, pub our_session_index: Option, pub rekey_in_progress: bool, pub rekey_draining: bool, pub current_k_bit: bool, pub mmp: Option, } /// Traffic counters for a session in `show_sessions`. #[derive(Clone, PartialEq)] pub(crate) struct SessionStats { pub packets_sent: u64, pub packets_recv: u64, pub bytes_sent: u64, pub bytes_recv: u64, } /// One end-to-end session in `show_sessions`. #[derive(Clone, PartialEq)] pub(crate) struct SessionRow { pub remote_addr: NodeAddr, pub display_name: String, pub state: &'static str, pub is_initiator: bool, pub last_activity_ms: u64, pub npub: String, pub stats: SessionStats, /// Handshake resend count, emitted only while not established. pub resend_count: Option, /// Established-only health block (session_start_ms, current_k_bit, /// coords_warmup_remaining, is_draining). `None` while handshaking. pub established: Option, pub mmp: Option, } /// Established-session health fields in `show_sessions` (emitted only when the /// session is established). #[derive(Clone, PartialEq)] pub(crate) struct SessionEstablished { pub session_start_ms: u64, pub current_k_bit: bool, pub coords_warmup_remaining: u8, pub is_draining: bool, } /// Stat counters for a link in `show_links`. #[derive(Clone, PartialEq)] pub(crate) struct LinkStats { pub packets_sent: u64, pub packets_recv: u64, pub bytes_sent: u64, pub bytes_recv: u64, pub last_recv_ms: u64, } /// One active link in `show_links`. #[derive(Clone, PartialEq)] pub(crate) struct LinkRow { pub link_id: u64, pub transport_id: u32, pub remote_addr: String, pub direction: String, pub state: String, pub created_at_ms: u64, pub stats: LinkStats, } /// One pending handshake in `show_connections`. `idle_ms` is derived at render /// time from the captured `last_activity_ms`. #[derive(Clone, PartialEq)] pub(crate) struct ConnectionRow { pub link_id: u64, pub direction: String, pub handshake_state: String, pub started_at_ms: u64, /// Absolute last-activity time (Unix ms); `idle_ms` derived at render time. pub last_activity_ms: u64, pub resend_count: u32, /// Expected peer npub, emitted only when the connection has an expected /// identity. pub expected_peer: Option, } /// One transport instance in `show_transports`. The `stats` and /// `tor_monitoring` fields are stored as already-projected `serde_json::Value` /// (data, produced by the transport handle), not as rendered `Response` /// envelopes. #[derive(Clone, PartialEq)] pub(crate) struct TransportRow { pub transport_id: u32, pub transport_type: String, pub state: String, pub mtu: u16, pub name: Option, pub local_addr: Option, pub tor_mode: Option, pub onion_address: Option, pub tor_monitoring: Option, pub stats: serde_json::Value, } /// MMP trend labels for a peer's link-layer block in `show_mmp` (each present /// only when the corresponding trend is initialized). #[derive(Clone, PartialEq)] pub(crate) struct MmpTrends { pub rtt_trend: Option<&'static str>, pub loss_trend: Option<&'static str>, pub goodput_trend: Option<&'static str>, pub jitter_trend: Option<&'static str>, } /// One peer's link-layer MMP block in `show_mmp`. #[derive(Clone, PartialEq)] pub(crate) struct MmpPeerRow { pub peer: NodeAddr, pub display_name: String, pub mode: String, pub loss_rate: f64, pub etx: f64, pub goodput_bps: f64, pub spin_bit_initiator: bool, pub smoothed_loss: Option, pub smoothed_etx: Option, pub srtt_ms: Option, /// `lqi`: present only when both `srtt_ms` and `smoothed_etx` are present. pub lqi: Option, pub trends: MmpTrends, pub delivery_ratio_forward: f64, pub delivery_ratio_reverse: f64, pub ecn_ce_count: u32, } /// MMP trend labels for a session's session-layer block in `show_mmp` (each /// present only when the corresponding trend is initialized). Mirrors the /// link-layer [`MmpTrends`] arrow semantics on the session columns: srtt /// (`rtt_trend`), loss (`loss_trend`), etx (`etx_trend`). #[derive(Clone, PartialEq)] pub(crate) struct MmpSessionTrends { pub rtt_trend: Option<&'static str>, pub loss_trend: Option<&'static str>, pub etx_trend: Option<&'static str>, } /// One session's session-layer MMP block in `show_mmp`. #[derive(Clone, PartialEq)] pub(crate) struct MmpSessionRow { pub remote: NodeAddr, pub display_name: String, pub mode: String, pub loss_rate: f64, pub etx: f64, pub path_mtu: u16, pub smoothed_loss: Option, pub smoothed_etx: Option, pub srtt_ms: Option, /// `sqi`: present only when both `srtt_ms` and `smoothed_etx` are present. pub sqi: Option, pub trends: MmpSessionTrends, } /// Reconcile a freshly-projected entity table against the previously published /// one, preserving structural sharing: an `Arc` from `prev` is reused /// (kept by pointer) whenever a new row matches an old row by identity `key` /// **and** compares equal by value, so only changed/new rows allocate a fresh /// `Arc`. This is the `Vec>` discipline the R4 umbrella mandates — a /// single-row change re-allocates one row, not the whole table, keeping the /// per-tick publish cost off the hot path at scale. /// /// `key` extracts a stable, hashable identity (e.g. `node_addr`, `link_id`) so /// matching is order-independent across the source table's iteration order. pub(crate) fn reconcile_rows(prev: &[Arc], new_rows: Vec, key: F) -> Vec> where R: PartialEq, K: std::hash::Hash + Eq, F: Fn(&R) -> K, { let index: HashMap> = prev.iter().map(|arc| (key(arc), arc)).collect(); new_rows .into_iter() .map(|row| match index.get(&key(&row)) { Some(old) if ***old == row => Arc::clone(old), _ => Arc::new(row), }) .collect() }