1use std::cell::Cell;
4use std::fmt;
5use std::sync::Arc;
6use std::time::Duration;
7
8use ::http::StatusCode;
9use bytes::Bytes;
10use tokio::sync::OnceCell;
11use tokio::time::Instant;
12
13use crate::Result;
14use crate::backend::Session;
15use crate::dataset::{Data, Dataset, Datasets};
16use crate::frontier::InMemFrontier;
17use crate::frontier::queue::Queue;
18use crate::http::{Request, Response, ResponseExt};
19
20pub struct Context<C> {
34 request: Request,
35 session: C,
36 queue: Queue,
37 datasets: Arc<Datasets>,
38 response: OnceCell<Response>,
39}
40
41#[derive(Debug, Default, Clone, Copy)]
47pub(crate) struct Fetch {
48 pub(crate) latency: Option<Duration>,
49 pub(crate) status: Option<StatusCode>,
50}
51
52tokio::task_local! {
53 pub(crate) static FETCH: Cell<Fetch>;
55}
56
57impl<C> Context<C> {
58 #[must_use]
64 pub fn new(request: Request, session: C) -> Self {
65 let queue = Queue::new(InMemFrontier::fifo()).child_of(&request);
66 Self::from_parts(request, session, queue, Arc::default())
67 }
68
69 pub(crate) fn from_parts(
70 request: Request,
71 session: C,
72 queue: Queue,
73 datasets: Arc<Datasets>,
74 ) -> Self {
75 Self {
76 request,
77 session,
78 queue,
79 datasets,
80 response: OnceCell::new(),
81 }
82 }
83
84 #[must_use]
86 pub fn with_queue(mut self, queue: &Queue) -> Self {
87 self.queue = queue.child_of(&self.request);
88 self
89 }
90
91 #[must_use]
93 pub fn with_dataset<T, D>(mut self, dataset: D) -> Self
94 where
95 T: Send + 'static,
96 D: Dataset<T>,
97 {
98 Arc::make_mut(&mut self.datasets).insert(dataset);
99 self
100 }
101
102 #[must_use]
104 pub const fn request(&self) -> &Request {
105 &self.request
106 }
107
108 pub const fn request_mut(&mut self) -> &mut Request {
110 &mut self.request
111 }
112
113 #[must_use]
115 pub const fn session(&self) -> &C {
116 &self.session
117 }
118
119 #[must_use]
121 pub const fn queue(&self) -> &Queue {
122 &self.queue
123 }
124
125 pub fn dataset<T: Send + 'static>(&self) -> Result<Data<T>> {
132 self.datasets.get()
133 }
134
135 pub async fn response(&self) -> Result<&Response>
146 where
147 C: Session,
148 {
149 self.response
150 .get_or_try_init(|| async {
151 let started = Instant::now();
152 let response = self.session.fetch(&self.request).await;
153 let fetch = Fetch {
156 latency: Some(started.elapsed()),
157 status: response.as_ref().ok().map(::http::Response::status),
158 };
159 let _ = FETCH.try_with(|cell| cell.set(fetch));
160 let response = response?;
161 if let Some(uri) = response.final_uri() {
162 self.queue.rebase(uri.clone());
163 }
164 Ok(response)
165 })
166 .await
167 }
168
169 pub async fn body(&self) -> Result<Bytes>
177 where
178 C: Session,
179 {
180 Ok(self.response().await?.body().clone())
181 }
182}
183
184impl<C> fmt::Debug for Context<C> {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 f.debug_struct("Context")
187 .field("method", self.request.method())
188 .field("uri", self.request.uri())
189 .field("queue", &self.queue)
190 .field("fetched", &self.response.initialized())
191 .finish_non_exhaustive()
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use std::future::Future;
198 use std::sync::atomic::{AtomicUsize, Ordering};
199
200 use super::*;
201 use crate::backend::Noop;
202 use crate::crawler::Depth;
203 use crate::dataset::InMemDataset;
204 use crate::frontier::seed::Seed;
205 use crate::http::RequestExt;
206
207 #[tokio::test]
208 async fn standalone_context() {
209 let request = Seed::get("https://example.com/").unwrap().into_request();
210 let cx = Context::new(request, Noop).with_dataset(InMemDataset::<u32>::new());
211
212 cx.dataset::<u32>().unwrap().write(1).await.unwrap();
213 assert!(cx.dataset::<String>().is_err());
214
215 cx.queue().follow("https://example.com/next").await.unwrap();
216 assert_eq!(cx.queue().size_hint(), Some(1));
217 assert_eq!(cx.request().depth(), Depth::ROOT);
218
219 let response = cx.response().await.unwrap();
220 assert!(response.status().is_success());
221 }
222
223 struct Counting {
226 fetches: AtomicUsize,
227 fail: usize,
228 body: std::sync::Mutex<&'static str>,
229 }
230
231 impl Session for Counting {
232 fn fetch(&self, _request: &Request) -> impl Future<Output = Result<Response>> + Send {
233 let fetch = self.fetches.fetch_add(1, Ordering::SeqCst);
234 std::future::ready(if fetch < self.fail {
235 Err(crate::Error::new(crate::ErrorKind::Http))
236 } else {
237 Ok(Response::new(Bytes::from_static(
238 self.body.lock().unwrap().as_bytes(),
239 )))
240 })
241 }
242 }
243
244 #[tokio::test]
245 async fn fetches_once_and_keeps_a_snapshot_of_the_body() {
246 let request = Seed::get("https://example.com/").unwrap().into_request();
247 let client = Counting {
248 fetches: AtomicUsize::new(0),
249 fail: 1,
250 body: std::sync::Mutex::new("loaded"),
251 };
252 let cx = Context::new(request, client);
253
254 assert!(cx.response().await.is_err());
256 assert_eq!(cx.body().await.unwrap(), "loaded");
257 *cx.session().body.lock().unwrap() = "clicked";
258 assert_eq!(cx.body().await.unwrap(), "loaded");
259 assert_eq!(cx.response().await.unwrap().body(), "loaded");
260 assert!(cx.response().await.unwrap().status().is_success());
261 assert_eq!(cx.session().fetches.load(Ordering::SeqCst), 2);
262 }
263}