myxine_core/page/
content.rs

1use bytes::Bytes;
2use serde::Serialize;
3use std::mem;
4use tokio::stream::{Stream, StreamExt};
5use tokio::sync::broadcast;
6use uuid::Uuid;
7
8use super::RefreshMode;
9
10/// The `Content` of a page is either `Dynamic` or `Static`. If it's dynamic, it
11/// has a title, body, and a set of SSE event listeners who are waiting for
12/// updates to the page. If it's static, it just has a fixed content type and a
13/// byte array of contents to be returned when fetched. `Page`s can be changed
14/// from dynamic to static and vice-versa: when changing from dynamic to static,
15/// the change is instantly reflected in the client web browser; in the other
16/// direction, it requires a manual refresh (because a static page has no
17/// injected javascript to make it update itself).
18#[derive(Debug, Clone)]
19pub enum Content {
20    Dynamic {
21        title: String,
22        body: String,
23        updates: broadcast::Sender<Command>,
24        other_commands: broadcast::Sender<Command>,
25    },
26    Static {
27        content_type: Option<String>,
28        raw_contents: Bytes,
29    },
30}
31
32/// A command sent directly to the code running in the browser page to tell it
33/// to update or perform some other action.
34#[derive(Debug, Clone, Serialize)]
35#[serde(rename_all = "camelCase", tag = "type")]
36pub enum Command {
37    /// Reload the page completely, i.e. via `window.location.reload()`.
38    #[serde(rename_all = "camelCase")]
39    Reload,
40    /// Update the page's dynamic content by setting `window.title` to the given
41    /// title, and setting the contents of the `<body>` to the given body,
42    /// either by means of a DOM diff (if `diff == true`) or directly by setting
43    /// `.innerHTML` (if `diff == false`).
44    #[serde(rename_all = "camelCase")]
45    Update {
46        /// The new title of the page.
47        title: String,
48        /// The new body of the page.
49        body: String,
50        /// Whether to use some diffing method to increase efficiency in the
51        /// update (this is usually `true` outside of some debugging contexts.)
52        diff: bool,
53    },
54    /// Evaluate some JavaScript code in the page.
55    #[serde(rename_all = "camelCase")]
56    Evaluate {
57        /// The text of the JavaScript to evaluate.
58        script: String,
59        /// If `statement_mode == true`, then the given script is evaluated
60        /// exactly as-is; otherwise, it is treated as an *expression* and
61        /// wrapped in an implicit `return (...);`.
62        statement_mode: bool,
63        /// The unique id of the request for evaluation, which will be used to
64        /// report the result once it is available.
65        id: Uuid,
66    },
67}
68
69/// The maximum number of updates to buffer before dropping an update. This is
70/// set to 1, because dropped updates are okay (the most recent update will
71/// always get through once things quiesce).
72const UPDATE_BUFFER_SIZE: usize = 1;
73
74/// The maximum number of non-update commands to buffer before dropping one.
75/// This is set to a medium sized number, because we don't want to drop a reload
76/// command or an evaluate command. Unlike the update buffer, clients likely
77/// won't fill this one, because it's used only for occasional full-reload
78/// commands and for evaluating JavaScript, neither of which should be done at
79/// an absurd rate.
80const OTHER_COMMAND_BUFFER_SIZE: usize = 16;
81// NOTE: This memory is allocated all at once, which means that the choice of
82// buffer size impacts myxine's memory footprint.
83
84impl Content {
85    /// Make a new empty (dynamic) page
86    pub fn new() -> Content {
87        Content::Dynamic {
88            title: String::new(),
89            body: String::new(),
90            updates: broadcast::channel(UPDATE_BUFFER_SIZE).0,
91            other_commands: broadcast::channel(OTHER_COMMAND_BUFFER_SIZE).0,
92        }
93    }
94
95    /// Test if this page is empty, where "empty" means that it is dynamic, with
96    /// an empty title, empty body, and no subscribers waiting on its page
97    /// events: that is, it's identical to `Content::new()`.
98    pub fn is_empty(&self) -> bool {
99        match self {
100            Content::Dynamic {
101                title,
102                body,
103                ref updates,
104                ref other_commands,
105            } if title == "" && body == "" => {
106                updates.receiver_count() == 0 && other_commands.receiver_count() == 0
107            }
108            _ => false,
109        }
110    }
111
112    /// Add a client to the dynamic content of a page, if it is dynamic. If it
113    /// is static, this has no effect and returns None. Otherwise, returns the
114    /// Body stream to give to the new client.
115    pub fn commands(&self) -> Option<impl Stream<Item = Command>> {
116        let result = match self {
117            Content::Dynamic {
118                updates,
119                other_commands,
120                ..
121            } => {
122                let merged = updates.subscribe().merge(other_commands.subscribe());
123                let stream_body = merged
124                    .filter_map(|result| {
125                        match result {
126                            // We ignore lagged items in the stream! If we don't
127                            // ignore these, we would terminate the Body on
128                            // every lag, which is undesirable.
129                            Err(broadcast::RecvError::Lagged(_)) => None,
130                            // Otherwise, if the stream is over, we end this stream.
131                            Err(broadcast::RecvError::Closed) => Some(Err(())),
132                            // But if the item is ok, forward it.
133                            Ok(item) => Some(Ok(item)),
134                        }
135                    })
136                    .take_while(|i| i.is_ok())
137                    .map(|i| i.unwrap());
138                Some(stream_body)
139            }
140            Content::Static { .. } => None,
141        };
142        // Make sure the page is up to date
143        self.refresh(RefreshMode::Diff);
144        result
145    }
146
147    /// Tell all clients to refresh the contents of a page, if it is dynamic.
148    /// This has no effect if it is (currently) static.
149    pub fn refresh(&self, refresh: RefreshMode) {
150        match self {
151            Content::Dynamic {
152                updates,
153                other_commands,
154                title,
155                body,
156            } => {
157                let _ = match refresh {
158                    RefreshMode::FullReload => other_commands.send(Command::Reload),
159                    RefreshMode::SetBody | RefreshMode::Diff => updates.send(Command::Update {
160                        title: title.clone(),
161                        body: body.clone(),
162                        diff: refresh == RefreshMode::Diff,
163                    }),
164                };
165            }
166            Content::Static { .. } => (),
167        };
168    }
169
170    /// Set the contents of the page to be a static raw set of bytes with no
171    /// self-refreshing functionality. All clients will be told to refresh their
172    /// page to load the new static content (which will not be able to update
173    /// itself until a client refreshes their page again).
174    pub fn set_static(&mut self, content_type: Option<String>, raw_contents: Bytes) {
175        let mut content = Content::Static {
176            content_type,
177            raw_contents,
178        };
179        mem::swap(&mut content, self);
180        content.refresh(RefreshMode::FullReload);
181    }
182
183    /// Tell all clients to change the body, if necessary. This converts the
184    /// page into a dynamic page, overwriting any static content that previously
185    /// existed, if any. Returns `true` if the page content was changed (either
186    /// converted from static, or altered whilst dynamic).
187    pub fn set(
188        &mut self,
189        new_title: impl Into<String>,
190        new_body: impl Into<String>,
191        refresh: RefreshMode,
192    ) -> bool {
193        let mut changed = false;
194        loop {
195            match self {
196                Content::Dynamic {
197                    ref mut title,
198                    ref mut body,
199                    ..
200                } => {
201                    let new_title = new_title.into();
202                    let new_body = new_body.into();
203                    if new_title != *title || new_body != *body {
204                        changed = true;
205                    }
206                    *title = new_title;
207                    *body = new_body;
208                    break; // values have been set
209                }
210                Content::Static { .. } => {
211                    *self = Content::new();
212                    changed = true;
213                    // and loop again to actually set
214                }
215            }
216        }
217        if changed {
218            self.refresh(refresh);
219        }
220        changed
221    }
222}