Skip to main content

valence_core/deletion/
dispatch.rs

1//! Optional dispatcher so the host can wire background workers without a core → job-runner dependency.
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::OnceLock;
6
7use crate::error::{Error, Result};
8
9/// Payload passed to the registered deletion dispatcher (typically starts a host job orchestrator).
10#[derive(Debug, Clone)]
11pub struct DeletionRequest {
12    pub run_id: String,
13    pub root_table: String,
14    pub root_record_id: String,
15    pub actor_json: serde_json::Value,
16}
17
18type DispatchFn =
19    dyn Fn(DeletionRequest) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync;
20
21static DISPATCHER: OnceLock<Box<DispatchFn>> = OnceLock::new();
22
23/// True if [`register_deletion_dispatcher`] (or a test dispatcher) was installed in this process.
24pub fn is_deletion_dispatcher_registered() -> bool {
25    DISPATCHER.get().is_some()
26}
27
28/// Register the process-wide deletion dispatcher (call once from server bootstrap).
29pub fn register_deletion_dispatcher(f: Box<DispatchFn>) {
30    let _ = DISPATCHER.set(f);
31}
32
33/// Install a no-op dispatcher when the slot is still empty (integration tests, harness crates).
34///
35/// `Model::delete` on app tables calls [`dispatch`]; without a host dispatcher this satisfies the
36/// hook so deletes complete. If a real dispatcher was already registered, this is a no-op.
37pub fn register_noop_deletion_dispatcher_for_tests() {
38    let noop: Box<DispatchFn> = Box::new(|_| Box::pin(async move { Ok(()) }));
39    let _ = DISPATCHER.set(noop);
40}
41
42/// Invoke the dispatcher if configured.
43pub async fn dispatch(req: DeletionRequest) -> Result<()> {
44    match DISPATCHER.get() {
45        Some(f) => (f)(req).await,
46        None => Err(Error::Internal(
47            "Deletion dispatcher not registered; register a host dispatcher from server bootstrap"
48                .into(),
49        )),
50    }
51}