Skip to main content

notedthat_api_http/
state.rs

1//! Shared application state for the axum router.
2
3use notedthat_core::{
4    AccessPolicy, Authenticator, EventPublisher, EventSource, KbDetails, KbSlug, Storage,
5};
6use notedthat_indexer::{IndexHealth, Searcher};
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10/// Application state shared across all axum handlers.
11///
12/// This is cloned cheaply for each request (all fields are behind [`Arc`]).
13#[derive(Clone)]
14pub struct AppState {
15    /// The backing storage implementation (injected at startup).
16    pub storage: Arc<dyn Storage>,
17    /// Canonical map of slug string → [`KbSlug`] for declared knowledge bases.
18    pub declared_kbs: Arc<BTreeMap<String, KbSlug>>,
19    /// Startup snapshot of manifest access policies, keyed by KB slug.
20    ///
21    /// One `Arc` per policy so a request-scoped authorization handle can hold
22    /// its knowledge base's policy cheaply instead of borrowing the whole map.
23    pub access_policies: Arc<BTreeMap<String, Arc<AccessPolicy>>>,
24    /// Startup snapshot of each manifest's display name and description,
25    /// keyed by KB slug: what `GET /api/v1/knowledgebases` shows (#98).
26    pub kb_details: Arc<BTreeMap<String, KbDetails>>,
27    /// The credential rules every request's principal is resolved through.
28    pub authenticator: Arc<Authenticator>,
29    /// Maximum accepted PUT body size in bytes (16 MiB in M2).
30    pub max_body_size: u64,
31    /// Maximum object size eligible for patch operations, in bytes.
32    pub max_patchable_size: u64,
33    /// Sender half of the async indexing queue.
34    pub indexer_tx: tokio::sync::mpsc::Sender<notedthat_indexer::IndexEvent>,
35    /// The search implementation (injected at startup).
36    pub searcher: Arc<dyn Searcher>,
37    /// The object change event log, when `NOTEDTHAT_EVENTS_BACKEND` selects one.
38    /// `None` is the default: writes are not announced and the events route
39    /// answers 404.
40    pub events: Option<Arc<dyn EventPublisher>>,
41    /// The per-knowledge-base index health record, shared with the indexer
42    /// worker and every write path (#97).
43    pub index_health: Arc<IndexHealth>,
44}
45
46impl AppState {
47    /// Where a write made through this surface is reported, attributed to
48    /// `source` — the HTTP API itself, or MCP when the request says so.
49    pub(crate) fn sinks(&self, source: EventSource) -> notedthat_write::WriteSinks<'_> {
50        notedthat_write::WriteSinks::new(
51            &self.indexer_tx,
52            self.events.as_deref(),
53            &self.index_health,
54            source,
55        )
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::testing::InMemoryStorage;
63    use std::collections::BTreeMap;
64    use std::sync::Arc;
65
66    fn minimal_state(tx: tokio::sync::mpsc::Sender<notedthat_indexer::IndexEvent>) -> AppState {
67        AppState {
68            storage: Arc::new(InMemoryStorage::default()),
69            declared_kbs: Arc::new(BTreeMap::new()),
70            access_policies: Arc::new(BTreeMap::new()),
71            kb_details: Arc::new(BTreeMap::new()),
72            authenticator: Arc::new(Authenticator::new("token")),
73            max_body_size: 1024,
74            max_patchable_size: 1024,
75            indexer_tx: tx,
76            searcher: Arc::new(crate::testing::NoopSearcher),
77            events: None,
78            index_health: Arc::new(notedthat_indexer::IndexHealth::new()),
79        }
80    }
81
82    #[tokio::test]
83    async fn clone_shares_same_channel() {
84        let (tx, mut rx) = tokio::sync::mpsc::channel(10);
85        let state = minimal_state(tx);
86        let cloned = state.clone();
87
88        let event = notedthat_indexer::IndexEvent::Tombstone {
89            kb: notedthat_core::KbSlug::try_new("test").expect("valid kb slug"),
90            object_key: notedthat_core::ObjectPath::try_from("a.md").expect("valid path"),
91        };
92        cloned
93            .indexer_tx
94            .send(event.clone())
95            .await
96            .expect("send on cloned tx");
97
98        let received = rx.recv().await.expect("receive on original rx");
99        assert_eq!(
100            received, event,
101            "cloned Sender must share the same underlying channel"
102        );
103    }
104}