use dataflow_rs::datalogic_rs;
use std::sync::Arc;
use serde_json::Value;
use super::ChannelRuntimeConfig;
use crate::connector::cache_backend::CacheBackend;
use crate::metrics;
use sha2::{Digest, Sha256};
pub(super) fn resolve_key_field<'a>(data: &'a Value, field: &str) -> Option<&'a Value> {
fn walk<'a>(mut cur: &'a Value, path: &str) -> Option<&'a Value> {
for segment in path.split('.') {
if segment.is_empty() {
return None;
}
cur = cur.get(segment)?;
}
Some(cur)
}
if let Some(v) = data.get(field) {
return Some(v);
}
if !field.contains('.') {
return None;
}
walk(data, field).or_else(|| field.strip_prefix("data.").and_then(|p| walk(data, p)))
}
pub(super) fn compute_cache_key(
channel: &str,
data: &Value,
metadata: &Value,
cache_cfg: &crate::channel::ChannelCacheConfig,
key_logic: Option<&datalogic_rs::Logic>,
datalogic: &datalogic_rs::Engine,
) -> Option<String> {
let mut h = Sha256::new();
fn feed(h: &mut Sha256, bytes: &[u8]) {
h.update((bytes.len() as u64).to_be_bytes());
h.update(bytes);
}
feed(
&mut h,
metadata
.get("http_method")
.and_then(Value::as_str)
.unwrap_or("")
.as_bytes(),
);
feed_object_sorted(&mut h, metadata.get("params"));
feed_object_sorted(&mut h, metadata.get("query"));
if let Some(ref fields) = cache_cfg.cache_key_fields {
let mut resolved = 0usize;
for f in fields {
feed(&mut h, f.as_bytes());
match resolve_key_field(data, f) {
Some(v) => {
resolved += 1;
h.update([1u8]);
feed(&mut h, &serde_json::to_vec(v).unwrap_or_default());
}
None => h.update([0u8]),
}
}
if resolved == 0 {
return None;
}
} else if let Some(compiled) = key_logic {
let context = serde_json::json!({ "data": data, "metadata": metadata });
let key = datalogic
.session()
.eval_into::<Value, _>(compiled, &context)
.ok()?;
if key.is_null() {
return None;
}
feed(&mut h, &serde_json::to_vec(&key).unwrap_or_default());
} else {
feed(&mut h, &serde_json::to_vec(data).unwrap_or_default());
};
let digest = h.finalize();
Some(format!("cache:{channel}:{}", hex::encode(&digest[..16])))
}
pub(super) fn feed_object_sorted(h: &mut Sha256, v: Option<&Value>) {
let Some(Value::Object(map)) = v else {
h.update([0u8]);
return;
};
h.update([1u8]);
h.update((map.len() as u64).to_be_bytes());
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable();
for k in keys {
h.update((k.len() as u64).to_be_bytes());
h.update(k.as_bytes());
let bytes = serde_json::to_vec(&map[k.as_str()]).unwrap_or_default();
h.update((bytes.len() as u64).to_be_bytes());
h.update(&bytes);
}
}
pub type CacheStoreCtx = (String, Arc<dyn CacheBackend>, u64);
pub(super) enum CacheLookup {
Hit(String),
Miss(Option<CacheStoreCtx>),
}
pub(super) async fn check_response_cache(
channel: &str,
data: &Value,
metadata: &Value,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
datalogic: &datalogic_rs::Engine,
) -> CacheLookup {
let Some(cfg) = channel_config else {
return CacheLookup::Miss(None);
};
let Some(ref cache_cfg) = cfg.parsed_config.cache else {
return CacheLookup::Miss(None);
};
if !cache_cfg.enabled {
return CacheLookup::Miss(None);
}
let Some(ref cache) = cfg.response_cache else {
return CacheLookup::Miss(None);
};
let Some(key) = compute_cache_key(
channel,
data,
metadata,
cache_cfg,
cfg.cache_key_logic.as_ref(),
datalogic,
) else {
tracing::warn!(
channel = %channel,
fields = ?cache_cfg.cache_key_fields,
has_key_logic = cfg.cache_key_logic.is_some(),
"No cache key resolved against the request; bypassing the response cache. \
Field names are literal payload keys or dotted paths (`user.id`, or \
`data.user_id` for a top-level `user_id`)."
);
return CacheLookup::Miss(None);
};
match cache.get(&key).await {
Ok(Some(cached)) => {
metrics::record_cache_hit(channel);
CacheLookup::Hit(cached)
}
_ => {
metrics::record_cache_miss(channel);
CacheLookup::Miss(Some((
key,
cache.clone(),
cache_cfg.ttl_secs.unwrap_or(300),
)))
}
}
}