chuchi_ssr/
lib.rs

1mod pool;
2mod runtime;
3
4use pool::PoolHandle;
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::{fmt, io};
10
11use serde::{Deserialize, Serialize};
12
13use chuchi::header::{Mime, StatusCode};
14use chuchi::{ChuchiShared, Request, Resource, Response};
15
16use aho_corasick::AhoCorasick;
17
18#[derive(Debug)]
19#[non_exhaustive]
20pub enum Error {
21	Panicked,
22	Io(io::Error),
23	Other(String),
24}
25
26impl fmt::Display for Error {
27	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28		fmt::Debug::fmt(self, f)
29	}
30}
31
32impl std::error::Error for Error {}
33
34// /// Request to ssr
35#[derive(Debug, Clone, Serialize, Deserialize)]
36struct SsrRequest {
37	// the ip address from the requestor
38	pub address: String,
39	/// GET, POST
40	pub method: String,
41	pub uri: String,
42	pub headers: HashMap<String, String>,
43	pub body: String,
44}
45
46// // pub struct Cache {
47// // 	pub
48// // }
49
50// /// Response from ssr
51#[derive(Debug, Clone, Serialize, Deserialize)]
52struct SsrResponse {
53	// pub cache: Option<Cache>
54	pub status: u16,
55	// those fields are replace in the index.html <!--ssr-field-->
56	// where `field` is the key
57	pub fields: HashMap<String, String>,
58}
59
60#[derive(Clone, Resource)]
61pub struct JsServer {
62	pool: PoolHandle,
63	index: Arc<String>, // tx: mpsc::Sender<(SsrRequest, oneshot::Receiver<SsrResponse>)>
64}
65
66impl JsServer {
67	/// this module should export { render } (which takes a SsrRequest)
68	/// and should return a SsrResponse
69	///
70	/// threads: how many concurrent js instances can exist
71	///
72	/// ## Panics if opts cannot be serialized to serde_json::Value
73	pub fn new<T: Serialize>(
74		base_dir: impl Into<PathBuf>,
75		index_html: impl Into<String>,
76		opts: T,
77		max_threads: usize,
78	) -> Self {
79		let pool = PoolHandle::new(
80			base_dir.into(),
81			max_threads,
82			serde_json::to_value(opts).unwrap(),
83		);
84
85		// runtime ready
86		Self {
87			pool,
88			index: Arc::new(index_html.into()),
89		}
90	}
91
92	/// Call this if you wan't to route requests internally without going over
93	/// the http stack
94	///
95	/// You need to pass a FirePit
96	pub async fn route_internally(&self, shared: ChuchiShared) {
97		self.pool.send_pit(shared).await;
98	}
99
100	pub async fn request(&self, req: &mut Request) -> Result<Response, Error> {
101		let body = req.take_body().into_string().await.map_err(Error::Io)?;
102
103		let header = &req.header;
104		let method = header.method.to_string().to_uppercase();
105
106		let headers = header.values.clone().into_inner();
107		let headers: HashMap<_, _> = headers
108			.iter()
109			.filter_map(|(key, val)| {
110				val.to_str().ok().map(|s| (key.to_string(), s.to_string()))
111			})
112			.collect();
113
114		let uri = if let Some(query) = header.uri.query() {
115			format!("{}?{}", header.uri.path(), query)
116		} else {
117			header.uri.path().to_string()
118		};
119
120		let ssr_request = SsrRequest {
121			address: header.address.to_string(),
122			method,
123			uri,
124			headers,
125			body,
126		};
127
128		let resp = self
129			.pool
130			.send_req(ssr_request)
131			.await
132			.ok_or(Error::Panicked)?;
133
134		let ac = AhoCorasick::new(
135			resp.fields.keys().map(|k| format!("<!--ssr-{k}-->")),
136		)
137		.expect("aho corasick limit exceeded");
138
139		let values: Vec<_> = resp.fields.values().collect();
140
141		let index = ac.replace_all(&self.index, &values);
142
143		let resp = Response::builder()
144			.status_code(
145				StatusCode::from_u16(resp.status)
146					.map_err(|e| Error::Other(e.to_string()))?,
147			)
148			.content_type(Mime::HTML)
149			.body(index)
150			.build();
151
152		Ok(resp)
153	}
154}