Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/src/serving.rs

//! ★ **What this monetize serves, said where its ONE box reads it** (A32-4): every product its
//! config declares, the ones SERVED (a product the registry holds: its guest loaded, its plugin
//! linked) and the ones refused, by name — written to `<data_dir>/`[`SERVING_FILE`] once the
//! gRPC door listens, and again on every look of the guest watcher (a feed taken, a guest
//! loaded). The ONE box's self-update trial confirms on this word (monetize-admin
//! `self_update::serves_whole`), never on a bare TCP connect: an app that refused every product
//! guest answered TCP and was confirmed (R3+R4, 2026-10-26).

use crate::registry::{GuestChange, Registry};

/// The file under `data_dir` this monetize says what it serves in.
pub const SERVING_FILE: &str = "serving.json";

/// **What this monetize serves** — as written to [`SERVING_FILE`].
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Serving {
    /// The process that wrote it.
    pub pid: u32,
    /// When it was written (unix ms): a word from before this start is not this app's.
    pub at_unix_ms: u64,
    /// Every product the config declares (`[products.<id>]`).
    pub declared: Vec<String>,
    /// The declared products served now.
    pub served: Vec<String>,
    /// The declared products NOT served, each `<id>: <why>` (its start refusal when one was said).
    pub refused: Vec<String>,
}

/// Pure: **what `reg` serves of `declared`**, at `now_unix_ms`.
pub fn of(reg: &Registry, declared: &[String], now_unix_ms: u64) -> Serving {
    let mut s = Serving {
        pid: std::process::id(),
        at_unix_ms: now_unix_ms,
        declared: declared.to_vec(),
        ..Serving::default()
    };
    for id in declared {
        if reg.find_product(id).is_some() {
            s.served.push(id.clone());
            continue;
        }
        let why = reg
            .start_refusals()
            .iter()
            .find_map(|r| match r {
                GuestChange::Refused {
                    id: rid,
                    clause,
                    why,
                } if rid == id => Some(format!("{clause} ({why})")),
                _ => None,
            })
            .unwrap_or_else(|| "not served (its guest is not loaded)".to_string());
        s.refused.push(format!("{id}: {why}"));
    }
    s
}

/// **Write `s` to `path`** whole (a temporary file renamed over it): a reader never sees half.
pub fn write(path: &std::path::Path, s: &Serving) -> std::io::Result<()> {
    if let Some(dir) = path.parent() {
        std::fs::create_dir_all(dir)?;
    }
    let tmp = path.with_extension("json.tmp");
    std::fs::write(
        &tmp,
        serde_json::to_vec_pretty(s).map_err(std::io::Error::other)?,
    )?;
    std::fs::rename(&tmp, path)
}

/// **Say what `state` serves of `cfg`'s products now**, to `cfg`'s [`SERVING_FILE`]. A write that
/// fails is said: the box's trial then waits (and rolls back), which is what it should do.
pub fn say(state: &crate::state::AppState, cfg: &crate::config::Config) {
    let declared: Vec<String> = cfg.products.keys().cloned().collect();
    let s = of(&state.registry, &declared, state.now());
    if let Err(e) = write(&cfg.serving_path(), &s) {
        eprintln!(
            "monetize-server: WARNING the serving word could not be written to {}: {e}",
            cfg.serving_path().display()
        );
    }
}