use crate::state::{ChannelId, ImState};
use helix_core::{Correlation, EffectSink};
pub fn parse_gap_channels(data: &serde_json::Value) -> Vec<ChannelId> {
let Some(arr) = data.get("gaps").and_then(serde_json::Value::as_array) else {
return Vec::new();
};
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for g in arr {
if let Some(id) = g
.get("channelId")
.and_then(serde_json::Value::as_str)
.and_then(ChannelId::from_str)
{
if seen.insert(id) {
out.push(id);
}
}
}
out
}
pub fn parse_hash_mismatch(data: &serde_json::Value) -> bool {
data.get("hashMismatch")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
}
pub fn compensate_from_pong(
state: &mut ImState,
api_base_url: &str,
now_ms: u64,
gap_channels: &[ChannelId],
hash_mismatch: bool,
alloc_corr: &mut dyn FnMut() -> Correlation,
out: &mut EffectSink,
) {
if state.recovery_session.is_collecting() && gap_channels.is_empty() {
tracing::info!(
hop = "pong.compensation_decision",
gap_count = gap_channels.len(),
hash_mismatch,
known_channel_count = state.channels.len(),
target_count = 0,
scheduler_inflight = state.sync_scheduler.inflight(),
scheduler_pending = state.sync_scheduler.pending_len(),
"hash-only pong compensation deferred until recovery authority boundary"
);
return;
}
let mut targets: Vec<ChannelId> = Vec::new();
for &ch in gap_channels {
if state.sync_scheduler.should_pong_compensate(ch, now_ms) {
targets.push(ch);
}
}
tracing::info!(
hop = "pong.compensation_decision",
gap_count = gap_channels.len(),
hash_mismatch,
known_channel_count = state.channels.len(),
target_count = targets.len(),
scheduler_inflight = state.sync_scheduler.inflight(),
scheduler_pending = state.sync_scheduler.pending_len(),
"pong gap compensation evaluated"
);
if targets.is_empty() {
return;
}
state.pong_gap_batch.begin_fanout(targets.len());
crate::sync_scheduler::prioritize_and_drain_with_trigger(
state,
api_base_url,
&targets,
crate::state::SyncTrigger::PongGap,
alloc_corr,
out,
);
}
#[cfg(test)]
#[path = "pong_compensate_tests.rs"]
mod tests;