use crate::channel::Channel;
use crate::pending_send::{PendingSend, TimelineReadbackContext};
use crate::send::upload_props::{
FailedMediaOp, PendingMediaCompletionCommit, PendingMediaOp, PendingMediaPrepare,
PendingMediaPut, PendingMediaRetryReset, PendingUpload, UploadTarget,
};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
mod values;
pub use values::{
is_canonical_post_id, ChannelId, ConnState, Cursor, Id26, InflightSync, SendStatus, Seq,
ServerId, TemporaryId,
};
#[path = "correlation.rs"]
mod correlation;
pub use correlation::{
ChannelLifecycleTransition, ChannelSettingsProjection, CorrelationContext,
HydrationPersistSnapshot, MakeTopicProjection, PendingSendReconciliation, SyncTrigger,
};
pub(crate) struct FileUploadProgressState {
pub temporary_id: TemporaryId,
pub channel_id: ChannelId,
pub timeline_readback: TimelineReadbackContext,
pub highest_observed_percent: u8,
pub committed_percent: u8,
pub inflight_persist: Option<(helix_core::Correlation, u8)>,
pub queued_latest: Option<u8>,
}
pub(crate) struct FileUploadProgressPersist {
pub upload_corr: helix_core::Correlation,
pub percent: u8,
}
pub struct ImState {
pub conn: ConnState,
pub connection_id: Option<String>,
pub channels: HashMap<ChannelId, Channel>,
pub pending_sends: HashMap<TemporaryId, PendingSend>,
pub pending_uploads: HashMap<helix_core::Correlation, PendingUpload>,
pub(crate) pending_schedule_media:
HashMap<helix_core::Correlation, crate::schedule_media::PendingScheduleMedia>,
pub pending_media_ops: HashMap<helix_core::Correlation, PendingMediaOp>,
pub(crate) file_upload_progress: HashMap<helix_core::Correlation, FileUploadProgressState>,
pub(crate) media_put_after_progress: HashMap<helix_core::Correlation, PendingMediaPut>,
pub(crate) file_upload_progress_persists:
HashMap<helix_core::Correlation, FileUploadProgressPersist>,
pub pending_media_after_optimistic: HashMap<TemporaryId, Vec<PendingMediaPrepare>>,
pub pending_media_stage_persists: HashMap<helix_core::Correlation, PendingMediaOp>,
pub media_completion_inflight: HashSet<TemporaryId>,
pub queued_media_completions: HashMap<TemporaryId, VecDeque<PendingMediaPut>>,
pub completed_media: HashSet<String>,
pub pending_media_completion_persists:
HashMap<helix_core::Correlation, PendingMediaCompletionCommit>,
pub pending_media_retry_resets: HashMap<helix_core::Correlation, PendingMediaRetryReset>,
pub failed_media_ops: HashMap<(TemporaryId, UploadTarget), FailedMediaOp>,
pub pending_media_rehydrate_corr: Option<helix_core::Correlation>,
pub pending_media_recovery_compensation: Option<(helix_core::Correlation, Vec<FailedMediaOp>)>,
pub media_recovery_ready: bool,
pub media_retry_inflight: HashSet<TemporaryId>,
pub corr_map: HashMap<helix_core::Correlation, CorrelationContext>,
pub(crate) category_chain: crate::category_chain::State,
pub(crate) chain_mutations: HashMap<String, crate::chain::ChainMutation>,
pub(crate) seen_chain_event_ids: HashSet<String>,
pub(crate) pending_chain_event_ids: HashSet<String>,
pub(crate) chain_revisions: HashMap<String, i64>,
pub reply_projection_revisions: HashMap<String, u64>,
pub reply_projection_seen_ids: HashMap<String, HashSet<String>>,
pub backoff_timer: Option<helix_core::TimerId>,
pub ping_timer: Option<helix_core::TimerId>,
pub increment_fetched: HashSet<ChannelId>,
pub increment_order: Vec<ChannelId>,
pub need_sync_skip: HashSet<ChannelId>,
pub increment_target: HashMap<ChannelId, Seq>,
pub backfill_healed: bool,
pub client_seq: u64,
pub sync_scheduler: crate::sync_scheduler::SyncScheduler,
pub recovery_session: crate::sync_session::RecoverySession,
pub(crate) pong_gap_batch: crate::sync::pong_batch::PongGapBatch,
pub about_me_post_ids: Vec<String>,
pub pending_increment_bootstrap_after_scan: bool,
pub(crate) increment_page_supported: bool,
pub(crate) increment_pull: Option<crate::increment_pull::IncrementPull>,
pub startup_channel_projection_ready: bool,
pub(crate) pending_increment_ops: Vec<helix_core::effect::StorageOp>,
pub(crate) pending_increment_projections: Vec<(ChannelId, Vec<u8>)>,
pub(crate) subtopic_sync_active: Option<String>,
pub(crate) subtopic_sync_completed: std::collections::HashSet<String>,
pub(crate) pending_increment_batch_id: Option<String>,
pub(crate) channel_sync_session: Option<crate::channel_sync::ChannelSyncSession>,
pub(crate) channel_sync_generation: u64,
pub(crate) channel_sync_batch_pending: bool,
pub(crate) channel_sync_persist_inflight: usize,
pub(crate) channel_sync_refresh_pending: bool,
pub hydration_pending: HashSet<ChannelId>,
pub hydration_req_ids: HashMap<ChannelId, String>,
pub hydration_emit_channel_increment: HashSet<ChannelId>,
pub hydration_ordered_rosters: HashMap<ChannelId, Vec<serde_json::Value>>,
pub hydration_authority_unreads: HashMap<ChannelId, i64>,
pub(crate) recent_message_coverage:
HashMap<ChannelId, crate::query::local_first::RecentMessageCoverage>,
pub(crate) message_query_generations: HashMap<(ChannelId, String), u64>,
pub(crate) timeline_state: crate::timeline_state::TimelineState,
pub(crate) timeline_navigation_coverage:
HashMap<String, crate::timeline_navigation::TimelineNavigationCoverage>,
pub(crate) timeline_navigation_pending: HashSet<String>,
pub(crate) pending_forward_deliveries: crate::forward::PendingForwardDeliveryLedger,
pub(crate) pending_schedule_requests: BTreeMap<ChannelId, String>,
pub(crate) pending_schedule_cancel_requests: BTreeMap<ChannelId, String>,
pub(crate) committed_schedule_revisions: BTreeMap<ChannelId, u64>,
pub(crate) inflight_schedule_revisions: BTreeMap<ChannelId, u64>,
pub(crate) inflight_channel_creates: HashSet<ChannelId>,
pub(crate) committed_channel_creates: HashSet<ChannelId>,
pub(crate) inflight_member_update_seqs: HashSet<(ChannelId, Seq)>,
pub(crate) committed_member_update_seqs: HashSet<(ChannelId, Seq)>,
pub(crate) inflight_member_role_updates: HashSet<ChannelId>,
pub(crate) committed_post_reads: BTreeMap<String, (i64, String)>,
pub(crate) announcement_versions: BTreeMap<ChannelId, u64>,
pub(crate) announcement_reload_versions: BTreeMap<ChannelId, u64>,
pub(crate) pinned_projection_epochs: BTreeMap<ChannelId, u64>,
pub(crate) query_session_epoch: u64,
}
impl ImState {
pub(crate) fn invalidate_pinned_projection(
&mut self,
account_id: &str,
channel_id: ChannelId,
) -> Option<helix_core::Effect> {
let epoch = self.pinned_projection_epochs.entry(channel_id).or_default();
*epoch = epoch.wrapping_add(1);
crate::query::pinned_projection::projection_key(account_id, channel_id)
.ok()
.map(|key| crate::query::pinned_projection::invalidate_effect(account_id, key))
}
pub fn next_client_seq(&mut self) -> u64 {
self.client_seq += 1;
self.client_seq
}
pub fn new() -> Self {
Self {
conn: ConnState::Disconnected,
connection_id: None,
channels: HashMap::new(),
pending_sends: HashMap::new(),
pending_uploads: HashMap::new(),
pending_schedule_media: HashMap::new(),
pending_media_ops: HashMap::new(),
file_upload_progress: HashMap::new(),
media_put_after_progress: HashMap::new(),
file_upload_progress_persists: HashMap::new(),
pending_media_after_optimistic: HashMap::new(),
pending_media_stage_persists: HashMap::new(),
media_completion_inflight: HashSet::new(),
queued_media_completions: HashMap::new(),
completed_media: HashSet::new(),
pending_media_completion_persists: HashMap::new(),
pending_media_retry_resets: HashMap::new(),
failed_media_ops: HashMap::new(),
pending_media_rehydrate_corr: None,
pending_media_recovery_compensation: None,
media_recovery_ready: false,
media_retry_inflight: HashSet::new(),
corr_map: HashMap::new(),
category_chain: crate::category_chain::State::default(),
chain_mutations: HashMap::new(),
seen_chain_event_ids: HashSet::new(),
pending_chain_event_ids: HashSet::new(),
chain_revisions: HashMap::new(),
reply_projection_revisions: HashMap::new(),
reply_projection_seen_ids: HashMap::new(),
backoff_timer: None,
ping_timer: None,
increment_fetched: HashSet::new(),
increment_order: Vec::new(),
need_sync_skip: HashSet::new(),
increment_target: HashMap::new(),
backfill_healed: false,
client_seq: 0,
sync_scheduler: crate::sync_scheduler::SyncScheduler::new(),
recovery_session: crate::sync_session::RecoverySession::default(),
pong_gap_batch: crate::sync::pong_batch::PongGapBatch::default(),
about_me_post_ids: Vec::new(),
pending_increment_bootstrap_after_scan: false,
increment_page_supported: false,
increment_pull: None,
startup_channel_projection_ready: false,
pending_increment_ops: Vec::new(),
pending_increment_projections: Vec::new(),
subtopic_sync_active: None,
subtopic_sync_completed: HashSet::new(),
pending_increment_batch_id: None,
channel_sync_session: None,
channel_sync_generation: 0,
channel_sync_batch_pending: true,
channel_sync_persist_inflight: 0,
channel_sync_refresh_pending: false,
hydration_pending: HashSet::new(),
hydration_req_ids: HashMap::new(),
hydration_emit_channel_increment: HashSet::new(),
hydration_ordered_rosters: HashMap::new(),
hydration_authority_unreads: HashMap::new(),
recent_message_coverage: HashMap::new(),
message_query_generations: HashMap::new(),
timeline_state: crate::timeline_state::TimelineState::default(),
timeline_navigation_coverage: HashMap::new(),
timeline_navigation_pending: HashSet::new(),
pending_forward_deliveries: crate::forward::PendingForwardDeliveryLedger::default(),
pending_schedule_requests: BTreeMap::new(),
pending_schedule_cancel_requests: BTreeMap::new(),
committed_schedule_revisions: BTreeMap::new(),
inflight_schedule_revisions: BTreeMap::new(),
inflight_channel_creates: HashSet::new(),
committed_channel_creates: HashSet::new(),
inflight_member_update_seqs: HashSet::new(),
committed_member_update_seqs: HashSet::new(),
inflight_member_role_updates: HashSet::new(),
committed_post_reads: BTreeMap::new(),
announcement_versions: BTreeMap::new(),
announcement_reload_versions: BTreeMap::new(),
pinned_projection_epochs: BTreeMap::new(),
query_session_epoch: 0,
}
}
pub fn reset_increment_batch(&mut self) {
self.increment_pull = None;
self.corr_map.retain(|_, context| {
!matches!(
context,
CorrelationContext::IncrementPullHttp | CorrelationContext::IncrementPullPersist
)
});
self.increment_fetched.clear();
self.increment_order.clear();
self.need_sync_skip.clear();
self.increment_target.clear();
self.backfill_healed = false;
self.sync_scheduler.reset();
self.pong_gap_batch.reset();
self.about_me_post_ids.clear();
self.pending_increment_bootstrap_after_scan = false;
self.pending_increment_ops.clear();
self.pending_increment_projections.clear();
self.inflight_member_role_updates.clear();
self.subtopic_sync_active = None;
self.subtopic_sync_completed.clear();
self.pending_increment_batch_id = None;
self.channel_sync_batch_pending = true;
self.channel_sync_persist_inflight = 0;
self.hydration_pending.clear();
self.hydration_req_ids.clear();
self.hydration_emit_channel_increment.clear();
self.hydration_ordered_rosters.clear();
self.hydration_authority_unreads.clear();
self.reply_projection_revisions.clear();
self.reply_projection_seen_ids.clear();
self.seen_chain_event_ids.clear();
self.pending_chain_event_ids.clear();
self.chain_revisions.clear();
self.reset_transport_query_session();
}
pub(crate) fn reset_recent_query_coverage(&mut self) {
self.reset_query_session_correlations();
self.timeline_state.reset();
}
pub(crate) fn reset_transport_query_session(&mut self) {
self.drop_query_session_correlations(true);
self.announcement_reload_versions.clear();
self.pinned_projection_epochs.clear();
}
fn reset_query_session_correlations(&mut self) {
self.drop_query_session_correlations(false);
self.clear_pending_action_state();
}
fn drop_query_session_correlations(&mut self, preserve_deferred_send_http: bool) {
self.recent_message_coverage.clear();
self.timeline_navigation_coverage.clear();
self.timeline_navigation_pending.clear();
self.message_query_generations.clear();
self.corr_map.retain(|_, context| match context {
CorrelationContext::MessageQueryLocal {
deferred_send_http, ..
} if preserve_deferred_send_http => deferred_send_http.is_some(),
CorrelationContext::MessageQueryLocal { .. }
| CorrelationContext::MessageQueryRemote { .. }
| CorrelationContext::MessageQueryCache { .. }
| CorrelationContext::MessageQueryReadback { .. } => false,
_ => true,
});
self.query_session_epoch = self.query_session_epoch.wrapping_add(1);
}
fn clear_pending_action_state(&mut self) {
self.pending_forward_deliveries = crate::forward::PendingForwardDeliveryLedger::default();
self.pending_schedule_requests.clear();
self.pending_schedule_media.clear();
self.pending_schedule_cancel_requests.clear();
self.inflight_schedule_revisions.clear();
self.inflight_channel_creates.clear();
}
pub(crate) fn reset_message_v3_identity(&mut self) {
self.increment_pull = None;
self.recovery_session.invalidate();
self.channels.clear();
self.committed_schedule_revisions.clear();
self.committed_channel_creates.clear();
self.inflight_member_update_seqs.clear();
self.committed_member_update_seqs.clear();
self.inflight_member_role_updates.clear();
self.committed_post_reads.clear();
self.chain_mutations.clear();
self.seen_chain_event_ids.clear();
self.pending_chain_event_ids.clear();
self.chain_revisions.clear();
self.announcement_versions.clear();
self.announcement_reload_versions.clear();
self.sync_scheduler.reset();
self.hydration_pending.clear();
self.hydration_req_ids.clear();
self.hydration_emit_channel_increment.clear();
self.hydration_ordered_rosters.clear();
self.hydration_authority_unreads.clear();
self.pending_increment_ops.clear();
self.pending_increment_projections.clear();
self.subtopic_sync_active = None;
self.subtopic_sync_completed.clear();
self.pending_increment_batch_id = None;
self.channel_sync_session = None;
self.channel_sync_generation = self.channel_sync_generation.wrapping_add(1);
self.channel_sync_batch_pending = false;
self.channel_sync_persist_inflight = 0;
self.channel_sync_refresh_pending = false;
self.reset_recent_query_coverage();
self.corr_map.retain(|_, context| {
!matches!(
context,
CorrelationContext::ChannelPersist { .. }
| CorrelationContext::ChainHttp { .. }
| CorrelationContext::ChainPersist { .. }
| CorrelationContext::ChainMutationPersist { .. }
| CorrelationContext::ChannelTerminalPersist { .. }
| CorrelationContext::MessageV3RevokeChannelPersist { .. }
| CorrelationContext::MessageV3RevokeChannelReadback
| CorrelationContext::MessageV3SyncDialogReadback
| CorrelationContext::SyncPull { .. }
| CorrelationContext::ScanChannelProjections
| CorrelationContext::IncrementMessageTimestampScan { .. }
| CorrelationContext::IncrementPullHttp
| CorrelationContext::IncrementPullPersist
| CorrelationContext::TooLongReload { .. }
| CorrelationContext::TooLongReloadPersist { .. }
| CorrelationContext::IncrementBatchPersist { .. }
| CorrelationContext::ChannelSyncPage { .. }
| CorrelationContext::ChannelSyncPageMemberSnapshot { .. }
| CorrelationContext::IncrementHydrationPersist { .. }
| CorrelationContext::HydrationChannelReadback { .. }
| CorrelationContext::HydrationMemberReadback { .. }
| CorrelationContext::HydrationMessagesReadback { .. }
| CorrelationContext::HydrationCursorReadback { .. }
| CorrelationContext::OutboundIncrementHydration { .. }
| CorrelationContext::UpdateChannelDialogPersist { .. }
| CorrelationContext::MemberProjectionPersist { .. }
| CorrelationContext::MemberProjectionReadback { .. }
| CorrelationContext::CanonicalStreamPersist { .. }
| CorrelationContext::NotifyChannelPersist { .. }
| CorrelationContext::DialogListQuery { .. }
| CorrelationContext::SubtopicsQuery { .. }
| CorrelationContext::PostReadPersist { .. }
| CorrelationContext::OutboundExactPosts { .. }
| CorrelationContext::ExactPostsPersist { .. }
| CorrelationContext::ExactPostsReadback { .. }
| CorrelationContext::OutboundChannelCreate { .. }
| CorrelationContext::ChannelCreatePersist { .. }
| CorrelationContext::ChannelMemberUpdatePersist { .. }
| CorrelationContext::ChannelMemberRoleScan { .. }
| CorrelationContext::ChannelMemberRolePersist { .. }
| CorrelationContext::ChannelMemberRoleChannelReadback { .. }
| CorrelationContext::ChannelMemberRoleReadback { .. }
)
});
}
pub(crate) fn begin_message_query_generation(
&mut self,
channel_id: ChannelId,
window_token: &str,
) -> u64 {
let key = (channel_id, window_token.to_string());
let Some(current) = self.message_query_generations.get(&key).copied() else {
self.message_query_generations.insert(key, 1);
return 1;
};
let Some(next) = current.checked_add(1) else {
self.reset_recent_query_coverage();
self.message_query_generations.insert(key, 1);
return 1;
};
self.message_query_generations.insert(key, next);
next
}
pub(crate) fn is_current_message_query_generation(
&self,
channel_id: ChannelId,
window_token: &str,
generation: u64,
) -> bool {
self.message_query_generations
.get(&(channel_id, window_token.to_string()))
.is_some_and(|current| *current == generation)
}
pub(crate) fn invalidate_recent_message_coverage(&mut self, channel_id: ChannelId) {
self.recent_message_coverage.remove(&channel_id);
}
pub(crate) fn invalidate_timeline_navigation_coverage(&mut self, channel_id: ChannelId) {
self.timeline_navigation_coverage
.retain(|_, coverage| coverage.channel_id != channel_id);
}
}
impl Default for ImState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "state_tests.rs"]
mod id26_tests;
#[cfg(test)]
pub(crate) use id26_tests::{test_channel_id, test_server_id};