use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use async_lock::RwLock;
use wacore_binary::CompactString;
use crate::iq::abprops::{AbDefault, AbProp};
use crate::iq::props::WATCHED;
pub struct AbPropsCache {
props: RwLock<HashMap<u32, CompactString>>,
interest: RwLock<HashSet<u32>>,
seeded: AtomicBool,
}
impl AbPropsCache {
pub fn new() -> Self {
Self {
props: RwLock::new(HashMap::new()),
interest: RwLock::new(WATCHED.iter().map(|p| p.code).collect()),
seeded: AtomicBool::new(false),
}
}
pub async fn watch(&self, prop: AbProp) {
self.interest.write().await.insert(prop.code);
}
pub async fn watch_many(&self, props: &[AbProp]) {
self.interest
.write()
.await
.extend(props.iter().map(|p| p.code));
}
pub fn is_seeded(&self) -> bool {
self.seeded.load(Ordering::Acquire)
}
pub async fn apply_props(
&self,
delta_update: bool,
props: impl Iterator<Item = (u32, CompactString)>,
) {
let interest = self.interest.read().await;
let mut map = self.props.write().await;
if !delta_update {
map.clear();
}
for (code, value) in props {
if interest.contains(&code) {
map.insert(code, value);
}
}
if !delta_update {
self.seeded.store(true, Ordering::Release);
}
}
pub async fn get(&self, prop: AbProp) -> Option<CompactString> {
self.props.read().await.get(&prop.code).cloned()
}
pub async fn is_enabled(&self, prop: AbProp) -> bool {
match self.props.read().await.get(&prop.code) {
Some(value) => {
value == "1"
|| value.eq_ignore_ascii_case("true")
|| value.eq_ignore_ascii_case("enabled")
}
None => matches!(prop.default, AbDefault::Bool(true)),
}
}
pub async fn get_int(&self, prop: AbProp) -> i64 {
let fallback = match prop.default {
AbDefault::Int(n) => n,
_ => 0,
};
match self.props.read().await.get(&prop.code) {
Some(value) => value.parse().unwrap_or(fallback),
None => fallback,
}
}
}
impl Default for AbPropsCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iq::abprops::{AbDefault, AbPropType, web};
fn flag(code: u32) -> AbProp {
AbProp {
name: "test",
code,
value_type: AbPropType::Bool,
default: AbDefault::Bool(false),
}
}
#[tokio::test]
async fn watched_props_are_retained() {
let cache = AbPropsCache::new();
cache.watch(flag(100)).await;
cache.watch(flag(200)).await;
let props = vec![
(100u32, CompactString::from("1")),
(200, CompactString::from("0")),
(300, CompactString::from("ignored")),
];
cache.apply_props(false, props.into_iter()).await;
assert!(cache.is_seeded());
assert_eq!(cache.get(flag(100)).await, Some(CompactString::from("1")));
assert_eq!(cache.get(flag(200)).await, Some(CompactString::from("0")));
assert_eq!(cache.get(flag(300)).await, None); }
#[tokio::test]
async fn is_enabled_checks_truthy_values() {
let cache = AbPropsCache::new();
cache
.watch_many(&[flag(1), flag(2), flag(3), flag(4), flag(5)])
.await;
let props = vec![
(1u32, CompactString::from("1")),
(2, CompactString::from("true")),
(3, CompactString::from("enabled")),
(4, CompactString::from("0")),
(5, CompactString::from("false")),
];
cache.apply_props(false, props.into_iter()).await;
assert!(cache.is_enabled(flag(1)).await);
assert!(cache.is_enabled(flag(2)).await);
assert!(cache.is_enabled(flag(3)).await);
assert!(!cache.is_enabled(flag(4)).await);
assert!(!cache.is_enabled(flag(5)).await);
assert!(!cache.is_enabled(flag(999)).await); }
#[tokio::test]
async fn delta_merges_without_clearing() {
let cache = AbPropsCache::new();
cache.watch_many(&[flag(100), flag(200), flag(300)]).await;
cache
.apply_props(
false,
vec![
(100u32, CompactString::from("old")),
(200, CompactString::from("keep")),
]
.into_iter(),
)
.await;
cache
.apply_props(
true,
vec![
(100u32, CompactString::from("new")),
(300, CompactString::from("added")),
]
.into_iter(),
)
.await;
assert_eq!(cache.get(flag(100)).await.as_deref(), Some("new"));
assert_eq!(cache.get(flag(200)).await.as_deref(), Some("keep"));
assert_eq!(cache.get(flag(300)).await.as_deref(), Some("added"));
}
#[tokio::test]
async fn default_interest_retains_production_flags() {
let cache = AbPropsCache::new();
let props = vec![
(
web::PRIVACY_TOKEN_SENDING_ON_ALL_1_ON_1_MESSAGES.code,
CompactString::from("1"),
),
(
web::WA_NCT_TOKEN_SEND_ENABLED.code,
CompactString::from("true"),
),
(web::TCTOKEN_DURATION.code, CompactString::from("604800")),
(web::TCTOKEN_NUM_BUCKETS.code, CompactString::from("4")),
(99999u32, CompactString::from("unwatched")),
];
cache.apply_props(false, props.into_iter()).await;
assert!(cache.is_seeded());
assert!(
cache
.is_enabled(web::PRIVACY_TOKEN_SENDING_ON_ALL_1_ON_1_MESSAGES)
.await
);
assert!(cache.is_enabled(web::WA_NCT_TOKEN_SEND_ENABLED).await);
assert_eq!(cache.get_int(web::TCTOKEN_DURATION).await, 604800);
assert_eq!(cache.get_int(web::TCTOKEN_NUM_BUCKETS).await, 4);
assert_eq!(cache.get(flag(99999)).await, None);
}
#[tokio::test]
async fn seeded_set_after_inserts() {
let cache = AbPropsCache::new();
assert!(!cache.is_seeded());
cache
.apply_props(
false,
vec![(web::TCTOKEN_DURATION.code, CompactString::from("100"))].into_iter(),
)
.await;
assert!(cache.is_seeded());
assert_eq!(cache.get_int(web::TCTOKEN_DURATION).await, 100);
}
}