Skip to main content

turbopack_ecmascript/chunk_list/
update.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use serde::Serialize;
5use turbo_tasks::{FxIndexMap, ResolvedVc, TraitRef, Vc};
6use turbopack_core::version::{
7    MergeableVersionedContent, PartialUpdate, TotalUpdate, Update, Version, VersionedContent,
8    VersionedContentMerger,
9};
10
11use super::version::ChunkListVersion;
12
13/// Update of a chunk list from one version to another.
14#[derive(Serialize)]
15#[serde(tag = "type")]
16#[serde(rename_all = "camelCase")]
17struct ChunkListUpdate<'a> {
18    /// A map from chunk path to a corresponding update of that chunk.
19    #[serde(skip_serializing_if = "FxIndexMap::is_empty")]
20    chunks: FxIndexMap<&'a str, ChunkUpdate>,
21    /// List of merged updates since the last version.
22    #[serde(skip_serializing_if = "Vec::is_empty")]
23    merged: Vec<Arc<serde_json::Value>>,
24}
25
26/// Update of a chunk from one version to another.
27#[derive(Serialize)]
28#[serde(tag = "type")]
29#[serde(rename_all = "camelCase")]
30enum ChunkUpdate {
31    /// The chunk was updated and must be reloaded.
32    Total,
33    /// The chunk was updated and can be merged with the previous version.
34    Partial { instruction: Arc<serde_json::Value> },
35    /// The chunk was added.
36    Added,
37    /// The chunk was deleted.
38    Deleted,
39}
40
41impl ChunkListUpdate<'_> {
42    /// Returns `true` if this update is empty.
43    fn is_empty(&self) -> bool {
44        let ChunkListUpdate { chunks, merged } = self;
45        chunks.is_empty() && merged.is_empty()
46    }
47}
48
49/// Computes the update of a chunk list from one version to another.
50///
51/// Runtime-agnostic (takes plain paths + [`VersionedContent`]) so the browser
52/// and node chunking contexts can share one implementation instead of each
53/// duplicating the merge-by-[`VersionedContentMerger`] logic.
54pub async fn update_chunk_list(
55    chunks_contents: &FxIndexMap<String, ResolvedVc<Box<dyn VersionedContent>>>,
56    to_version: Vc<ChunkListVersion>,
57    from_version: ResolvedVc<Box<dyn Version>>,
58) -> Result<Vc<Update>> {
59    let from_version =
60        if let Some(from) = ResolvedVc::try_downcast_type::<ChunkListVersion>(from_version) {
61            from
62        } else {
63            // It's likely `from_version` is `NotFoundVersion`.
64            return Ok(Update::Total(TotalUpdate {
65                to: Vc::upcast::<Box<dyn Version>>(to_version)
66                    .into_trait_ref()
67                    .await?,
68            })
69            .cell());
70        };
71
72    let to = to_version.await?;
73    let from = from_version.await?;
74
75    // When to and from point to the same value we can skip comparing them. This will happen since
76    // `TraitRef::<Box<dyn Version>>::cell` will not clone the value, but only make the cell point
77    // to the same immutable value (`Arc`).
78    if from.ptr_eq(&to) {
79        return Ok(Update::None.cell());
80    }
81
82    // Group mergeable chunks by merger so their updates collapse into one
83    // `EcmascriptMergedUpdate`; everything else is diffed individually by path.
84    let mut by_merger = FxIndexMap::<_, Vec<_>>::default();
85    let mut by_path = FxIndexMap::<_, _>::default();
86
87    for (chunk_path, chunk_content) in chunks_contents {
88        if let Some(mergeable) =
89            ResolvedVc::try_sidecast::<Box<dyn MergeableVersionedContent>>(*chunk_content)
90        {
91            let merger = mergeable.get_merger().to_resolved().await?;
92            by_merger.entry(merger).or_default().push(*chunk_content);
93        } else {
94            by_path.insert(chunk_path, chunk_content);
95        }
96    }
97
98    let mut chunks = FxIndexMap::<_, _>::default();
99
100    for (chunk_path, from_chunk_version) in &from.by_path {
101        if let Some(chunk_content) = by_path.swap_remove(chunk_path) {
102            let chunk_update = chunk_content
103                .update(TraitRef::cell(from_chunk_version.clone()))
104                .await?;
105
106            match &*chunk_update {
107                Update::Total(_) => {
108                    chunks.insert(chunk_path.as_ref(), ChunkUpdate::Total);
109                }
110                Update::Partial(partial) => {
111                    chunks.insert(
112                        chunk_path.as_ref(),
113                        ChunkUpdate::Partial {
114                            instruction: partial.instruction.clone(),
115                        },
116                    );
117                }
118                Update::Missing | Update::None => {}
119            }
120        } else {
121            chunks.insert(chunk_path.as_ref(), ChunkUpdate::Deleted);
122        }
123    }
124
125    for chunk_path in by_path.keys() {
126        chunks.insert(chunk_path.as_ref(), ChunkUpdate::Added);
127    }
128
129    let mut merged = vec![];
130
131    for (merger, chunks_contents) in by_merger {
132        if let Some(from_version) = from.by_merger.get(&merger) {
133            let content = merger.merge(Vc::cell(chunks_contents));
134
135            let chunk_update = content.update(TraitRef::cell(from_version.clone())).await?;
136
137            match &*chunk_update {
138                // Getting a total or not found update from a merger is unexpected. If it
139                // happens, we have no better option than to short-circuit
140                // the update.
141                Update::Total(_) => {
142                    return Ok(Update::Total(TotalUpdate {
143                        to: Vc::upcast::<Box<dyn Version>>(to_version)
144                            .into_trait_ref()
145                            .await?,
146                    })
147                    .cell());
148                }
149                Update::Partial(partial) => {
150                    merged.push(partial.instruction.clone());
151                }
152                Update::Missing | Update::None => {}
153            }
154        }
155    }
156    let update = ChunkListUpdate { chunks, merged };
157
158    let update = if update.is_empty() {
159        Update::None
160    } else {
161        Update::Partial(PartialUpdate {
162            to: Vc::upcast::<Box<dyn Version>>(to_version)
163                .into_trait_ref()
164                .await?,
165            instruction: Arc::new(serde_json::to_value(&update)?),
166        })
167    };
168
169    Ok(update.cell())
170}