Skip to main content

ic_query/nns/neuron/report/collection/
mod.rs

1//! Module: nns::neuron::report::collection
2//!
3//! Responsibility: advance caller-persisted complete public-neuron walks one page at a time.
4//! Does not own: stable memory, filesystem caches, scheduling, retries, or publication.
5//! Boundary: validates resumable input and derives the next state from one admitted source page.
6
7#[cfg(feature = "nns-host")]
8use super::NnsNeuronHostError;
9use super::{
10    NnsNeuronError,
11    model::{NnsNeuronListReport, NnsNeuronListRequest},
12    source::{NnsNeuronSource, build_nns_neuron_list_report_with_source, validate_page_size},
13};
14#[cfg(feature = "nns-host")]
15use crate::nns::LiveNnsSource;
16use crate::nns::governance::{
17    GovernanceCollectionState, NnsGovernanceRequest, governance_collection_accessors,
18    validate_governance_continuation, validate_governance_request,
19};
20use serde::{Deserialize, Serialize};
21
22/// Version of the persistable resumable NNS neuron collection state.
23pub const NNS_NEURON_COLLECTION_STATE_SCHEMA_VERSION: u32 = 1;
24
25///
26/// NnsNeuronCollectionState
27///
28/// Serializable continuation state for an explicitly bounded public-neuron walk.
29///
30
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(deny_unknown_fields)]
33pub struct NnsNeuronCollectionState {
34    #[serde(flatten)]
35    collection: GovernanceCollectionState,
36    neurons_fetched: u64,
37    next_start_neuron_id: Option<u64>,
38}
39
40impl NnsNeuronCollectionState {
41    /// Start an empty neuron walk with explicit per-page and cumulative call ceilings.
42    pub fn new(
43        request: &NnsGovernanceRequest,
44        page_size: u32,
45        max_pages: u32,
46    ) -> Result<Self, NnsNeuronError> {
47        validate_governance_request(request)?;
48        validate_page_size(page_size)?;
49        if max_pages == 0 {
50            return Err(NnsNeuronError::InvalidCollectionMaxPages);
51        }
52        Ok(Self {
53            collection: GovernanceCollectionState::new(request, page_size, max_pages),
54            neurons_fetched: 0,
55            next_start_neuron_id: None,
56        })
57    }
58
59    #[cfg(feature = "nns-host")]
60    pub(in crate::nns) const fn from_complete_collection(
61        collection: GovernanceCollectionState,
62        rows: u64,
63    ) -> Self {
64        Self {
65            collection,
66            neurons_fetched: rows,
67            next_start_neuron_id: None,
68        }
69    }
70
71    #[cfg(feature = "nns-host")]
72    pub(in crate::nns) fn collection_evidence(
73        &self,
74    ) -> crate::nns::governance::collection::NnsGovernanceCollectionEvidence {
75        crate::nns::governance::collection::NnsGovernanceCollectionEvidence::from_state(
76            &self.collection,
77        )
78    }
79
80    governance_collection_accessors!(next_start_neuron_id);
81
82    /// Return the number of successfully admitted neuron rows.
83    #[must_use]
84    pub const fn neurons_fetched(&self) -> u64 {
85        self.neurons_fetched
86    }
87
88    /// Return the exclusive lower neuron-id bound for the next page.
89    #[must_use]
90    pub const fn next_start_neuron_id(&self) -> Option<u64> {
91        self.next_start_neuron_id
92    }
93}
94
95///
96/// NnsNeuronCollectionStep
97///
98/// One admitted bounded page and the continuation state that follows it.
99///
100
101#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102pub struct NnsNeuronCollectionStep {
103    /// Page returned by the shared public-neuron report builder.
104    pub page: NnsNeuronListReport,
105    /// Validated state to persist only after retaining the page.
106    pub state: NnsNeuronCollectionState,
107}
108
109/// Advance a resumable public-neuron walk through the native replica adapter.
110#[cfg(feature = "nns-host")]
111pub async fn advance_nns_neuron_collection(
112    request: &NnsGovernanceRequest,
113    state: &NnsNeuronCollectionState,
114) -> Result<NnsNeuronCollectionStep, NnsNeuronHostError> {
115    Ok(
116        advance_nns_neuron_collection_with_source(request, state, &LiveNnsSource::default())
117            .await?,
118    )
119}
120
121/// Advance a resumable public-neuron walk by exactly one caller-runtime source call.
122pub async fn advance_nns_neuron_collection_with_source(
123    request: &NnsGovernanceRequest,
124    state: &NnsNeuronCollectionState,
125    source: &dyn NnsNeuronSource,
126) -> Result<NnsNeuronCollectionStep, NnsNeuronError> {
127    validate_governance_request(request)?;
128    validate_collection_state(state)?;
129    validate_governance_continuation!(request, state, NnsNeuronError)?;
130
131    let mut page_request = NnsNeuronListRequest::new(request.clone(), state.collection.page_size);
132    page_request.exclusive_start_neuron_id = state.next_start_neuron_id;
133    let page = build_nns_neuron_list_report_with_source(&page_request, source).await?;
134    let (collection, neurons_fetched) = state.collection.after_page(
135        request,
136        page.context.source.clone(),
137        state.neurons_fetched,
138        page.neurons.len(),
139        |expected, actual| NnsNeuronError::CollectionSourceChanged { expected, actual },
140        NnsNeuronError::CollectionAccountingOverflow,
141    )?;
142    let next_start_neuron_id = page.next_start_neuron_id;
143    let next_state = NnsNeuronCollectionState {
144        collection,
145        neurons_fetched,
146        next_start_neuron_id,
147    };
148    Ok(NnsNeuronCollectionStep {
149        page,
150        state: next_state,
151    })
152}
153
154pub(super) fn validate_collection_state(
155    state: &NnsNeuronCollectionState,
156) -> Result<(), NnsNeuronError> {
157    let invalid = |reason| NnsNeuronError::InvalidCollectionState { reason };
158    state
159        .collection
160        .validate(NNS_NEURON_COLLECTION_STATE_SCHEMA_VERSION, invalid)?;
161    validate_page_size(state.collection.page_size)?;
162
163    let neurons_fetched = state.neurons_fetched;
164    let maximum_rows =
165        state
166            .collection
167            .validate_row_bounds("neurons_fetched", neurons_fetched, invalid)?;
168
169    state.collection.validate_lifecycle(
170        neurons_fetched,
171        maximum_rows,
172        state.next_start_neuron_id.is_some(),
173        neurons_fetched < maximum_rows,
174        invalid,
175    )
176}
177
178#[cfg(test)]
179mod tests;