Skip to main content

sofie_http/
handler.rs

1use std::sync::Arc;
2
3use sofie_core::Result;
4use sofie_db::{AnyOutcome, BoxFuture, Call, Context, Db, ReadOnly, Reply, Run, erase};
5use tracing::Instrument;
6
7use crate::context::Matched;
8use crate::trace::{HandlerName, handler_name, handler_span};
9use crate::{Area, ReadContext, RequestContext};
10
11/// Area erased, so routes of every area share one router.
12pub(crate) type Endpoint =
13    Arc<dyn Fn(Db, Matched) -> BoxFuture<'static, Result<Reply>> + Send + Sync>;
14
15/// What a GET, a screen or an island is answered by.
16#[diagnostic::on_unimplemented(
17    message = "`{Self}` cannot answer a GET, a screen or an island: a GET never writes",
18    label = "takes a write-side argument or returns a writable outcome",
19    note = "take only read-side arguments (`Read`, a read model, `Principal`, `Features`, `Permissions`, `Listing`, config, the clock) and return a read model, `Paginated`, `Screen` or another reply; a write belongs in a POST, or in a job a domain event triggers"
20)]
21pub trait Loader<A: Area, Args>: Call<ReadContext<A>, Args, Outcome: ReadOnly> {}
22
23impl<A: Area, Args, H: Call<ReadContext<A>, Args, Outcome: ReadOnly>> Loader<A, Args> for H {}
24
25pub(crate) fn endpoint<A: Area, Args, F: Call<RequestContext<A>, Args>>(handler: F) -> Endpoint {
26    writing(erase(handler), handler_name::<F>())
27}
28
29fn writing<A: Area>(run: Run<RequestContext<A>>, name: HandlerName) -> Endpoint {
30    Arc::new(move |db, matched| {
31        let run = Arc::clone(&run);
32        instrumented(name, matched.route, async move {
33            let mut context = RequestContext::<A>::new(db, matched);
34            #[cfg(feature = "idempotency")]
35            if let Some(work) = context.matched.work.clone() {
36                return crate::keyed::answer_once(&run, context, &work).await;
37            }
38            let outcome = run(&mut context).await?;
39            committed(context.db(), &context.matched, outcome).await
40        })
41    })
42}
43
44/// Lets the session write what the handler asked only once its outcome is committed.
45pub(crate) async fn committed(
46    db: &Db,
47    matched: &Matched,
48    mut outcome: Box<dyn AnyOutcome>,
49) -> Result<Reply> {
50    matched.deadline.lift();
51    db.commit_with(&mut *outcome, Vec::new(), matched.reach)
52        .await?;
53    matched.session.committed();
54    outcome.into_reply()
55}
56
57pub(crate) fn loader_endpoint<A: Area, Args, F: Loader<A, Args>>(loader: F) -> Endpoint {
58    let run = erase::<ReadContext<A>, _, _>(loader);
59    let name = handler_name::<F>();
60    Arc::new(move |db, matched| {
61        let run = Arc::clone(&run);
62        instrumented(name, matched.route, async move {
63            let mut context = ReadContext::<A>::new(db, matched);
64            run(&mut context).await?.into_reply()
65        })
66    })
67}
68
69/// History records the route, or else the handler's path, for every commit the handler makes.
70pub(crate) fn instrumented(
71    name: HandlerName,
72    route: Option<&'static str>,
73    work: impl Future<Output = Result<Reply>> + Send + 'static,
74) -> BoxFuture<'static, Result<Reply>> {
75    let action = route.unwrap_or(name.0);
76    let work = sofie_db::action::acting_for(action, work).instrument(handler_span(name));
77    #[cfg(feature = "metrics")]
78    let work = crate::metrics::timed(name, work);
79    Box::pin(work)
80}