Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 4 additions & 2 deletions advanced/state-management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,9 @@ The `get` → mutate → `insert` sequence is **not atomic**. A concurrent notif
| --- | --- | --- | --- |
| Device registry | Device add/remove/update notifications | Yes (backend store) | Hash-only notifications trigger full invalidation |
| Group metadata | Participant add/remove notifications and API calls | No (cache-only) | `leave()` and cache eviction trigger server re-fetch |
| LID-PN mappings | Usync, peer messages, device notifications | Yes (backend store) | Timestamp conflict resolution prevents stale overwrites |
| LID-PN mappings | Usync, peer messages, device notifications | Yes (backend store) | Source-aware write policy (see below) prevents stale or unverified overwrites |
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

For LID-PN mappings specifically, "patched on" is no longer a plain write — with the source-aware write policy in [Storage — Granular cache patching](/concepts/storage#granular-cache-patching), the `LearningSource` of an incoming pair also decides whether it may overwrite what the cache already holds; a conflicting observational pair queues a live re-resolve instead of being applied directly.

For full details including patching methods and data flow, see [Granular cache patching](/concepts/storage#granular-cache-patching).

Expand All @@ -614,4 +616,4 @@ For full details including patching methods and data flow, see [Granular cache p
- Commands: `wacore/src/store/commands.rs`
- Device structure: `wacore/src/store/device.rs`
- Backend trait: `wacore/src/store/traits.rs`
- SQLite backend: `storages/sqlite-storage/src/sqlite_store.rs`
- SQLite backend: `storages/sqlite-storage/src/sqlite_store.rs`
38 changes: 23 additions & 15 deletions api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1029,21 +1029,29 @@ if let Some(entry) = client.get_lid_pn_entry(&lid_jid).await {

#### LearningSource

The `LearningSource` enum indicates how a LID-PN mapping was discovered:

| Variant | Description |
|---------|-------------|
| `Usync` | From a device sync query response |
| `PeerPnMessage` | From an incoming message with `sender_lid` attribute (sender is PN) |
| `PeerLidMessage` | From an incoming message with `sender_pn` attribute (sender is LID) |
| `RecipientLatestLid` | From looking up a recipient's latest LID |
| `MigrationSyncLatest` | From latest history sync migration |
| `MigrationSyncOld` | From old history sync records |
| `BlocklistActive` | From an active blocklist entry |
| `BlocklistInactive` | From an inactive blocklist entry |
| `Pairing` | From device pairing (own JID to LID) |
| `DeviceNotification` | From a device notification with `lid` attribute |
| `Other` | From an unknown source |
The `LearningSource` enum indicates how a LID-PN mapping was discovered. The source is not mere provenance — it also selects the **write policy** applied when the pair reaches the cache, mirroring WhatsApp Web's `createLidPnMappings` (`WAWebDBCreateLidPnMappings`) `switch (learningSource)`:

- **Directed sources** (`Usync`, `PeerPnMessage`, `PeerLidMessage`, `RecipientLatestLid`, `MigrationSyncLatest`, `MigrationSyncOld`, `BlocklistActive`, `BlocklistInactive`) overwrite the cache on any change from what's already stored.
- **Observational bulk sources** (`Other`, `Pairing`, `DeviceNotification`) only seed a LID that isn't cached yet. If the pair conflicts with an already-known LID for that phone, the observational pair is **not** applied — the client instead fires one background live LID query (`LidQuerySpec`) and learns the authoritative result under `Usync`, which can never itself trigger another reconcile.
- **Known-stale sources** (`MigrationSyncOld`, `BlocklistInactive`) are additionally stamped with `created_at = 0`, so a fresher mapping for the same phone always outranks them in the cache's most-recent-wins (PN→LID) resolution. This only guards the forward direction — the LID→PN reverse map always takes the latest write.

| Variant | Description | Write policy |
|---------|-------------|--------------|
| `Usync` | From a device sync query response | Directed — overwrites on conflict |
| `PeerPnMessage` | From an incoming message with `sender_lid` attribute (sender is PN) | Directed — overwrites on conflict |
| `PeerLidMessage` | From an incoming message with `sender_pn` attribute (sender is LID) | Directed — overwrites on conflict |
| `RecipientLatestLid` | From looking up a recipient's latest LID | Directed — overwrites on conflict |
| `MigrationSyncLatest` | From the live 1:1 LID migration flow | Directed — overwrites on conflict |
| `MigrationSyncOld` | From old history sync records | Directed — overwrites on conflict; `created_at = 0` (stale) |
| `BlocklistActive` | From an active blocklist entry | Directed — overwrites on conflict |
| `BlocklistInactive` | From an inactive blocklist entry | Directed — overwrites on conflict; `created_at = 0` (stale) |
| `Pairing` | From device pairing (own JID to LID) | Observational — seeds new LIDs only |
| `DeviceNotification` | From a device notification with `lid` attribute | Observational — seeds new LIDs only |
| `Other` | From an unknown/bulk source (e.g. the history-sync `phoneNumberToLidMappings` seed) | Observational — seeds new LIDs only |

<Note>
A pair that already matches the cache's current mapping (exact match, or a reverse-only match surviving a capacity-bounded eviction) always re-affirms durability regardless of source — it is never treated as a conflict.
</Note>

### persistence_manager

Expand Down
12 changes: 10 additions & 2 deletions concepts/storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1327,9 +1327,17 @@ Group patches are cache-only — they are not persisted to the backend. If the c

Only `leave()` uses full invalidation.

**LID-PN mappings** (`src/lid_pn_cache.rs`)
**LID-PN mappings** (`src/client/lid_pn.rs`, `src/lid_pn_cache.rs`)

The `LidPnCache` uses timestamp-based conflict resolution when adding new mappings. The PN → entry map only updates if the new entry's `created_at` is newer than or equal to the existing entry's, preventing older stale mappings from overwriting newer ones.
The `LidPnCache` uses timestamp-based conflict resolution when adding new mappings: the PN → entry map only updates if the new entry's `created_at` is newer than or equal to the existing entry's, preventing older stale mappings from overwriting newer ones.

On top of that, the write itself is gated by a **source-aware write policy** (`lid_pn_write_policy`, mirroring WhatsApp Web's `createLidPnMappings` `switch (learningSource)`): the incoming pair's [`LearningSource`](/api/client#learningsource) decides whether it's even allowed to overwrite what's cached, before the timestamp comparison ever applies.

- Directed sources (device-sync, peer messages, migration, blocklist) overwrite the cache on any change.
- Observational bulk sources (`Other`, `Pairing`, `DeviceNotification` — e.g. history-sync's `phoneNumberToLidMappings` seed, group participant lists) only ever seed a **new** LID. If the pair conflicts with a LID the cache already knows for that phone, the write is skipped and the phone number is queued for a background live LID query (`LidQuerySpec`) instead — the result of that query is learned under `Usync`, which is a directed source and cannot itself re-trigger a reconcile, so there's no query → learn → query loop.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
- `MigrationSyncOld` and `BlocklistInactive` are known-stale: they still write, but with `created_at = 0`, so a fresher mapping for the same phone keeps winning the PN→LID resolution.

This means a bulk/observational seed can no longer clobber a freshly, authoritatively learned LID for a peer — it self-heals through the same live-query path WA Web uses instead of last-write-wins.

### Patching vs. invalidation summary

Expand Down