Skip to main content

turbopack_ecmascript/chunk_list/
update.rs

1use anyhow::Result;
2use serde::Serialize;
3use turbo_rcstr::RcStr;
4use turbo_tasks::{FxIndexMap, NonLocalValue, ResolvedVc, TraitRef, Vc};
5use turbopack_core::{
6    update_instruction::UpdateInstruction,
7    version::{
8        MergeableVersionedContent, PartialUpdate, TotalUpdate, Update, Version, VersionedContent,
9        VersionedContentMerger,
10    },
11};
12
13use super::{merged_update::EcmascriptMergedUpdate, version::ChunkListVersion};
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, NonLocalValue)]
16#[serde(untagged)]
17pub enum EcmascriptUpdateInstruction {
18    ChunkList(ChunkListUpdate),
19    Merged(EcmascriptMergedUpdate),
20}
21
22/// Update of a chunk list from one version to another.
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, NonLocalValue)]
24#[serde(tag = "type", rename = "ChunkListUpdate", rename_all = "camelCase")]
25pub struct ChunkListUpdate {
26    /// A map from chunk path to a corresponding update of that chunk.
27    #[serde(skip_serializing_if = "FxIndexMap::is_empty")]
28    pub chunks: FxIndexMap<RcStr, ChunkUpdate>,
29    /// List of merged updates since the last version.
30    #[serde(skip_serializing_if = "Vec::is_empty")]
31    pub merged: Vec<EcmascriptMergedUpdate>,
32}
33
34impl ChunkListUpdate {
35    pub fn into_instruction(self) -> UpdateInstruction {
36        UpdateInstruction::new(EcmascriptUpdateInstruction::ChunkList(self))
37    }
38}
39
40/// Update of a chunk from one version to another.
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, NonLocalValue)]
42#[serde(tag = "type")]
43#[serde(rename_all = "camelCase")]
44pub enum ChunkUpdate {
45    /// The chunk was updated and must be reloaded.
46    Total,
47    /// The chunk was updated and can be merged with the previous version.
48    Partial { instruction: EcmascriptMergedUpdate },
49    /// The chunk was added.
50    Added,
51    /// The chunk was deleted.
52    Deleted,
53}
54
55/// Computes the update of a chunk list from one version to another.
56///
57/// Runtime-agnostic (takes plain paths + [`VersionedContent`]) so the browser
58/// and node chunking contexts can share one implementation instead of each
59/// duplicating the merge-by-[`VersionedContentMerger`] logic.
60pub async fn update_chunk_list(
61    chunks_contents: &FxIndexMap<String, ResolvedVc<Box<dyn VersionedContent>>>,
62    to_version: Vc<ChunkListVersion>,
63    from_version: ResolvedVc<Box<dyn Version>>,
64) -> Result<Vc<Update>> {
65    let from_version =
66        if let Some(from) = ResolvedVc::try_downcast_type::<ChunkListVersion>(from_version) {
67            from
68        } else {
69            // It's likely `from_version` is `NotFoundVersion`.
70            return Ok(Update::Total(TotalUpdate {
71                to: Vc::upcast::<Box<dyn Version>>(to_version)
72                    .into_trait_ref()
73                    .await?,
74            })
75            .cell());
76        };
77
78    let to = to_version.await?;
79    let from = from_version.await?;
80
81    // When to and from point to the same value we can skip comparing them. This will happen since
82    // `TraitRef::<Box<dyn Version>>::cell` will not clone the value, but only make the cell point
83    // to the same immutable value (`Arc`).
84    if from.ptr_eq(&to) {
85        return Ok(Update::None.cell());
86    }
87
88    // Group mergeable chunks by merger so their updates collapse into one
89    // `EcmascriptMergedUpdate`; everything else is diffed individually by path.
90    let mut by_merger = FxIndexMap::<_, Vec<_>>::default();
91    let mut by_path = FxIndexMap::<_, _>::default();
92
93    for (chunk_path, chunk_content) in chunks_contents {
94        if let Some(mergeable) =
95            ResolvedVc::try_sidecast::<Box<dyn MergeableVersionedContent>>(*chunk_content)
96        {
97            let merger = mergeable.get_merger().to_resolved().await?;
98            by_merger.entry(merger).or_default().push(*chunk_content);
99        } else {
100            by_path.insert(chunk_path, chunk_content);
101        }
102    }
103
104    let mut chunks = FxIndexMap::<_, _>::default();
105
106    for (chunk_path, from_chunk_version) in &from.by_path {
107        if let Some(chunk_content) = by_path.swap_remove(chunk_path) {
108            let chunk_update = chunk_content
109                .update(TraitRef::cell(from_chunk_version.clone()))
110                .await?;
111
112            match &*chunk_update {
113                Update::Total(_) => {
114                    chunks.insert(chunk_path.clone().into(), ChunkUpdate::Total);
115                }
116                Update::Partial(partial) => {
117                    let instruction = expect_merged_instruction_from_partial(partial);
118                    chunks.insert(
119                        chunk_path.clone().into(),
120                        ChunkUpdate::Partial {
121                            instruction: instruction.clone(),
122                        },
123                    );
124                }
125                Update::Missing | Update::None => {}
126            }
127        } else {
128            chunks.insert(chunk_path.clone().into(), ChunkUpdate::Deleted);
129        }
130    }
131
132    for chunk_path in by_path.keys() {
133        chunks.insert((*chunk_path).clone().into(), ChunkUpdate::Added);
134    }
135
136    let mut merged = vec![];
137
138    for (merger, chunks_contents) in by_merger {
139        if let Some(from_version) = from.by_merger.get(&merger) {
140            let content = merger.merge(Vc::cell(chunks_contents));
141
142            let chunk_update = content.update(TraitRef::cell(from_version.clone())).await?;
143
144            match &*chunk_update {
145                // Getting a total or not found update from a merger is unexpected. If it
146                // happens, we have no better option than to short-circuit
147                // the update.
148                Update::Total(_) => {
149                    return Ok(Update::Total(TotalUpdate {
150                        to: Vc::upcast::<Box<dyn Version>>(to_version)
151                            .into_trait_ref()
152                            .await?,
153                    })
154                    .cell());
155                }
156                Update::Partial(partial) => {
157                    let instruction = expect_merged_instruction_from_partial(partial);
158                    merged.push(instruction.clone());
159                }
160                Update::Missing | Update::None => {}
161            }
162        }
163    }
164    let update = if chunks.is_empty() && merged.is_empty() {
165        Update::None
166    } else {
167        Update::Partial(PartialUpdate {
168            to: Vc::upcast::<Box<dyn Version>>(to_version)
169                .into_trait_ref()
170                .await?,
171            instruction: ChunkListUpdate { chunks, merged }.into_instruction(),
172        })
173    };
174
175    Ok(update.cell())
176}
177
178fn expect_merged_instruction_from_partial(partial: &PartialUpdate) -> &EcmascriptMergedUpdate {
179    let Some(EcmascriptUpdateInstruction::Merged(instruction)) = partial
180        .instruction
181        .downcast_ref::<EcmascriptUpdateInstruction>(
182    ) else {
183        panic!("ECMAScript partial updates must contain a merged instruction");
184    };
185    instruction
186}
187
188#[cfg(test)]
189mod tests {
190    use serde_json::json;
191    use turbo_frozenmap::{FrozenMap, FrozenSet};
192    use turbo_tasks::FxIndexMap;
193
194    use super::{ChunkListUpdate, ChunkUpdate, EcmascriptUpdateInstruction};
195    use crate::chunk_list::merged_update::{
196        EcmascriptMergedChunkAdded, EcmascriptMergedChunkUpdate, EcmascriptMergedUpdate,
197    };
198
199    #[test]
200    fn instruction_wire_format() {
201        let instruction = EcmascriptUpdateInstruction::ChunkList(ChunkListUpdate {
202            chunks: FxIndexMap::from_iter([("app.js".into(), ChunkUpdate::Total)]),
203            merged: vec![EcmascriptMergedUpdate {
204                entries: FrozenMap::default(),
205                chunks: FrozenMap::from_iter([(
206                    "app.js".into(),
207                    EcmascriptMergedChunkUpdate::Added(EcmascriptMergedChunkAdded {
208                        modules: FrozenSet::default(),
209                    }),
210                )]),
211            }],
212        });
213
214        assert_eq!(
215            serde_json::to_value(instruction).unwrap(),
216            json!({
217                "type": "ChunkListUpdate",
218                "chunks": { "app.js": { "type": "total" } },
219                "merged": [{
220                    "type": "EcmascriptMergedUpdate",
221                    "chunks": { "app.js": { "type": "added" } },
222                }],
223            })
224        );
225    }
226}