turbopack_dev_server/update/
stream.rs1use std::pin::Pin;
2
3use anyhow::Result;
4use async_trait::async_trait;
5use futures::prelude::*;
6use tokio::sync::mpsc::Sender;
7use tokio_stream::wrappers::ReceiverStream;
8use tracing::Instrument;
9use turbo_rcstr::{RcStr, rcstr};
10use turbo_tasks::{
11 NonLocalValue, OperationVc, PrettyPrintError, ReadRef, ResolvedVc, TransientInstance, Vc,
12 trace::{TraceRawVcs, TraceRawVcsContext},
13};
14use turbo_tasks_fs::{FileSystem, FileSystemPath};
15use turbopack_core::{
16 issue::{
17 CollectibleIssuesExt, Issue, IssueFilter, IssueSeverity, IssueStage, PlainIssue,
18 StyledString,
19 },
20 server_fs::ServerFileSystem,
21 version::{
22 NotFoundVersion, PartialUpdate, TotalUpdate, Update, Version, VersionState,
23 VersionedContent,
24 },
25};
26
27use crate::source::{ProxyResult, resolve::ResolveSourceRequestResult};
28
29struct TypedGetContentFn<C> {
30 capture: C,
31 func: for<'a> fn(&'a C) -> OperationVc<ResolveSourceRequestResult>,
32}
33
34unsafe impl<C: NonLocalValue> NonLocalValue for TypedGetContentFn<C> {}
37
38impl<C: TraceRawVcs> TraceRawVcs for TypedGetContentFn<C> {
40 fn trace_raw_vcs(&self, trace_context: &mut TraceRawVcsContext) {
41 self.capture.trace_raw_vcs(trace_context);
42 }
43}
44
45trait TypedGetContentFnTrait: NonLocalValue + TraceRawVcs {
46 fn call(&self) -> OperationVc<ResolveSourceRequestResult>;
47}
48
49impl<C> TypedGetContentFnTrait for TypedGetContentFn<C>
50where
51 C: NonLocalValue + TraceRawVcs,
52{
53 fn call(&self) -> OperationVc<ResolveSourceRequestResult> {
54 (self.func)(&self.capture)
55 }
56}
57
58#[derive(NonLocalValue, TraceRawVcs)]
64pub struct GetContentFn {
65 inner: Box<dyn TypedGetContentFnTrait + Send + Sync>,
66}
67
68impl GetContentFn {
69 pub fn new<C>(
72 capture: C,
73 func: for<'a> fn(&'a C) -> OperationVc<ResolveSourceRequestResult>,
74 ) -> Self
75 where
76 C: NonLocalValue + TraceRawVcs + Send + Sync + 'static,
77 {
78 Self {
79 inner: Box::new(TypedGetContentFn { capture, func }),
80 }
81 }
82}
83
84impl GetContentFn {
85 fn call(&self) -> OperationVc<ResolveSourceRequestResult> {
86 self.inner.call()
87 }
88}
89
90async fn peek_issues<T: Send>(source: OperationVc<T>) -> Result<Vec<ReadRef<PlainIssue>>> {
91 let captured = source.peek_issues();
92
93 captured.get_plain_issues(&IssueFilter::everything()).await
94}
95
96fn extend_issues(issues: &mut Vec<ReadRef<PlainIssue>>, new_issues: Vec<ReadRef<PlainIssue>>) {
97 for issue in new_issues {
98 if issues.contains(&issue) {
99 continue;
100 }
101
102 issues.push(issue);
103 }
104}
105
106#[turbo_tasks::function(operation, root)]
107fn versioned_content_update_operation(
108 content: ResolvedVc<Box<dyn VersionedContent>>,
109 from: ResolvedVc<Box<dyn Version>>,
110) -> Vc<Update> {
111 content.update(*from)
112}
113
114#[turbo_tasks::function(operation, root)]
118async fn initial_version_operation(
119 content: OperationVc<ResolveSourceRequestResult>,
120) -> Result<Vc<Box<dyn Version>>> {
121 Ok(match *content.read_strongly_consistent().await? {
122 ResolveSourceRequestResult::Static(static_content, _) => {
123 static_content.await?.content.version()
124 }
125 ResolveSourceRequestResult::HttpProxy(proxy_result) => Vc::upcast(proxy_result.connect()),
126 _ => Vc::upcast(NotFoundVersion::new()),
127 })
128}
129
130#[turbo_tasks::function(operation, root)]
131async fn get_update_stream_item_operation(
132 resource: RcStr,
133 from: ResolvedVc<VersionState>,
134 get_content: TransientInstance<GetContentFn>,
135) -> Result<Vc<UpdateStreamItem>> {
136 let content_op = get_content.call();
137 let content_result = content_op.read_strongly_consistent().await;
138 let mut plain_issues = peek_issues(content_op).await?;
139
140 let content_value = match content_result {
141 Ok(content) => content,
142 Err(e) => {
143 plain_issues.push(
144 PlainIssue::from_issue(
145 Vc::upcast(
146 FatalStreamIssue {
147 resource,
148 description: StyledString::Text(
149 format!("{}", PrettyPrintError(&e)).into(),
150 )
151 .resolved_cell(),
152 }
153 .cell(),
154 ),
155 None,
156 )
157 .await?,
158 );
159
160 let update = Update::Total(TotalUpdate {
161 to: Vc::upcast::<Box<dyn Version>>(NotFoundVersion::new())
162 .into_trait_ref()
163 .await?,
164 })
165 .cell();
166 return Ok(UpdateStreamItem::Found {
167 update: update.await?,
168 issues: plain_issues,
169 }
170 .cell());
171 }
172 };
173
174 match *content_value {
175 ResolveSourceRequestResult::Static(static_content_vc, _) => {
176 let static_content = static_content_vc.await?;
177
178 if static_content.status_code == 404 {
180 return Ok(UpdateStreamItem::NotFound.cell());
181 }
182
183 let resolved_content = static_content.content;
184 let from = from.get().to_resolved().await?;
185 let update_op = versioned_content_update_operation(resolved_content, from);
186
187 extend_issues(&mut plain_issues, peek_issues(update_op).await?);
188
189 Ok(UpdateStreamItem::Found {
190 update: update_op.connect().await?,
191 issues: plain_issues,
192 }
193 .cell())
194 }
195 ResolveSourceRequestResult::HttpProxy(proxy_result_op) => {
196 let proxy_result_vc = proxy_result_op.connect();
197 let proxy_result_value = proxy_result_vc.await?;
198
199 if proxy_result_value.status == 404 {
200 return Ok(UpdateStreamItem::NotFound.cell());
201 }
202
203 extend_issues(&mut plain_issues, peek_issues(proxy_result_op).await?);
204
205 let from = from.get();
206 if let Some(from) =
207 ResolvedVc::try_downcast_type::<ProxyResult>(from.to_resolved().await?)
208 && from.await? == proxy_result_value
209 {
210 return Ok(UpdateStreamItem::Found {
211 update: Update::None.cell().await?,
212 issues: plain_issues,
213 }
214 .cell());
215 }
216
217 Ok(UpdateStreamItem::Found {
218 update: Update::Total(TotalUpdate {
219 to: Vc::upcast::<Box<dyn Version>>(proxy_result_vc)
220 .into_trait_ref()
221 .await?,
222 })
223 .cell()
224 .await?,
225 issues: plain_issues,
226 }
227 .cell())
228 }
229 _ => {
230 let update = if plain_issues.is_empty() {
231 Update::Total(TotalUpdate {
236 to: Vc::upcast::<Box<dyn Version>>(NotFoundVersion::new())
237 .into_trait_ref()
238 .await?,
239 })
240 .cell()
241 } else {
242 Update::None.cell()
243 };
244
245 Ok(UpdateStreamItem::Found {
246 update: update.await?,
247 issues: plain_issues,
248 }
249 .cell())
250 }
251 }
252}
253
254#[derive(TraceRawVcs)]
255struct ComputeUpdateStreamSender(
256 #[turbo_tasks(trace_ignore)] Sender<Result<ReadRef<UpdateStreamItem>>>,
263);
264
265#[turbo_tasks::function]
268async fn compute_update_stream(
269 resource: RcStr,
270 from: ResolvedVc<VersionState>,
271 get_content: TransientInstance<GetContentFn>,
272 sender: TransientInstance<ComputeUpdateStreamSender>,
273) -> () {
274 let item = get_update_stream_item_operation(resource, from, get_content)
275 .read_strongly_consistent()
276 .await;
277
278 let _ = sender.0.send(item).await;
280}
281
282pub(super) struct UpdateStream(
283 Pin<Box<dyn Stream<Item = Result<ReadRef<UpdateStreamItem>>> + Send + Sync>>,
284);
285
286impl UpdateStream {
287 #[tracing::instrument(skip(get_content), name = "UpdateStream::new")]
288 pub async fn new(
289 resource: RcStr,
290 get_content: TransientInstance<GetContentFn>,
291 ) -> Result<UpdateStream> {
292 let (sx, rx) = tokio::sync::mpsc::channel(32);
293
294 let content = get_content.call();
295 let version = initial_version_operation(content)
300 .read_trait_strongly_consistent()
301 .await?;
302 let version_state = VersionState::new(version).await?;
303
304 let _ = compute_update_stream(
305 resource,
306 version_state,
307 get_content,
308 TransientInstance::new(ComputeUpdateStreamSender(sx)),
309 );
310
311 let mut last_had_issues = false;
312
313 let stream = ReceiverStream::new(rx).filter_map(move |item| {
314 {
315 let (has_issues, issues_changed) =
316 if let Ok(UpdateStreamItem::Found { issues, .. }) = item.as_deref() {
317 let has_issues = !issues.is_empty();
318 let issues_changed = has_issues != last_had_issues;
319 last_had_issues = has_issues;
320 (has_issues, issues_changed)
321 } else {
322 (false, false)
323 };
324
325 async move {
326 match item.as_deref() {
327 Ok(UpdateStreamItem::Found { update, .. }) => {
328 match &**update {
329 Update::Partial(PartialUpdate { to, .. })
330 | Update::Total(TotalUpdate { to }) => {
331 version_state
332 .set(to.clone())
333 .await
334 .expect("failed to update version");
335
336 Some(item)
337 }
338 Update::None | Update::Missing => {
340 if has_issues || issues_changed {
341 Some(item)
342 } else {
343 None
344 }
345 }
346 }
347 }
348 _ => {
349 Some(item)
351 }
352 }
353 }
354 .in_current_span()
355 }
356 .in_current_span()
357 });
358
359 Ok(UpdateStream(Box::pin(stream)))
360 }
361}
362
363impl Stream for UpdateStream {
364 type Item = Result<ReadRef<UpdateStreamItem>>;
365
366 fn poll_next(
367 self: Pin<&mut Self>,
368 cx: &mut std::task::Context<'_>,
369 ) -> std::task::Poll<Option<Self::Item>> {
370 Pin::new(&mut self.get_mut().0).poll_next(cx)
371 }
372}
373
374#[turbo_tasks::value(serialization = "skip")]
375#[derive(Debug)]
376pub enum UpdateStreamItem {
377 NotFound,
378 Found {
379 update: ReadRef<Update>,
380 issues: Vec<ReadRef<PlainIssue>>,
381 },
382}
383
384#[turbo_tasks::value(serialization = "skip")]
385struct FatalStreamIssue {
386 description: ResolvedVc<StyledString>,
387 resource: RcStr,
388}
389
390#[async_trait]
391#[turbo_tasks::value_impl]
392impl Issue for FatalStreamIssue {
393 fn severity(&self) -> IssueSeverity {
394 IssueSeverity::Fatal
395 }
396
397 fn stage(&self) -> IssueStage {
398 IssueStage::Other(rcstr!("websocket"))
399 }
400
401 async fn file_path(&self) -> Result<FileSystemPath> {
402 ServerFileSystem::new().root().await?.join(&self.resource)
403 }
404
405 async fn title(&self) -> Result<StyledString> {
406 Ok(StyledString::Text(rcstr!(
407 "Fatal error while getting content to stream"
408 )))
409 }
410
411 async fn description(&self) -> Result<Option<StyledString>> {
412 Ok(Some((*self.description.await?).clone()))
413 }
414}
415
416#[cfg(test)]
417pub mod test {
418 use std::sync::{
419 Arc,
420 atomic::{AtomicI32, Ordering},
421 };
422
423 use turbo_tasks::TurboTasks;
424 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
425
426 use super::*;
427
428 #[turbo_tasks::function(operation, root)]
429 pub fn noop_operation() -> Vc<ResolveSourceRequestResult> {
430 ResolveSourceRequestResult::NotFound.cell()
431 }
432
433 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
434 async fn test_get_content_fn() {
435 let tt = TurboTasks::new(TurboTasksBackend::new(
436 BackendOptions::default(),
437 noop_backing_storage(),
438 ));
439 tt.run_once(async move {
440 let number = Arc::new(AtomicI32::new(0));
441 fn func(number: &Arc<AtomicI32>) -> OperationVc<ResolveSourceRequestResult> {
442 number.store(42, Ordering::SeqCst);
443 noop_operation()
444 }
445 let wrapped_func = GetContentFn::new(number.clone(), func);
446 let return_value = wrapped_func
447 .call()
448 .read_strongly_consistent()
449 .await
450 .unwrap();
451 assert_eq!(number.load(Ordering::SeqCst), 42);
452 assert!(*return_value == ResolveSourceRequestResult::NotFound);
454 Ok(())
455 })
456 .await
457 .unwrap();
458 }
459}