Skip to content

Commit 909b482

Browse files
committed
Catch up MLS backlog before group sends in app path
1 parent a5dfe40 commit 909b482

3 files changed

Lines changed: 203 additions & 6 deletions

File tree

rust/src/core/chat_media.rs

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,7 +1746,7 @@ impl AppCore {
17461746
self.last_outgoing_ts
17471747
};
17481748

1749-
let (client, wrapper, relays, rumor_id_hex) = {
1749+
let (client, relays, rumor, rumor_id_hex) = {
17501750
let Some(sess) = self.session.as_mut() else {
17511751
return;
17521752
};
@@ -1788,6 +1788,149 @@ impl AppCore {
17881788
},
17891789
);
17901790

1791+
let relays: Vec<RelayUrl> = if network_enabled {
1792+
sess.mdk
1793+
.get_relays(&group.mls_group_id)
1794+
.ok()
1795+
.map(|s| s.into_iter().collect())
1796+
.filter(|v: &Vec<RelayUrl>| !v.is_empty())
1797+
.unwrap_or_else(|| fallback_relays.clone())
1798+
} else {
1799+
vec![]
1800+
};
1801+
1802+
(sess.client.clone(), relays, rumor, rumor_id_hex)
1803+
};
1804+
1805+
self.prune_local_outbox(&chat_id);
1806+
self.refresh_chat_list_from_storage();
1807+
self.refresh_current_chat_if_open(&chat_id);
1808+
1809+
if !network_enabled {
1810+
let _ = self.core_sender.send(CoreMsg::Internal(Box::new(
1811+
InternalEvent::PublishMessageResult {
1812+
chat_id,
1813+
rumor_id: rumor_id_hex,
1814+
ok: false,
1815+
error: Some("offline".into()),
1816+
},
1817+
)));
1818+
return;
1819+
}
1820+
1821+
let tx = self.core_sender.clone();
1822+
let mut seen_ids = self.processed_group_event_ids.clone();
1823+
let chat_id_for_task = chat_id.clone();
1824+
self.runtime.spawn(async move {
1825+
let filter = Filter::new()
1826+
.kind(Kind::MlsGroupMessage)
1827+
.custom_tags(
1828+
SingleLetterTag::lowercase(Alphabet::H),
1829+
vec![chat_id_for_task.clone()],
1830+
)
1831+
.limit(200);
1832+
let (mut backlog_events, error) = match client
1833+
.fetch_events_from(relays.clone(), filter, std::time::Duration::from_secs(8))
1834+
.await
1835+
{
1836+
Ok(evts) => (evts.into_iter().collect::<Vec<Event>>(), None),
1837+
Err(e) => (Vec::new(), Some(format!("group backlog fetch failed: {e}"))),
1838+
};
1839+
backlog_events.retain(|ev| seen_ids.insert(ev.id));
1840+
backlog_events.sort_by_key(|ev| ev.created_at.as_secs());
1841+
1842+
let _ = tx.send(CoreMsg::Internal(Box::new(
1843+
InternalEvent::GroupSendCatchupCompleted {
1844+
chat_id: chat_id_for_task,
1845+
rumor,
1846+
backlog_events,
1847+
error,
1848+
},
1849+
)));
1850+
});
1851+
}
1852+
1853+
pub(super) fn handle_group_send_catchup_completed(
1854+
&mut self,
1855+
chat_id: String,
1856+
rumor: UnsignedEvent,
1857+
backlog_events: Vec<Event>,
1858+
error: Option<String>,
1859+
) {
1860+
if let Some(err) = error {
1861+
tracing::warn!(%chat_id, %err, "group send backlog fetch failed");
1862+
}
1863+
self.ingest_group_backlog_events(backlog_events);
1864+
self.publish_prepared_group_rumor(chat_id, rumor);
1865+
}
1866+
1867+
fn ingest_group_backlog_events(&mut self, backlog_events: Vec<Event>) {
1868+
let mut remaining = backlog_events;
1869+
let mut processed_any = false;
1870+
for _ in 0..3 {
1871+
if remaining.is_empty() {
1872+
break;
1873+
}
1874+
let current = std::mem::take(&mut remaining);
1875+
let mut next = Vec::new();
1876+
for event in current.into_iter() {
1877+
let event_id = event.id;
1878+
let result = {
1879+
let Some(sess) = self.session.as_mut() else {
1880+
return;
1881+
};
1882+
sess.mdk.process_message(&event)
1883+
};
1884+
match result {
1885+
Ok(r) => {
1886+
self.note_processed_group_event_id_in_memory(event_id);
1887+
processed_any = true;
1888+
self.handle_message_processing_result(r);
1889+
}
1890+
Err(e) => {
1891+
tracing::debug!(event_id = %event.id.to_hex(), %e, "deferred backlog message");
1892+
next.push(event);
1893+
}
1894+
}
1895+
}
1896+
if next.is_empty() {
1897+
break;
1898+
}
1899+
remaining = next;
1900+
}
1901+
if !remaining.is_empty() {
1902+
tracing::warn!(remaining = remaining.len(), "failed to ingest some backlog events");
1903+
}
1904+
if processed_any {
1905+
self.persist_processed_group_event_ids_cache();
1906+
}
1907+
}
1908+
1909+
fn publish_prepared_group_rumor(&mut self, chat_id: String, mut rumor: UnsignedEvent) {
1910+
let network_enabled = self.network_enabled();
1911+
let fallback_relays = self.default_relays();
1912+
let rumor_id_hex = rumor.id().to_hex();
1913+
1914+
let mark_publish_failed = |core: &mut Self, reason: &str| {
1915+
core.handle_publish_message_result(
1916+
chat_id.clone(),
1917+
rumor_id_hex.clone(),
1918+
false,
1919+
Some(reason.to_string()),
1920+
);
1921+
};
1922+
1923+
let (client, wrapper, relays) = {
1924+
let Some(sess) = self.session.as_mut() else {
1925+
mark_publish_failed(self, "session lost");
1926+
return;
1927+
};
1928+
let Some(group) = sess.groups.get(&chat_id).cloned() else {
1929+
self.toast("Chat not found");
1930+
mark_publish_failed(self, "chat not found");
1931+
return;
1932+
};
1933+
17911934
let wrapper = match sess.mdk.create_message(&group.mls_group_id, rumor) {
17921935
Ok(e) => e,
17931936
Err(e) => {
@@ -1821,13 +1964,9 @@ impl AppCore {
18211964
vec![]
18221965
};
18231966

1824-
(sess.client.clone(), wrapper, relays, rumor_id_hex)
1967+
(sess.client.clone(), wrapper, relays)
18251968
};
18261969

1827-
self.prune_local_outbox(&chat_id);
1828-
self.refresh_chat_list_from_storage();
1829-
self.refresh_current_chat_if_open(&chat_id);
1830-
18311970
if !network_enabled {
18321971
let _ = self.core_sender.send(CoreMsg::Internal(Box::new(
18331972
InternalEvent::PublishMessageResult {

rust/src/core/mod.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,7 @@ pub struct AppCore {
771771
// Nostr kind:0 profile cache (survives across session refreshes).
772772
profiles: HashMap<String, ProfileCache>, // hex pubkey -> cached global profile
773773
group_profiles: HashMap<String, HashMap<String, ProfileCache>>, // chat_id -> (pubkey -> profile)
774+
processed_group_event_ids: HashSet<EventId>,
774775
profile_db: Option<rusqlite::Connection>,
775776
chat_media_db: Option<rusqlite::Connection>,
776777

@@ -887,6 +888,8 @@ impl AppCore {
887888

888889
let push_device_id = Self::load_or_create_push_device_id(&data_dir);
889890
let push_subscribed_chat_ids = Self::load_push_subscriptions(&data_dir);
891+
let processed_group_event_ids =
892+
pika_marmot_runtime::load_processed_mls_event_ids(std::path::Path::new(&data_dir));
890893

891894
let mut this = Self {
892895
state,
@@ -919,6 +922,7 @@ impl AppCore {
919922
local_outbox: HashMap::new(),
920923
profiles,
921924
group_profiles: HashMap::new(),
925+
processed_group_event_ids,
922926
profile_db,
923927
typing_state: HashMap::new(),
924928
last_typing_sent: HashMap::new(),
@@ -3139,6 +3143,7 @@ impl AppCore {
31393143
self.cancel_call_offer_timeout();
31403144
self.cancel_voice_recording_ticks();
31413145

3146+
self.clear_processed_group_event_ids_cache();
31423147
let root = std::path::Path::new(&self.data_dir);
31433148
match std::fs::read_dir(root) {
31443149
Ok(entries) => {
@@ -3327,6 +3332,12 @@ impl AppCore {
33273332
ok,
33283333
error,
33293334
} => self.handle_publish_message_result(chat_id, rumor_id, ok, error),
3335+
InternalEvent::GroupSendCatchupCompleted {
3336+
chat_id,
3337+
rumor,
3338+
backlog_events,
3339+
error,
3340+
} => self.handle_group_send_catchup_completed(chat_id, rumor, backlog_events, error),
33303341
InternalEvent::ChatMediaUploadCompleted {
33313342
request_id,
33323343
uploaded_url,
@@ -4451,6 +4462,7 @@ impl AppCore {
44514462
}
44524463

44534464
pub(crate) fn handle_group_message(&mut self, event: Event) {
4465+
let event_id = event.id;
44544466
let result = {
44554467
let Some(sess) = self.session.as_mut() else {
44564468
tracing::warn!("group_message but no session");
@@ -4465,9 +4477,48 @@ impl AppCore {
44654477
}
44664478
}
44674479
};
4480+
self.note_processed_group_event_id(event_id);
44684481
self.handle_message_processing_result(result);
44694482
}
44704483

4484+
fn note_processed_group_event_id(&mut self, event_id: EventId) {
4485+
if !self.processed_group_event_ids.insert(event_id) {
4486+
return;
4487+
}
4488+
self.persist_processed_group_event_ids_cache();
4489+
}
4490+
4491+
fn note_processed_group_event_id_in_memory(&mut self, event_id: EventId) {
4492+
self.processed_group_event_ids.insert(event_id);
4493+
}
4494+
4495+
fn persist_processed_group_event_ids_cache(&mut self) {
4496+
if let Err(e) = pika_marmot_runtime::persist_processed_mls_event_ids(
4497+
std::path::Path::new(&self.data_dir),
4498+
&self.processed_group_event_ids,
4499+
) {
4500+
tracing::warn!(%e, "failed to persist processed group event ids");
4501+
}
4502+
}
4503+
4504+
fn clear_processed_group_event_ids_cache(&mut self) {
4505+
self.processed_group_event_ids.clear();
4506+
let path = pika_marmot_runtime::processed_mls_event_ids_path(
4507+
std::path::Path::new(&self.data_dir),
4508+
);
4509+
match std::fs::remove_file(&path) {
4510+
Ok(()) => {}
4511+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
4512+
Err(e) => {
4513+
tracing::warn!(
4514+
%e,
4515+
path = %path.display(),
4516+
"failed to delete processed group event id cache file"
4517+
);
4518+
}
4519+
}
4520+
}
4521+
44714522
fn handle_message_processing_result(&mut self, result: MessageProcessingResult) {
44724523
// Phase 1: Extract mls_group_id and optional app message.
44734524
let (mls_group_id, app_msg) = match result {
@@ -4729,6 +4780,7 @@ impl AppCore {
47294780
tracing::info!(path = %db_path.display(), "deleted mdk db on logout");
47304781
}
47314782
}
4783+
self.clear_processed_group_event_ids_cache();
47324784
self.clear_push_subscriptions();
47334785
self.stop_session();
47344786
self.state.auth = AuthState::LoggedOut;

rust/src/updates.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ pub enum InternalEvent {
5555
ok: bool,
5656
error: Option<String>,
5757
},
58+
GroupSendCatchupCompleted {
59+
chat_id: String,
60+
rumor: nostr_sdk::prelude::UnsignedEvent,
61+
backlog_events: Vec<nostr_sdk::prelude::Event>,
62+
error: Option<String>,
63+
},
5864
ChatMediaUploadCompleted {
5965
request_id: String,
6066
uploaded_url: Option<String>,

0 commit comments

Comments
 (0)