refactor(call-to-play): merge histories atomically
Replace per-event insertion with a transactional batch merge. The store now validates and deduplicates an entire history before committing it, rejects conflicting event IDs without partial mutation, evaluates retention after all batch events are present, and reports applied, duplicate, obsolete, and missing-root outcomes explicitly. Derive event identity from retained history instead of preserving an unbounded ID set. Expired histories can therefore be restored by a complete Create plus AddTime batch, while orphan actions request history and terminal tombstones continue to reject stale resurrection. Capacity applies only to unresolved history, allowing Start and Cancel to settle a full call. Only retained events reach the UI or live broadcast path. Handshake and live merge callers log invalid or incomplete histories without publishing events that compaction discarded. Test Plan: - `just fmt` -- passed - `just clippy` -- passed - `just test` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
4 files changed
+582
-216
No files matched your search
@@ -195,9 +195,21 @@ async fn merge_call_to_play_events(
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
incoming: Vec<lanspread_proto::CallToPlayEvent>,
|
||||
) {
|
||||
let accepted = store.write().await.insert_all(incoming);
|
||||
if !accepted.is_empty() {
|
||||
events::send(tx_notify_ui, PeerEvent::CallToPlayEvents(accepted));
|
||||
match store.write().await.merge_batch(incoming) {
|
||||
Ok(merged) => {
|
||||
if merged.needs_history() {
|
||||
log::warn!(
|
||||
"Call to Play handshake omitted roots for calls: {}",
|
||||
merged.missing_call_ids.join(", ")
|
||||
);
|
||||
}
|
||||
if !merged.applied.is_empty() {
|
||||
events::send(tx_notify_ui, PeerEvent::CallToPlayEvents(merged.applied));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Rejecting Call to Play handshake history: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,8 +383,8 @@ mod tests {
|
||||
ctx.call_to_play
|
||||
.write()
|
||||
.await
|
||||
.insert(call_to_play_event())
|
||||
.expect("valid event should be inserted");
|
||||
.merge_batch(vec![call_to_play_event()])
|
||||
.expect("valid event should be merged");
|
||||
|
||||
let hello = build_hello_from_state(&ctx)
|
||||
.await
|
||||
|
||||
@@ -146,12 +146,24 @@ async fn handle_call_to_play_events(
|
||||
return;
|
||||
}
|
||||
|
||||
let accepted = ctx.call_to_play.write().await.insert_all(incoming);
|
||||
if !accepted.is_empty() {
|
||||
events::send(
|
||||
&ctx.tx_notify_ui,
|
||||
crate::PeerEvent::CallToPlayEvents(accepted),
|
||||
);
|
||||
match ctx.call_to_play.write().await.merge_batch(incoming) {
|
||||
Ok(merged) => {
|
||||
if merged.needs_history() {
|
||||
log::warn!(
|
||||
"Ignoring Call to Play actions without history from {peer_id}: {}",
|
||||
merged.missing_call_ids.join(", ")
|
||||
);
|
||||
}
|
||||
if !merged.applied.is_empty() {
|
||||
events::send(
|
||||
&ctx.tx_notify_ui,
|
||||
crate::PeerEvent::CallToPlayEvents(merged.applied),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Rejecting Call to Play events from {peer_id}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user