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};
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
33unsafe 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#[derive(NonLocalValue)]
56pub struct GetContentFn {
57 inner: Box<dyn TypedGetContentFnTrait + Send + Sync>,
58}
59
60impl GetContentFn {
61 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#[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 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 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#[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 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 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 Update::None | Update::Missing => {
323 if has_issues || issues_changed {
324 Some(item)
325 } else {
326 None
327 }
328 }
329 }
330 }
331 _ => {
332 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 assert!(*return_value == ResolveSourceRequestResult::NotFound);
437 Ok(())
438 })
439 .await
440 .unwrap();
441 }
442}