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
11pub(crate) type Endpoint =
13 Arc<dyn Fn(Db, Matched) -> BoxFuture<'static, Result<Reply>> + Send + Sync>;
14
15#[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
44pub(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
69pub(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}