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}