Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ tracing-pii = ["wacore/tracing-pii", "wacore-binary/tracing-pii"]
danger-skip-tls-verify = ["whatsapp-rust-tokio-transport?/danger-skip-tls-verify"]
danger-skip-cert-chain-verify = ["wacore/danger-skip-cert-chain-verify"]
default = [
Comment thread
jlucaso1 marked this conversation as resolved.
"moka-cache",
"simd",
"sqlite-storage",
"tokio-transport",
Expand All @@ -123,7 +122,6 @@ default = [
"tokio-native",
"signal",
]
moka-cache = ["dep:moka"]
simd = ["wacore/simd"]
ureq-client = ["dep:whatsapp-rust-ureq-http-client"]
tokio-transport = ["dep:whatsapp-rust-tokio-transport"]
Expand Down Expand Up @@ -171,13 +169,6 @@ whatsapp-rust-ureq-http-client = { path = "./http_clients/ureq-client", version
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
getrandom = { workspace = true, features = ["wasm_js"] }

# moka is thread-based (crossbeam/uuid) and does not build on wasm32, so it is
# scoped to non-wasm targets. The `moka-cache` feature can still be enabled on
# wasm32 (it stays in `default`); `dep:moka` is simply inert there and the cache
# falls back to PortableCache via the target gate in src/cache.rs.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
moka = { version = "0.12.12", features = ["future"], optional = true }

[dev-dependencies]
aes = { workspace = true }
cbc = { version = "0.2", features = ["alloc", "block-padding"] }
Expand Down
20 changes: 5 additions & 15 deletions src/cache.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
//! Unified cache type backed by moka or [`PortableCache`](crate::portable_cache::PortableCache).
//! The client's in-process cache type.
//!
//! The selection below is target-gated, not just feature-gated, because moka is
//! thread-based (crossbeam/uuid) and can't build on wasm32: a defaults-based wasm32
//! build must fall back to PortableCache instead of failing deep inside moka's deps.
//! Backed by [`PortableCache`](crate::portable_cache::PortableCache): a
//! runtime-agnostic cache (capacity + TTL/TTI eviction, single-flight
//! `get_with`) that builds on every target, including wasm32.

#[cfg(all(feature = "moka-cache", not(target_arch = "wasm32")))]
mod inner {
pub type Cache<K, V> = moka::future::Cache<K, V>;
}

#[cfg(any(not(feature = "moka-cache"), target_arch = "wasm32"))]
mod inner {
pub type Cache<K, V> = crate::portable_cache::PortableCache<K, V>;
}

pub use inner::Cache;
pub use crate::portable_cache::PortableCache as Cache;
Comment thread
jlucaso1 marked this conversation as resolved.
Comment thread
jlucaso1 marked this conversation as resolved.
Comment thread
jlucaso1 marked this conversation as resolved.
Comment thread
jlucaso1 marked this conversation as resolved.
12 changes: 6 additions & 6 deletions src/cache_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub use wacore::store::cache::CacheStore;

/// Configuration for a single cache instance.
///
/// Controls the expiry timeout and maximum capacity of a moka cache.
/// Controls the expiry timeout and maximum capacity of an in-process cache.
/// The `timeout` field is used as either TTL (`build_with_ttl`) or TTI
/// (`build_with_tti`) depending on which builder method is called.
/// Set `timeout` to `None` to disable time-based expiry (entries stay until
Expand Down Expand Up @@ -56,7 +56,7 @@ impl CacheEntryConfig {
{
match store {
Some(s) => TypedCache::from_store(s, namespace, self.timeout),
None => TypedCache::from_moka(self.build_with_ttl()),
None => TypedCache::from_local(self.build_with_ttl()),
}
}

Expand All @@ -77,7 +77,7 @@ impl CacheEntryConfig {
/// Per-cache custom store overrides.
///
/// Each field is an optional [`CacheStore`] for that specific cache. When
/// `None`, the default in-process moka cache is used.
/// `None`, the default in-process cache is used.
///
/// # Example — group and device registry on Redis
///
Expand Down Expand Up @@ -166,7 +166,7 @@ pub struct CacheConfig {
/// LID-to-phone cache. WAWebLidPnCache uses plain Maps with no expiry
/// and no size cap; evicting a still-valid mapping silently downgrades
/// Signal addresses to `@c.us`. Default: no timeout, capacity u64::MAX
/// (effectively unbounded — moka doesn't expose an `unbounded()` builder).
/// (effectively unbounded — the cache has no dedicated `unbounded()` builder).
pub lid_pn_cache: CacheEntryConfig,
/// Optional L1 in-memory cache for sent messages (retry support).
/// Default: capacity 0 (disabled — DB-only, matching WA Web).
Expand Down Expand Up @@ -248,8 +248,8 @@ pub struct CacheConfig {
/// Per-cache custom store overrides.
///
/// For each field set to `Some(store)`, the corresponding cache uses that
/// backend instead of the default in-process moka cache. Fields left as
/// `None` keep the default moka behaviour.
/// backend instead of the default in-process cache. Fields left as
/// `None` keep the default in-process behaviour.
///
/// Coordination caches (`session_locks`, `chat_lanes`), the signal write-behind
/// cache, and `pdo_pending_requests` always stay in-process — they hold live Rust
Expand Down
50 changes: 24 additions & 26 deletions src/cache_store.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Typed cache wrapper that dispatches to either moka (in-process) or a custom
//! [`CacheStore`] backend (e.g., Redis).
//! Typed cache wrapper that dispatches to either the in-process
//! [`Cache`](crate::cache::Cache) or a custom [`CacheStore`] backend (e.g., Redis).
//!
//! [`TypedCache`] presents the same interface regardless of the backing store.
//! Keys are serialised via [`Display`]; values are serialised with `serde_json`
//! only on the custom-store path — the moka path has zero extra overhead.
//! only on the custom-store path — the in-process path has zero extra overhead.

use std::borrow::Borrow;
use std::fmt::Display;
Expand All @@ -19,7 +19,7 @@ pub use wacore::store::cache::CacheStore;
// ── Internal storage variant ──────────────────────────────────────────────────

enum Inner<K, V> {
Moka(Cache<K, V>),
Local(Cache<K, V>),
Custom {
store: Arc<dyn CacheStore>,
namespace: &'static str,
Expand All @@ -30,13 +30,11 @@ enum Inner<K, V> {

// ── TypedCache ─────────────────────────────────────────────────────────────────

/// A cache over `K → V` backed by either moka or any [`CacheStore`].
/// A cache over `K → V` backed by either the in-process cache or any [`CacheStore`].
///
/// The moka path has **zero extra overhead** — values are stored in-process
/// without any serialisation. The custom-store path serialises values with
/// `serde_json` and keys via [`Display`].
///
/// Methods mirror moka's API so call sites need no changes.
/// The in-process path has **zero extra overhead** — values are stored in
/// memory without any serialisation. The custom-store path serialises values
/// with `serde_json` and keys via [`Display`].
pub struct TypedCache<K, V> {
inner: Inner<K, V>,
}
Expand All @@ -46,10 +44,10 @@ where
K: std::hash::Hash + Eq + Clone + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
{
/// Wrap an existing cache (zero overhead vs. using the cache directly).
pub fn from_moka(cache: Cache<K, V>) -> Self {
/// Wrap an in-process [`Cache`] (zero overhead vs. using the cache directly).
pub fn from_local(cache: Cache<K, V>) -> Self {
Self {
inner: Inner::Moka(cache),
inner: Inner::Local(cache),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -91,7 +89,7 @@ where
Q: std::hash::Hash + Eq + Display + ?Sized,
{
match &self.inner {
Inner::Moka(cache) => cache.get(key).await,
Inner::Local(cache) => cache.get(key).await,
Inner::Custom {
store, namespace, ..
} => {
Expand All @@ -117,7 +115,7 @@ where
/// Insert or update a value (takes ownership of key and value).
pub async fn insert(&self, key: K, value: V) {
match &self.inner {
Inner::Moka(cache) => cache.insert(key, value).await,
Inner::Local(cache) => cache.insert(key, value).await,
Inner::Custom {
store,
namespace,
Expand Down Expand Up @@ -148,7 +146,7 @@ where
Q: std::hash::Hash + Eq + Display + ?Sized,
{
match &self.inner {
Inner::Moka(cache) => cache.invalidate(key).await,
Inner::Local(cache) => cache.invalidate(key).await,
Inner::Custom {
store, namespace, ..
} => {
Expand All @@ -162,14 +160,14 @@ where

/// Remove all entries.
///
/// For the moka backend this is synchronous (matching moka's API).
/// For the in-process backend this is synchronous.
/// For the custom backend this spawns a fire-and-forget task via
/// [`tokio::runtime::Handle::try_current`] (requires `tokio-runtime`
/// feature) to avoid panicking if called outside a Tokio runtime.
/// Without `tokio-runtime`, the clear is skipped with a warning.
pub fn invalidate_all(&self) {
match &self.inner {
Inner::Moka(cache) => cache.invalidate_all(),
Inner::Local(cache) => cache.invalidate_all(),
Inner::Custom {
store, namespace, ..
} => {
Expand Down Expand Up @@ -197,7 +195,7 @@ where
/// Remove all entries, awaiting completion for custom backends.
pub async fn clear(&self) {
match &self.inner {
Inner::Moka(cache) => cache.invalidate_all(),
Inner::Local(cache) => cache.invalidate_all(),
Inner::Custom {
store, namespace, ..
} => {
Expand All @@ -208,13 +206,13 @@ where
}
}

/// Run any pending internal housekeeping tasks (moka only).
/// Run any pending internal housekeeping tasks (in-process backend only).
///
/// For the moka backend this ensures all writes have been applied before
/// calling [`entry_count`](Self::entry_count), which can otherwise lag.
/// For custom backends this is a no-op.
/// For the in-process backend this evicts expired entries so a subsequent
/// [`entry_count`](Self::entry_count) reflects them. For custom backends
/// this is a no-op.
pub async fn run_pending_tasks(&self) {
if let Inner::Moka(cache) = &self.inner {
if let Inner::Local(cache) = &self.inner {
cache.run_pending_tasks().await;
}
}
Expand All @@ -225,15 +223,15 @@ where
/// [`entry_count_async`](Self::entry_count_async) instead.
pub fn entry_count(&self) -> u64 {
match &self.inner {
Inner::Moka(cache) => cache.entry_count(),
Inner::Local(cache) => cache.entry_count(),
Inner::Custom { .. } => 0,
}
}

/// Approximate entry count, delegating to the custom backend if available.
pub async fn entry_count_async(&self) -> u64 {
match &self.inner {
Inner::Moka(cache) => cache.entry_count(),
Inner::Local(cache) => cache.entry_count(),
Inner::Custom {
store, namespace, ..
} => store.entry_count(namespace).await.unwrap_or(0),
Expand Down
6 changes: 3 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ const TRANSPORT_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);

/// Snapshot of internal collection sizes for memory leak detection.
///
/// All counts are approximate (moka caches may have pending evictions).
/// All counts are approximate (caches may have pending evictions).
/// Call [`Client::memory_diagnostics`] to obtain a snapshot.
///
/// Requires the `debug-diagnostics` feature.
Expand Down Expand Up @@ -426,7 +426,7 @@ pub struct Client {
pub(crate) connection_generation: Arc<AtomicU64>,

/// Cache for recent messages (serialized bytes) for retry functionality.
/// Uses moka cache with TTL and max capacity for automatic eviction.
/// Uses an in-process cache with TTL and max capacity for automatic eviction.
pub(crate) recent_messages: Cache<ChatMessageId, Arc<Vec<u8>>>,

pub(crate) sender_key_device_cache: crate::sender_key_device_cache::SenderKeyDeviceCache,
Expand All @@ -438,7 +438,7 @@ pub struct Client {
/// Track retry attempts per message to prevent infinite retry loops.
/// Key: "{chat}:{msg_id}:{sender}", Value: retry count plus the most
/// recent `RetryReason` we attached, fused so the decrypt-failure path
/// does one cache write and the binary carries one moka instantiation
/// does one cache write and the binary carries one cache instantiation
/// instead of two. The reason is `None` when the count was learned from
/// the sender's echoed stanza `count` attribute rather than a local
/// decrypt failure; diagnostics and regression tests read it to tell
Expand Down
8 changes: 4 additions & 4 deletions src/client/device_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ impl Client {
}

/// Batched variant of [`update_device_list`]. Cache is populated
/// synchronously per record (cheap moka inserts); the backend write
/// synchronously per record (cheap in-process inserts); the backend write
/// collapses into a single transaction. Used by usync after fetching
/// device lists for many users at once, where the per-row commit
/// dominated wall-clock time on large groups.
Expand Down Expand Up @@ -879,13 +879,13 @@ impl Client {
/// then the backend database.
#[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.get_devices_from_registry", level = "trace", skip_all, fields(peer = %jid.observe())))]
pub(crate) async fn get_devices_from_registry(&self, jid: &Jid) -> Option<Vec<Jid>> {
// Use the borrowed `&str` keys directly: both the moka cache and the
// Use the borrowed `&str` keys directly: both the in-process cache and the
// backend take `&str`, so going through `get_lookup_keys` (which re-owns
// the already-cloned keys into a `Vec<String>`) just churns per member on
// every group send. `lookup` owns the key Strings for the duration here.
let lookup = self.resolve_lookup_keys_for_jid(jid).await;

// L1: device_registry_cache (moka, fast)
// L1: device_registry_cache (in-process, fast)
for key in lookup.all_keys() {
if let Some(record) = self.device_registry_cache.get(key).await {
let devices = Self::reconstruct_device_jids(jid, &record);
Expand Down Expand Up @@ -2103,7 +2103,7 @@ mod tests {

let client = create_test_client().await;

// Seed backend DB directly (bypassing moka cache)
// Seed backend DB directly (bypassing the in-process cache)
let record = DeviceListRecord {
user: "15551234567".into(),
devices: vec![DeviceInfo {
Expand Down
2 changes: 1 addition & 1 deletion src/client/device_topology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ impl DeviceRegistryCache {
self.cache.entry_count()
}

/// Test-only passthrough for moka maintenance flushes.
/// Test-only passthrough for cache maintenance flushes.
#[cfg(test)]
pub(crate) async fn run_pending_tasks(&self) {
self.cache.run_pending_tasks().await;
Expand Down
3 changes: 0 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ pub use wacore_binary;
pub use waproto;

pub mod cache;
// Available whenever the cache falls back to PortableCache: moka off, or wasm32
// (where moka can't build) even if `moka-cache` is enabled. Mirrors src/cache.rs.
#[cfg(any(not(feature = "moka-cache"), target_arch = "wasm32"))]
pub mod portable_cache;

pub mod cache_config;
Expand Down
Loading
Loading