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}