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    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
34// Manual (non-derive) impl required due to: https://github.com/rust-lang/rust/issues/70263
35// Safety: `capture` is `NonLocalValue`, `func` stores no data (is a static pointer to code)
36unsafe impl<C: NonLocalValue> NonLocalValue for TypedGetContentFn<C> {}
37
38// Manual (non-derive) impl required due to: https://github.com/rust-lang/rust/issues/70263
39impl<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/// A wrapper type returning [`OperationVc<ResolveSourceRequestResult>`][ResolveSourceRequestResult]
59/// that implements [`NonLocalValue`] and [`TraceRawVcs`].
60///
61/// The capture (e.g. moved values in a closure) and function pointer are stored separately to allow
62/// safe implementation of these desired traits.
63#[derive(NonLocalValue, TraceRawVcs)]
64pub struct GetContentFn {
65    inner: Box<dyn TypedGetContentFnTrait + Send + Sync>,
66}
67
68impl GetContentFn {
69    /// Wrap a function and an optional capture variable (used to simulate a closure) in
70    /// `GetContentFn`.
71    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/// Computes the initial [`Version`] for an update stream from a resolved source request. Runs as
115/// an `operation` so [`UpdateStream::new`] can read it strongly consistently from its top-level
116/// task without performing an eventually-consistent read.
117#[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            // This can happen when a chunk is removed from the asset graph.
179            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                // Client requested a non-existing asset
232                // It might be removed in meantime, reload client
233                // TODO add special instructions for removed assets to handled it in a better
234                // way
235                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    // HACK: `trace_ignore`: It's not correct or safe to send `Vc`s across this mpsc channel, but
257    // (without nightly auto traits) there's no easy way for us to statically assert that
258    // `UpdateStreamItem` does not contain a `RawVc`.
259    //
260    // It could be safe (at least for the GC use-case) if we had some way of wrapping arbitrary
261    // objects in a GC root container.
262    #[turbo_tasks(trace_ignore)] Sender<Result<ReadRef<UpdateStreamItem>>>,
263);
264
265/// This function sends an [`UpdateStreamItem`] to `sender` every time it gets recomputed by
266/// turbo-tasks due to invalidation.
267#[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    // Send update. Ignore channel closed error.
279    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        // We can ignore issues reported in content here since [compute_update_stream]
296        // will handle them. This runs in a top-level task (`UpdateServer::run`'s
297        // `start_once_process`), so the initial version is computed in a dedicated `operation`
298        // task (where the per-content reads are legal) and read strongly consistently.
299        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                                // Do not propagate empty updates.
339                                Update::None | Update::Missing => {
340                                    if has_issues || issues_changed {
341                                        Some(item)
342                                    } else {
343                                        None
344                                    }
345                                }
346                            }
347                        }
348                        _ => {
349                            // Propagate other updates
350                            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            // ResolveSourceRequestResult doesn't impl Debug
453            assert!(*return_value == ResolveSourceRequestResult::NotFound);
454            Ok(())
455        })
456        .await
457        .unwrap();
458    }
459}