1use std::{fs, net::Shutdown, path::PathBuf, sync::Arc};
2
3use http::Uri;
4use serde::Serialize;
5use tokio::{
6 io::{AsyncReadExt, AsyncWriteExt},
7 net::TcpStream,
8};
9use uuid::Uuid;
10
11use crate::{
12 error::{InitializationError, RenderingError},
13 worker::{Port, Worker},
14};
15
16pub enum JsRenderer {
18 Global,
21 PerRequest {
23 path: PathBuf,
25 },
26}
27
28pub enum JsWorkerLog {
30 Minimal,
32 Verbose,
34}
35
36impl JsWorkerLog {
37 pub(crate) fn to_str(&self) -> &str {
38 match self {
39 JsWorkerLog::Minimal => "minimal",
40 JsWorkerLog::Verbose => "verbose",
41 }
42 }
43}
44
45pub struct SsrConfig {
47 pub port: u16,
49 pub js_worker: PathBuf,
52 pub js_worker_log: JsWorkerLog,
54 pub global_js_renderer: Option<PathBuf>,
61}
62
63#[derive(Clone)]
65pub struct Ssr {
66 worker: Arc<Worker>,
67 js_worker: PathBuf,
68 global_js_renderer: Option<PathBuf>,
69}
70
71impl Ssr {
72 pub async fn new(cfg: SsrConfig) -> Result<Self, InitializationError> {
88 let port = Port::new(cfg.port);
89 let js_worker = match fs::canonicalize(cfg.js_worker) {
90 Ok(path) => path,
91 Err(err) => return Err(InitializationError::InvalidJsWorkerPath(err)),
92 };
93 let global_js_renderer = match cfg.global_js_renderer {
94 Some(path) => match fs::canonicalize(path) {
95 Ok(path) => Some(path),
96 Err(err) => return Err(InitializationError::InvalidGlobalJsRendererPath(err)),
97 },
98 None => None,
99 };
100 let worker =
101 Worker::new(&port, &js_worker, &cfg.js_worker_log, &global_js_renderer).await?;
102 Ok(Self {
103 worker: Arc::new(worker),
104 js_worker,
105 global_js_renderer,
106 })
107 }
108
109 pub async fn render<D: Serialize>(
124 &self,
125 uri: &Uri,
126 data: &D,
127 js_renderer: JsRenderer,
128 ) -> Result<String, RenderingError> {
129 let request_id = Uuid::new_v4();
130
131 trace!("Starting request {}", request_id);
132
133 let worker = &self.worker;
134
135 let mut stream = match worker.connect().await {
136 Ok(stream) => stream,
137 Err(err) => {
138 error!(
139 "{worker}: Failed to connect: {err}",
140 worker = worker.display_with_request_id(&request_id),
141 err = err
142 );
143 return Err(RenderingError::ConnectionError(err));
144 }
145 };
146
147 let url = match uri.path_and_query() {
148 Some(url) => url,
149 None => {
150 Self::finalize_rendering_session(&worker, &stream, &request_id);
151 return Err(RenderingError::InvalidUri);
152 }
153 };
154
155 let request_renderer = match (&self.global_js_renderer, js_renderer) {
156 (Some(_), JsRenderer::Global) => None,
157 (_, JsRenderer::PerRequest { path }) => Some(path),
158 (None, JsRenderer::Global) => {
159 Self::finalize_rendering_session(&worker, &stream, &request_id);
160 return Err(RenderingError::GlobalRendererNotProvided);
161 }
162 };
163
164 let meta = json!({
165 "requestId": request_id,
166 "requestRenderer": request_renderer,
167 "url": json!({"path": url.path(), "query": url.query()}),
168 });
169 let meta_bytes = match serde_json::to_vec(&meta) {
170 Ok(bytes) => bytes,
171 Err(err) => {
172 Self::finalize_rendering_session(&worker, &stream, &request_id);
173 return Err(RenderingError::UrlSerializationError(err));
174 }
175 };
176 let data = match serde_json::to_string(&data) {
177 Ok(data) => data,
178 Err(err) => {
179 Self::finalize_rendering_session(&worker, &stream, &request_id);
180 return Err(RenderingError::DataSerializationError(err));
181 }
182 };
183 let data_bytes = match crate::json::to_vec(&data) {
184 Ok(bytes) => bytes,
185 Err(err) => {
186 Self::finalize_rendering_session(&worker, &stream, &request_id);
187 return Err(RenderingError::DataSerializationError(err));
188 }
189 };
190 let meta_len = meta_bytes.len() as u32;
191 let data_len = data_bytes.len() as u32;
192 let meta_len_bytes = meta_len.to_be_bytes();
193 let data_len_bytes = data_len.to_be_bytes();
194 let mut input = meta_len_bytes.to_vec();
195 input.extend_from_slice(&data_len_bytes);
196 input.extend(meta_bytes);
197 input.extend(data_bytes);
198
199 let mut res = String::new();
200
201 trace!(
202 "{worker}: Writing input to socket",
203 worker = worker.display_with_request_id(&request_id),
204 );
205
206 if let Err(err) = stream.write_all(input.as_slice()).await {
207 Self::finalize_rendering_session(&worker, &stream, &request_id);
208 return Err(RenderingError::RenderRequestError(err));
209 };
210
211 trace!(
212 "{worker}: Input written to socket",
213 worker = worker.display_with_request_id(&request_id),
214 );
215
216 if let Err(err) = stream.read_to_string(&mut res).await {
217 Self::finalize_rendering_session(&worker, &stream, &request_id);
218 return Err(RenderingError::RenderResponseError(err));
219 };
220
221 trace!(
222 "{worker}: Output written to result buffer",
223 worker = worker.display_with_request_id(&request_id),
224 );
225
226 if res.starts_with("ERROR:") {
228 trace!(
229 "{worker}: Output is an error",
230 worker = worker.display_with_request_id(&request_id),
231 );
232 let stack = res.strip_prefix("ERROR:").unwrap();
233 Err(RenderingError::JsExceptionDuringRendering(stack.to_string()))
234 } else {
235 trace!(
236 "{worker}: Output is ok",
237 worker = worker.display_with_request_id(&request_id),
238 );
239 Ok(res)
240 }
241 }
242
243 fn finalize_rendering_session(worker: &Worker, connection: &TcpStream, request_id: &Uuid) {
244 if let Err(err) = connection.shutdown(Shutdown::Both) {
245 warn!(
246 "{worker}: Failed to shutdown connection to the js worker: {err}",
247 worker = worker.display_with_request_id(&request_id),
248 err = err
249 );
250 };
251 }
252}