Skip to main content

notedthat_api_http/
state.rs

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