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
2 changes: 1 addition & 1 deletion src/client/device_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ mod tests {
async fn setup_lid_pn(client: &Arc<Client>, lid: &str, pn: &str) {
use crate::lid_pn_cache::LidPnEntry;
let entry = LidPnEntry::new(lid.to_string(), pn.to_string(), LearningSource::Usync);
client.lid_pn_cache.add(entry).await;
client.lid_pn_cache.add(&entry).await;
}

async fn setup_device_record(client: &Arc<Client>, user: &str, device_ids: &[u32]) {
Expand Down
134 changes: 108 additions & 26 deletions src/client/lid_pn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@

use anyhow::Result;
use log::debug;
use wacore::store::traits::LidPnMappingEntry;
use wacore_binary::Jid;

use super::Client;
use crate::lid_pn_cache::{LearningSource, LidPnEntry};

/// Backend `LidPnMappingEntry` → in-memory `LidPnEntry`.
fn mapping_to_entry(m: LidPnMappingEntry) -> LidPnEntry {
LidPnEntry::with_timestamp(
m.lid,
m.phone_number,
m.created_at,
LearningSource::parse(&m.learning_source),
)
}

impl Client {
/// Warm up the LID-PN cache from persistent storage.
/// This is called during client initialization to populate the in-memory cache
Expand All @@ -29,19 +40,9 @@ impl Client {
return Ok(());
}

let cache_entries: Vec<LidPnEntry> = entries
.into_iter()
.map(|e| {
LidPnEntry::with_timestamp(
e.lid,
e.phone_number,
e.created_at,
LearningSource::parse(&e.learning_source),
)
})
.collect();

self.lid_pn_cache.warm_up(cache_entries).await;
self.lid_pn_cache
.warm_up(entries.into_iter().map(mapping_to_entry))
.await;
Ok(())
}

Expand All @@ -66,7 +67,7 @@ impl Client {

// Add to in-memory cache
let entry = LidPnEntry::new(lid.to_string(), phone_number.to_string(), source);
self.lid_pn_cache.add(entry.clone()).await;
self.lid_pn_cache.add(&entry).await;

// Persist to storage
let backend = self.persistence_manager.backend();
Expand Down Expand Up @@ -270,18 +271,39 @@ impl Client {
}
}

/// Look up the LID↔phone mapping for a JID.
/// Look up the LID↔phone mapping for a JID. Cache-aside: falls back to
/// the backend on cache miss so mappings survive cache eviction and any
/// backend implementation gets the fallback without warm-up.
///
/// Routes automatically: LID JIDs search by LID, PN JIDs search by phone.
/// Returns `None` for non-user JIDs (groups, newsletters, etc.).
pub async fn get_lid_pn_entry(&self, jid: &Jid) -> Option<LidPnEntry> {
if jid.is_lid() {
self.lid_pn_cache.get_entry_by_lid(&jid.user).await
/// Backend errors are propagated — callers can distinguish "no mapping"
/// (`Ok(None)`) from "lookup failed" (`Err(_)`).
pub async fn get_lid_pn_entry(&self, jid: &Jid) -> Result<Option<LidPnEntry>> {
let (hit, is_lid) = if jid.is_lid() {
(self.lid_pn_cache.get_entry_by_lid(&jid.user).await, true)
} else if jid.is_pn() {
self.lid_pn_cache.get_entry_by_phone(&jid.user).await
(self.lid_pn_cache.get_entry_by_phone(&jid.user).await, false)
} else {
None
return Ok(None);
};

if let Some(entry) = hit {
return Ok(Some(entry));
}

let backend = self.persistence_manager.backend();
let mapping = if is_lid {
backend.get_lid_mapping(&jid.user).await?
} else {
backend.get_pn_mapping(&jid.user).await?
};

let Some(mapping) = mapping else {
return Ok(None);
};

let entry = mapping_to_entry(mapping);
self.lid_pn_cache.add(&entry).await;
Ok(Some(entry))
}
}

Expand Down Expand Up @@ -340,14 +362,24 @@ mod tests {
let pn = "55999999999";
let lid = "100000012345678";

assert!(client.get_lid_pn_entry(&Jid::pn(pn)).await.is_none());
assert!(
client
.get_lid_pn_entry(&Jid::pn(pn))
.await
.unwrap()
.is_none()
);

client
.add_lid_pn_mapping(lid, pn, LearningSource::Usync)
.await
.unwrap();

let entry = client.get_lid_pn_entry(&Jid::pn(pn)).await.unwrap();
let entry = client
.get_lid_pn_entry(&Jid::pn(pn))
.await
.unwrap()
.unwrap();
assert_eq!(entry.lid, lid);
assert_eq!(entry.phone_number, pn);
}
Expand All @@ -358,15 +390,65 @@ mod tests {
let pn = "55999999999";
let lid = "100000012345678";

assert!(client.get_lid_pn_entry(&Jid::lid(lid)).await.is_none());
assert!(
client
.get_lid_pn_entry(&Jid::lid(lid))
.await
.unwrap()
.is_none()
);

client
.add_lid_pn_mapping(lid, pn, LearningSource::Usync)
.await
.unwrap();

let entry = client.get_lid_pn_entry(&Jid::lid(lid)).await.unwrap();
let entry = client
.get_lid_pn_entry(&Jid::lid(lid))
.await
.unwrap()
.unwrap();
assert_eq!(entry.lid, lid);
assert_eq!(entry.phone_number, pn);
}

/// Cache-aside fallback: if the in-memory cache is missing an entry the
/// backend has, the lookup should still succeed and re-populate the cache.
#[tokio::test]
async fn test_get_lid_pn_entry_falls_back_to_backend() {
use wacore::store::traits::LidPnMappingEntry;

let client: Arc<Client> = create_test_client().await;
let pn = "15555550123";
let lid = "100000000000123";

let backend = client.persistence_manager.backend();
backend
.put_lid_mapping(&LidPnMappingEntry {
lid: lid.into(),
phone_number: pn.into(),
created_at: 1,
updated_at: 1,
learning_source: "usync".into(),
})
.await
.unwrap();

// Cache was never warmed from this backend write → cache miss path.
let entry = client
.get_lid_pn_entry(&Jid::lid(lid))
.await
.unwrap()
.unwrap();
assert_eq!(entry.lid, lid);
assert_eq!(entry.phone_number, pn);

// Subsequent lookup served from cache.
let entry = client
.get_lid_pn_entry(&Jid::pn(pn))
.await
.unwrap()
.unwrap();
assert_eq!(entry.lid, lid);
}
}
2 changes: 1 addition & 1 deletion src/features/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl<'a> Groups<'a> {
let entry = self
.client
.get_lid_pn_entry(&participant.jid)
.await
.await?
.ok_or_else(|| {
anyhow::anyhow!("Missing phone number mapping for LID {}", participant.jid)
})?;
Expand Down
18 changes: 9 additions & 9 deletions src/lid_pn_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ impl LidPnCache {
/// backends (e.g., Redis), concurrent `add()` calls for the same phone
/// number can race. This is acceptable because the cache is best-effort
/// and backed by persistent storage for correctness.
pub async fn add(&self, entry: LidPnEntry) {
pub async fn add(&self, entry: &LidPnEntry) {
// Check if PN map needs update first
let should_update_pn = match self.pn_to_entry.get(entry.phone_number.as_str()).await {
Some(existing) => existing.created_at <= entry.created_at,
Expand All @@ -131,7 +131,7 @@ impl LidPnCache {
// Update PN -> Entry map (only if newer or equal timestamp)
if should_update_pn {
self.pn_to_entry
.insert(entry.phone_number.clone(), entry)
.insert(entry.phone_number.clone(), entry.clone())
.await;
}
}
Expand All @@ -145,7 +145,7 @@ impl LidPnCache {
let mut count = 0;

for entry in entries {
self.add(entry).await;
self.add(&entry).await;
count += 1;
}

Expand Down Expand Up @@ -196,7 +196,7 @@ mod tests {
"559980000001".to_string(),
LearningSource::Usync,
);
cache.add(entry).await;
cache.add(&entry).await;

// Should be retrievable both ways
assert_eq!(
Expand All @@ -220,7 +220,7 @@ mod tests {
1000,
LearningSource::Other,
);
cache.add(old_entry).await;
cache.add(&old_entry).await;

assert_eq!(
cache.get_current_lid("559980000001").await,
Expand All @@ -234,7 +234,7 @@ mod tests {
2000,
LearningSource::Usync,
);
cache.add(new_entry).await;
cache.add(&new_entry).await;

// Should return the newer LID for PN lookup
assert_eq!(
Expand Down Expand Up @@ -264,7 +264,7 @@ mod tests {
2000,
LearningSource::Usync,
);
cache.add(new_entry).await;
cache.add(&new_entry).await;

// Try to add older mapping
let old_entry = LidPnEntry::with_timestamp(
Expand All @@ -273,7 +273,7 @@ mod tests {
1000,
LearningSource::Other,
);
cache.add(old_entry).await;
cache.add(&old_entry).await;

// PN -> LID should still return the newer one
assert_eq!(
Expand Down Expand Up @@ -326,7 +326,7 @@ mod tests {
"559980000001".to_string(),
LearningSource::Usync,
);
cache.add(entry).await;
cache.add(&entry).await;

assert_eq!(cache.lid_count().await, 1);
assert_eq!(cache.pn_count().await, 1);
Expand Down
4 changes: 2 additions & 2 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3614,7 +3614,7 @@ mod tests {
phone.to_string(),
crate::lid_pn_cache::LearningSource::PeerLidMessage,
);
client.lid_pn_cache.add(entry).await;
client.lid_pn_cache.add(&entry).await;

// Verify the cache has the mapping
let cached_lid = client.lid_pn_cache.get_current_lid(phone).await;
Expand Down Expand Up @@ -3753,7 +3753,7 @@ mod tests {
phone.to_string(),
crate::lid_pn_cache::LearningSource::PeerLidMessage,
);
client.lid_pn_cache.add(entry).await;
client.lid_pn_cache.add(&entry).await;

// Parse a PN-addressed DM message WITHOUT sender_lid attribute
let dm_node_without_sender_lid = wacore_binary::builder::NodeBuilder::new("message")
Expand Down
8 changes: 4 additions & 4 deletions src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2487,7 +2487,7 @@ mod tests {
// Now add a LID mapping (simulates mapping arriving between send and retry)
client
.lid_pn_cache
.add(wacore::types::lid_pn::LidPnEntry {
.add(&wacore::types::lid_pn::LidPnEntry {
lid: lid_jid.user.to_string(),
phone_number: pn_jid.user.to_string(),
created_at: 0,
Expand Down Expand Up @@ -2536,7 +2536,7 @@ mod tests {

client
.lid_pn_cache
.add(wacore::types::lid_pn::LidPnEntry {
.add(&wacore::types::lid_pn::LidPnEntry {
lid: lid_jid.user.to_string(),
phone_number: pn_jid.user.to_string(),
created_at: 0,
Expand Down Expand Up @@ -2604,7 +2604,7 @@ mod tests {
// Add LID mapping
client
.lid_pn_cache
.add(wacore::types::lid_pn::LidPnEntry {
.add(&wacore::types::lid_pn::LidPnEntry {
lid: lid_jid.user.to_string(),
phone_number: pn_jid.user.to_string(),
created_at: 0,
Expand Down Expand Up @@ -2702,7 +2702,7 @@ mod tests {
// Add mapping but don't store any message
client
.lid_pn_cache
.add(wacore::types::lid_pn::LidPnEntry {
.add(&wacore::types::lid_pn::LidPnEntry {
lid: lid_jid.user.to_string(),
phone_number: pn_jid.user.to_string(),
created_at: 0,
Expand Down
Loading