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 #[serde(rename = "type")]
54 pub msg_type: String,
55 pub message: String,
56 pub simple_message: String,
58 pub props: String,
60 pub user_snapshot: String,
62 pub create_at: i64,
63 pub update_at: i64,
65 pub read_bits: String,
68 pub snapshot_id: String,
70 pub viewers: Vec<String>,
74 pub mentions: Vec<String>,
76 pub expedite_map: String,
78 pub quick_reply: String,
80 pub topic: String,
82 pub reply_id: String,
85 pub reply_root_id: String,
86 pub reply_first_level_id: String,
87 pub replied_message: String,
90 pub reply_messages: String,
91 pub reply_count: i64,
92 #[serde(skip)]
94 pub(crate) present_fields: u64,
95}
96
97impl PostFields {
98 pub(crate) fn has_field(&self, field: u64) -> bool {
100 self.present_fields & field != 0
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct EventEnvelope {
107 pub id: ChannelId,
109 pub channel_id: ChannelId,
110 pub seq: Seq,
111 pub kind: EventKind,
112 pub fields: PostFields,
114 pub msg_id: Option<String>,
120 pub event_id: String,
123 pub actor_id: String,
124 pub occurred_at: i64,
125 pub event_payload: String,
127 pub effect_id: String,
129 pub redacted: bool,
131 pub unread_bump: Option<crate::channel_write::PostChannelUpdate>,
139 pub viewer_user_id: String,
141 pub causation_id: Option<String>,
143}
144
145impl EventEnvelope {
146 pub fn new(channel_id: ChannelId, seq: Seq, kind: EventKind, fields: PostFields) -> Self {
154 let msg_id = if !fields.id.is_empty() {
155 Some(fields.id.clone())
156 } else if !fields.temporary_id.is_empty() {
157 Some(fields.temporary_id.clone())
158 } else {
159 None
160 };
161 Self {
162 id: channel_id,
163 channel_id,
164 seq,
165 kind,
166 fields,
167 msg_id,
168 event_id: String::new(),
169 actor_id: String::new(),
170 occurred_at: 0,
171 event_payload: String::new(),
172 effect_id: String::new(),
173 redacted: false,
174 unread_bump: None,
175 viewer_user_id: String::new(),
176 causation_id: None,
177 }
178 }
179
180 pub fn with_msg_id(mut self, msg_id: Option<String>) -> Self {
185 if let Some(id) = msg_id.filter(|s| !s.is_empty()) {
186 self.msg_id = Some(id);
187 }
188 self
189 }
190
191 pub fn with_event_identity(
193 mut self,
194 event_id: Option<String>,
195 actor_id: Option<String>,
196 occurred_at: i64,
197 event_payload: String,
198 ) -> Self {
199 self.event_id = event_id.filter(|id| !id.is_empty()).unwrap_or_default();
200 self.actor_id = actor_id.filter(|id| !id.is_empty()).unwrap_or_default();
201 self.occurred_at = occurred_at.max(0);
202 self.event_payload = event_payload;
203 self
204 }
205
206 pub fn with_effect(mut self, effect_id: Option<String>, redacted: bool) -> Self {
207 self.effect_id = effect_id.filter(|id| !id.is_empty()).unwrap_or_default();
208 self.redacted = redacted;
209 self
210 }
211
212 pub fn with_unread_bump(
215 mut self,
216 bump: Option<crate::channel_write::PostChannelUpdate>,
217 ) -> Self {
218 self.unread_bump = bump;
219 self
220 }
221
222 pub fn with_viewer_user_id(mut self, viewer_user_id: &str) -> Self {
223 self.viewer_user_id = viewer_user_id.to_string();
224 self
225 }
226
227 pub fn with_causation_id(mut self, causation_id: Option<String>) -> Self {
228 self.causation_id = causation_id.filter(|value| !value.is_empty());
229 self
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct IncrementChannel {
240 pub channel_id: ChannelId,
241 pub last_event_seq: Seq,
242 pub need_sync: bool,
243 pub raw: bytes::Bytes,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub enum EventKind {
249 PostUpsert,
251 PostEdit,
253 PostRevoke,
255 PostRead,
257 ChannelTerminalClosed,
263 Other(u8),
265}
266
267impl EventKind {
268 pub fn type_num(&self) -> u8 {
270 match self {
271 EventKind::PostUpsert => 1,
272 EventKind::PostEdit => 2,
273 EventKind::PostRevoke => 3,
274 EventKind::PostRead => 6,
275 EventKind::ChannelTerminalClosed => 7,
276 EventKind::Other(n) => *n,
277 }
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Default)]
283pub struct SyncPersona {
284 pub membership_state: String,
285 pub epoch_start_seq: Option<Seq>,
286 pub epoch_end_seq: Option<Seq>,
287 pub member_projection: Option<serde_json::Value>,
288}
289
290#[derive(Debug)]
291pub enum SyncResponse {
292 NoChange { next_seq: Seq, persona: SyncPersona },
294 Events {
296 events: Vec<EventEnvelope>,
297 messages: std::collections::HashMap<String, PostFields>,
303 next_seq: Seq,
307 needs_continuation: bool,
308 persona: SyncPersona,
309 },
310 Snapshot(ChannelSnapshot),
312 TooLong { reset_to: Seq },
314}
315
316#[derive(Debug)]
318pub struct ChannelSnapshot {
319 pub channel_id: ChannelId,
320 pub reset_to: Seq,
321 pub messages: Vec<EventEnvelope>,
322}
323
324pub struct SyncSession {
326 pub channel_id: ChannelId,
327 pub from_seq: Seq,
328 pub corr: helix_core::Correlation,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct RecoverySession {
336 pub phase: RecoveryPhase,
337 pub session_epoch: u64,
338 pub actor_id: String,
339 pub pending_commits: std::collections::BTreeMap<ChannelId, Seq>,
340 completion_published: bool,
341}
342
343impl Default for RecoverySession {
344 fn default() -> Self {
345 Self {
346 phase: RecoveryPhase::Idle,
347 session_epoch: 0,
348 actor_id: String::new(),
349 pending_commits: std::collections::BTreeMap::new(),
350 completion_published: false,
351 }
352 }
353}
354
355impl RecoverySession {
356 pub fn begin(&mut self, actor_id: &str) {
359 self.session_epoch = self.session_epoch.saturating_add(1).max(1);
360 self.actor_id.clear();
361 self.actor_id.push_str(actor_id);
362 self.pending_commits.clear();
363 self.completion_published = false;
364 self.phase = RecoveryPhase::Comparing;
365 }
366
367 pub fn invalidate(&mut self) {
368 self.pending_commits.clear();
369 self.completion_published = false;
370 self.phase = RecoveryPhase::Idle;
371 self.actor_id.clear();
372 }
373
374 pub fn compare(
375 &mut self,
376 local: CommittedRecoveryHead,
377 authority: AuthorityHead,
378 ) -> RecoveryComparison {
379 let comparison = compare_committed_recovery(local, authority);
380 self.phase = match comparison {
381 RecoveryComparison::Equal => RecoveryPhase::Recovered,
382 RecoveryComparison::Pull { .. } => RecoveryPhase::Pulling,
383 RecoveryComparison::AuthorityReloadRequired => RecoveryPhase::Blocked,
384 };
385 comparison
386 }
387
388 pub fn await_commit(&mut self, channel_id: ChannelId, committed_to: Seq) {
389 self.pending_commits.insert(channel_id, committed_to);
390 self.phase = RecoveryPhase::AwaitingCommit;
391 }
392
393 pub fn commit_ok(&mut self, channel_id: ChannelId, committed_to: Seq) -> bool {
397 if self.pending_commits.remove(&channel_id) != Some(committed_to) {
398 return false;
399 }
400 self.phase = if self.pending_commits.is_empty() {
401 RecoveryPhase::Recovered
402 } else {
403 RecoveryPhase::AwaitingCommit
404 };
405 true
406 }
407
408 pub fn commit_failed(&mut self, channel_id: ChannelId) {
409 self.pending_commits.remove(&channel_id);
410 self.phase = RecoveryPhase::Failed;
411 }
412
413 pub fn is_active_for(&self, actor_id: &str) -> bool {
414 self.session_epoch != 0 && self.actor_id == actor_id && !self.actor_id.is_empty()
415 }
416
417 pub fn is_collecting_for(&self, actor_id: &str) -> bool {
418 self.is_active_for(actor_id) && !self.completion_published
419 }
420
421 pub fn is_collecting(&self) -> bool {
422 self.session_epoch != 0 && !self.actor_id.is_empty() && !self.completion_published
423 }
424
425 pub fn has_pending_commits(&self) -> bool {
426 !self.pending_commits.is_empty()
427 }
428
429 pub fn mark_completion_published(&mut self) {
430 self.completion_published = true;
431 }
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub enum RecoveryPhase {
439 Idle,
440 Comparing,
441 Pulling,
442 AwaitingCommit,
443 Recovered,
444 Failed,
445 Blocked,
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct AuthorityHead {
450 pub event_seq: Seq,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
454pub struct CommittedRecoveryHead {
455 pub cursor: Seq,
456 pub ledger_to_seq: Seq,
457 pub coverage_to_seq: Seq,
458}
459
460impl CommittedRecoveryHead {
461 pub const fn is_coherent(self) -> bool {
462 self.cursor.0 == self.ledger_to_seq.0 && self.cursor.0 == self.coverage_to_seq.0
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum RecoveryComparison {
468 Equal,
469 Pull {
470 from_exclusive: Seq,
471 to_inclusive: Seq,
472 },
473 AuthorityReloadRequired,
474}
475
476#[derive(Debug, Clone)]
480pub struct SyncBatchFacts {
481 pub channel_id: ChannelId,
482 pub from_exclusive: Seq,
483 pub authority_head: AuthorityHead,
484 pub events: Vec<EventEnvelope>,
485}
486
487impl SyncBatchFacts {
488 pub fn from_events(
489 channel_id: ChannelId,
490 from_exclusive: Seq,
491 authority_head: Seq,
492 events: Vec<EventEnvelope>,
493 ) -> Result<Self, &'static str> {
494 if events.is_empty() {
495 return Err("sync batch facts require at least one event");
496 }
497 let mut previous = from_exclusive;
498 for event in &events {
499 if event.channel_id != channel_id || event.id != channel_id {
500 return Err("sync batch event channel differs from request channel");
501 }
502 if event.seq <= previous {
506 return Err("sync batch event sequence is not strictly increasing");
507 }
508 previous = event.seq;
509 }
510 let last = events.last().map(|event| event.seq).unwrap_or(Seq(0));
511 if authority_head < last {
512 return Err("sync batch authority head precedes final event");
513 }
514 Ok(Self {
515 channel_id,
516 from_exclusive,
517 authority_head: AuthorityHead {
518 event_seq: authority_head,
519 },
520 events,
521 })
522 }
523}
524
525pub const fn compare_committed_recovery(
529 local: CommittedRecoveryHead,
530 authority: AuthorityHead,
531) -> RecoveryComparison {
532 if !local.is_coherent() || local.cursor.0 > authority.event_seq.0 {
533 return RecoveryComparison::AuthorityReloadRequired;
534 }
535 if local.cursor.0 == authority.event_seq.0 {
536 RecoveryComparison::Equal
537 } else {
538 RecoveryComparison::Pull {
539 from_exclusive: local.cursor,
540 to_inclusive: authority.event_seq,
541 }
542 }
543}