use std::sync::Arc;
use std::time::Duration;
use meerkat_core::comms::TrustedPeerDescriptor;
use super::MobSupervisorBridge;
use super::bridge_protocol::{
BridgeCommand, BridgeLiveChannelPayload, BridgeLiveControlOutcome, BridgeLiveControlPayload,
BridgeLiveControlVerb, BridgeLiveControlledResponse, BridgeLiveOpenPayload,
BridgeLiveOpenedResponse, BridgeLiveStatusPayload, BridgeProtocolVersion, LiveCloseStatus,
LiveOpenResult, LiveOpenTransport, RealtimeTurningMode, WireLiveAdapterStatus,
};
use crate::MobError;
pub(crate) const LIVE_OPEN_BRIDGE_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const LIVE_CHANNEL_BRIDGE_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Debug, Clone)]
pub struct MemberLiveStatusDomain {
pub channel_id: String,
pub status: WireLiveAdapterStatus,
}
pub(crate) async fn open_remote_member_live_channel(
bridge: &Arc<MobSupervisorBridge>,
peer: &TrustedPeerDescriptor,
expected_member: super::bridge_protocol::BridgeMemberIncarnation,
turning_mode: Option<RealtimeTurningMode>,
transport: Option<LiveOpenTransport>,
) -> Result<LiveOpenResult, MobError> {
let authority = bridge.authority().await;
let sup_spec = bridge.supervisor_spec_for_recipient(peer).await?;
let command = BridgeCommand::OpenMemberLiveChannel(BridgeLiveOpenPayload {
supervisor: sup_spec.into(),
epoch: authority.epoch,
protocol_version: BridgeProtocolVersion::V4,
expected_member,
turning_mode,
transport,
});
let _ = bridge.trust_recipient(peer).await?;
let value = bridge
.send_bridge_command(peer, &command, LIVE_OPEN_BRIDGE_TIMEOUT)
.await?;
let opened: BridgeLiveOpenedResponse =
super::bridge_protocol::decode_bridge_payload(&command, value, "open member live channel")?;
Ok(opened.open)
}
pub(crate) async fn close_remote_member_live_channel(
bridge: &Arc<MobSupervisorBridge>,
peer: &TrustedPeerDescriptor,
expected_member: super::bridge_protocol::BridgeMemberIncarnation,
channel_id: String,
) -> Result<LiveCloseStatus, MobError> {
let authority = bridge.authority().await;
let sup_spec = bridge.supervisor_spec_for_recipient(peer).await?;
let command = BridgeCommand::CloseMemberLiveChannel(BridgeLiveChannelPayload {
supervisor: sup_spec.into(),
epoch: authority.epoch,
protocol_version: BridgeProtocolVersion::V4,
expected_member,
channel_id,
});
let _ = bridge.trust_recipient(peer).await?;
let value = bridge
.send_bridge_command(peer, &command, LIVE_CHANNEL_BRIDGE_TIMEOUT)
.await?;
super::bridge_protocol::decode_bridge_payload(&command, value, "close member live channel")
}
pub(crate) async fn remote_member_live_status(
bridge: &Arc<MobSupervisorBridge>,
peer: &TrustedPeerDescriptor,
expected_member: super::bridge_protocol::BridgeMemberIncarnation,
channel_id: Option<String>,
) -> Result<MemberLiveStatusDomain, MobError> {
let authority = bridge.authority().await;
let sup_spec = bridge.supervisor_spec_for_recipient(peer).await?;
let command = BridgeCommand::MemberLiveChannelStatus(BridgeLiveStatusPayload {
supervisor: sup_spec.into(),
epoch: authority.epoch,
protocol_version: BridgeProtocolVersion::V4,
expected_member,
channel_id,
});
let _ = bridge.trust_recipient(peer).await?;
let value = bridge
.send_bridge_command(peer, &command, LIVE_CHANNEL_BRIDGE_TIMEOUT)
.await?;
super::bridge_protocol::decode_bridge_payload(&command, value, "member live channel status")
}
pub(crate) async fn control_remote_member_live_channel(
bridge: &Arc<MobSupervisorBridge>,
peer: &TrustedPeerDescriptor,
expected_member: super::bridge_protocol::BridgeMemberIncarnation,
channel_id: String,
verb: BridgeLiveControlVerb,
) -> Result<BridgeLiveControlOutcome, MobError> {
let authority = bridge.authority().await;
let sup_spec = bridge.supervisor_spec_for_recipient(peer).await?;
let command = BridgeCommand::ControlMemberLiveChannel(BridgeLiveControlPayload {
supervisor: sup_spec.into(),
epoch: authority.epoch,
protocol_version: BridgeProtocolVersion::V4,
expected_member,
channel_id,
verb,
});
let _ = bridge.trust_recipient(peer).await?;
let value = bridge
.send_bridge_command(peer, &command, LIVE_CHANNEL_BRIDGE_TIMEOUT)
.await?;
let controlled: BridgeLiveControlledResponse = super::bridge_protocol::decode_bridge_payload(
&command,
value,
"control member live channel",
)?;
Ok(controlled.outcome)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::super::bridge_protocol::{
BridgeMemberIncarnation, BridgePeerSpec, BridgeProtocolVersion, BridgeRejectionCause,
BridgeReply, WireLiveTransportBootstrap, decode_bridge_payload,
};
use super::*;
use meerkat_contracts::wire::{WireLiveChannelCapabilities, WireLiveContinuityMode};
fn sample_peer_spec() -> BridgePeerSpec {
BridgePeerSpec {
name: "mob-a/__mob_supervisor__".to_string(),
peer_id: "peer-lead".to_string(),
address: "inproc://mob/supervisor/lead".to_string(),
pubkey: [7u8; 32],
}
}
fn open_command() -> BridgeCommand {
BridgeCommand::OpenMemberLiveChannel(BridgeLiveOpenPayload {
supervisor: sample_peer_spec(),
epoch: 1,
protocol_version: BridgeProtocolVersion::V4,
turning_mode: None,
transport: Some(LiveOpenTransport::Websocket),
expected_member: BridgeMemberIncarnation::default(),
})
}
fn status_command(channel_id: Option<String>) -> BridgeCommand {
BridgeCommand::MemberLiveChannelStatus(BridgeLiveStatusPayload {
supervisor: sample_peer_spec(),
epoch: 1,
protocol_version: BridgeProtocolVersion::V4,
channel_id,
expected_member: BridgeMemberIncarnation::default(),
})
}
fn sample_open_result() -> LiveOpenResult {
LiveOpenResult {
channel_id: "chan-open-1".to_string(),
transport: WireLiveTransportBootstrap::Websocket {
url: "ws://live.advertised.test:19777/live/ws?token=tok-bytes&channel=chan-open-1"
.to_string(),
token: "tok-bytes".to_string(),
},
capabilities: WireLiveChannelCapabilities {
audio_in: true,
audio_out: true,
text_in: true,
text_out: true,
image_in: false,
video_in: false,
transcript_supported: true,
barge_in_supported: true,
provider_native_resume: false,
},
continuity: WireLiveContinuityMode::Fresh,
}
}
#[test]
fn opened_reply_decodes_live_open_result_verbatim() {
let expected = sample_open_result();
let reply = BridgeReply::MemberLiveChannelOpened(BridgeLiveOpenedResponse {
open: expected.clone(),
});
let value = serde_json::to_value(&reply).expect("serialize opened reply");
let decoded: BridgeLiveOpenedResponse =
decode_bridge_payload(&open_command(), value, "open member live channel")
.expect("opened payload decodes");
assert_eq!(decoded.open, expected);
}
#[test]
fn rejected_reply_surfaces_typed_live_cause() {
let reply = BridgeReply::Rejected {
cause: BridgeRejectionCause::LiveChannelAlreadyBound,
reason: "session already has an active live channel".to_string(),
};
let value = serde_json::to_value(&reply).expect("serialize rejection");
let error = decode_bridge_payload::<BridgeLiveOpenedResponse>(
&open_command(),
value,
"open member live channel",
)
.expect_err("rejection must surface as a typed error");
match error {
MobError::BridgeCommandRejected { cause, reason } => {
assert_eq!(cause, BridgeRejectionCause::LiveChannelAlreadyBound);
assert!(reason.contains("active live channel"));
}
other => panic!("expected BridgeCommandRejected, got {other:?}"),
}
}
#[test]
fn status_report_extracts_into_domain_carrier() {
let reply = BridgeReply::MemberLiveChannelStatusReport {
channel_id: "chan-disc-1".to_string(),
status: WireLiveAdapterStatus::Ready,
};
let value = serde_json::to_value(&reply).expect("serialize status report");
let domain: MemberLiveStatusDomain =
decode_bridge_payload(&status_command(None), value, "member live channel status")
.expect("status report decodes");
assert_eq!(domain.channel_id, "chan-disc-1");
assert_eq!(domain.status, WireLiveAdapterStatus::Ready);
}
#[test]
fn reply_kind_mismatch_is_a_typed_protocol_fault() {
let reply = BridgeReply::MemberLiveChannelClosed {
status: LiveCloseStatus::Closed,
};
let value = serde_json::to_value(&reply).expect("serialize closed reply");
let error = decode_bridge_payload::<BridgeLiveOpenedResponse>(
&open_command(),
value,
"open member live channel",
)
.expect_err("kind mismatch must surface as an error");
assert!(
error
.to_string()
.contains("unexpected open member live channel bridge reply"),
"mismatch is reported as a typed protocol error: {error}"
);
}
#[test]
fn adjudicated_bridge_budgets_hold() {
assert_eq!(LIVE_OPEN_BRIDGE_TIMEOUT, Duration::from_secs(30));
assert_eq!(LIVE_CHANNEL_BRIDGE_TIMEOUT, Duration::from_secs(15));
assert!(LIVE_CHANNEL_BRIDGE_TIMEOUT < LIVE_OPEN_BRIDGE_TIMEOUT);
}
}