1use crate::state::{ChannelId, Seq};
11
12pub(crate) const POST_FIELD_ID: u64 = 1 << 0;
13pub(crate) const POST_FIELD_CHANNEL_ID: u64 = 1 << 1;
14pub(crate) const POST_FIELD_USER_ID: u64 = 1 << 2;
15pub(crate) const POST_FIELD_TYPE: u64 = 1 << 3;
16pub(crate) const POST_FIELD_MESSAGE: u64 = 1 << 4;
17pub(crate) const POST_FIELD_SIMPLE_MESSAGE: u64 = 1 << 5;
18pub(crate) const POST_FIELD_PROPS: u64 = 1 << 6;
19pub(crate) const POST_FIELD_USER_SNAPSHOT: u64 = 1 << 7;
20pub(crate) const POST_FIELD_CREATE_AT: u64 = 1 << 8;
21pub(crate) const POST_FIELD_UPDATE_AT: u64 = 1 << 9;
22pub(crate) const POST_FIELD_READ_BITS: u64 = 1 << 10;
23pub(crate) const POST_FIELD_SNAPSHOT_ID: u64 = 1 << 11;
24pub(crate) const POST_FIELD_VIEWERS: u64 = 1 << 12;
25pub(crate) const POST_FIELD_MENTIONS: u64 = 1 << 13;
26pub(crate) const POST_FIELD_EXPEDITE_MAP: u64 = 1 << 14;
27pub(crate) const POST_FIELD_QUICK_REPLY: u64 = 1 << 15;
28pub(crate) const POST_FIELD_TOPIC: u64 = 1 << 16;
29pub(crate) const POST_FIELD_REPLY_ID: u64 = 1 << 17;
30pub(crate) const POST_FIELD_REPLY_ROOT_ID: u64 = 1 << 18;
31pub(crate) const POST_FIELD_REPLY_FIRST_LEVEL_ID: u64 = 1 << 19;
32pub(crate) const POST_FIELD_REPLIED_MESSAGE: u64 = 1 << 20;
33pub(crate) const POST_FIELD_REPLY_MESSAGES: u64 = 1 << 21;
34pub(crate) const POST_FIELD_REPLY_COUNT: u64 = 1 << 22;
35
36#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
45pub struct PostFields {
46 pub temporary_id: String,
48 pub id: String,
49 pub channel_id: String,
51 pub user_id: String,
52 pub team_id: String,
54 #[serde(rename = "type")]
56 pub msg_type: String,
57 pub message: String,
58 pub simple_message: String,
60 pub props: String,
62 pub user_snapshot: String,
64 pub create_at: i64,
65 pub update_at: i64,
67 pub read_bits: String,
70 pub snapshot_id: String,
72 pub viewers: Vec<String>,
76 pub mentions: Vec<String>,
78 pub expedite_map: String,
80 pub quick_reply: String,
82 pub topic: String,
84 pub reply_id: String,
87 pub reply_root_id: String,
88 pub reply_first_level_id: String,
89 pub replied_message: String,
92 pub reply_messages: String,
93 pub reply_count: i64,
94 #[serde(skip)]
96 pub(crate) present_fields: u64,
97}
98
99impl PostFields {
100 pub(crate) fn has_field(&self, field: u64) -> bool {
102 self.present_fields & field != 0
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct EventEnvelope {
109 pub id: ChannelId,
111 pub channel_id: ChannelId,
112 pub seq: Seq,
113 pub kind: EventKind,
114 pub fields: PostFields,
116 pub msg_id: Option<String>,
122 pub event_id: String,
125 pub actor_id: String,
126 pub occurred_at: i64,
127 pub event_payload: String,
129 pub effect_id: String,
131 pub redacted: bool,
133 pub unread_bump: Option<crate::channel_write::PostChannelUpdate>,
141 pub viewer_user_id: String,
143 pub causation_id: Option<String>,
145}
146
147impl EventEnvelope {
148 pub fn new(channel_id: ChannelId, seq: Seq, kind: EventKind, fields: PostFields) -> Self {
156 let msg_id = if !fields.id.is_empty() {
157 Some(fields.id.clone())
158 } else if !fields.temporary_id.is_empty() {
159 Some(fields.temporary_id.clone())
160 } else {
161 None
162 };
163 Self {
164 id: channel_id,
165 channel_id,
166 seq,
167 kind,
168 fields,
169 msg_id,
170 event_id: String::new(),
171 actor_id: String::new(),
172 occurred_at: 0,
173 event_payload: String::new(),
174 effect_id: String::new(),
175 redacted: false,
176 unread_bump: None,
177 viewer_user_id: String::new(),
178 causation_id: None,
179 }
180 }
181
182 pub fn with_msg_id(mut self, msg_id: Option<String>) -> Self {
187 if let Some(id) = msg_id.filter(|s| !s.is_empty()) {
188 self.msg_id = Some(id);
189 }
190 self
191 }
192
193 pub fn with_event_identity(
195 mut self,
196 event_id: Option<String>,
197 actor_id: Option<String>,
198 occurred_at: i64,
199 event_payload: String,
200 ) -> Self {
201 self.event_id = event_id.filter(|id| !id.is_empty()).unwrap_or_default();
202 self.actor_id = actor_id.filter(|id| !id.is_empty()).unwrap_or_default();
203 self.occurred_at = occurred_at.max(0);
204 self.event_payload = event_payload;
205 self
206 }
207
208 pub fn with_effect(mut self, effect_id: Option<String>, redacted: bool) -> Self {
209 self.effect_id = effect_id.filter(|id| !id.is_empty()).unwrap_or_default();
210 self.redacted = redacted;
211 self
212 }
213
214 pub fn with_unread_bump(
217 mut self,
218 bump: Option<crate::channel_write::PostChannelUpdate>,
219 ) -> Self {
220 self.unread_bump = bump;
221 self
222 }
223
224 pub fn with_viewer_user_id(mut self, viewer_user_id: &str) -> Self {
225 self.viewer_user_id = viewer_user_id.to_string();
226 self
227 }
228
229 pub fn with_causation_id(mut self, causation_id: Option<String>) -> Self {
230 self.causation_id = causation_id.filter(|value| !value.is_empty());
231 self
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct IncrementChannel {
243 pub channel_id: ChannelId,
244 pub last_event_seq: Seq,
245 pub need_sync: bool,
246 pub unread_post_id: Option<String>,
248 pub last_read_seq: Option<i64>,
250 pub projection_revision: Option<u64>,
252 pub raw: bytes::Bytes,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
257pub enum EventKind {
258 PostUpsert,
260 PostEdit,
262 PostRevoke,
264 PostRead,
266 ChannelTerminalClosed,
272 Other(u8),
274}
275
276impl EventKind {
277 pub fn type_num(&self) -> u8 {
279 match self {
280 EventKind::PostUpsert => 1,
281 EventKind::PostEdit => 2,
282 EventKind::PostRevoke => 3,
283 EventKind::PostRead => 6,
284 EventKind::ChannelTerminalClosed => 7,
285 EventKind::Other(n) => *n,
286 }
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Default)]
292pub struct SyncPersona {
293 pub membership_state: String,
294 pub epoch_start_seq: Option<Seq>,
295 pub epoch_end_seq: Option<Seq>,
296 pub member_projection: Option<serde_json::Value>,
297}
298
299#[derive(Debug)]
300pub enum SyncResponse {
301 NoChange { next_seq: Seq, persona: SyncPersona },
303 Events {
305 events: Vec<EventEnvelope>,
306 messages: std::collections::HashMap<String, PostFields>,
312 next_seq: Seq,
316 needs_continuation: bool,
317 persona: SyncPersona,
318 },
319 Snapshot(ChannelSnapshot),
321 TooLong { reset_to: Seq },
323}
324
325#[derive(Debug)]
327pub struct ChannelSnapshot {
328 pub channel_id: ChannelId,
329 pub reset_to: Seq,
330 pub messages: Vec<EventEnvelope>,
331}
332
333pub struct SyncSession {
335 pub channel_id: ChannelId,
336 pub from_seq: Seq,
337 pub corr: helix_core::Correlation,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct RecoverySession {
345 pub phase: RecoveryPhase,
346 pub session_epoch: u64,
347 pub actor_id: String,
348 pub pending_commits: std::collections::BTreeMap<ChannelId, Seq>,
349 completion_published: bool,
350}
351
352impl Default for RecoverySession {
353 fn default() -> Self {
354 Self {
355 phase: RecoveryPhase::Idle,
356 session_epoch: 0,
357 actor_id: String::new(),
358 pending_commits: std::collections::BTreeMap::new(),
359 completion_published: false,
360 }
361 }
362}
363
364impl RecoverySession {
365 pub fn begin(&mut self, actor_id: &str) {
368 self.session_epoch = self.session_epoch.saturating_add(1).max(1);
369 self.actor_id.clear();
370 self.actor_id.push_str(actor_id);
371 self.pending_commits.clear();
372 self.completion_published = false;
373 self.phase = RecoveryPhase::Comparing;
374 }
375
376 pub fn invalidate(&mut self) {
377 self.pending_commits.clear();
378 self.completion_published = false;
379 self.phase = RecoveryPhase::Idle;
380 self.actor_id.clear();
381 }
382
383 pub fn compare(
384 &mut self,
385 local: CommittedRecoveryHead,
386 authority: AuthorityHead,
387 ) -> RecoveryComparison {
388 let comparison = compare_committed_recovery(local, authority);
389 self.phase = match comparison {
390 RecoveryComparison::Equal => RecoveryPhase::Recovered,
391 RecoveryComparison::Pull { .. } => RecoveryPhase::Pulling,
392 RecoveryComparison::AuthorityReloadRequired => RecoveryPhase::Blocked,
393 };
394 comparison
395 }
396
397 pub fn await_commit(&mut self, channel_id: ChannelId, committed_to: Seq) {
398 self.pending_commits.insert(channel_id, committed_to);
399 self.phase = RecoveryPhase::AwaitingCommit;
400 }
401
402 pub fn commit_ok(&mut self, channel_id: ChannelId, committed_to: Seq) -> bool {
406 if self.pending_commits.remove(&channel_id) != Some(committed_to) {
407 return false;
408 }
409 self.phase = if self.pending_commits.is_empty() {
410 RecoveryPhase::Recovered
411 } else {
412 RecoveryPhase::AwaitingCommit
413 };
414 true
415 }
416
417 pub fn commit_failed(&mut self, channel_id: ChannelId) {
418 self.pending_commits.remove(&channel_id);
419 self.phase = RecoveryPhase::Failed;
420 }
421
422 pub fn is_active_for(&self, actor_id: &str) -> bool {
423 self.session_epoch != 0 && self.actor_id == actor_id && !self.actor_id.is_empty()
424 }
425
426 pub fn is_collecting_for(&self, actor_id: &str) -> bool {
427 self.is_active_for(actor_id) && !self.completion_published
428 }
429
430 pub fn is_collecting(&self) -> bool {
431 self.session_epoch != 0 && !self.actor_id.is_empty() && !self.completion_published
432 }
433
434 pub fn has_pending_commits(&self) -> bool {
435 !self.pending_commits.is_empty()
436 }
437
438 pub fn mark_completion_published(&mut self) {
439 self.completion_published = true;
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum RecoveryPhase {
448 Idle,
449 Comparing,
450 Pulling,
451 AwaitingCommit,
452 Recovered,
453 Failed,
454 Blocked,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub struct AuthorityHead {
459 pub event_seq: Seq,
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub struct CommittedRecoveryHead {
464 pub cursor: Seq,
465 pub ledger_to_seq: Seq,
466 pub coverage_to_seq: Seq,
467}
468
469impl CommittedRecoveryHead {
470 pub const fn is_coherent(self) -> bool {
471 self.cursor.0 == self.ledger_to_seq.0 && self.cursor.0 == self.coverage_to_seq.0
472 }
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq)]
476pub enum RecoveryComparison {
477 Equal,
478 Pull {
479 from_exclusive: Seq,
480 to_inclusive: Seq,
481 },
482 AuthorityReloadRequired,
483}
484
485#[derive(Debug, Clone)]
489pub struct SyncBatchFacts {
490 pub channel_id: ChannelId,
491 pub from_exclusive: Seq,
492 pub authority_head: AuthorityHead,
493 pub events: Vec<EventEnvelope>,
494}
495
496impl SyncBatchFacts {
497 pub fn from_events(
498 channel_id: ChannelId,
499 from_exclusive: Seq,
500 authority_head: Seq,
501 events: Vec<EventEnvelope>,
502 ) -> Result<Self, &'static str> {
503 if events.is_empty() {
504 return Err("sync batch facts require at least one event");
505 }
506 let mut previous = from_exclusive;
507 for event in &events {
508 if event.channel_id != channel_id || event.id != channel_id {
509 return Err("sync batch event channel differs from request channel");
510 }
511 if event.seq <= previous {
515 return Err("sync batch event sequence is not strictly increasing");
516 }
517 previous = event.seq;
518 }
519 let last = events.last().map(|event| event.seq).unwrap_or(Seq(0));
520 if authority_head < last {
521 return Err("sync batch authority head precedes final event");
522 }
523 Ok(Self {
524 channel_id,
525 from_exclusive,
526 authority_head: AuthorityHead {
527 event_seq: authority_head,
528 },
529 events,
530 })
531 }
532}
533
534pub const fn compare_committed_recovery(
538 local: CommittedRecoveryHead,
539 authority: AuthorityHead,
540) -> RecoveryComparison {
541 if !local.is_coherent() || local.cursor.0 > authority.event_seq.0 {
542 return RecoveryComparison::AuthorityReloadRequired;
543 }
544 if local.cursor.0 == authority.event_seq.0 {
545 RecoveryComparison::Equal
546 } else {
547 RecoveryComparison::Pull {
548 from_exclusive: local.cursor,
549 to_inclusive: authority.event_seq,
550 }
551 }
552}