turbopack_ecmascript/chunk_list/
update.rs1use 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, NonLocalValue)]
24#[serde(tag = "type", rename = "ChunkListUpdate", rename_all = "camelCase")]
25pub struct ChunkListUpdate {
26 #[serde(skip_serializing_if = "FxIndexMap::is_empty")]
28 pub chunks: FxIndexMap<RcStr, ChunkUpdate>,
29 #[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#[derive(Clone, Debug, PartialEq, Eq, Serialize, NonLocalValue)]
42#[serde(tag = "type")]
43#[serde(rename_all = "camelCase")]
44pub enum ChunkUpdate {
45 Total,
47 Partial { instruction: EcmascriptMergedUpdate },
49 Added,
51 Deleted,
53}
54
55pub 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 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 if from.ptr_eq(&to) {
85 return Ok(Update::None.cell());
86 }
87
88 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 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}