1use std::collections::BTreeMap;
9
10use secrecy::SecretString;
11use serde::{Deserialize, Serialize};
12
13mod connection_lifecycle;
14mod outbound_batch;
15mod request_flow;
16mod request_tracker;
17mod server_events;
18mod sticky_replay;
19mod timers;
20
21use outbound_batch::{FlushMode, OutboundBatcher};
22use request_tracker::RequestTracker;
23use sticky_replay::StickyReplayState;
24use timers::RequestTimeoutId;
25
26use crate::{
27 shared::{
28 AvailableFeatures, DownloadStates, JsonPayload, RecordingState, RecordingStateUpdate,
29 StreamType, UserId, UserInfo,
30 },
31 signaling::{
32 AuthPayload, ClientBroadcastPayload, ClientEnvelope, ClientMessage, MAX_ENVELOPE_BATCH_LEN,
33 NegotiationUploadSlot, PeerSnapshot, RequestId, ServerEnvelope, StreamIntentPayload,
34 SubscribePayload, TrackBinding, WebSocketCloseCode, WelcomePayload, decode_envelope_batch,
35 },
36 wire::ServerMessage,
37};
38
39pub const RECOVERY_TIMER_ID: u32 = 1;
41const BATCH_FLUSH_TIMER_ID: u32 = 2;
42const INITIAL_RECOVERY_DELAY_MS: u32 = 1_000;
43const MAX_RECOVERY_DELAY_MS: u32 = 30_000;
44const BATCH_FLUSH_DELAY_MS: u32 = 100;
45const REQUEST_TIMEOUT_MS: u32 = 5_000;
46const MAX_OUTBOUND_BATCH_LEN: usize = 16;
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
52#[serde(tag = "kind", rename_all = "camelCase")]
53pub enum Command {
54 SendWebSocket {
56 frame: String,
57 },
58 ApplyNegotiation {
60 #[serde(rename = "requestId")]
61 request_id: RequestId,
62 #[serde(rename = "negotiationKind")]
63 kind: NegotiationKind,
64 sdp: String,
65 #[serde(rename = "uploadSlots")]
66 upload_slots: Vec<NegotiationUploadSlot>,
67 },
68 ClosePeerConnection,
69 CloseWebSocket {
70 code: u16,
71 },
72 EmitStateChange {
75 state: ConnectionState,
76 cause: Option<String>,
77 },
78 SetAvailableFeatures {
79 #[serde(serialize_with = "crate::host_bridge::serialize_features")]
80 features: AvailableFeatures,
81 },
82 SetRecordingState {
83 #[serde(serialize_with = "crate::host_bridge::serialize_recording_state")]
84 state: RecordingState,
85 },
86 #[serde(rename = "emitUpdate")]
88 EmitEvent {
89 #[serde(
90 rename = "update",
91 serialize_with = "crate::host_bridge::serialize_protocol_event"
92 )]
93 event: ProtocolEvent,
94 },
95 BeginPendingRequest {
96 request: PendingRequest,
97 },
98 CompletePendingRequest {
100 #[serde(rename = "requestId")]
101 request_id: RequestId,
102 #[serde(rename = "timeoutTimerId")]
103 timeout_timer_id: u32,
104 ok: bool,
105 },
106 RejectPendingRequest {
108 #[serde(rename = "requestId")]
109 request_id: RequestId,
110 #[serde(rename = "timeoutTimerId")]
111 timeout_timer_id: u32,
112 error: String,
113 },
114 ScheduleTimer {
117 id: u32,
118 ms: u32,
119 },
120 CancelTimer {
121 id: u32,
122 },
123 Connect {
125 url: String,
126 },
127}
128
129pub(crate) type Commands = Vec<Command>;
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum ConnectionState {
134 Disconnected,
135 Connecting,
136 Authenticated,
137 Connected,
138 Recovering,
139 Closed,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum ProtocolEvent {
144 PeerSnapshot {
145 peers: Vec<PeerSnapshot>,
146 },
147 TrackSnapshot {
148 bindings: Vec<TrackBinding>,
149 },
150 PeerInfo {
151 user_id: UserId,
152 info: UserInfo,
153 },
154 PeerLeft {
155 user_id: UserId,
156 },
157 Broadcast {
158 sender_id: UserId,
159 message: JsonPayload,
160 },
161 RecordingStateChanged {
162 state: RecordingStateUpdate,
163 },
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub enum NegotiationKind {
169 Offer,
170 Renegotiate,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub(crate) enum PendingRequestKind {
175 StartRecording,
176 StopRecording,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct PendingRequest {
182 pub request_id: RequestId,
183 pub timeout_timer_id: u32,
184 pub timeout_ms: u32,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
188struct ConnectContext {
189 url: String,
190 jwt: String,
191 room: Option<String>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195enum ProtocolPhase {
196 Disconnected,
197 Connecting,
198 Authenticated(Option<RequestId>),
199 Connected(Option<RequestId>),
200 Recovering,
201 Closed,
202}
203
204impl ProtocolPhase {
205 const fn connection_state(&self) -> ConnectionState {
206 match self {
207 Self::Disconnected => ConnectionState::Disconnected,
208 Self::Connecting => ConnectionState::Connecting,
209 Self::Authenticated(_) => ConnectionState::Authenticated,
210 Self::Connected(_) => ConnectionState::Connected,
211 Self::Recovering => ConnectionState::Recovering,
212 Self::Closed => ConnectionState::Closed,
213 }
214 }
215
216 const fn is_awaiting_welcome(&self) -> bool {
217 matches!(self, Self::Connecting | Self::Recovering)
218 }
219
220 const fn can_send_client_messages(&self) -> bool {
221 matches!(self, Self::Authenticated(_) | Self::Connected(_))
222 }
223
224 const fn can_enter_connected(&self) -> bool {
225 matches!(self, Self::Authenticated(None))
226 }
227
228 fn accept_negotiation(
229 &mut self,
230 request_id: &RequestId,
231 kind: NegotiationKind,
232 ) -> Result<(), NegotiationRejection> {
233 match (self, kind) {
234 (Self::Authenticated(pending), NegotiationKind::Offer)
235 | (Self::Connected(pending), NegotiationKind::Renegotiate) => {
236 if pending.is_some() {
237 return Err(NegotiationRejection::ProtocolError);
238 }
239 *pending = Some(request_id.clone());
240 Ok(())
241 }
242 (Self::Authenticated(_), NegotiationKind::Renegotiate)
243 | (Self::Connected(_), NegotiationKind::Offer) => {
244 Err(NegotiationRejection::ProtocolError)
245 }
246 (
247 Self::Disconnected | Self::Connecting | Self::Recovering | Self::Closed,
248 NegotiationKind::Offer | NegotiationKind::Renegotiate,
249 ) => Err(NegotiationRejection::Ignored),
250 }
251 }
252
253 fn resolve_negotiation(&mut self, request_id: &RequestId, kind: NegotiationKind) -> bool {
254 let pending = match (self, kind) {
255 (Self::Authenticated(pending), NegotiationKind::Offer)
256 | (Self::Connected(pending), NegotiationKind::Renegotiate) => pending,
257 (Self::Authenticated(_), NegotiationKind::Renegotiate)
258 | (Self::Connected(_), NegotiationKind::Offer)
259 | (
260 Self::Disconnected | Self::Connecting | Self::Recovering | Self::Closed,
261 NegotiationKind::Offer | NegotiationKind::Renegotiate,
262 ) => return false,
263 };
264 if pending.as_ref() != Some(request_id) {
265 return false;
266 }
267 *pending = None;
268 true
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub(super) enum NegotiationRejection {
274 Ignored,
275 ProtocolError,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct ProtocolCore {
284 phase: ProtocolPhase,
286 track_users_by_mid: BTreeMap<String, UserId>,
291 sticky_replay: StickyReplayState,
299 connect_context: Option<ConnectContext>,
305 recovery_delay_ms: u32,
311 outbound_batch: OutboundBatcher,
318 request_tracker: RequestTracker,
324}
325
326impl Default for ProtocolCore {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332impl ProtocolCore {
333 #[must_use]
339 pub fn new() -> Self {
340 Self {
341 phase: ProtocolPhase::Disconnected,
342 track_users_by_mid: BTreeMap::new(),
343 sticky_replay: StickyReplayState::new(),
344 connect_context: None,
345 recovery_delay_ms: INITIAL_RECOVERY_DELAY_MS,
346 outbound_batch: OutboundBatcher::new(),
347 request_tracker: RequestTracker::new(),
348 }
349 }
350
351 #[must_use]
352 pub const fn state(&self) -> ConnectionState {
353 self.phase.connection_state()
354 }
355
356 pub fn on_ws_open(&mut self) -> Vec<Command> {
361 if !self.phase.is_awaiting_welcome() {
362 return Vec::new();
363 }
364 let Some(connect_context) = self.connect_context.as_ref() else {
365 return Vec::new();
366 };
367 self.enqueue_client_message(
368 ClientMessage::Auth(AuthPayload {
369 jwt: SecretString::from(connect_context.jwt.clone()),
370 channel: connect_context.room.clone(),
371 }),
372 FlushMode::Immediate,
373 )
374 }
375
376 pub fn on_ws_message(&mut self, frame: &str) -> Vec<Command> {
382 let Ok(batch) = decode_envelope_batch(frame, MAX_ENVELOPE_BATCH_LEN) else {
383 return close_for_protocol_error();
384 };
385 let Ok(envelopes) = batch
386 .into_iter()
387 .map(ServerEnvelope::decode)
388 .collect::<Result<Vec<_>, _>>()
389 else {
390 return close_for_protocol_error();
391 };
392 let mut commands = Vec::new();
393 for envelope in envelopes {
394 match envelope {
395 ServerEnvelope::Message(message) => {
396 if self.phase.is_awaiting_welcome()
397 && !matches!(message, ServerMessage::Welcome(_))
398 {
399 return close_for_protocol_error();
400 }
401 commands.extend(server_events::handle_server_message(self, message));
402 }
403 ServerEnvelope::Request {
404 request_id,
405 request,
406 } => {
407 commands.extend(request_flow::handle_server_request(
408 self, request_id, request,
409 ));
410 }
411 ServerEnvelope::Response {
412 response_to,
413 response,
414 } => {
415 commands.extend(request_flow::handle_server_response(
416 self,
417 &response_to,
418 response,
419 ));
420 }
421 }
422 }
423 commands
424 }
425
426 fn accept_welcome(&mut self, payload: WelcomePayload) -> Commands {
427 if !self.phase.is_awaiting_welcome() {
428 return Vec::new();
429 }
430 let WelcomePayload {
431 features,
432 recording,
433 peers,
434 } = payload;
435 self.recovery_delay_ms = INITIAL_RECOVERY_DELAY_MS;
436 self.phase = ProtocolPhase::Authenticated(None);
437
438 let mut commands = vec![
439 Command::SetAvailableFeatures { features },
440 Command::SetRecordingState { state: recording },
441 Command::EmitStateChange {
442 state: self.phase.connection_state(),
443 cause: None,
444 },
445 ];
446 if !peers.is_empty() {
447 commands.push(Command::EmitEvent {
448 event: ProtocolEvent::PeerSnapshot { peers },
449 });
450 }
451 commands.extend(self.replay_session_state());
452 commands
453 }
454
455 pub fn on_transport_ready(&mut self) -> Vec<Command> {
461 if !self.phase.can_enter_connected() {
462 return Vec::new();
463 }
464 self.phase = ProtocolPhase::Connected(None);
465 let mut commands = vec![Command::EmitStateChange {
466 state: self.state(),
467 cause: None,
468 }];
469 commands.extend(self.replay_publication_state());
470 commands
471 }
472
473 pub fn publish(&mut self, stream_type: StreamType, active: bool) -> Vec<Command> {
478 self.sticky_replay.set_publish_active(stream_type, active);
479 if !matches!(&self.phase, ProtocolPhase::Connected(_)) {
480 return Vec::new();
481 }
482 let message = if active {
483 ClientMessage::Publish(StreamIntentPayload { stream_type })
484 } else {
485 ClientMessage::Unpublish(StreamIntentPayload { stream_type })
486 };
487 self.enqueue_client_message(message, FlushMode::Batched)
488 }
489
490 pub fn subscribe(&mut self, user_id: UserId, states: DownloadStates) -> Vec<Command> {
496 self.sticky_replay
497 .remember_subscription_states(&user_id, &states);
498 if !self.phase.can_send_client_messages() {
499 return Vec::new();
500 }
501 self.enqueue_client_message(
502 ClientMessage::Subscribe(SubscribePayload { user_id, states }),
503 FlushMode::Batched,
504 )
505 }
506
507 pub fn update_info(&mut self, info: UserInfo) -> Vec<Command> {
513 self.sticky_replay.remember_info(&info);
514 if !self.phase.can_send_client_messages() {
515 return Vec::new();
516 }
517 self.enqueue_client_message(ClientMessage::Info(info), FlushMode::Batched)
518 }
519
520 pub fn broadcast(&mut self, message: JsonPayload) -> Vec<Command> {
526 if !self.phase.can_send_client_messages() {
527 return Vec::new();
528 }
529 self.enqueue_client_message(
530 ClientMessage::Broadcast(ClientBroadcastPayload { message }),
531 FlushMode::Batched,
532 )
533 }
534
535 pub fn on_timer(&mut self, timer_id: u32) -> Vec<Command> {
541 if timer_id == RECOVERY_TIMER_ID {
542 return connection_lifecycle::handle_recovery_timer(self);
543 }
544 if timer_id == BATCH_FLUSH_TIMER_ID {
545 return self.outbound_batch.flush(false);
546 }
547 if let Some(commands) = RequestTimeoutId::try_from_raw(timer_id)
548 .and_then(|timeout_id| self.request_tracker.resolve_timeout(timeout_id))
549 {
550 return commands;
551 }
552 Vec::new()
553 }
554
555 fn enqueue_client_message(&mut self, message: ClientMessage, mode: FlushMode) -> Commands {
556 let Some(envelope) = ClientEnvelope::Message(message).into_envelope().ok() else {
557 return Vec::new();
558 };
559 self.outbound_batch.enqueue(envelope, mode)
560 }
561
562 fn clear_runtime_state(&mut self) {
563 self.track_users_by_mid.clear();
564 self.outbound_batch.clear();
565 self.request_tracker.clear();
566 }
567
568 fn teardown_runtime_state(&mut self) -> Commands {
574 let mut commands = self.outbound_batch.discard_pending();
575 commands.extend(self.request_tracker.fail_all());
576 if !self.track_users_by_mid.is_empty() {
577 self.track_users_by_mid.clear();
578 commands.push(Command::EmitEvent {
579 event: ProtocolEvent::TrackSnapshot {
580 bindings: Vec::new(),
581 },
582 });
583 }
584 commands
585 }
586
587 fn replay_session_state(&mut self) -> Commands {
589 if !self.phase.can_send_client_messages() {
590 return Vec::new();
591 }
592 let Some(replay_batch) = self.sticky_replay.replay_session_batch() else {
593 return Vec::new();
594 };
595 self.outbound_batch.extend(replay_batch);
596 self.outbound_batch.flush(true)
597 }
598
599 fn replay_publication_state(&mut self) -> Commands {
601 if !self.phase.can_send_client_messages() {
602 return Vec::new();
603 }
604 let replay_batch: Vec<_> = self
605 .sticky_replay
606 .active_publications()
607 .filter_map(|stream_type| {
608 ClientEnvelope::Message(ClientMessage::Publish(StreamIntentPayload { stream_type }))
609 .into_envelope()
610 .ok()
611 })
612 .collect();
613 if replay_batch.is_empty() {
614 return Vec::new();
615 }
616 self.outbound_batch.extend(replay_batch);
617 self.outbound_batch.flush(true)
618 }
619}
620
621fn empty_features() -> AvailableFeatures {
622 AvailableFeatures {
623 rtc: false,
624 transcription: false,
625 audio_recording: false,
626 video_recording: false,
627 }
628}
629
630fn close_for_protocol_error() -> Commands {
631 vec![Command::CloseWebSocket {
632 code: u16::from(WebSocketCloseCode::ProtocolError),
633 }]
634}
635
636fn next_recovery_delay(current_delay_ms: u32) -> u32 {
641 (current_delay_ms.saturating_mul(3) / 2).min(MAX_RECOVERY_DELAY_MS)
642}
643
644#[cfg(test)]
645#[path = "core/TESTS/mod.rs"]
646mod tests;