Skip to main content

pulse_pixelstream_types/
viewer.rs

1use myko::entities::client::{Client, ClientId};
2use myko::prelude::*;
3use myko_macros::myko_item;
4
5use crate::stream::{Stream, StreamId};
6
7/// Ephemeral presence: one row per viewer per stream. As a child of the
8/// framework `Client`, it is cascade-deleted when the browser tab disconnects,
9/// so presence auto-clears with no timers.
10#[myko_item]
11pub struct Viewer {
12    #[belongs_to(Stream)]
13    pub stream_id: StreamId,
14    pub viewer_id: String,
15    pub name: String,
16    pub color: String,
17    /// Optional OIDC issuer for display identity enrichment. Presence and
18    /// control remain available when this is absent.
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub identity_issuer: Option<String>,
21    /// Optional OIDC subject. Client-asserted display data only; never used as
22    /// authorization or as the control-lock owner key.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub identity_subject: Option<String>,
25    /// Optional profile image URL from the identity provider.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub avatar_url: Option<String>,
28    #[serde(default)]
29    pub cursor: Option<(f32, f32)>,
30    #[myko_client_id]
31    #[belongs_to(Client)]
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub client_id: Option<ClientId>,
34}
35
36impl Viewer {
37    /// One row per **connection** (`conn` = the WebSocket client id), not per
38    /// viewer_id. A browser's tabs share the persisted viewer_id but are distinct
39    /// connections, so each gets its own presence row — independently cascade-cleaned
40    /// on disconnect — and only the connection holding the lock drives. Closing the
41    /// driving tab therefore frees its row and the survivor is reconciled the wheel.
42    pub fn row_id(stream_id: &StreamId, conn: &str) -> ViewerId {
43        format!("{}:{}", stream_id.0, conn).into()
44    }
45}