use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transport {
Web,
Mcp,
Api,
Cli,
System,
}
impl Transport {
pub fn as_str(&self) -> &'static str {
match self {
Transport::Web => "web",
Transport::Mcp => "mcp",
Transport::Api => "api",
Transport::Cli => "cli",
Transport::System => "system",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ActorCtx {
pub user_id: Option<i64>,
pub transport: Transport,
}
tokio::task_local! {
static ACTOR: ActorCtx;
}
static DEFAULT_TRANSPORT: OnceLock<Transport> = OnceLock::new();
pub fn set_default_transport(t: Transport) {
let _ = DEFAULT_TRANSPORT.set(t);
}
pub async fn scope<F: std::future::Future>(ctx: ActorCtx, fut: F) -> F::Output {
ACTOR.scope(ctx, fut).await
}
pub fn current() -> ActorCtx {
ACTOR.try_with(|a| *a).unwrap_or(ActorCtx {
user_id: None,
transport: *DEFAULT_TRANSPORT.get().unwrap_or(&Transport::System),
})
}
pub fn stamp(conn: &rusqlite::Connection, ctx: &ActorCtx) {
let _ = conn.execute(
"UPDATE _actor_state SET user_id = ?1, transport = ?2 WHERE id = 1",
rusqlite::params![ctx.user_id, ctx.transport.as_str()],
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_without_scope_is_system() {
let actor = current();
assert!(actor.user_id.is_none());
assert_eq!(actor.transport, Transport::System);
}
#[tokio::test]
async fn scoped_actor_is_visible_inside_scope() {
let ctx = ActorCtx {
user_id: Some(7),
transport: Transport::Mcp,
};
let seen = scope(ctx, async { current() }).await;
assert_eq!(seen.user_id, Some(7));
assert_eq!(seen.transport, Transport::Mcp);
}
#[tokio::test]
async fn nested_scopes_inner_wins() {
let outer = ActorCtx {
user_id: Some(1),
transport: Transport::Api,
};
let inner = ActorCtx {
user_id: Some(2),
transport: Transport::Mcp,
};
let seen = scope(outer, async move { scope(inner, async { current() }).await }).await;
assert_eq!(seen.user_id, Some(2));
assert_eq!(seen.transport, Transport::Mcp);
}
}