Skip to main content
PR #860 removes the moka dependency and makes PortableCache the only in-process cache backend on every target, including wasm32.

Why moka was removed

moka was the single largest contributor to the release binary — 1.8 MiB of .text (15.8%) — almost entirely from per-cache-type monomorphization. Its do_run_pending_tasks alone was emitted 84 times (710 KiB) across the ~15 distinct Cache types the client instantiates; each new typed cache dragged in moka’s full generic machinery (~100 KiB+). PortableCache was already shipping on wasm32 targets and mirrors the full moka Cache API (capacity + TTL/TTI eviction, single-flight get_with/get_with_by_ref). Making it the sole backend required no call-site changes.

Binary size impact

Real release profile (fat LTO, codegen-units=1, panic=abort, strip):
Metricbefore (moka)after (PortableCache)Δ
Stripped size13.35 MiB10.81 MiB−2.55 MiB (−19.1%)
.text11.31 MiB8.89 MiB−2.42 MiB (−21.4%)
LLVM IR lines1,275,789664,220−47.9%
Cargo.lock crates357354−3
The net delta exceeds moka’s own 1.8 MiB line because dropping moka also removes its transitive deps (crossbeam-channel/epoch, quanta, part of uuid) and unlocks further LTO savings. CodSpeed reports no performance change across 172 benchmarks.

PortableCache hardening

Making PortableCache the sole native backend surfaced a few behavioural gaps that were addressed in this PR:
  • Monotonic TTL/TTI — expiry now uses wacore::time::Instant instead of the wall clock, so a system-clock jump can’t expire entries early. This matches moka’s timer semantics and prevents session_recreate_history’s throttle backstop from being bypassed.
  • Eager single-flight init-lock reclamationget_with/get_with_by_ref now drop a key’s init lock once no other caller holds it, instead of waiting for run_pending_tasks. Fixes unbounded init_locks growth in high-cardinality caches (session locks, chat lanes, message-id dedup) that never call run_pending_tasks.
  • Reliable async clear() — new PortableCache::clear() awaits the write lock. cleanup_connection_state and TypedCache::clear now use it instead of the best-effort sync invalidate_all(), which could skip the clear under contention and leave a stale ChatLane after reconnect.
  • snapshot_entries() — new async method that awaits the read lock for a reliable snapshot; used by SenderKeyDeviceCache::invalidate_entries_for_device where a missed entry would silently drop an SKDM fanout.

Breaking changes

moka-cache feature removed

The moka-cache Cargo feature no longer exists. Remove it from your Cargo.toml:
# Before
whatsapp-rust = { version = "0.6", default-features = false, features = [
    "sqlite-storage", "tokio-transport", "tokio-runtime",
    "ureq-client", "tokio-native", "signal",
    "moka-cache",   # ← remove this line
] }

# After
whatsapp-rust = { version = "0.6", default-features = false, features = [
    "sqlite-storage", "tokio-transport", "tokio-runtime",
    "ureq-client", "tokio-native", "signal",
] }

TypedCache::from_moka renamed to from_local

If you construct a TypedCache directly in your own code, update the constructor name:
// Before
let cache = TypedCache::from_moka(my_cache);

// After
let cache = TypedCache::from_local(my_cache);

portable_cache module is now always public

whatsapp_rust::portable_cache is no longer cfg-gated. Any conditional compilation on #[cfg(any(not(feature = "moka-cache"), target_arch = "wasm32"))] around imports of that module should be removed.

Trade-offs

PortableCache differs from moka in two ways relevant to very high-throughput deployments:
  • Eviction policy: FIFO instead of TinyLFU (lower hit rate under heavily skewed access patterns)
  • Concurrency: one RwLock per cache vs moka’s sharded, lock-free reads (more contention under heavy concurrent access)
For typical bot/single-account workloads this is unlikely to matter; the integration benchmarks (CodSpeed) confirmed no performance change across all 172 benchmarks.