use std::collections::HashMap;
use std::sync::LazyLock;
use tokio::sync::RwLock;
use crate::core::models::{Task, TaskStatus};
tokio::task_local! {
static CURRENT_TASK: Task;
}
pub async fn with_task<F, T>(task: Task, f: F) -> T
where
F: std::future::Future<Output = T>,
{
CURRENT_TASK.scope(task, f).await
}
pub fn get_current_task() -> Option<Task> {
CURRENT_TASK.try_with(|t| t.clone()).ok()
}
tokio::task_local! {
static SUPPRESS_NETWORK_EVENT: bool;
}
pub fn is_network_event_suppressed() -> bool {
SUPPRESS_NETWORK_EVENT.try_with(|v| *v).unwrap_or(false)
}
pub async fn suppress_network_event<F, T>(fut: F) -> T
where
F: std::future::Future<Output = T>,
{
SUPPRESS_NETWORK_EVENT.scope(true, fut).await
}
#[derive(Clone, Debug, Default)]
pub struct DexcostContext {
pub customer_id: Option<String>,
pub project_id: Option<String>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub agent: Option<String>,
}
static CURRENT_CONTEXT: LazyLock<RwLock<Option<DexcostContext>>> =
LazyLock::new(|| RwLock::new(None));
pub async fn set_context(ctx: DexcostContext) {
let mut guard = CURRENT_CONTEXT.write().await;
*guard = Some(ctx);
}
pub async fn get_dexcost_context() -> Option<DexcostContext> {
CURRENT_CONTEXT.read().await.clone()
}
pub fn get_dexcost_context_sync() -> Option<DexcostContext> {
match CURRENT_CONTEXT.try_read() {
Ok(guard) => guard.clone(),
Err(_) => {
eprintln!("[dexcost] could not read ambient context (lock contention), skipping");
None
}
}
}
pub async fn clear_dexcost_context() {
let mut guard = CURRENT_CONTEXT.write().await;
*guard = None;
}
pub async fn create_auto_task(task_type: &str) -> Task {
let ctx = get_dexcost_context().await;
let effective_type = ctx
.as_ref()
.and_then(|c| c.agent.clone())
.unwrap_or_else(|| task_type.to_string());
let mut task = Task::new(&effective_type);
task.status = TaskStatus::Pending;
if let Some(c) = ctx {
task.customer_id = c.customer_id;
task.project_id = c.project_id;
if let Some(meta) = c.metadata {
task.metadata = meta;
}
}
task
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::LazyLock;
use tokio::sync::Mutex;
static CTX_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
#[tokio::test]
async fn test_set_and_get_context() {
let _g = CTX_LOCK.lock().await;
set_context(DexcostContext {
customer_id: Some("acme".into()),
project_id: Some("chatbot".into()),
metadata: None,
agent: None,
})
.await;
let ctx = get_dexcost_context().await;
assert!(ctx.is_some());
let ctx = ctx.unwrap();
assert_eq!(ctx.customer_id.as_deref(), Some("acme"));
assert_eq!(ctx.project_id.as_deref(), Some("chatbot"));
clear_dexcost_context().await;
}
#[tokio::test]
async fn test_get_context_returns_none_when_not_set() {
let _g = CTX_LOCK.lock().await;
clear_dexcost_context().await;
let ctx = get_dexcost_context().await;
assert!(ctx.is_none());
}
#[tokio::test]
async fn test_clear_context() {
let _g = CTX_LOCK.lock().await;
set_context(DexcostContext {
customer_id: Some("test".into()),
project_id: None,
metadata: None,
agent: None,
})
.await;
clear_dexcost_context().await;
let ctx = get_dexcost_context().await;
assert!(ctx.is_none());
}
#[tokio::test]
async fn test_create_auto_task_with_context() {
let _g = CTX_LOCK.lock().await;
set_context(DexcostContext {
customer_id: Some("auto-customer".into()),
project_id: Some("auto-project".into()),
metadata: None,
agent: None,
})
.await;
let task = create_auto_task("openai.chat").await;
assert_eq!(task.customer_id.as_deref(), Some("auto-customer"));
assert_eq!(task.project_id.as_deref(), Some("auto-project"));
assert_eq!(task.task_type, "openai.chat");
assert_eq!(task.status, TaskStatus::Pending);
assert!(!task.task_id.is_empty());
clear_dexcost_context().await;
}
#[tokio::test]
async fn test_create_auto_task_uses_agent_as_task_type() {
let _g = CTX_LOCK.lock().await;
set_context(DexcostContext {
customer_id: Some("acme".into()),
project_id: None,
metadata: None,
agent: Some("support_bot".into()),
})
.await;
let task = create_auto_task("openai.chat").await;
assert_eq!(task.task_type, "support_bot");
assert_eq!(task.customer_id.as_deref(), Some("acme"));
clear_dexcost_context().await;
}
#[tokio::test]
async fn test_create_auto_task_without_context() {
let _g = CTX_LOCK.lock().await;
clear_dexcost_context().await;
let task = create_auto_task("test.call").await;
assert!(task.customer_id.is_none());
assert!(task.project_id.is_none());
assert_eq!(task.task_type, "test.call");
}
#[tokio::test]
async fn suppress_is_false_outside_scope() {
assert!(!is_network_event_suppressed());
}
#[tokio::test]
async fn suppress_is_true_inside_scope() {
suppress_network_event(async {
assert!(is_network_event_suppressed());
})
.await;
}
#[tokio::test]
async fn suppress_resets_after_scope() {
suppress_network_event(async {
assert!(is_network_event_suppressed());
})
.await;
assert!(!is_network_event_suppressed());
}
#[tokio::test]
async fn suppress_propagates_through_nested_awaits() {
suppress_network_event(async {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
assert!(is_network_event_suppressed());
suppress_network_event(async {
assert!(is_network_event_suppressed());
})
.await;
assert!(is_network_event_suppressed());
})
.await;
}
}