Skip to main content

turbopack_ecmascript/hmr/
update.rs

1use anyhow::Result;
2use turbo_frozenmap::{FrozenMap, FrozenSet};
3use turbo_tasks::{FxIndexMap, FxIndexSet, ReadRef, ResolvedVc, TryJoinIterExt, Vc};
4use turbopack_core::{
5    chunk::ModuleId,
6    code_builder::Code,
7    update_instruction::UpdateInstruction,
8    version::{PartialUpdate, TotalUpdate, Update, Version},
9};
10
11use crate::{
12    chunk::EcmascriptChunkContentEntries,
13    chunk_list::{
14        merged_update::{
15            EcmascriptMergedChunkAdded, EcmascriptMergedChunkDeleted, EcmascriptMergedChunkPartial,
16            EcmascriptMergedChunkUpdate, EcmascriptMergedUpdate, EcmascriptModuleEntry,
17        },
18        update::EcmascriptUpdateInstruction,
19    },
20    hmr::{
21        EcmascriptHmrChunkContent,
22        content::EcmascriptMergedChunkContent,
23        version::{EcmascriptChunkVersion, EcmascriptMergedChunkVersion},
24    },
25};
26
27/// The module-level difference between two versions of a single chunk.
28enum EcmascriptChunkUpdate {
29    None,
30    Partial {
31        added: FxIndexMap<ModuleId, AddedModule>,
32        modified: FxIndexMap<ModuleId, ResolvedVc<Code>>,
33        deleted: FxIndexMap<ModuleId, u128>,
34    },
35}
36
37struct AddedModule {
38    hash: u128,
39    code: ResolvedVc<Code>,
40}
41
42/// Diffs two versions of a single chunk's content, as one step of building a
43/// merged update.
44async fn update_ecmascript_hmr_chunk_content(
45    content: Vc<Box<dyn EcmascriptHmrChunkContent>>,
46    to: &ReadRef<EcmascriptChunkVersion>,
47    from: &ReadRef<EcmascriptChunkVersion>,
48) -> Result<EcmascriptChunkUpdate> {
49    let mut added = FxIndexMap::default();
50    let mut modified = FxIndexMap::default();
51    let mut deleted = FxIndexMap::default();
52
53    // Lazily resolve the entries map only when we actually need to ship code
54    // bytes for an added or modified module. For chunks that only have deletions
55    // (or no changes that need code beyond hashes), this avoids materializing
56    // any `Vc<Code>`.
57    let mut entries_ref = None;
58
59    // Check for deleted and modified modules
60    for (id, from_hash) in &from.entries_hashes {
61        if let Some(to_hash) = to.entries_hashes.get(id) {
62            if *to_hash != *from_hash {
63                // Module was modified
64                let entries = match &entries_ref {
65                    Some(entries) => entries,
66                    None => entries_ref.insert(content.entries().await?),
67                };
68                if let Some(entry) = entries.get(id) {
69                    modified.insert(id.clone(), entry.code);
70                }
71            }
72        } else {
73            // Module was deleted
74            deleted.insert(id.clone(), *from_hash);
75        }
76    }
77
78    // Check for added modules
79    for (id, hash) in &to.entries_hashes {
80        if !from.entries_hashes.contains_key(id) {
81            let entries = match &entries_ref {
82                Some(entries) => entries,
83                None => entries_ref.insert(content.entries().await?),
84            };
85            if let Some(entry) = entries.get(id) {
86                added.insert(
87                    id.clone(),
88                    AddedModule {
89                        hash: *hash,
90                        code: entry.code,
91                    },
92                );
93            }
94        }
95    }
96
97    Ok(
98        if added.is_empty() && modified.is_empty() && deleted.is_empty() {
99            EcmascriptChunkUpdate::None
100        } else {
101            EcmascriptChunkUpdate::Partial {
102                added,
103                modified,
104                deleted,
105            }
106        },
107    )
108}
109
110/// Looks up a module's hash across several chunk versions, avoiding the cost of
111/// merging them into a single map.
112fn module_hash(versions: &[ReadRef<EcmascriptChunkVersion>], id: &ModuleId) -> Option<u128> {
113    versions
114        .iter()
115        .find_map(|version| version.entries_hashes.get(id).copied())
116}
117
118/// Code only has to be shipped once per update: skip any module another chunk in
119/// the group already provides at the same hash.
120async fn insert_entry_unless_shipped(
121    entries: &mut FxIndexMap<ModuleId, EcmascriptModuleEntry>,
122    from_versions: &[ReadRef<EcmascriptChunkVersion>],
123    id: ModuleId,
124    hash: u128,
125    code: Vc<Code>,
126    chunk_path: &str,
127) -> Result<()> {
128    if module_hash(from_versions, &id) != Some(hash) {
129        let entry = EcmascriptModuleEntry::from_code(&id, code, chunk_path).await?;
130        entries.insert(id, entry);
131    }
132    Ok(())
133}
134
135/// Translates a single chunk's module diff into its merged-update payload.
136async fn partial_chunk_update(
137    update: EcmascriptChunkUpdate,
138    chunk_path: &str,
139    from_versions: &[ReadRef<EcmascriptChunkVersion>],
140    entries: &mut FxIndexMap<ModuleId, EcmascriptModuleEntry>,
141) -> Result<EcmascriptMergedChunkUpdate> {
142    let EcmascriptChunkUpdate::Partial {
143        added,
144        modified,
145        deleted,
146    } = update
147    else {
148        unreachable!("caller filters out EcmascriptChunkUpdate::None");
149    };
150
151    let mut added_modules = FxIndexSet::default();
152
153    for (id, AddedModule { hash, code }) in added {
154        added_modules.insert(id.clone());
155        insert_entry_unless_shipped(entries, from_versions, id, hash, *code, chunk_path).await?;
156    }
157
158    for (id, code) in modified {
159        let entry = EcmascriptModuleEntry::from_code(&id, *code, chunk_path).await?;
160        entries.insert(id, entry);
161    }
162
163    Ok(EcmascriptMergedChunkUpdate::Partial(
164        EcmascriptMergedChunkPartial {
165            added: FrozenSet::from(added_modules),
166            deleted: deleted.into_keys().collect(),
167        },
168    ))
169}
170
171/// Builds the payload for a chunk that wasn't present in the previous version.
172async fn added_chunk_update(
173    chunk_entries: &ReadRef<EcmascriptChunkContentEntries>,
174    chunk_path: &str,
175    from_versions: &[ReadRef<EcmascriptChunkVersion>],
176    entries: &mut FxIndexMap<ModuleId, EcmascriptModuleEntry>,
177) -> Result<EcmascriptMergedChunkUpdate> {
178    let mut modules = FxIndexSet::default();
179
180    for (id, entry) in chunk_entries.iter() {
181        modules.insert(id.clone());
182        insert_entry_unless_shipped(
183            entries,
184            from_versions,
185            id.clone(),
186            *entry.hash.await?,
187            *entry.code,
188            chunk_path,
189        )
190        .await?;
191    }
192
193    Ok(EcmascriptMergedChunkUpdate::Added(
194        EcmascriptMergedChunkAdded {
195            modules: FrozenSet::from(modules),
196        },
197    ))
198}
199
200/// Computes a single [`Update`] covering every chunk in a merged chunk content.
201///
202/// Runtime-agnostic: both the browser and node chunk lists share this one
203/// implementation.
204pub async fn update_ecmascript_merged_chunk(
205    content: Vc<EcmascriptMergedChunkContent>,
206    from_version: ResolvedVc<Box<dyn Version>>,
207) -> Result<Update> {
208    let to_merged_version = content.version();
209    let Some(from_merged_version) =
210        ResolvedVc::try_downcast_type::<EcmascriptMergedChunkVersion>(from_version)
211    else {
212        // It's likely `from_version` is `NotFoundVersion`.
213        return Ok(Update::Total(TotalUpdate {
214            to: Vc::upcast::<Box<dyn Version>>(to_merged_version)
215                .into_trait_ref()
216                .await?,
217        }));
218    };
219
220    let to = to_merged_version.await?;
221    let from = from_merged_version.await?;
222
223    // When to and from point to the same value we can skip comparing them
224    if from.ptr_eq(&to) {
225        return Ok(Update::None);
226    }
227
228    let mut from_versions_by_chunk_path: FxIndexMap<_, _> = from
229        .versions
230        .iter()
231        .map(|version| (&*version.chunk_path, version))
232        .collect();
233
234    let from_versions = &from.versions;
235
236    let content = content.await?;
237    let to_contents = content
238        .contents
239        .iter()
240        .map(async |content| {
241            let entries = content.entries().await?;
242            let version = content.ecmascript_chunk_version().await?;
243            Ok((*content, entries, version))
244        })
245        .try_join()
246        .await?;
247
248    let mut merged_entries = FxIndexMap::default();
249    let mut chunks = FxIndexMap::default();
250
251    for (content, entries, to_version) in &to_contents {
252        let chunk_path = to_version.chunk_path.as_str();
253
254        let chunk_update = match from_versions_by_chunk_path.swap_remove(chunk_path) {
255            Some(from_version) => {
256                match update_ecmascript_hmr_chunk_content(**content, to_version, from_version)
257                    .await?
258                {
259                    EcmascriptChunkUpdate::None => continue,
260                    update => {
261                        partial_chunk_update(update, chunk_path, from_versions, &mut merged_entries)
262                            .await?
263                    }
264                }
265            }
266            None => {
267                added_chunk_update(entries, chunk_path, from_versions, &mut merged_entries).await?
268            }
269        };
270
271        chunks.insert(to_version.chunk_path.clone(), chunk_update);
272    }
273
274    for chunk_version in from_versions_by_chunk_path.into_values() {
275        let hashes = &chunk_version.entries_hashes;
276        chunks.insert(
277            chunk_version.chunk_path.clone(),
278            EcmascriptMergedChunkUpdate::Deleted(EcmascriptMergedChunkDeleted {
279                modules: hashes.keys().cloned().collect(),
280            }),
281        );
282    }
283
284    let merged_update = EcmascriptMergedUpdate {
285        entries: FrozenMap::from(merged_entries),
286        chunks: FrozenMap::from(chunks),
287    };
288
289    Ok(if merged_update.is_empty() {
290        Update::None
291    } else {
292        Update::Partial(PartialUpdate {
293            to: Vc::upcast::<Box<dyn Version>>(to_merged_version)
294                .into_trait_ref()
295                .await?,
296            instruction: UpdateInstruction::new(EcmascriptUpdateInstruction::Merged(merged_update)),
297        })
298    })
299}