use crate::error::ImError;
use crate::module::ImModule;
use helix_core::effect::{GetSpec, Row, SqlValue, StorageOp};
use helix_core::tick::PortOutcome;
use helix_core::{Effect, EffectSink};
fn row_carries_an_active_schedule(row: &Row) -> bool {
let schedule_id = row_text(row, "schedule_id").unwrap_or_default();
let status = row_text(row, "status").unwrap_or_default();
!schedule_id.is_empty() && status != "canceled"
}
fn row_text<'a>(row: &'a Row, column: &str) -> Option<&'a str> {
row.iter().find_map(|(name, value)| match value {
SqlValue::Text(value) if name == column => Some(value.as_str()),
_ => None,
})
}
impl ImModule {
pub(super) fn handle_message_v3_schedule_created_persist_reply(
&mut self,
channel_id: crate::state::ChannelId,
revision: u64,
causation_id: Option<String>,
outcome: &PortOutcome,
out: &mut EffectSink,
) {
if self
.state
.inflight_schedule_revisions
.get(&channel_id)
.is_some_and(|inflight| *inflight == revision)
{
self.state.inflight_schedule_revisions.remove(&channel_id);
}
let causation_id = causation_id.filter(|request_id| {
self.state.pending_schedule_requests.get(&channel_id) == Some(request_id)
});
if causation_id.is_some() {
self.state.pending_schedule_requests.remove(&channel_id);
}
let PortOutcome::Ok(_) = outcome else {
if let Some(req_id) = causation_id.as_deref() {
out.push(crate::read_relay::emit_read_error(
req_id,
"SCHEDULE_PERSIST_FAILED",
));
}
tracing::warn!(
channel_id = channel_id.as_str(),
revision,
"schedule authority persist failed; suppressing MessageV3 event"
);
return;
};
self.state
.committed_schedule_revisions
.insert(channel_id, revision);
if let Some(req_id) = causation_id.as_deref() {
out.push(crate::read_relay::emit_read_body(
req_id,
serde_json::json!({"ok": true}),
));
}
let corr = self.alloc_corr_internal();
self.state.corr_map.insert(
corr,
crate::state::CorrelationContext::MessageV3ScheduleCreatedReadback,
);
out.push(Effect::Persist {
corr,
ops: vec![StorageOp::Get(GetSpec {
table: "channel_schedule",
key_col: "channel_id",
key_val: SqlValue::Text(channel_id.as_str().to_string()),
})],
});
}
pub(super) fn handle_message_v3_schedule_created_readback_reply(
&mut self,
outcome: &PortOutcome,
out: &mut EffectSink,
) -> Result<(), ImError> {
let PortOutcome::Ok(reply) = outcome else {
return Ok(());
};
let rows = helix_core::port_codec::rows_from_reply_bytes(&reply.0)
.map_err(|error| ImError::Parse(format!("schedule readback: {error}")))?;
let Some(row) = rows.first() else {
tracing::warn!(
"schedule created readback returned no durable row; terminal projection suppressed"
);
return Ok(());
};
if !row_carries_an_active_schedule(row) {
tracing::warn!(
"schedule created readback landed on a canceled/empty durable row; \
hasSchedulePost=true projection suppressed"
);
return Ok(());
}
let Some(event) = crate::event::schedule::created_from_row(row)? else {
return Ok(());
};
out.push(event.into_effect());
Ok(())
}
pub(super) fn handle_message_v3_schedule_canceled_persist_reply(
&mut self,
channel_id: crate::state::ChannelId,
revision: u64,
causation_id: Option<String>,
outcome: &PortOutcome,
out: &mut EffectSink,
) {
if self
.state
.inflight_schedule_revisions
.get(&channel_id)
.is_some_and(|inflight| *inflight == revision)
{
self.state.inflight_schedule_revisions.remove(&channel_id);
}
let causation_id = causation_id.filter(|request_id| {
self.state.pending_schedule_cancel_requests.get(&channel_id) == Some(request_id)
});
if causation_id.is_some() {
self.state
.pending_schedule_cancel_requests
.remove(&channel_id);
}
let PortOutcome::Ok(_) = outcome else {
if let Some(req_id) = causation_id.as_deref() {
out.push(crate::read_relay::emit_read_error(
req_id,
"SCHEDULE_PERSIST_FAILED",
));
}
tracing::warn!(
channel_id = channel_id.as_str(),
revision,
"schedule cancel persist failed; suppressing MessageV3 event"
);
return;
};
self.state
.committed_schedule_revisions
.insert(channel_id, revision);
if let Some(req_id) = causation_id.as_deref() {
out.push(crate::read_relay::emit_read_body(
req_id,
serde_json::json!({"ok": true}),
));
}
let corr = self.alloc_corr_internal();
self.state.corr_map.insert(
corr,
crate::state::CorrelationContext::MessageV3ScheduleCanceledReadback,
);
out.push(Effect::Persist {
corr,
ops: vec![StorageOp::Get(GetSpec {
table: "channel_schedule",
key_col: "channel_id",
key_val: SqlValue::Text(channel_id.as_str().to_string()),
})],
});
}
pub(super) fn handle_message_v3_schedule_canceled_readback_reply(
&mut self,
outcome: &PortOutcome,
out: &mut EffectSink,
) -> Result<(), ImError> {
let PortOutcome::Ok(reply) = outcome else {
tracing::warn!("schedule cancel readback failed; suppressing MessageV3 event");
return Ok(());
};
let rows = helix_core::port_codec::rows_from_reply_bytes(&reply.0)
.map_err(|error| ImError::Parse(format!("schedule cancel readback: {error}")))?;
let Some(row) = rows.first() else {
tracing::warn!("schedule cancel readback returned no durable row; MV3-G04b terminal projection suppressed");
return Ok(());
};
if row_carries_an_active_schedule(row) {
tracing::warn!(
"schedule cancel readback landed on an active durable row; \
hasSchedulePost=false projection suppressed"
);
return Ok(());
}
let Some(event) = crate::event::schedule::canceled_from_row(row)? else {
return Ok(());
};
out.push(event.into_effect());
Ok(())
}
}