diff --git a/advanced/signal-protocol.mdx b/advanced/signal-protocol.mdx index aa7e1cff..98778592 100644 --- a/advanced/signal-protocol.mdx +++ b/advanced/signal-protocol.mdx @@ -641,7 +641,7 @@ pub(crate) struct SenderKeyDeviceCache { - **Time-to-idle eviction:** The cache uses TTI semantics (default: 1 hour, 500 entries), so entries for inactive groups are automatically evicted while frequently-used groups stay cached - **Pre-parsed, pre-indexed maps:** Database rows are parsed into a `SenderKeyDeviceMap` struct that provides O(1) lookups by user and device ID, avoiding per-query string parsing -- **Single-flight initialization:** The `get_or_init` method uses moka's built-in coalescing — if multiple concurrent group sends for the same group trigger a cache miss simultaneously, only one database read executes and all callers share the result +- **Single-flight initialization:** The `get_or_init` method uses `PortableCache`'s single-flight `get_with` — if multiple concurrent group sends for the same group trigger a cache miss simultaneously, only one database read executes and all callers share the result - **Explicit invalidation:** The cache is invalidated when sender key state changes (rotation, error recovery, retry failures) so stale data is never served ```rust @@ -1115,7 +1115,7 @@ When a message from your own primary phone or another linked companion fails to | `NoSession` | `SessionNotFound` (no Signal session yet for the device) | Request a fresh prekey bundle via retry receipt; install the new session before retrying decryption. | | `BadMac` | Ratchet desync (`InvalidMessage` / mac failure) on an existing session | Mark the session for re-creation, throttled per peer via the `session_recreate_history` cache so repeated BadMacs don't loop, and re-send via a peer-addressed `pkmsg` carrying our identity. | -The throttle is a per-peer cooldown (1-hour TTL after the last recreate). In v0.6 the implementation moved from a `Mutex>` to a bounded TTL cache (moka, ~256 entries): the per-peer check-and-stamp is now atomic (serialized by the existing per-peer session lock) and lock-free at the map level, so concurrent retry-receipt spawns from the same peer can't trigger duplicate recreates. The behavior is unchanged — if a peer is already in cooldown, the client skips re-creation and falls back to a normal retry receipt rather than thrashing the session. Under more than ~256 distinct peers retrying within the window, the cache may evict a recent entry, costing at most one extra recreate (bounded and self-healing). Peer-addressed `pkmsg` carries the protocol identity so the receiver can verify ownership before installing the new session, blocking spoofed sibling recoveries. +The throttle is a per-peer cooldown (1-hour TTL after the last recreate). In v0.6 the implementation moved from a `Mutex>` to a bounded TTL cache (`PortableCache`, ~256 entries): the per-peer check-and-stamp is now atomic (serialized by the existing per-peer session lock) and lock-free at the map level, so concurrent retry-receipt spawns from the same peer can't trigger duplicate recreates. The behavior is unchanged — if a peer is already in cooldown, the client skips re-creation and falls back to a normal retry receipt rather than thrashing the session. Under more than ~256 distinct peers retrying within the window, the cache may evict a recent entry, costing at most one extra recreate (bounded and self-healing). Peer-addressed `pkmsg` carries the protocol identity so the receiver can verify ownership before installing the new session, blocking spoofed sibling recoveries. This closed a deadlock where self-DM fan-out to a sibling device produced repeated BadMac decrypt failures: the recipient would request a retry, the sender would re-encrypt against the same broken session, and the cycle would continue until the user manually relogged. With the throttled re-creation plus identity-validated pkmsg, the second receipt installs a fresh session and decryption resumes. diff --git a/advanced/state-management.mdx b/advanced/state-management.mdx index c60a9a12..6b7bd722 100644 --- a/advanced/state-management.mdx +++ b/advanced/state-management.mdx @@ -381,9 +381,9 @@ pub struct Client { } ``` -Both use [moka](https://github.com/moka-rs/moka) `Cache` with capacity-based eviction (configurable via `CacheConfig`), so stale entries are automatically cleaned up. +Both use `PortableCache` with capacity-based eviction (configurable via `CacheConfig`), so stale entries are automatically cleaned up. -On disconnect, `chat_lanes` is explicitly invalidated via `invalidate_all()` to drop per-chat queue senders. This causes worker tasks from the old connection to exit via channel close, preventing them from surviving reconnects with outdated Signal session state that would cause decryption failures. See [disconnect cleanup](/concepts/architecture#disconnect-cleanup) for the full list of resources reset on disconnect. +On disconnect, `chat_lanes` is cleared via the async `clear()` to drop per-chat queue senders. This causes worker tasks from the old connection to exit via channel close, preventing them from surviving reconnects with outdated Signal session state that would cause decryption failures. See [disconnect cleanup](/concepts/architecture#disconnect-cleanup) for the full list of resources reset on disconnect. Location: `src/client.rs` diff --git a/api/bot.mdx b/api/bot.mdx index 657b64eb..5a0f8252 100644 --- a/api/bot.mdx +++ b/api/bot.mdx @@ -956,7 +956,7 @@ let config = CacheConfig { }; ``` -Fields left as `None` keep the default in-process moka behavior. See [Custom backends — cache store](/guides/custom-backends#custom-cache-store) for a full implementation guide. +Fields left as `None` keep the default in-process `PortableCache` behaviour. See [Custom backends — cache store](/guides/custom-backends#custom-cache-store) for a full implementation guide. Coordination caches (`session_locks`, `chat_lanes`), the signal write-behind cache, and `pdo_pending_requests` always stay in-process — they hold live Rust objects that cannot be serialized to an external store. diff --git a/api/store.mdx b/api/store.mdx index 457473da..6ae75ace 100644 --- a/api/store.mdx +++ b/api/store.mdx @@ -484,7 +484,7 @@ async fn main() -> Result<(), Box> { ## CacheStore Trait -The `CacheStore` trait enables pluggable cache backends for the client's data caches. By default, caches use in-process moka; implementing this trait lets you use Redis, Memcached, or any other external cache. +The `CacheStore` trait enables pluggable cache backends for the client's data caches. By default, caches use the in-process `PortableCache`; implementing this trait lets you use Redis, Memcached, or any other external cache. **Location:** `wacore/src/store/cache.rs` @@ -557,20 +557,20 @@ See [Custom backends — cache store](/guides/custom-backends#custom-cache-store ## TypedCache -`TypedCache` is a generic wrapper that dispatches to either moka or a custom `CacheStore` backend. +`TypedCache` is a generic wrapper that dispatches to either the in-process `PortableCache` or a custom `CacheStore` backend. **Location:** `src/cache_store.rs` | Method | Signature | Description | |--------|-----------|-------------| -| `from_moka` | `fn from_moka(cache: Cache) -> Self` | Wrap an existing moka cache (zero overhead) | +| `from_local` | `fn from_local(cache: Cache) -> Self` | Wrap an in-process `PortableCache` (zero overhead) | | `from_store` | `fn from_store(store: Arc, namespace: &'static str, ttl: Option) -> Self` | Create a cache backed by a custom store | | `get` | `async fn get(&self, key: &Q) -> Option` | Look up a value. Misses and deserialization failures return `None` | | `insert` | `async fn insert(&self, key: K, value: V)` | Insert or update a value | | `invalidate` | `async fn invalidate(&self, key: &Q)` | Remove a single key | -| `invalidate_all` | `fn invalidate_all(&self)` | Remove all entries (sync). Requires `tokio-runtime` for custom backends | -| `clear` | `async fn clear(&self)` | Remove all entries (async). Awaits completion for custom backends | -| `run_pending_tasks` | `async fn run_pending_tasks(&self)` | Run internal housekeeping (moka only) | +| `invalidate_all` | `fn invalidate_all(&self)` | Remove all entries (sync). Best-effort for in-process backend; requires `tokio-runtime` for custom backends | +| `clear` | `async fn clear(&self)` | Remove all entries (async). Awaits the write lock for in-process backend; awaits completion for custom backends | +| `run_pending_tasks` | `async fn run_pending_tasks(&self)` | Evict expired entries (in-process backend only; no-op for custom backends) | | `entry_count` | `fn entry_count(&self) -> u64` | Approximate entry count (sync). Returns `0` for custom backends | | `entry_count_async` | `async fn entry_count_async(&self) -> u64` | Approximate entry count, delegating to custom backend if available | diff --git a/changelog/2026-06-14-drop-moka-portable-cache.mdx b/changelog/2026-06-14-drop-moka-portable-cache.mdx new file mode 100644 index 00000000..026e4307 --- /dev/null +++ b/changelog/2026-06-14-drop-moka-portable-cache.mdx @@ -0,0 +1,80 @@ +--- +title: "June 14, 2026 — Drop moka: PortableCache is now the sole in-process cache backend" +description: "Removes the moka dependency (-2.55 MiB stripped, -21% .text). PortableCache gains monotonic TTL/TTI, eager init-lock reclamation, and a reliable async clear(). TypedCache::from_moka is renamed to from_local." +--- + +PR [#860](https://github.com/oxidezap/whatsapp-rust/pull/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): + +| Metric | before (moka) | after (PortableCache) | Δ | +|---|---:|---:|---:| +| Stripped size | 13.35 MiB | 10.81 MiB | **−2.55 MiB (−19.1%)** | +| `.text` | 11.31 MiB | 8.89 MiB | **−2.42 MiB (−21.4%)** | +| LLVM IR lines | 1,275,789 | 664,220 | **−47.9%** | +| `Cargo.lock` crates | 357 | 354 | −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 reclamation** — `get_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`: + +```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: + +```rust +// 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. diff --git a/concepts/storage.mdx b/concepts/storage.mdx index c9d0ee89..13b0251b 100644 --- a/concepts/storage.mdx +++ b/concepts/storage.mdx @@ -476,7 +476,7 @@ pub struct LidPnCache { **Defaults match `WAWebLidPnCache`:** - **Time-based expiry**: none — entries do not idle out -- **Capacity**: effectively unbounded (`u64::MAX`; moka has no `unbounded()` builder) +- **Capacity**: effectively unbounded (`u64::MAX`; no dedicated `unbounded()` builder) A custom `CacheEntryConfig` can impose a capacity bound if memory pressure requires it. Be aware that capacity-LRU eviction silently downgrades Signal addresses from `@lid` (or `@hosted.lid`) back to `@c.us`, which can cause `SessionNotFound` decryption failures. Notably, this keeps `status@broadcast` participants resolving to `@lid` for the entire session, matching WA Web behavior. @@ -1030,39 +1030,26 @@ snapshots/ ### Overview -By default, whatsapp-rust uses in-process [moka](https://github.com/moka-rs/moka) caches for group metadata, device lists, device registry, and LID-PN mappings. The pluggable cache store adapter lets you replace any of these with an external backend (Redis, Memcached, etc.) by implementing the `CacheStore` trait. +By default, whatsapp-rust uses in-process `PortableCache` caches for group metadata, device lists, device registry, and LID-PN mappings. The pluggable cache store adapter lets you replace any of these with an external backend (Redis, Memcached, etc.) by implementing the `CacheStore` trait. -### PortableCache (WASM-compatible alternative) +### PortableCache -When the `moka-cache` feature is disabled, whatsapp-rust automatically switches to `PortableCache` — a platform-agnostic, runtime-independent cache implementation suitable for WASM and other non-standard runtimes. It mirrors the moka `Cache` API surface so call-sites can switch transparently. +`PortableCache` is the client's sole in-process cache backend on every target, including wasm32. It is a platform-agnostic, runtime-independent implementation. `PortableCache` supports: -- **Maximum capacity** with oldest-inserted eviction +- **Maximum capacity** with oldest-inserted (FIFO) eviction - **Time-to-live (TTL)** — entries expire a fixed duration after insertion - **Time-to-idle (TTI)** — entries expire after a fixed duration of no access +- **Monotonic expiry** — TTL/TTI use `wacore::time::Instant` (not the wall clock), so system-clock jumps cannot expire entries early - **Single-flight `get_with`** — concurrent initializations for the same key coalesce into a single call, which is critical for caches storing coordination primitives (mutexes, channels) -- **Eager `iter()` snapshot** — `PortableCache::iter()` mirrors `moka::Cache::iter()` and yields `(Arc, V)` pairs. Unlike moka's lazy iterator, the snapshot is captured up front (so it survives concurrent mutations) and may include entries that are expired but not yet evicted, matching moka's semantics. - -All time checks use `wacore::time::now_millis` instead of `std::time::Instant`, making it compatible with environments where monotonic clocks are unavailable. - -```toml -# Disable moka to use PortableCache (e.g., for WASM targets) -[dependencies] -whatsapp-rust = { version = "0.6", default-features = false, features = [ - "sqlite-storage", - "tokio-transport", - "tokio-runtime", - "ureq-client", - "tokio-native", - "signal", - # "moka-cache" omitted — PortableCache is used instead -] } -``` +- **Eager init-lock reclamation** — `get_with`/`get_with_by_ref` drop a key's init lock once no other caller holds it, preventing unbounded `init_locks` growth in high-cardinality caches (session locks, chat lanes, dedup) +- **Reliable async `clear()`** — awaits the write lock; prefer this over the best-effort sync `invalidate_all()` in async contexts +- **`snapshot_entries()`** — reliable awaited snapshot of `(Arc, V)` pairs for invalidation passes; `iter()` is a best-effort sync spin that can yield an empty snapshot under write contention ```mermaid graph TB A[Client] --> B[TypedCache] - B -->|Default| C[Moka In-Process] + B -->|Default| C[PortableCache In-Process] B -->|Custom| D[CacheStore Trait] D --> E[Redis] D --> F[Memcached] @@ -1088,17 +1075,17 @@ Cache operations are best-effort. The client falls back gracefully when cache re ### TypedCache -`TypedCache` is a generic wrapper that dispatches to either moka or a custom `CacheStore` backend. The moka path has zero extra overhead — values are stored in-process without any serialization. The custom-store path serializes values with `serde_json` and keys via `Display`. +`TypedCache` is a generic wrapper that dispatches to either the in-process `PortableCache` or a custom `CacheStore` backend. The in-process path has zero extra overhead — values are stored in memory without any serialization. The custom-store path serializes values with `serde_json` and keys via `Display`. ```rust -// Moka path (zero overhead) -let cache = TypedCache::from_moka(moka_cache); +// In-process path (zero overhead) +let cache = TypedCache::from_local(portable_cache); // Custom store path (serde_json serialization) let cache = TypedCache::from_store(store, "group", Some(Duration::from_secs(3600))); ``` -`CacheEntryConfig` provides a `build_typed_ttl` convenience method that automatically selects the right backend: if a custom `CacheStore` is provided, it creates a `TypedCache` backed by the store; otherwise it falls back to an in-process moka cache. +`CacheEntryConfig` provides a `build_typed_ttl` convenience method that automatically selects the right backend: if a custom `CacheStore` is provided, it creates a `TypedCache` backed by the store; otherwise it falls back to an in-process `PortableCache`. ```rust let cache: TypedCache> = config.group_cache.build_typed_ttl( @@ -1113,7 +1100,7 @@ The group cache stores `Arc` so that warm sends and repeated `query_i `TypedCache` provides two ways to remove all entries: -- **`invalidate_all()`** — synchronous. For moka backends this works immediately. For custom `CacheStore` backends, it spawns a fire-and-forget task via `tokio::runtime::Handle::try_current()`, which **requires the `tokio-runtime` feature**. Without `tokio-runtime` enabled, the clear is skipped and a warning is logged. +- **`invalidate_all()`** — synchronous. For the in-process backend this is a best-effort spin; under sustained write contention it can silently skip the clear. For custom `CacheStore` backends, it spawns a fire-and-forget task via `tokio::runtime::Handle::try_current()`, which **requires the `tokio-runtime` feature**. Without `tokio-runtime` enabled, the clear is skipped and a warning is logged. - **`clear()`** — async. Awaits completion for custom backends and is the recommended approach when you need to ensure all entries are removed. @@ -1132,7 +1119,7 @@ pub struct CacheStores { } ``` -Fields left as `None` keep the default moka behavior. Use `CacheStores::all(store)` to set the same backend for all pluggable caches at once. +Fields left as `None` keep the default in-process `PortableCache` behaviour. Use `CacheStores::all(store)` to set the same backend for all pluggable caches at once. Coordination caches (`session_locks`, `chat_lanes`), the signal write-behind cache, and `pdo_pending_requests` always stay in-process — they hold live Rust objects (mutexes, channel senders) that cannot be serialized to an external store. diff --git a/docs.json b/docs.json index cc98762e..d142b1a0 100644 --- a/docs.json +++ b/docs.json @@ -138,6 +138,7 @@ "group": "Changelog", "pages": [ "changelog/overview", + "changelog/2026-06-14-drop-moka-portable-cache", "changelog/2026-06-12-binary-size-ci", "changelog/2026-06-11-bot-api-overhaul", "changelog/2026-06-11-dockerfile-share-generics", diff --git a/guides/custom-backends.mdx b/guides/custom-backends.mdx index 6d1caf1e..f0053cd0 100644 --- a/guides/custom-backends.mdx +++ b/guides/custom-backends.mdx @@ -941,7 +941,7 @@ let backend = Arc::new(SqliteStore::new("whatsapp.db").await?); ## Custom cache store -The pluggable cache store adapter lets you replace the default in-process moka caches with an external backend like Redis or Memcached. This is useful for sharing cache state across multiple client instances or for deployments where in-process memory is limited. +The pluggable cache store adapter lets you replace the default in-process caches with an external backend like Redis or Memcached. This is useful for sharing cache state across multiple client instances or for deployments where in-process memory is limited. ### The CacheStore trait @@ -1119,7 +1119,7 @@ The following namespaces are used internally by the client: ### Design considerations - **Error handling is best-effort.** Cache misses and failures are logged as warnings but don't break the client — it falls back to fetching from the authoritative source. -- **Serialization uses `serde_json`.** Values are serialized to JSON bytes on the custom-store path. The moka path has zero serialization overhead. +- **Serialization uses `serde_json`.** Values are serialized to JSON bytes on the custom-store path. The in-process path has zero serialization overhead. - **TTL is forwarded from `CacheEntryConfig`.** Your implementation receives the same TTL configured in `CacheConfig`. - **Coordination caches cannot be externalized.** Session locks, message queues, and enqueue locks hold live Rust objects (mutexes, channels) and always stay in-process. - **`invalidate_all()` requires `tokio-runtime`.** The synchronous `invalidate_all()` method on `TypedCache` spawns a fire-and-forget task via Tokio for custom backends. Without the `tokio-runtime` feature, the clear is skipped with a warning. Use the async `clear()` method instead if you disable `tokio-runtime`. diff --git a/installation.mdx b/installation.mdx index abefa1d3..6dee2e21 100644 --- a/installation.mdx +++ b/installation.mdx @@ -63,7 +63,6 @@ whatsapp-rust = { version = "0.6", default-features = false, features = [ "ureq-client", "tokio-native", "signal", - "moka-cache", ] } wacore = { version = "0.6", default-features = false } tokio = { version = "1.48", features = ["macros", "rt-multi-thread"] } @@ -82,7 +81,6 @@ whatsapp-rust supports several optional features: | `tokio-transport` | Tokio WebSocket transport | ✅ Yes | | `ureq-client` | Ureq HTTP client | ✅ Yes | | `sqlite-storage` | SQLite storage backend | ✅ Yes | -| `moka-cache` | Moka in-memory cache | ✅ Yes | | `simd` | SIMD-optimized binary protocol encoding/decoding (**requires nightly Rust**) | ✅ Yes | | `signal` | Unix signal handling (graceful shutdown on SIGTERM/Ctrl+C) | ✅ Yes | | `tracing` | Emit `tracing` spans/events across connect, send, receive, IQ, app state, pairing, media, and session flows. See [Observability](/advanced/observability) | ❌ No | @@ -156,7 +154,6 @@ whatsapp-rust = { version = "0.6", default-features = false, features = [ "ureq-client", "tokio-native", "signal", - "moka-cache", ] } # wacore also needs default-features = false to prevent feature unification # from re-enabling simd diff --git a/pt/installation.mdx b/pt/installation.mdx index 1e656911..372d1482 100644 --- a/pt/installation.mdx +++ b/pt/installation.mdx @@ -40,7 +40,6 @@ whatsapp-rust = { version = "0.6", default-features = false, features = [ "ureq-client", "tokio-native", "signal", - "moka-cache", ] } whatsapp-rust-sqlite-storage = "0.6" whatsapp-rust-tokio-transport = "0.6" @@ -63,7 +62,6 @@ whatsapp-rust suporta diversas features opcionais: | `tokio-transport` | Transporte WebSocket Tokio | ✅ Sim | | `ureq-client` | Cliente HTTP Ureq | ✅ Sim | | `sqlite-storage` | Backend de armazenamento SQLite | ✅ Sim | -| `moka-cache` | Cache em memória Moka | ✅ Sim | | `simd` | Codificação/decodificação do protocolo binário otimizada com SIMD (**requer Rust nightly**) | ✅ Sim | | `signal` | Manipulação de sinais Unix (desligamento gracioso em SIGTERM/Ctrl+C) | ✅ Sim | | `danger-skip-tls-verify` | Pula a verificação TLS (inseguro) | ❌ Não | @@ -134,7 +132,6 @@ whatsapp-rust = { version = "0.6", default-features = false, features = [ "ureq-client", "tokio-native", "signal", - "moka-cache", ] } # wacore também precisa de default-features = false para evitar que # a unificação de features reabilite simd