pulse-pixelstream-types 0.9.1

Shared Myko entity and command types for the Pulse Pixelstream recording cell.
Documentation
use myko::entities::client::{Client, ClientId};
use myko::prelude::*;
use myko_macros::myko_item;

use crate::stream::{Stream, StreamId};

/// Ephemeral presence: one row per viewer per stream. As a child of the
/// framework `Client`, it is cascade-deleted when the browser tab disconnects,
/// so presence auto-clears with no timers.
#[myko_item]
pub struct Viewer {
    #[belongs_to(Stream)]
    pub stream_id: StreamId,
    pub viewer_id: String,
    pub name: String,
    pub color: String,
    /// Optional OIDC issuer for display identity enrichment. Presence and
    /// control remain available when this is absent.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity_issuer: Option<String>,
    /// Optional OIDC subject. Client-asserted display data only; never used as
    /// authorization or as the control-lock owner key.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity_subject: Option<String>,
    /// Optional profile image URL from the identity provider.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub avatar_url: Option<String>,
    #[serde(default)]
    pub cursor: Option<(f32, f32)>,
    #[myko_client_id]
    #[belongs_to(Client)]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub client_id: Option<ClientId>,
}

impl Viewer {
    /// One row per **connection** (`conn` = the WebSocket client id), not per
    /// viewer_id. A browser's tabs share the persisted viewer_id but are distinct
    /// connections, so each gets its own presence row — independently cascade-cleaned
    /// on disconnect — and only the connection holding the lock drives. Closing the
    /// driving tab therefore frees its row and the survivor is reconciled the wheel.
    pub fn row_id(stream_id: &StreamId, conn: &str) -> ViewerId {
        format!("{}:{}", stream_id.0, conn).into()
    }
}