pulse-pixelstream-types 0.14.1

Shared Myko entity and command types for the Pulse Pixelstream recording cell.
Documentation
use myko::prelude::*;
use myko::TS;
use myko_macros::myko_item;
use serde::{Deserialize, Serialize};

use crate::StoredCaptureContext;

/// Contract version for the synchronized nDisplay frame-capture surface.
/// Gated in the client exactly like the recording contract: a deployment that
/// advertises an older version hides the capture action entirely.
pub const FRAME_CAPTURE_CONTRACT_VERSION: &str = "0.1.0";

/// One operator request for a synchronized nDisplay frame capture.
///
/// The browser NEVER talks to Pulse Cluster. This entity is the whole
/// client-facing surface: the UI writes typed editorial context plus a typed
/// target selection, and an off-browser bridge (holding the cluster
/// credential) submits it and mirrors authoritative status back as
/// [`FrameCaptureStatus`]. Keyed by streamer id — one in-flight capture per
/// stream, mirroring RecordingJobRequest.
#[myko_item]
pub struct FrameCaptureRequest {
    pub streamer_id: String,
    /// Stable id for this capture across submit/status/receipt. Client-minted
    /// (UUIDv7) so retries of the same intent are idempotent at the bridge.
    pub capture_id: String,
    /// Makes a re-submitted command idempotent, like RecordingJobRequest.
    pub command_id: String,
    /// Frozen editorial identity: typed collection ref, optional label,
    /// optional content revision. Required — capture output is filed by
    /// editorial context, and the collection must be explicit, never inferred.
    #[ts(type = "unknown")]
    pub capture_context: StoredCaptureContext,
    /// Typed target selection. The UI picks from the summaries the bridge
    /// mirrors; it never constructs cluster addresses.
    pub target: FrameCaptureTarget,
    /// Explicit operator override. Cluster's force submits despite missing or
    /// stalled frame-progress telemetry ONLY; exact target identity,
    /// generation, revision, and capture capability remain required. Never set
    /// automatically — the operator asks for it after a refusal.
    #[serde(default)]
    pub force: bool,
    #[serde(default)]
    pub requested_at_ms: u64,
}

/// Which cluster the capture is fired on, plus the generation the operator
/// believed was live when they asked. The bridge rejects a mismatch rather
/// than capturing a different deployment than the one on screen.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct FrameCaptureTarget {
    pub cluster_name: String,
    /// Recorded revision/generation the UI displayed at request time. Empty
    /// means "whatever is live" and the bridge stamps what it actually used.
    #[serde(default)]
    pub expected_generation: String,
}

/// Lifecycle of a capture as reported by the bridge. Deliberately coarse and
/// UI-facing: the cluster's internal step machine stays behind the bridge.
#[derive(
    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TS, PartialOrd, Ord, Hash,
)]
#[serde(rename_all = "snake_case")]
pub enum FrameCapturePhase {
    #[default]
    Idle,
    /// Accepted by the bridge, not yet acknowledged by Cluster.
    Submitted,
    /// Cluster is preparing the nodes.
    Arming,
    /// The synchronized frame is being taken.
    Firing,
    /// Cluster is aggregating per-node artifacts.
    Aggregating,
    Complete,
    Failed,
}

impl FrameCapturePhase {
    pub fn is_running(&self) -> bool {
        matches!(
            self,
            Self::Submitted | Self::Arming | Self::Firing | Self::Aggregating
        )
    }

    pub fn is_terminal(&self) -> bool {
        matches!(self, Self::Complete | Self::Failed)
    }
}

/// One artifact reference exactly as Cluster reported it. Pixelstream renders
/// these verbatim and constructs no paths of its own — the capture tree layout
/// is PulseNode-owned implementation detail.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct FrameCaptureReceipt {
    /// Opaque reference string from Cluster's typed receipt (path or URI).
    pub reference: String,
    /// Optional node/view label for display grouping, as reported.
    #[serde(default)]
    pub label: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn running_phases_are_not_terminal() {
        for phase in [
            FrameCapturePhase::Submitted,
            FrameCapturePhase::Arming,
            FrameCapturePhase::Firing,
            FrameCapturePhase::Aggregating,
        ] {
            assert!(phase.is_running());
            assert!(!phase.is_terminal());
        }
        for phase in [FrameCapturePhase::Complete, FrameCapturePhase::Failed] {
            assert!(!phase.is_running());
            assert!(phase.is_terminal());
        }
        assert!(!FrameCapturePhase::Idle.is_running());
        assert!(!FrameCapturePhase::Idle.is_terminal());
    }
}