Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 0 additions & 12 deletions .github/workflows/wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,3 @@ jobs:
cargo build -p whatsapp-rust --lib --release
--target wasm32-unknown-unknown
--no-default-features --features debug-diagnostics
# moka-cache is in the default feature set and is thread-based (can't build
# on wasm32), so a defaults-based consumer that forgets --no-default-features
# would hit it. The target gate in src/cache.rs must fall back to
# PortableCache instead of pulling moka; this step exercises that path so it
# can't silently regress.
- name: Build whatsapp-rust lib for wasm32 with moka-cache (release)
env:
RUSTFLAGS: '--cfg getrandom_backend="wasm_js"'
run: >
cargo build -p whatsapp-rust --lib --release
--target wasm32-unknown-unknown
--no-default-features --features debug-diagnostics,moka-cache
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.clear().await,
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
12 changes: 6 additions & 6 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 All @@ -191,7 +191,7 @@ pub struct MemoryDiagnostics {
pub undecryptable_dispatched: u64,
pub pdo_pending_requests: u64,
pub pdo_requested: u64,
// -- Moka caches (capacity-only, no TTL) --
// -- Capacity-only caches (no TTL) --
pub session_locks: u64,
pub chat_lanes: u64,
// -- Unbounded collections --
Expand All @@ -213,7 +213,7 @@ pub struct MemoryDiagnostics {
impl std::fmt::Display for MemoryDiagnostics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Memory Diagnostics ===")?;
writeln!(f, "--- Moka caches (TTL-bounded) ---")?;
writeln!(f, "--- TTL-bounded caches ---")?;
writeln!(f, " group_cache: {}", self.group_cache)?;
writeln!(
f,
Expand All @@ -236,7 +236,7 @@ impl std::fmt::Display for MemoryDiagnostics {
)?;
writeln!(f, " pdo_pending_requests: {}", self.pdo_pending_requests)?;
writeln!(f, " pdo_requested: {}", self.pdo_requested)?;
writeln!(f, "--- Moka caches (capacity-only) ---")?;
writeln!(f, "--- Capacity-only caches ---")?;
writeln!(f, " session_locks: {}", self.session_locks)?;
writeln!(f, " chat_lanes: {}", self.chat_lanes)?;
writeln!(f, "--- Unbounded collections ---")?;
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
Loading
Loading