Skip to main content

turbopack_dev_server/update/
stream.rs

1use 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};
13use turbo_tasks_fs::{FileSystem, FileSystemPath};
14use turbopack_core::{
15    issue::{
16        CollectibleIssuesExt, Issue, IssueFilter, IssueSeverity, IssueStage, PlainIssue,
17        StyledString,
18    },
19    server_fs::ServerFileSystem,
20    version::{
21        NotFoundVersion, PartialUpdate, TotalUpdate, Update, Version, VersionState,
22        VersionedContent,
23    },
24};
25
26use crate::source::{ProxyResult, resolve::ResolveSourceRequestResult};
27
28struct TypedGetContentFn<C> {
29    capture: C,
30    func: for<'a> fn(&'a C) -> OperationVc<ResolveSourceRequestResult>,
31}
32
33// Manual (non-derive) impl required due to: https://github.com/rust-lang/rust/issues/70263
34// Safety: `capture` is `NonLocalValue`, `func` stores no data (is a static pointer to code)
35unsafe impl<C: NonLocalValue> NonLocalValue for TypedGetContentFn<C> {}
36
37trait TypedGetContentFnTrait: NonLocalValue {
38    fn call(&self) -> OperationVc<ResolveSourceRequestResult>;
39}
40
41impl<C> TypedGetContentFnTrait for TypedGetContentFn<C>
42where
43    C: NonLocalValue,
44{
45    fn call(&self) -> OperationVc<ResolveSourceRequestResult> {
46        (self.func)(&self.capture)
47    }
48}
49
50/// A wrapper type returning [`OperationVc<ResolveSourceRequestResult>`][ResolveSourceRequestResult]
51/// that implements [`NonLocalValue`].
52///
53/// The capture (e.g. moved values in a closure) and function pointer are stored separately to allow
54/// a safe implementation of this trait.
55#[derive(NonLocalValue)]
56pub struct GetContentFn {
57    inner: Box<dyn TypedGetContentFnTrait + Send + Sync>,
58}
59
60impl GetContentFn {
61    /// Wrap a function and an optional capture variable (used to simulate a closure) in
62    /// `GetContentFn`.
63    pub fn new<C>(
64        capture: C,
65        func: for<'a> fn(&'a C) -> OperationVc<ResolveSourceRequestResult>,
66    ) -> Self
67    where
68        C: NonLocalValue + Send + Sync + 'static,
69    {
70        Self {
71            inner: Box::new(TypedGetContentFn { capture, func }),
72        }
73    }
74}
75
76impl GetContentFn {
77    fn call(&self) -> OperationVc<ResolveSourceRequestResult> {
78        self.inner.call()
79    }
80}
81
82async fn peek_issues<T: Send>(source: OperationVc<T>) -> Result<Vec<ReadRef<PlainIssue>>> {
83    let captured = source.peek_issues();
84
85    captured.get_plain_issues(&IssueFilter::everything()).await
86}
87
88fn extend_issues(issues: &mut Vec<ReadRef<PlainIssue>>, new_issues: Vec<ReadRef<PlainIssue>>) {
89    for issue in new_issues {
90        if issues.contains(&issue) {
91            continue;
92        }
93
94        issues.push(issue);
95    }
96}
97
98#[turbo_tasks::function(operation, root)]
99fn versioned_content_update_operation(
100    content: ResolvedVc<Box<dyn VersionedContent>>,
101    from: ResolvedVc<Box<dyn Version>>,
102) -> Vc<Update> {
103    content.update(*from)
104}
105
106/// Computes the initial [`Version`] for an update stream from a resolved source request. Runs as
107/// an `operation` so [`UpdateStream::new`] can read it strongly consistently from its top-level
108/// task without performing an eventually-consistent read.
109#[turbo_tasks::function(operation, root)]
110async fn initial_version_operation(
111    content: OperationVc<ResolveSourceRequestResult>,
112) -> Result<Vc<Box<dyn Version>>> {
113    Ok(match *content.read_strongly_consistent().await? {
114        ResolveSourceRequestResult::Static(static_content, _) => {
115            static_content.await?.content.version()
116        }
117        ResolveSourceRequestResult::HttpProxy(proxy_result) => Vc::upcast(proxy_result.connect()),
118        _ => Vc::upcast(NotFoundVersion::new()),
119    })
120}
121
122#[turbo_tasks::function(operation, root)]
123async fn get_update_stream_item_operation(
124    resource: RcStr,
125    from: ResolvedVc<VersionState>,
126    get_content: TransientInstance<GetContentFn>,
127) -> Result<Vc<UpdateStreamItem>> {
128    let content_op = get_content.call();
129    let content_result = content_op.read_strongly_consistent().await;
130    let mut plain_issues = peek_issues(content_op).await?;
131
132    let content_value = match content_result {
133        Ok(content) => content,
134        Err(e) => {
135            plain_issues.push(
136                PlainIssue::from_issue(
137                    Vc::upcast(
138                        FatalStreamIssue {
139                            resource,
140                            description: StyledString::Text(
141                                format!("{}", PrettyPrintError(&e)).into(),
142                            )
143                            .resolved_cell(),
144                        }
145                        .cell(),
146                    ),
147                    None,
148                )
149                .await?,
150            );
151
152            let update = Update::Total(TotalUpdate {
153                to: Vc::upcast::<Box<dyn Version>>(NotFoundVersion::new())
154                    .into_trait_ref()
155                    .await?,
156            })
157            .cell();
158            return Ok(UpdateStreamItem::Found {
159                update: update.await?,
160                issues: plain_issues,
161            }
162            .cell());
163        }
164    };
165
166    match *content_value {
167        ResolveSourceRequestResult::Static(static_content_vc, _) => {
168            let static_content = static_content_vc.await?;
169
170            // This can happen when a chunk is removed from the asset graph.
171            if static_content.status_code == 404 {
172                return Ok(UpdateStreamItem::NotFound.cell());
173            }
174
175            let resolved_content = static_content.content;
176            let from = from.get().to_resolved().await?;
177            let update_op = versioned_content_update_operation(resolved_content, from);
178
179            extend_issues(&mut plain_issues, peek_issues(update_op).await?);
180
181            Ok(UpdateStreamItem::Found {
182                update: update_op.connect().await?,
183                issues: plain_issues,
184            }
185            .cell())
186        }
187        ResolveSourceRequestResult::HttpProxy(proxy_result_op) => {
188            let proxy_result_vc = proxy_result_op.connect();
189            let proxy_result_value = proxy_result_vc.await?;
190
191            if proxy_result_value.status == 404 {
192                return Ok(UpdateStreamItem::NotFound.cell());
193            }
194
195            extend_issues(&mut plain_issues, peek_issues(proxy_result_op).await?);
196
197            let from = from.get();
198            if let Some(from) =
199                ResolvedVc::try_downcast_type::<ProxyResult>(from.to_resolved().await?)
200                && from.await? == proxy_result_value
201            {
202                return Ok(UpdateStreamItem::Found {
203                    update: Update::None.cell().await?,
204                    issues: plain_issues,
205                }
206                .cell());
207            }
208
209            Ok(UpdateStreamItem::Found {
210                update: Update::Total(TotalUpdate {
211                    to: Vc::upcast::<Box<dyn Version>>(proxy_result_vc)
212                        .into_trait_ref()
213                        .await?,
214                })
215                .cell()
216                .await?,
217                issues: plain_issues,
218            }
219            .cell())
220        }
221        _ => {
222            let update = if plain_issues.is_empty() {
223                // Client requested a non-existing asset
224                // It might be removed in meantime, reload client
225                // TODO add special instructions for removed assets to handled it in a better
226                // way
227                Update::Total(TotalUpdate {
228                    to: Vc::upcast::<Box<dyn Version>>(NotFoundVersion::new())
229                        .into_trait_ref()
230                        .await?,
231                })
232                .cell()
233            } else {
234                Update::None.cell()
235            };
236
237            Ok(UpdateStreamItem::Found {
238                update: update.await?,
239                issues: plain_issues,
240            }
241            .cell())
242        }
243    }
244}
245
246struct ComputeUpdateStreamSender(Sender<Result<ReadRef<UpdateStreamItem>>>);
247
248/// This function sends an [`UpdateStreamItem`] to `sender` every time it gets recomputed by
249/// turbo-tasks due to invalidation.
250#[turbo_tasks::function]
251async fn compute_update_stream(
252    resource: RcStr,
253    from: ResolvedVc<VersionState>,
254    get_content: TransientInstance<GetContentFn>,
255    sender: TransientInstance<ComputeUpdateStreamSender>,
256) -> () {
257    let item = get_update_stream_item_operation(resource, from, get_content)
258        .read_strongly_consistent()
259        .await;
260
261    // Send update. Ignore channel closed error.
262    let _ = sender.0.send(item).await;
263}
264
265pub(super) struct UpdateStream(
266    Pin<Box<dyn Stream<Item = Result<ReadRef<UpdateStreamItem>>> + Send + Sync>>,
267);
268
269impl UpdateStream {
270    #[tracing::instrument(skip(get_content), name = "UpdateStream::new")]
271    pub async fn new(
272        resource: RcStr,
273        get_content: TransientInstance<GetContentFn>,
274    ) -> Result<UpdateStream> {
275        let (sx, rx) = tokio::sync::mpsc::channel(32);
276
277        let content = get_content.call();
278        // We can ignore issues reported in content here since [compute_update_stream]
279        // will handle them. This runs in a top-level task (`UpdateServer::run`'s
280        // `start_once_process`), so the initial version is computed in a dedicated `operation`
281        // task (where the per-content reads are legal) and read strongly consistently.
282        let version = initial_version_operation(content)
283            .read_trait_strongly_consistent()
284            .await?;
285        let version_state = VersionState::new(version).await?;
286
287        let _ = compute_update_stream(
288            resource,
289            version_state,
290            get_content,
291            TransientInstance::new(ComputeUpdateStreamSender(sx)),
292        );
293
294        let mut last_had_issues = false;
295
296        let stream = ReceiverStream::new(rx).filter_map(move |item| {
297            {
298                let (has_issues, issues_changed) =
299                    if let Ok(UpdateStreamItem::Found { issues, .. }) = item.as_deref() {
300                        let has_issues = !issues.is_empty();
301                        let issues_changed = has_issues != last_had_issues;
302                        last_had_issues = has_issues;
303                        (has_issues, issues_changed)
304                    } else {
305                        (false, false)
306                    };
307
308                async move {
309                    match item.as_deref() {
310                        Ok(UpdateStreamItem::Found { update, .. }) => {
311                            match &**update {
312                                Update::Partial(PartialUpdate { to, .. })
313                                | Update::Total(TotalUpdate { to }) => {
314                                    version_state
315                                        .set(to.clone())
316                                        .await
317                                        .expect("failed to update version");
318
319                                    Some(item)
320                                }
321                                // Do not propagate empty updates.
322                                Update::None | Update::Missing => {
323                                    if has_issues || issues_changed {
324                                        Some(item)
325                                    } else {
326                                        None
327                                    }
328                                }
329                            }
330                        }
331                        _ => {
332                            // Propagate other updates
333                            Some(item)
334                        }
335                    }
336                }
337                .in_current_span()
338            }
339            .in_current_span()
340        });
341
342        Ok(UpdateStream(Box::pin(stream)))
343    }
344}
345
346impl Stream for UpdateStream {
347    type Item = Result<ReadRef<UpdateStreamItem>>;
348
349    fn poll_next(
350        self: Pin<&mut Self>,
351        cx: &mut std::task::Context<'_>,
352    ) -> std::task::Poll<Option<Self::Item>> {
353        Pin::new(&mut self.get_mut().0).poll_next(cx)
354    }
355}
356
357#[turbo_tasks::value(serialization = "skip")]
358#[derive(Debug)]
359pub enum UpdateStreamItem {
360    NotFound,
361    Found {
362        update: ReadRef<Update>,
363        issues: Vec<ReadRef<PlainIssue>>,
364    },
365}
366
367#[turbo_tasks::value(serialization = "skip")]
368struct FatalStreamIssue {
369    description: ResolvedVc<StyledString>,
370    resource: RcStr,
371}
372
373#[async_trait]
374#[turbo_tasks::value_impl]
375impl Issue for FatalStreamIssue {
376    fn severity(&self) -> IssueSeverity {
377        IssueSeverity::Fatal
378    }
379
380    fn stage(&self) -> IssueStage {
381        IssueStage::Other(rcstr!("websocket"))
382    }
383
384    async fn file_path(&self) -> Result<FileSystemPath> {
385        ServerFileSystem::new().root().await?.join(&self.resource)
386    }
387
388    async fn title(&self) -> Result<StyledString> {
389        Ok(StyledString::Text(rcstr!(
390            "Fatal error while getting content to stream"
391        )))
392    }
393
394    async fn description(&self) -> Result<Option<StyledString>> {
395        Ok(Some((*self.description.await?).clone()))
396    }
397}
398
399#[cfg(test)]
400pub mod test {
401    use std::sync::{
402        Arc,
403        atomic::{AtomicI32, Ordering},
404    };
405
406    use turbo_tasks::TurboTasks;
407    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
408
409    use super::*;
410
411    #[turbo_tasks::function(operation, root)]
412    pub fn noop_operation() -> Vc<ResolveSourceRequestResult> {
413        ResolveSourceRequestResult::NotFound.cell()
414    }
415
416    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
417    async fn test_get_content_fn() {
418        let tt = TurboTasks::new(TurboTasksBackend::new(
419            BackendOptions::default(),
420            noop_backing_storage(),
421        ));
422        tt.run_once(async move {
423            let number = Arc::new(AtomicI32::new(0));
424            fn func(number: &Arc<AtomicI32>) -> OperationVc<ResolveSourceRequestResult> {
425                number.store(42, Ordering::SeqCst);
426                noop_operation()
427            }
428            let wrapped_func = GetContentFn::new(number.clone(), func);
429            let return_value = wrapped_func
430                .call()
431                .read_strongly_consistent()
432                .await
433                .unwrap();
434            assert_eq!(number.load(Ordering::SeqCst), 42);
435            // ResolveSourceRequestResult doesn't impl Debug
436            assert!(*return_value == ResolveSourceRequestResult::NotFound);
437            Ok(())
438        })
439        .await
440        .unwrap();
441    }
442}