Skip to main content

turbopack_ecmascript/hmr/
update.rs

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