use crate::state::{ChannelId, Seq};
pub(crate) const POST_FIELD_ID: u64 = 1 << 0;
pub(crate) const POST_FIELD_CHANNEL_ID: u64 = 1 << 1;
pub(crate) const POST_FIELD_USER_ID: u64 = 1 << 2;
pub(crate) const POST_FIELD_TYPE: u64 = 1 << 3;
pub(crate) const POST_FIELD_MESSAGE: u64 = 1 << 4;
pub(crate) const POST_FIELD_SIMPLE_MESSAGE: u64 = 1 << 5;
pub(crate) const POST_FIELD_PROPS: u64 = 1 << 6;
pub(crate) const POST_FIELD_USER_SNAPSHOT: u64 = 1 << 7;
pub(crate) const POST_FIELD_CREATE_AT: u64 = 1 << 8;
pub(crate) const POST_FIELD_UPDATE_AT: u64 = 1 << 9;
pub(crate) const POST_FIELD_READ_BITS: u64 = 1 << 10;
pub(crate) const POST_FIELD_SNAPSHOT_ID: u64 = 1 << 11;
pub(crate) const POST_FIELD_VIEWERS: u64 = 1 << 12;
pub(crate) const POST_FIELD_MENTIONS: u64 = 1 << 13;
pub(crate) const POST_FIELD_EXPEDITE_MAP: u64 = 1 << 14;
pub(crate) const POST_FIELD_QUICK_REPLY: u64 = 1 << 15;
pub(crate) const POST_FIELD_TOPIC: u64 = 1 << 16;
pub(crate) const POST_FIELD_REPLY_ID: u64 = 1 << 17;
pub(crate) const POST_FIELD_REPLY_ROOT_ID: u64 = 1 << 18;
pub(crate) const POST_FIELD_REPLY_FIRST_LEVEL_ID: u64 = 1 << 19;
pub(crate) const POST_FIELD_REPLIED_MESSAGE: u64 = 1 << 20;
pub(crate) const POST_FIELD_REPLY_MESSAGES: u64 = 1 << 21;
pub(crate) const POST_FIELD_REPLY_COUNT: u64 = 1 << 22;
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
pub struct PostFields {
pub temporary_id: String,
pub id: String,
pub channel_id: String,
pub user_id: String,
#[serde(rename = "type")]
pub msg_type: String,
pub message: String,
pub simple_message: String,
pub props: String,
pub user_snapshot: String,
pub create_at: i64,
pub update_at: i64,
pub read_bits: String,
pub snapshot_id: String,
pub viewers: Vec<String>,
pub mentions: Vec<String>,
pub expedite_map: String,
pub quick_reply: String,
pub topic: String,
pub reply_id: String,
pub reply_root_id: String,
pub reply_first_level_id: String,
pub replied_message: String,
pub reply_messages: String,
pub reply_count: i64,
#[serde(skip)]
pub(crate) present_fields: u64,
}
impl PostFields {
pub(crate) fn has_field(&self, field: u64) -> bool {
self.present_fields & field != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventEnvelope {
pub id: ChannelId,
pub channel_id: ChannelId,
pub seq: Seq,
pub kind: EventKind,
pub fields: PostFields,
pub msg_id: Option<String>,
pub event_id: String,
pub actor_id: String,
pub occurred_at: i64,
pub event_payload: String,
pub effect_id: String,
pub redacted: bool,
pub unread_bump: Option<crate::channel_write::PostChannelUpdate>,
pub viewer_user_id: String,
pub causation_id: Option<String>,
}
impl EventEnvelope {
pub fn new(channel_id: ChannelId, seq: Seq, kind: EventKind, fields: PostFields) -> Self {
let msg_id = if !fields.id.is_empty() {
Some(fields.id.clone())
} else if !fields.temporary_id.is_empty() {
Some(fields.temporary_id.clone())
} else {
None
};
Self {
id: channel_id,
channel_id,
seq,
kind,
fields,
msg_id,
event_id: String::new(),
actor_id: String::new(),
occurred_at: 0,
event_payload: String::new(),
effect_id: String::new(),
redacted: false,
unread_bump: None,
viewer_user_id: String::new(),
causation_id: None,
}
}
pub fn with_msg_id(mut self, msg_id: Option<String>) -> Self {
if let Some(id) = msg_id.filter(|s| !s.is_empty()) {
self.msg_id = Some(id);
}
self
}
pub fn with_event_identity(
mut self,
event_id: Option<String>,
actor_id: Option<String>,
occurred_at: i64,
event_payload: String,
) -> Self {
self.event_id = event_id.filter(|id| !id.is_empty()).unwrap_or_default();
self.actor_id = actor_id.filter(|id| !id.is_empty()).unwrap_or_default();
self.occurred_at = occurred_at.max(0);
self.event_payload = event_payload;
self
}
pub fn with_effect(mut self, effect_id: Option<String>, redacted: bool) -> Self {
self.effect_id = effect_id.filter(|id| !id.is_empty()).unwrap_or_default();
self.redacted = redacted;
self
}
pub fn with_unread_bump(
mut self,
bump: Option<crate::channel_write::PostChannelUpdate>,
) -> Self {
self.unread_bump = bump;
self
}
pub fn with_viewer_user_id(mut self, viewer_user_id: &str) -> Self {
self.viewer_user_id = viewer_user_id.to_string();
self
}
pub fn with_causation_id(mut self, causation_id: Option<String>) -> Self {
self.causation_id = causation_id.filter(|value| !value.is_empty());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IncrementChannel {
pub channel_id: ChannelId,
pub last_event_seq: Seq,
pub need_sync: bool,
pub raw: bytes::Bytes,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventKind {
PostUpsert,
PostEdit,
PostRevoke,
PostRead,
ChannelTerminalClosed,
Other(u8),
}
impl EventKind {
pub fn type_num(&self) -> u8 {
match self {
EventKind::PostUpsert => 1,
EventKind::PostEdit => 2,
EventKind::PostRevoke => 3,
EventKind::PostRead => 6,
EventKind::ChannelTerminalClosed => 7,
EventKind::Other(n) => *n,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SyncPersona {
pub membership_state: String,
pub epoch_start_seq: Option<Seq>,
pub epoch_end_seq: Option<Seq>,
pub member_projection: Option<serde_json::Value>,
}
#[derive(Debug)]
pub enum SyncResponse {
NoChange { next_seq: Seq, persona: SyncPersona },
Events {
events: Vec<EventEnvelope>,
messages: std::collections::HashMap<String, PostFields>,
next_seq: Seq,
needs_continuation: bool,
persona: SyncPersona,
},
Snapshot(ChannelSnapshot),
TooLong { reset_to: Seq },
}
#[derive(Debug)]
pub struct ChannelSnapshot {
pub channel_id: ChannelId,
pub reset_to: Seq,
pub messages: Vec<EventEnvelope>,
}
pub struct SyncSession {
pub channel_id: ChannelId,
pub from_seq: Seq,
pub corr: helix_core::Correlation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecoverySession {
pub phase: RecoveryPhase,
pub session_epoch: u64,
pub actor_id: String,
pub pending_commits: std::collections::BTreeMap<ChannelId, Seq>,
completion_published: bool,
}
impl Default for RecoverySession {
fn default() -> Self {
Self {
phase: RecoveryPhase::Idle,
session_epoch: 0,
actor_id: String::new(),
pending_commits: std::collections::BTreeMap::new(),
completion_published: false,
}
}
}
impl RecoverySession {
pub fn begin(&mut self, actor_id: &str) {
self.session_epoch = self.session_epoch.saturating_add(1).max(1);
self.actor_id.clear();
self.actor_id.push_str(actor_id);
self.pending_commits.clear();
self.completion_published = false;
self.phase = RecoveryPhase::Comparing;
}
pub fn invalidate(&mut self) {
self.pending_commits.clear();
self.completion_published = false;
self.phase = RecoveryPhase::Idle;
self.actor_id.clear();
}
pub fn compare(
&mut self,
local: CommittedRecoveryHead,
authority: AuthorityHead,
) -> RecoveryComparison {
let comparison = compare_committed_recovery(local, authority);
self.phase = match comparison {
RecoveryComparison::Equal => RecoveryPhase::Recovered,
RecoveryComparison::Pull { .. } => RecoveryPhase::Pulling,
RecoveryComparison::AuthorityReloadRequired => RecoveryPhase::Blocked,
};
comparison
}
pub fn await_commit(&mut self, channel_id: ChannelId, committed_to: Seq) {
self.pending_commits.insert(channel_id, committed_to);
self.phase = RecoveryPhase::AwaitingCommit;
}
pub fn commit_ok(&mut self, channel_id: ChannelId, committed_to: Seq) -> bool {
if self.pending_commits.remove(&channel_id) != Some(committed_to) {
return false;
}
self.phase = if self.pending_commits.is_empty() {
RecoveryPhase::Recovered
} else {
RecoveryPhase::AwaitingCommit
};
true
}
pub fn commit_failed(&mut self, channel_id: ChannelId) {
self.pending_commits.remove(&channel_id);
self.phase = RecoveryPhase::Failed;
}
pub fn is_active_for(&self, actor_id: &str) -> bool {
self.session_epoch != 0 && self.actor_id == actor_id && !self.actor_id.is_empty()
}
pub fn is_collecting_for(&self, actor_id: &str) -> bool {
self.is_active_for(actor_id) && !self.completion_published
}
pub fn is_collecting(&self) -> bool {
self.session_epoch != 0 && !self.actor_id.is_empty() && !self.completion_published
}
pub fn has_pending_commits(&self) -> bool {
!self.pending_commits.is_empty()
}
pub fn mark_completion_published(&mut self) {
self.completion_published = true;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryPhase {
Idle,
Comparing,
Pulling,
AwaitingCommit,
Recovered,
Failed,
Blocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AuthorityHead {
pub event_seq: Seq,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommittedRecoveryHead {
pub cursor: Seq,
pub ledger_to_seq: Seq,
pub coverage_to_seq: Seq,
}
impl CommittedRecoveryHead {
pub const fn is_coherent(self) -> bool {
self.cursor.0 == self.ledger_to_seq.0 && self.cursor.0 == self.coverage_to_seq.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryComparison {
Equal,
Pull {
from_exclusive: Seq,
to_inclusive: Seq,
},
AuthorityReloadRequired,
}
#[derive(Debug, Clone)]
pub struct SyncBatchFacts {
pub channel_id: ChannelId,
pub from_exclusive: Seq,
pub authority_head: AuthorityHead,
pub events: Vec<EventEnvelope>,
}
impl SyncBatchFacts {
pub fn from_events(
channel_id: ChannelId,
from_exclusive: Seq,
authority_head: Seq,
events: Vec<EventEnvelope>,
) -> Result<Self, &'static str> {
if events.is_empty() {
return Err("sync batch facts require at least one event");
}
let mut previous = from_exclusive;
for event in &events {
if event.channel_id != channel_id || event.id != channel_id {
return Err("sync batch event channel differs from request channel");
}
if event.seq <= previous {
return Err("sync batch event sequence is not strictly increasing");
}
previous = event.seq;
}
let last = events.last().map(|event| event.seq).unwrap_or(Seq(0));
if authority_head < last {
return Err("sync batch authority head precedes final event");
}
Ok(Self {
channel_id,
from_exclusive,
authority_head: AuthorityHead {
event_seq: authority_head,
},
events,
})
}
}
pub const fn compare_committed_recovery(
local: CommittedRecoveryHead,
authority: AuthorityHead,
) -> RecoveryComparison {
if !local.is_coherent() || local.cursor.0 > authority.event_seq.0 {
return RecoveryComparison::AuthorityReloadRequired;
}
if local.cursor.0 == authority.event_seq.0 {
RecoveryComparison::Equal
} else {
RecoveryComparison::Pull {
from_exclusive: local.cursor,
to_inclusive: authority.event_seq,
}
}
}