turbopack_ecmascript/hmr/
update.rs1use 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
24enum 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
39async 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 let mut entries_ref = None;
55
56 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 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 deleted.insert(id.clone(), *from_hash);
72 }
73 }
74
75 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
107fn 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
115async 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
132async 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
165async 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
190pub 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 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 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}