fiberplane_models/
realtime.rs

1use crate::comments::{Thread, ThreadItem, UserSummary};
2use crate::events::Event;
3use crate::labels::LabelValidationError;
4use crate::notebooks::operations::Operation;
5use base64uuid::Base64Uuid;
6#[cfg(feature = "fp-bindgen")]
7use fp_bindgen::prelude::*;
8use serde::{Deserialize, Serialize};
9use std::cmp::Ordering;
10use std::fmt::Debug;
11use time::OffsetDateTime;
12use typed_builder::TypedBuilder;
13
14/// Real-time message sent by the client over a WebSocket connection.
15#[derive(Clone, Debug, Deserialize, Serialize)]
16#[cfg_attr(
17    feature = "fp-bindgen",
18    derive(Serializable),
19    fp(rust_module = "fiberplane_models::realtime")
20)]
21#[non_exhaustive]
22#[serde(tag = "type", rename_all = "snake_case")]
23pub enum ClientRealtimeMessage {
24    /// Authenticate this client
25    Authenticate(AuthenticateMessage),
26
27    /// Subscribe to changes from a specific Notebook.
28    Subscribe(SubscribeMessage),
29
30    /// Unsubscribe to changes from a specific Notebook.
31    Unsubscribe(UnsubscribeMessage),
32
33    /// Apply an operation to a specific Notebook.
34    ApplyOperation(Box<ApplyOperationMessage>),
35
36    /// Apply multiple operations to a specific Notebook.
37    ApplyOperationBatch(Box<ApplyOperationBatchMessage>),
38
39    /// Request a DebugResponse from the server.
40    DebugRequest(DebugRequestMessage),
41
42    FocusInfo(FocusInfoMessage),
43
44    /// User started typing a comment.
45    UserTypingComment(UserTypingCommentClientMessage),
46
47    /// Subscribe to workspace activities
48    SubscribeWorkspace(SubscribeWorkspaceMessage),
49
50    /// Unsubscribe from workspace activities
51    UnsubscribeWorkspace(UnsubscribeWorkspaceMessage),
52}
53
54impl ClientRealtimeMessage {
55    pub fn op_id(&self) -> &Option<String> {
56        use ClientRealtimeMessage::*;
57        match self {
58            Authenticate(msg) => &msg.op_id,
59            Subscribe(msg) => &msg.op_id,
60            Unsubscribe(msg) => &msg.op_id,
61            ApplyOperation(msg) => &msg.op_id,
62            ApplyOperationBatch(msg) => &msg.op_id,
63            DebugRequest(msg) => &msg.op_id,
64            FocusInfo(msg) => &msg.op_id,
65            UserTypingComment(msg) => &msg.op_id,
66            SubscribeWorkspace(msg) => &msg.op_id,
67            UnsubscribeWorkspace(msg) => &msg.op_id,
68        }
69    }
70}
71
72/// Real-time message sent by the server over a WebSocket connection.
73#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
74#[cfg_attr(
75    feature = "fp-bindgen",
76    derive(Serializable),
77    fp(rust_module = "fiberplane_models::realtime")
78)]
79#[non_exhaustive]
80#[serde(tag = "type", rename_all = "snake_case")]
81pub enum ServerRealtimeMessage {
82    /// Apply an operation to a specific Notebook.
83    ApplyOperation(Box<ApplyOperationMessage>),
84
85    /// An Ack message will be sent once an operation is received and processed.
86    /// No Ack message will sent if the op_id of the original message was empty.
87    Ack(AckMessage),
88
89    /// An Err message will be sent once an operation is received, but could not
90    /// be processed. It includes the op_id if that was present.
91    Err(ErrMessage),
92
93    /// Response from a DebugRequest. Contains some useful data regarding the
94    /// connection.
95    DebugResponse(DebugResponseMessage),
96
97    /// New event was added to the workspace
98    EventAdded(EventAddedMessage),
99
100    /// Event was updated in the workspace
101    EventUpdated(EventUpdatedMessage),
102
103    /// Event was deleted from the workspace
104    EventDeleted(EventDeletedMessage),
105
106    /// Notifies a mentioned user of the fact they've been mentioned by someone
107    /// else.
108    Mention(MentionMessage),
109
110    /// An apply operation got rejected by the server, see message for the
111    /// reason.
112    Rejected(RejectedMessage),
113
114    /// A user has joined as a subscriber to a notebook.
115    SubscriberAdded(SubscriberAddedMessage),
116
117    /// A previously subscribed user has left a notebook.
118    SubscriberRemoved(SubscriberRemovedMessage),
119
120    SubscriberChangedFocus(SubscriberChangedFocusMessage),
121
122    /// A new comment thread was added to the notebook.
123    ThreadAdded(ThreadAddedMessage),
124
125    /// A new item was added to a comment thread (e.g. a comment or a thread status change).
126    ThreadItemAdded(ThreadItemAddedMessage),
127
128    /// A new item was added to a comment thread (e.g. a comment or a thread status change).
129    ThreadItemUpdated(ThreadItemUpdatedMessage),
130
131    /// A comment thread was deleted
132    ThreadDeleted(ThreadDeletedMessage),
133
134    /// A user started typing a comment
135    UserTypingComment(UserTypingCommentServerMessage),
136}
137
138#[derive(Clone, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
139#[cfg_attr(
140    feature = "fp-bindgen",
141    derive(Serializable),
142    fp(rust_module = "fiberplane_models::realtime")
143)]
144#[non_exhaustive]
145#[serde(rename_all = "camelCase")]
146pub struct AuthenticateMessage {
147    /// Bearer token
148    #[builder(setter(into))]
149    pub token: String,
150
151    /// Operation ID
152    #[builder(default, setter(into, strip_option))]
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub op_id: Option<String>,
155}
156
157impl Debug for AuthenticateMessage {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("AuthenticateMessage")
160            .field("token", &"[REDACTED]")
161            .field("op_id", &self.op_id)
162            .finish()
163    }
164}
165
166#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
167#[cfg_attr(
168    feature = "fp-bindgen",
169    derive(Serializable),
170    fp(rust_module = "fiberplane_models::realtime")
171)]
172#[non_exhaustive]
173#[serde(rename_all = "camelCase")]
174pub struct SubscribeMessage {
175    /// ID of the notebook
176    #[builder(setter(into))]
177    pub notebook_id: String,
178
179    /// The current revision that the client knows about. If this is not the
180    /// current revision according to the server, than the server will sent
181    /// all operations starting from this revision.
182    #[builder(default, setter(into))]
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub revision: Option<u32>,
185
186    /// Operation ID
187    #[builder(default, setter(into, strip_option))]
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub op_id: Option<String>,
190}
191
192#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
193#[cfg_attr(
194    feature = "fp-bindgen",
195    derive(Serializable),
196    fp(rust_module = "fiberplane_models::realtime")
197)]
198#[non_exhaustive]
199#[serde(rename_all = "camelCase")]
200pub struct UnsubscribeMessage {
201    /// ID of the notebook
202    #[builder(setter(into))]
203    pub notebook_id: String,
204
205    /// Operation ID
206    #[builder(default, setter(into, strip_option))]
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub op_id: Option<String>,
209}
210
211#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TypedBuilder)]
212#[cfg_attr(
213    feature = "fp-bindgen",
214    derive(Serializable),
215    fp(rust_module = "fiberplane_models::realtime")
216)]
217#[non_exhaustive]
218#[serde(rename_all = "camelCase")]
219pub struct ApplyOperationMessage {
220    /// ID of the notebook
221    #[builder(setter(into))]
222    pub notebook_id: String,
223
224    /// Operation
225    pub operation: Operation,
226
227    /// Revision, for a client sending this message it means the desired new
228    /// revision. When it is sent from a server it is the actual revision.
229    pub revision: u32,
230
231    /// Operation ID
232    #[builder(default, setter(into))]
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub op_id: Option<String>,
235}
236
237impl ApplyOperationMessage {
238    pub fn new(
239        notebook_id: String,
240        operation: Operation,
241        revision: u32,
242        op_id: Option<String>,
243    ) -> Self {
244        Self {
245            notebook_id,
246            operation,
247            revision,
248            op_id,
249        }
250    }
251}
252
253#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TypedBuilder)]
254#[cfg_attr(
255    feature = "fp-bindgen",
256    derive(Serializable),
257    fp(rust_module = "fiberplane_models::realtime")
258)]
259#[non_exhaustive]
260#[serde(rename_all = "camelCase")]
261pub struct ApplyOperationBatchMessage {
262    /// ID of the notebook
263    #[builder(setter(into))]
264    pub notebook_id: String,
265
266    /// Operation
267    #[builder(default)]
268    pub operations: Vec<Operation>,
269
270    /// Revision, this will be the revision of the first operation. The other
271    /// operations will keep bumping the revision.
272    pub revision: u32,
273
274    /// Operation ID
275    #[builder(default, setter(into, strip_option))]
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub op_id: Option<String>,
278}
279
280impl ApplyOperationBatchMessage {
281    pub fn new(
282        notebook_id: String,
283        operations: Vec<Operation>,
284        revision: u32,
285        op_id: Option<String>,
286    ) -> Self {
287        Self {
288            notebook_id,
289            operations,
290            revision,
291            op_id,
292        }
293    }
294}
295
296#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
297#[cfg_attr(
298    feature = "fp-bindgen",
299    derive(Serializable),
300    fp(rust_module = "fiberplane_models::realtime")
301)]
302#[non_exhaustive]
303#[serde(rename_all = "camelCase")]
304pub struct AckMessage {
305    /// Operation ID.
306    pub op_id: String,
307}
308
309impl AckMessage {
310    pub fn new(op_id: String) -> Self {
311        Self { op_id }
312    }
313}
314
315#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
316#[cfg_attr(
317    feature = "fp-bindgen",
318    derive(Serializable),
319    fp(rust_module = "fiberplane_models::realtime")
320)]
321#[non_exhaustive]
322#[serde(rename_all = "camelCase")]
323pub struct ErrMessage {
324    /// Error message.
325    #[builder(setter(into))]
326    pub error_message: String,
327
328    /// Operation ID. Empty if the user has not provided a op_id.
329    #[builder(default, setter(into))]
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub op_id: Option<String>,
332}
333
334#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
335#[cfg_attr(
336    feature = "fp-bindgen",
337    derive(Serializable),
338    fp(rust_module = "fiberplane_models::realtime")
339)]
340#[non_exhaustive]
341#[serde(rename_all = "camelCase")]
342pub struct DebugRequestMessage {
343    /// Operation ID.
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub op_id: Option<String>,
346}
347
348#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
349#[cfg_attr(
350    feature = "fp-bindgen",
351    derive(Serializable),
352    fp(rust_module = "fiberplane_models::realtime")
353)]
354#[non_exhaustive]
355#[serde(rename_all = "camelCase")]
356pub struct DebugResponseMessage {
357    /// Session ID.
358    pub sid: String,
359
360    /// Notebooks that the user is subscribed to.
361    #[builder(default)]
362    pub subscribed_notebooks: Vec<String>,
363
364    /// Operation ID. Empty if the user has not provided a op_id.
365    #[builder(default, setter(into))]
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub op_id: Option<String>,
368}
369
370#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
371#[cfg_attr(
372    feature = "fp-bindgen",
373    derive(Serializable),
374    fp(rust_module = "fiberplane_models::realtime")
375)]
376#[non_exhaustive]
377#[serde(rename_all = "camelCase")]
378pub struct EventAddedMessage {
379    /// ID of workspace in which the event was added.
380    pub workspace_id: Base64Uuid,
381
382    /// The event that was added.
383    pub event: Event,
384}
385
386#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
387#[cfg_attr(
388    feature = "fp-bindgen",
389    derive(Serializable),
390    fp(rust_module = "fiberplane_models::realtime")
391)]
392#[non_exhaustive]
393#[serde(rename_all = "camelCase")]
394pub struct EventUpdatedMessage {
395    /// ID of workspace in which the event was updated.
396    pub workspace_id: Base64Uuid,
397
398    /// The event that was updated.
399    pub event: Event,
400}
401
402#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
403#[cfg_attr(
404    feature = "fp-bindgen",
405    derive(Serializable),
406    fp(rust_module = "fiberplane_models::realtime")
407)]
408#[non_exhaustive]
409#[serde(rename_all = "camelCase")]
410pub struct EventDeletedMessage {
411    /// ID of workspace in which the event was deleted.
412    pub workspace_id: Base64Uuid,
413
414    /// ID of the event that was deleted.
415    pub event_id: String,
416}
417
418#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
419#[cfg_attr(
420    feature = "fp-bindgen",
421    derive(Serializable),
422    fp(rust_module = "fiberplane_models::realtime")
423)]
424#[non_exhaustive]
425#[serde(rename_all = "camelCase")]
426pub struct MentionMessage {
427    /// ID of the notebook in which the user was mentioned.
428    pub notebook_id: String,
429
430    /// ID of the cell in which the user was mentioned.
431    pub cell_id: String,
432
433    /// Who mentioned the user?
434    pub mentioned_by: MentionedBy,
435}
436
437#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
438#[cfg_attr(
439    feature = "fp-bindgen",
440    derive(Serializable),
441    fp(rust_module = "fiberplane_models::realtime")
442)]
443#[non_exhaustive]
444#[serde(rename_all = "camelCase")]
445pub struct MentionedBy {
446    pub name: String,
447}
448
449/// Message sent when an apply operation was rejected by the server.
450#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
451#[cfg_attr(
452    feature = "fp-bindgen",
453    derive(Serializable),
454    fp(rust_module = "fiberplane_models::realtime")
455)]
456#[non_exhaustive]
457#[serde(rename_all = "camelCase")]
458pub struct RejectedMessage {
459    /// The reason why the apply operation was rejected.
460    pub reason: Box<RejectReason>,
461
462    /// Operation ID. Empty if the user has not provided a op_id.
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub op_id: Option<String>,
465}
466
467impl RejectedMessage {
468    pub fn new(reason: RejectReason, op_id: Option<String>) -> Self {
469        Self {
470            reason: Box::new(reason),
471            op_id,
472        }
473    }
474}
475
476/// Reason why the apply operation was rejected.
477#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
478#[cfg_attr(
479    feature = "fp-bindgen",
480    derive(Serializable),
481    fp(rust_module = "fiberplane_models::realtime")
482)]
483#[non_exhaustive]
484#[serde(tag = "type", rename_all = "snake_case")]
485pub enum RejectReason {
486    /// The operation referenced an invalid cell index.
487    CellIndexOutOfBounds,
488
489    /// The operation referenced a non-existing cell.
490    #[serde(rename_all = "camelCase")]
491    CellNotFound { cell_id: String },
492
493    /// The operation tried to insert a cell with a non-unique ID.
494    #[serde(rename_all = "camelCase")]
495    DuplicateCellId { cell_id: String },
496
497    /// A label was submitted for already exists for the notebook.
498    DuplicateLabel(DuplicateLabelRejectReason),
499
500    /// The operation failed some miscellaneous precondition.
501    #[serde(rename_all = "camelCase")]
502    FailedPrecondition { message: String },
503
504    /// A label was submitted that was invalid.
505    InvalidLabel(InvalidLabelRejectReason),
506
507    /// Current notebook state does not match old state in operation.
508    InconsistentState,
509
510    /// Attempted to perform a text operation on a non-text cell.
511    #[serde(rename_all = "camelCase")]
512    NoTextCell { cell_id: String },
513
514    /// The requested apply operation was for an old version. The u32 contains
515    /// the current revision.
516    Outdated(OutdatedRejectReason),
517}
518
519#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
520#[cfg_attr(
521    feature = "fp-bindgen",
522    derive(Serializable),
523    fp(rust_module = "fiberplane_models::realtime")
524)]
525#[non_exhaustive]
526#[serde(rename_all = "camelCase")]
527pub struct OutdatedRejectReason {
528    /// The current revision for the notebook.
529    pub current_revision: u32,
530}
531
532#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
533#[cfg_attr(
534    feature = "fp-bindgen",
535    derive(Serializable),
536    fp(rust_module = "fiberplane_models::realtime")
537)]
538#[non_exhaustive]
539#[serde(rename_all = "camelCase")]
540pub struct InvalidLabelRejectReason {
541    /// The key of the label that was invalid.
542    pub key: String,
543
544    /// The specific reason why the label was invalid.
545    pub validation_error: LabelValidationError,
546}
547
548#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
549#[cfg_attr(
550    feature = "fp-bindgen",
551    derive(Serializable),
552    fp(rust_module = "fiberplane_models::realtime")
553)]
554#[non_exhaustive]
555#[serde(rename_all = "camelCase")]
556pub struct DuplicateLabelRejectReason {
557    /// The key of the label that was already present.
558    pub key: String,
559}
560
561#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
562#[cfg_attr(
563    feature = "fp-bindgen",
564    derive(Serializable),
565    fp(rust_module = "fiberplane_models::realtime")
566)]
567#[non_exhaustive]
568#[serde(rename_all = "camelCase")]
569pub struct SubscriberAddedMessage {
570    /// The ID of the notebook that the user subscribed to.
571    #[builder(setter(into))]
572    pub notebook_id: String,
573
574    /// ID associated with the newly connected session. There can be multiple
575    /// sessions for a single (notebook|user) pair. The ID can be used multiple
576    /// times for different (notebook|user) pairs. The combination of notebook,
577    /// user and session will be unique.
578    #[builder(setter(into))]
579    pub session_id: String,
580
581    /// The moment the session was created.
582    #[builder(setter(into))]
583    #[serde(with = "time::serde::rfc3339")]
584    pub created_at: OffsetDateTime,
585
586    /// The last time the user was active in this session.
587    #[builder(setter(into))]
588    #[serde(with = "time::serde::rfc3339")]
589    pub updated_at: OffsetDateTime,
590
591    /// User details associated with the session.
592    pub user: User,
593
594    /// User's focus within the notebook.
595    #[builder(default)]
596    #[serde(default)]
597    pub focus: NotebookFocus,
598}
599
600#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
601#[cfg_attr(
602    feature = "fp-bindgen",
603    derive(Serializable),
604    fp(rust_module = "fiberplane_models::realtime")
605)]
606#[non_exhaustive]
607#[serde(rename_all = "camelCase")]
608pub struct SubscriberRemovedMessage {
609    /// The ID of the notebook that the user unsubscribed from.
610    pub notebook_id: String,
611
612    /// ID of the session that was removed.
613    pub session_id: String,
614}
615
616#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
617#[cfg_attr(
618    feature = "fp-bindgen",
619    derive(Serializable),
620    fp(rust_module = "fiberplane_models::realtime")
621)]
622#[non_exhaustive]
623#[serde(rename_all = "camelCase")]
624pub struct User {
625    /// The ID of the user. Will always be the same for the same user, so can be
626    /// used for de-dupping or input for color generation.
627    #[builder(setter(into))]
628    pub id: String,
629
630    /// Name of the user
631    #[builder(setter(into))]
632    pub name: String,
633}
634
635#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
636#[cfg_attr(
637    feature = "fp-bindgen",
638    derive(Serializable),
639    fp(rust_module = "fiberplane_models::realtime")
640)]
641#[non_exhaustive]
642#[serde(rename_all = "camelCase")]
643pub struct FocusInfoMessage {
644    /// ID of the notebook.
645    #[builder(setter(into))]
646    pub notebook_id: String,
647
648    /// User's focus within the notebook.
649    #[builder(default)]
650    #[serde(default)]
651    pub focus: NotebookFocus,
652
653    /// Operation ID. Empty if the user has not provided a op_id.
654    #[builder(default, setter(into, strip_option))]
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub op_id: Option<String>,
657}
658
659#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
660#[cfg_attr(
661    feature = "fp-bindgen",
662    derive(Serializable),
663    fp(rust_module = "fiberplane_models::realtime")
664)]
665#[non_exhaustive]
666#[serde(rename_all = "camelCase")]
667pub struct UserTypingCommentClientMessage {
668    #[builder(setter(into))]
669    pub notebook_id: Base64Uuid,
670    #[builder(setter(into))]
671    pub thread_id: Base64Uuid,
672
673    /// Operation ID. Empty if the user has not provided a op_id.
674    #[builder(default, setter(into, strip_option))]
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub op_id: Option<String>,
677}
678
679#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
680#[cfg_attr(
681    feature = "fp-bindgen",
682    derive(Serializable),
683    fp(rust_module = "fiberplane_models::realtime")
684)]
685#[non_exhaustive]
686#[serde(rename_all = "camelCase")]
687pub struct SubscribeWorkspaceMessage {
688    /// ID of the workspace
689    pub workspace_id: Base64Uuid,
690
691    /// Operation ID
692    #[builder(default, setter(into, strip_option))]
693    #[serde(skip_serializing_if = "Option::is_none")]
694    pub op_id: Option<String>,
695}
696
697#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
698#[cfg_attr(
699    feature = "fp-bindgen",
700    derive(Serializable),
701    fp(rust_module = "fiberplane_models::realtime")
702)]
703#[non_exhaustive]
704#[serde(rename_all = "camelCase")]
705pub struct UnsubscribeWorkspaceMessage {
706    /// ID of the workspace
707    pub workspace_id: Base64Uuid,
708
709    /// Operation ID
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub op_id: Option<String>,
712}
713
714#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
715#[cfg_attr(
716    feature = "fp-bindgen",
717    derive(Serializable),
718    fp(rust_module = "fiberplane_models::realtime")
719)]
720#[non_exhaustive]
721#[serde(rename_all = "camelCase")]
722pub struct SubscriberChangedFocusMessage {
723    /// ID of the session.
724    pub session_id: String,
725
726    /// ID of the notebook.
727    pub notebook_id: String,
728
729    /// User's focus within the notebook.
730    #[serde(default)]
731    pub focus: NotebookFocus,
732}
733
734/// A single focus position within a notebook.
735///
736/// Focus can be placed within a cell, and optionally within separate fields
737/// within the cell. An offset can be specified to indicate the exact position
738/// of the cursor within a text field.
739#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
740#[cfg_attr(
741    feature = "fp-bindgen",
742    derive(Serializable),
743    fp(rust_module = "fiberplane_models::realtime")
744)]
745#[non_exhaustive]
746#[serde(rename_all = "camelCase")]
747pub struct FocusPosition {
748    /// ID of the focused cell.
749    ///
750    /// May be the ID of an actual cell, or a so-called "surrogate ID", such as
751    /// the ID that indicates focus is on the title field.
752    #[builder(setter(into))]
753    pub cell_id: String,
754
755    /// Key to identify which field inside a cell has focus.
756    /// May be `None` for cells that have only one (or no) text field.
757    /// E.g.: For time range cells, “to” or “from” could be used.
758    ///
759    /// Note that fields do not necessarily have to be text fields. For example,
760    /// we could also use this to indicate the user has focused a button for
761    /// graph navigation.
762    #[serde(skip_serializing_if = "Option::is_none")]
763    pub field: Option<String>,
764
765    /// Offset within the text field.
766    /// May be `None` if the focus is not inside a text field.
767    #[serde(skip_serializing_if = "Option::is_none")]
768    pub offset: Option<u32>,
769}
770
771/// Specifies the user's focus and optional selection within the notebook.
772#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
773#[cfg_attr(
774    feature = "fp-bindgen",
775    derive(Serializable),
776    fp(rust_module = "fiberplane_models::realtime")
777)]
778#[non_exhaustive]
779#[serde(rename_all = "snake_case", tag = "type")]
780pub enum NotebookFocus {
781    /// The user has no focus within the notebook.
782    None,
783    /// The user focus is within the notebook and the focus is on a single
784    /// position. I.e. there is no selection.
785    Collapsed(FocusPosition),
786    /// The user has a selection within the notebook that started at the given
787    /// anchor position and ends at the given focus position.
788    Selection {
789        anchor: FocusPosition,
790        focus: FocusPosition,
791    },
792}
793
794impl NotebookFocus {
795    pub fn anchor_cell_id(&self) -> Option<&str> {
796        match self {
797            Self::None => None,
798            Self::Collapsed(collapsed) => Some(&collapsed.cell_id),
799            Self::Selection { anchor, .. } => Some(&anchor.cell_id),
800        }
801    }
802
803    pub fn anchor_cell_index(&self, cell_ids: &[&str]) -> Option<usize> {
804        cell_ids
805            .iter()
806            .position(|cell_id| Some(*cell_id) == self.anchor_cell_id())
807    }
808
809    pub fn anchor_field(&self) -> Option<&str> {
810        match self {
811            Self::None => None,
812            Self::Collapsed(collapsed) => collapsed.field.as_deref(),
813            Self::Selection { anchor, .. } => anchor.field.as_deref(),
814        }
815    }
816
817    pub fn anchor_offset(&self) -> u32 {
818        match self {
819            Self::None => 0,
820            Self::Collapsed(position) => position.offset.unwrap_or_default(),
821            Self::Selection { anchor, .. } => anchor.offset.unwrap_or_default(),
822        }
823    }
824
825    pub fn anchor_position(&self) -> Option<&FocusPosition> {
826        match self {
827            Self::None => None,
828            Self::Collapsed(position) => Some(position),
829            Self::Selection { anchor, .. } => Some(anchor),
830        }
831    }
832
833    pub fn end_cell_id(&self, cell_ids: &[&str]) -> Option<&str> {
834        match self {
835            Self::None => None,
836            Self::Collapsed(position) => Some(&position.cell_id),
837            Self::Selection { anchor, focus } => {
838                let anchor_cell_index = self.anchor_cell_index(cell_ids).unwrap_or_default();
839                let focus_cell_index = self.focus_cell_index(cell_ids).unwrap_or_default();
840                if anchor_cell_index > focus_cell_index {
841                    Some(&anchor.cell_id)
842                } else {
843                    Some(&focus.cell_id)
844                }
845            }
846        }
847    }
848
849    pub fn end_offset(&self, cell_ids: &[&str]) -> u32 {
850        match self {
851            Self::None => 0,
852            Self::Collapsed(position) => position.offset.unwrap_or_default(),
853            Self::Selection { anchor, focus } => {
854                let anchor_cell_index = self.anchor_cell_index(cell_ids).unwrap_or_default();
855                let anchor_offset = anchor.offset.unwrap_or_default();
856                let focus_cell_index = self.focus_cell_index(cell_ids).unwrap_or_default();
857                let focus_offset = focus.offset.unwrap_or_default();
858                match anchor_cell_index.cmp(&focus_cell_index) {
859                    Ordering::Greater => anchor_offset,
860                    Ordering::Equal => std::cmp::max(anchor_offset, focus_offset),
861                    Ordering::Less => focus_offset,
862                }
863            }
864        }
865    }
866
867    pub fn focus_cell_id(&self) -> Option<&str> {
868        match self {
869            Self::None => None,
870            Self::Collapsed(collapsed) => Some(&collapsed.cell_id),
871            Self::Selection { focus, .. } => Some(&focus.cell_id),
872        }
873    }
874
875    pub fn focus_cell_index(&self, cell_ids: &[&str]) -> Option<usize> {
876        cell_ids
877            .iter()
878            .position(|cell_id| Some(*cell_id) == self.focus_cell_id())
879    }
880
881    pub fn focus_field(&self) -> Option<&str> {
882        match self {
883            Self::None => None,
884            Self::Collapsed(collapsed) => collapsed.field.as_deref(),
885            Self::Selection { focus, .. } => focus.field.as_deref(),
886        }
887    }
888
889    pub fn focus_offset(&self) -> u32 {
890        match self {
891            Self::None => 0,
892            Self::Collapsed(position) => position.offset.unwrap_or_default(),
893            Self::Selection { focus, .. } => focus.offset.unwrap_or_default(),
894        }
895    }
896
897    pub fn focus_position(&self) -> Option<&FocusPosition> {
898        match self {
899            Self::None => None,
900            Self::Collapsed(position) => Some(position),
901            Self::Selection { focus, .. } => Some(focus),
902        }
903    }
904
905    pub fn has_selection(&self) -> bool {
906        !self.is_collapsed()
907    }
908
909    /// Returns whether the cursor position is collapsed, ie. the opposite of
910    /// `has_selection()`.
911    pub fn is_collapsed(&self) -> bool {
912        match self {
913            Self::None | Self::Collapsed(_) => true,
914            Self::Selection { focus, anchor } => *focus == *anchor,
915        }
916    }
917
918    pub fn is_none(&self) -> bool {
919        matches!(self, Self::None)
920    }
921
922    pub fn start_cell_id(&self, cell_ids: &[&str]) -> Option<&str> {
923        match self {
924            Self::None => None,
925            Self::Collapsed(position) => Some(&position.cell_id),
926            Self::Selection { anchor, focus } => {
927                if self.anchor_cell_index(cell_ids).unwrap_or_default()
928                    < self.focus_cell_index(cell_ids).unwrap_or_default()
929                {
930                    Some(&anchor.cell_id)
931                } else {
932                    Some(&focus.cell_id)
933                }
934            }
935        }
936    }
937
938    pub fn start_offset(&self, cell_ids: &[&str]) -> u32 {
939        match self {
940            Self::None => 0,
941            Self::Collapsed(position) => position.offset.unwrap_or_default(),
942            Self::Selection { anchor, focus } => {
943                let anchor_cell_index = self.anchor_cell_index(cell_ids).unwrap_or_default();
944                let anchor_offset = anchor.offset.unwrap_or_default();
945                let focus_cell_index = self.focus_cell_index(cell_ids).unwrap_or_default();
946                let focus_offset = focus.offset.unwrap_or_default();
947                match anchor_cell_index.cmp(&focus_cell_index) {
948                    Ordering::Less => anchor_offset,
949                    Ordering::Equal => std::cmp::min(anchor_offset, focus_offset),
950                    Ordering::Greater => focus_offset,
951                }
952            }
953        }
954    }
955}
956
957impl Default for NotebookFocus {
958    fn default() -> Self {
959        Self::None
960    }
961}
962
963#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
964#[cfg_attr(
965    feature = "fp-bindgen",
966    derive(Serializable),
967    fp(rust_module = "fiberplane_models::realtime")
968)]
969#[non_exhaustive]
970#[serde(rename_all = "camelCase")]
971pub struct ThreadAddedMessage {
972    pub notebook_id: String,
973    pub thread: Thread,
974}
975
976#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
977#[cfg_attr(
978    feature = "fp-bindgen",
979    derive(Serializable),
980    fp(rust_module = "fiberplane_models::realtime")
981)]
982#[non_exhaustive]
983#[serde(rename_all = "camelCase")]
984pub struct ThreadItemAddedMessage {
985    pub notebook_id: String,
986    pub thread_id: String,
987    pub thread_item: ThreadItem,
988}
989
990#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
991#[cfg_attr(
992    feature = "fp-bindgen",
993    derive(Serializable),
994    fp(rust_module = "fiberplane_models::realtime")
995)]
996#[non_exhaustive]
997#[serde(rename_all = "camelCase")]
998pub struct ThreadItemUpdatedMessage {
999    pub notebook_id: String,
1000    pub thread_id: String,
1001    pub thread_item: ThreadItem,
1002}
1003
1004#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
1005#[cfg_attr(
1006    feature = "fp-bindgen",
1007    derive(Serializable),
1008    fp(rust_module = "fiberplane_models::realtime")
1009)]
1010#[non_exhaustive]
1011#[serde(rename_all = "camelCase")]
1012pub struct ThreadDeletedMessage {
1013    pub notebook_id: String,
1014    pub thread_id: String,
1015}
1016
1017#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
1018#[cfg_attr(
1019    feature = "fp-bindgen",
1020    derive(Serializable),
1021    fp(rust_module = "fiberplane_models::realtime")
1022)]
1023#[non_exhaustive]
1024#[serde(rename_all = "camelCase")]
1025pub struct UserTypingCommentServerMessage {
1026    pub notebook_id: String,
1027    pub thread_id: String,
1028    pub user: UserSummary,
1029    #[serde(with = "time::serde::rfc3339")]
1030    pub updated_at: OffsetDateTime,
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035    use super::*;
1036
1037    #[test]
1038    fn serialize_reject_reason() {
1039        let reason = OutdatedRejectReason {
1040            current_revision: 1,
1041        };
1042        let reason = RejectReason::Outdated(reason);
1043        let result = serde_json::to_string(&reason);
1044        if let Err(err) = result {
1045            panic!("Unexpected error occurred: {err:?}");
1046        }
1047    }
1048}