ic-query 0.93.1

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
//! Module: snapshot_cache::refresh
//!
//! Responsibility: run paged snapshot refresh adapters.
//! Does not own: source APIs, cache publication, refresh-attempt schemas, or process output.
//! Boundary: emits structured progress while driving page fetches and attempt writes.

use super::PagedCollectionPage;
use crate::{QueryProgress, QueryProgressEvent, QueryProgressState};

///
/// PagedSnapshotRefresh
///
/// Command-specific adapter for running a complete paged snapshot refresh.
///

pub trait PagedSnapshotRefresh {
    type Complete;
    type Error;

    /// Family diagnostic retained when a page ceiling prevents API exhaustion.
    const PAGE_LIMIT_REASON: &'static str = "max pages reached before API exhaustion";

    fn progress(&self) -> crate::PagedQueryProgress;

    fn max_pages_reached(&self) -> bool;
    fn incomplete_refresh_error(&self, reason: &'static str) -> Self::Error;
    fn fetch_next_page(
        &mut self,
    ) -> crate::report::SourceFuture<'_, Result<PagedCollectionPage, Self::Error>>;
    /// Persist admitted progress from adapter state when this collection has a page sidecar.
    fn write_running_attempt(&self) -> Result<(), Self::Error> {
        Ok(())
    }
    fn into_complete(self) -> Self::Complete;
}

/// Run a complete paged snapshot refresh and emit structured progress events.
pub async fn run_paged_snapshot_refresh_with_progress<Refresh>(
    mut refresh: Refresh,
    progress: &mut dyn QueryProgress,
) -> Result<Refresh::Complete, Refresh::Error>
where
    Refresh: PagedSnapshotRefresh,
{
    report_progress(progress, &refresh, QueryProgressState::Running);

    loop {
        if refresh.max_pages_reached() {
            report_progress(progress, &refresh, QueryProgressState::Stopped);
            return Err(refresh.incomplete_refresh_error(Refresh::PAGE_LIMIT_REASON));
        }

        let page = match refresh.fetch_next_page().await.and_then(|page| {
            refresh.write_running_attempt()?;
            Ok(page)
        }) {
            Ok(page) => page,
            Err(err) => {
                report_progress(progress, &refresh, QueryProgressState::Failed);
                return Err(err);
            }
        };
        report_progress(progress, &refresh, QueryProgressState::Running);

        if page.collection_exhausted {
            break;
        }
        if page.has_no_new_rows() {
            report_progress(progress, &refresh, QueryProgressState::Stalled);
            return Err(refresh.incomplete_refresh_error(
                "page returned no new rows while advertising another cursor",
            ));
        }
    }

    report_progress(progress, &refresh, QueryProgressState::Complete);
    Ok(refresh.into_complete())
}

fn report_progress(
    progress: &mut dyn QueryProgress,
    refresh: &impl PagedSnapshotRefresh,
    state: QueryProgressState,
) {
    progress.report(QueryProgressEvent::PagedRefresh {
        progress: refresh.progress(),
        state,
    });
}