Skip to main content

systemprompt_models/documents/
awareness.rs

1//! Awareness: the ephemeral state each participant of a live document
2//! shares with the others (who they are, their colour, caret, selection and
3//! viewport), and the live channel's own control events.
4//!
5//! Positions are engine cursors encoded by the client's CRDT engine
6//! (`anchor` and `head` are opaque base64 strings tied to a block id), so
7//! they survive concurrent edits; the server stores and relays them but
8//! never decodes them. An awareness state expires `ttl` after it was sent.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use chrono::{DateTime, Utc};
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16use systemprompt_identifiers::{DocumentEditSessionId, DocumentId, DocumentNodeId, UserId};
17
18pub const AWARENESS_COLOR_MAX: usize = 32;
19pub const AWARENESS_CURSOR_MAX: usize = 512;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
22#[serde(rename_all = "snake_case")]
23pub enum AwarenessMode {
24    Viewing,
25    Editing,
26    Commenting,
27    Presenting,
28}
29
30/// A caret or selection: `anchor` and `head` are engine cursors inside the
31/// block, cell or shape named by `block`; equal positions mean a caret.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33pub struct AwarenessCursor {
34    pub block: DocumentNodeId,
35    pub anchor: String,
36    pub head: String,
37}
38
39/// The visible extent of the participant's canvas, as the first and last
40/// visible block (a range of rows or one slide for sheets and decks).
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42pub struct AwarenessViewport {
43    pub first: DocumentNodeId,
44    pub last: DocumentNodeId,
45}
46
47/// What a client posts to `POST /documents/{id}/collab/awareness`.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
49pub struct AwarenessState {
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub session_id: Option<DocumentEditSessionId>,
52    pub color: String,
53    pub mode: AwarenessMode,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub cursor: Option<AwarenessCursor>,
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub selection: Vec<AwarenessCursor>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub viewport: Option<AwarenessViewport>,
60    #[schemars(
61        description = "Set when the participant is an agent acting for the user, named so a client draws it as its own caret beside the person's."
62    )]
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub agent: Option<String>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
68pub struct DocumentAwarenessEvent {
69    pub document_id: DocumentId,
70    pub user_id: UserId,
71    pub display_name: String,
72    pub state: AwarenessState,
73    pub expires_at: DateTime<Utc>,
74}
75
76impl DocumentAwarenessEvent {
77    pub const NAME: &'static str = "document.awareness";
78}
79
80/// Why a live channel asks its client to re-join and reload the snapshot.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82#[serde(rename_all = "snake_case")]
83pub enum LiveResyncReason {
84    SessionChanged,
85    UpdatesCompacted,
86    Lagged,
87    RelayLost,
88}
89
90/// The first event of every live channel, and of every resumed one.
91///
92/// `seq` is the last update the open edit session had logged when the
93/// channel subscribed; every later update reaches the channel. A client whose
94/// editor holds less reconnects with `Last-Event-ID: <session>:<its seq>`.
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
96pub struct DocumentLiveReadyEvent {
97    pub document_id: DocumentId,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub edit_session_id: Option<DocumentEditSessionId>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub seq: Option<i64>,
102    pub replayed: usize,
103}
104
105impl DocumentLiveReadyEvent {
106    pub const NAME: &'static str = "document.live.ready";
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
110pub struct DocumentLiveResyncEvent {
111    pub document_id: DocumentId,
112    pub reason: LiveResyncReason,
113}
114
115impl DocumentLiveResyncEvent {
116    pub const NAME: &'static str = "document.live.resync";
117}
118
119/// Sent before the server closes a channel whose caller lost view access.
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
121pub struct DocumentLiveClosedEvent {
122    pub document_id: DocumentId,
123}
124
125impl DocumentLiveClosedEvent {
126    pub const NAME: &'static str = "document.live.closed";
127}