use crate::state::{SendStatus, ServerId, TemporaryId};
use helix_core::effect::{SqlValue, StorageOp, UpsertSpec};
use helix_core::{Correlation, Effect, EffectSink, TimerId};
use serde_json::Value;
mod persistence;
pub use persistence::{
optimistic_message_persist_op, send_status_persist_op, upload_progress_persist_op,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TimelineReadbackContext {
pub window_token: Option<String>,
pub causation_id: Option<String>,
}
pub struct PendingSend {
pub temporary_id: TemporaryId,
pub status: SendStatus,
pub timeout_timer: TimerId,
pub persist_corr: Option<Correlation>,
pub connection_id: Option<String>,
pub timeline_readback: TimelineReadbackContext,
pub body: Option<Value>,
pub http_started: bool,
pub authoritative_readback_after_http: bool,
pub authoritative_readback_attempt: u8,
pub authoritative_readback_timer: Option<TimerId>,
pub remaining_uploads: usize,
pub upload_failed: bool,
}
pub(crate) const AUTHORITATIVE_READBACK_MAX_ATTEMPTS: u8 = 5;
pub(crate) fn authoritative_readback_backoff_ms(attempt: u8) -> u64 {
match attempt {
1 => 100,
2 => 250,
3 => 500,
4 => 1_000,
_ => 2_000,
}
}
impl PendingSend {
pub fn new(
temporary_id: TemporaryId,
timeout_timer: TimerId,
connection_id: Option<String>,
) -> Self {
Self {
temporary_id,
status: SendStatus::Local,
timeout_timer,
persist_corr: None,
connection_id,
timeline_readback: TimelineReadbackContext::default(),
body: None,
http_started: false,
authoritative_readback_after_http: true,
authoritative_readback_attempt: 0,
authoritative_readback_timer: None,
remaining_uploads: 0,
upload_failed: false,
}
}
pub fn reconcile(&mut self, server_id: ServerId, corr: Correlation, fx: &mut EffectSink) {
fx.push(Effect::Persist {
corr,
ops: vec![StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "message",
rows: vec![vec![
(
"temporary_id".to_string(),
SqlValue::Text(self.temporary_id.0.clone()),
),
(
"id".to_string(),
SqlValue::Text(server_id.as_str().to_string()),
),
(
"send_status".to_string(),
SqlValue::Text("sent".to_string()),
),
]],
conflict_key: Some("temporary_id"),
exclude_from_update: Vec::new(),
})],
});
fx.push(Effect::CancelTimer {
id: self.timeout_timer,
});
self.status = SendStatus::Sent;
}
pub fn mark_sent_pending_server_id(&mut self, corr: Correlation, fx: &mut EffectSink) {
fx.push(Effect::Persist {
corr,
ops: vec![StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "message",
rows: vec![vec![
(
"temporary_id".to_string(),
SqlValue::Text(self.temporary_id.0.clone()),
),
(
"send_status".to_string(),
SqlValue::Text("sent".to_string()),
),
]],
conflict_key: Some("temporary_id"),
exclude_from_update: Vec::new(),
})],
});
fx.push(Effect::CancelTimer {
id: self.timeout_timer,
});
self.status = SendStatus::Sent;
}
pub fn mark_failed_immediately(&mut self, corr: Correlation, fx: &mut EffectSink) -> bool {
if self.status == SendStatus::Sent || self.status == SendStatus::UnSend {
return false;
}
self.status = SendStatus::UnSend;
fx.push(Effect::Persist {
corr,
ops: vec![StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "message",
rows: vec![vec![
(
"temporary_id".to_string(),
SqlValue::Text(self.temporary_id.0.clone()),
),
(
"send_status".to_string(),
SqlValue::Text("unsend".to_string()),
),
]],
conflict_key: Some("temporary_id"),
exclude_from_update: Vec::new(),
})],
});
fx.push(Effect::CancelTimer {
id: self.timeout_timer,
});
true
}
pub fn on_timeout(&mut self, fx: &mut EffectSink) {
if self.upload_failed {
return;
}
if self.status == SendStatus::Sent || self.status == SendStatus::UnSend {
return;
}
self.persist_unsend(fx);
}
fn persist_unsend(&mut self, fx: &mut EffectSink) {
self.status = SendStatus::UnSend;
fx.push(Effect::PersistFire {
ops: vec![StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "message",
rows: vec![vec![
(
"temporary_id".to_string(),
SqlValue::Text(self.temporary_id.0.clone()),
),
(
"send_status".to_string(),
SqlValue::Text("unsend".to_string()),
),
]],
conflict_key: Some("temporary_id"),
exclude_from_update: Vec::new(),
})],
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_send_enables_authoritative_readback_fallback() {
let pending = PendingSend::new(
TemporaryId("tmp-first-send".to_string()),
TimerId::from_raw(1),
None,
);
assert!(pending.authoritative_readback_after_http);
}
#[test]
fn authoritative_readback_backoff_is_bounded() {
let delays = (1..=AUTHORITATIVE_READBACK_MAX_ATTEMPTS)
.map(authoritative_readback_backoff_ms)
.collect::<Vec<_>>();
assert_eq!(delays, vec![100, 250, 500, 1_000, 2_000]);
assert!(delays.windows(2).all(|pair| pair[0] < pair[1]));
assert!(delays.iter().sum::<u64>() < 15_000);
}
}
pub use crate::send::upload_props::props_persist_op;