Skip to main content

fiberplane_models/
realtime.rs

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