Skip to content

Commit 13427a0

Browse files
authored
fix: Avoid removing out-of-viewport items from cache (#23141)
Fixes a regression that was introduced in #22328. Removing items from the cache turned out to cause issues with the collapse(T item) method, which became unable to find such "unloaded" items and clean up their sub-caches if they were expanded. Simplifies the implementation and restores the behavior to what it was in Vaadin 24, where items were removed from the key mapper but remained in the hierarchy mapper. Note, as part of the fix, a new objects() method was added to KeyMapper to allow HierarchicalDataCommunicator to get all items that have keys and clean up those that are no longer in the viewport. Since KeyMapper always contains fewer items than the Cache, this API helps reduce the number of iterations needed during cleanup. Fixes: vaadin/flow-components#8427
1 parent f10f150 commit 13427a0

File tree

5 files changed

+65
-53
lines changed

5 files changed

+65
-53
lines changed

flow-data/src/main/java/com/vaadin/flow/data/provider/KeyMapper.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.vaadin.flow.data.provider;
1717

18+
import java.util.Collection;
19+
import java.util.Collections;
1820
import java.util.HashMap;
1921
import java.util.Map;
2022

@@ -161,6 +163,13 @@ public void refresh(V dataObject) {
161163
}
162164
}
163165

166+
/**
167+
* Gets all mapped objects.
168+
*/
169+
public Collection<V> objects() {
170+
return Collections.unmodifiableCollection(keyObjectMap.values());
171+
}
172+
164173
@Override
165174
public void setIdentifierGetter(ValueProvider<V, Object> identifierGetter) {
166175
if (this.identifierGetter != identifierGetter) {

flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalDataCommunicator.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
public class HierarchicalDataCommunicator<T> extends DataCommunicator<T> {
9999
private final Set<Object> expandedItemIds = new HashSet<>();
100100
private final StateNode stateNode;
101+
private final KeyMapper<T> keyMapper;
101102
private final ArrayUpdater arrayUpdater;
102103
private final DataGenerator<T> dataGenerator;
103104
private final SerializableSupplier<ValueProvider<T, String>> uniqueKeyProviderSupplier;
@@ -136,9 +137,8 @@ public HierarchicalDataCommunicator(CompositeDataGenerator<T> dataGenerator,
136137
this.arrayUpdater = arrayUpdater;
137138
this.dataGenerator = dataGenerator;
138139
this.uniqueKeyProviderSupplier = uniqueKeyProviderSupplier;
139-
140-
KeyMapperWrapper<T> keyMapperWrapper = new KeyMapperWrapper<>();
141-
setKeyMapper(keyMapperWrapper);
140+
this.keyMapper = new KeyMapperWrapper<>();
141+
setKeyMapper(keyMapper);
142142

143143
setDataProvider(new TreeDataProvider<>(new TreeData<>()), null);
144144

@@ -174,7 +174,7 @@ private FlushRequest<T> requestFlush() {
174174
public void reset() {
175175
if (rootCache != null) {
176176
rootCache = null;
177-
getKeyMapper().removeAll();
177+
keyMapper.removeAll();
178178
dataGenerator.destroyAllData();
179179
}
180180

@@ -230,7 +230,7 @@ public void refresh(T item, boolean refreshChildren) {
230230
""");
231231
}
232232

233-
getKeyMapper().refresh(item);
233+
keyMapper.refresh(item);
234234
dataGenerator.refreshData(item);
235235

236236
if (rootCache == null) {
@@ -470,7 +470,7 @@ public boolean hasExpandedItems() {
470470

471471
private JsonNode generateItemJson(T item) {
472472
ObjectNode json = JacksonUtils.createObjectNode();
473-
json.put("key", getKeyMapper().key(item));
473+
json.put("key", keyMapper.key(item));
474474
dataGenerator.generateData(item, json);
475475
return json;
476476
}
@@ -717,6 +717,7 @@ public void confirmUpdate(int updateId) {
717717
return;
718718
}
719719

720+
// Collect IDs of items that are currently in the viewport
720721
HashSet<Object> viewportItemIds = new HashSet<>();
721722
for (int i = viewportRange.getStart(); i < viewportRange
722723
.getEnd(); i++) {
@@ -734,9 +735,18 @@ public void confirmUpdate(int updateId) {
734735
viewportItemIds.add(getDataProvider().getId(item));
735736
}
736737

737-
// Remove items from the cache, keyMapper, and dataGenerator
738-
rootCache.removeDescendantItemIf((item) -> {
739-
return !viewportItemIds.contains(getDataProvider().getId(item));
738+
// Collect items that have an active key in the keyMapper but
739+
// are no longer in the viewport
740+
var itemsToRemove = keyMapper.objects().stream().filter((item) -> {
741+
var itemId = getDataProvider().getId(item);
742+
return !viewportItemIds.contains(itemId);
743+
}).toList();
744+
745+
// Remove those items from the keyMapper and dataGenerator to
746+
// release any associated client-side resources.
747+
itemsToRemove.forEach((item) -> {
748+
dataGenerator.destroyData(item);
749+
keyMapper.remove(item);
740750
});
741751
}
742752

@@ -833,9 +843,9 @@ private RootCache<T> ensureRootCache() {
833843
void onItemRemoved(T item) {
834844
super.onItemRemoved(item);
835845

836-
if (getKeyMapper().has(item)) {
846+
if (keyMapper.has(item)) {
837847
dataGenerator.destroyData(item);
838-
getKeyMapper().remove(item);
848+
keyMapper.remove(item);
839849
}
840850
}
841851
};

flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/RootCache.java

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.Map;
2222
import java.util.Map.Entry;
2323

24-
import com.vaadin.flow.function.SerializablePredicate;
2524
import com.vaadin.flow.function.ValueProvider;
2625

2726
/**
@@ -191,25 +190,6 @@ public ItemContext<T> getContextByItem(T item) {
191190
return itemIdToContext.get(itemId);
192191
}
193192

194-
/**
195-
* Removes all descendant items that match the given predicate.
196-
*
197-
* @param predicate
198-
* the predicate to match items against
199-
*/
200-
public void removeDescendantItemIf(SerializablePredicate<T> predicate) {
201-
itemIdToContext.values().stream().filter((itemContext) -> {
202-
var cache = itemContext.cache();
203-
var index = itemContext.index();
204-
var item = cache.getItem(index);
205-
return predicate.test(item);
206-
}).toList().forEach((itemContext) -> {
207-
var cache = itemContext.cache();
208-
var index = itemContext.index();
209-
cache.removeItem(index);
210-
});
211-
}
212-
213193
void onItemAdded(T item, Cache<T> cache, int index) {
214194
Object itemId = getItemId(item);
215195
itemIdToContext.put(itemId, new ItemContext<>(cache, index));

flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalDataCommunicatorDataGenerationTest.java

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Arrays;
1919

20+
import org.junit.Assert;
2021
import org.junit.Before;
2122
import org.junit.Test;
2223
import org.mockito.Mock;
@@ -48,6 +49,16 @@ public void init() {
4849
dataCommunicator.setDataProvider(treeDataProvider, null);
4950

5051
compositeDataGenerator.addDataGenerator(dataGenerator);
52+
53+
Mockito.doAnswer((invocation) -> {
54+
Item item = invocation.getArgument(0);
55+
56+
Assert.assertTrue(
57+
"Item should be in keyMapper when generateData is called",
58+
dataCommunicator.getKeyMapper().has(item));
59+
60+
return null;
61+
}).when(dataGenerator).destroyData(Mockito.any());
5162
}
5263

5364
@Test
@@ -153,24 +164,4 @@ public void collapseItems_destroyDataCalledForCollapsedChildrenInKeyMapper() {
153164
Mockito.verify(dataGenerator, Mockito.never())
154165
.destroyData(new Item("Item 1"));
155166
}
156-
157-
@Test
158-
public void collapseItems_destroyDataCalledBeforeItemRemovedFromKeyMapper() {
159-
populateTreeData(treeData, 4, 2, 2);
160-
dataCommunicator.expand(
161-
Arrays.asList(new Item("Item 0"), new Item("Item 0-0")));
162-
dataCommunicator.setViewportRange(0, 4);
163-
fakeClientCommunication();
164-
165-
Mockito.doAnswer((invocation) -> {
166-
Item item = invocation.getArgument(0);
167-
if (!dataCommunicator.getKeyMapper().has(item)) {
168-
throw new AssertionError(
169-
"Item should still be in keyMapper when destroyData is called");
170-
}
171-
return null;
172-
}).when(dataGenerator).destroyData(Mockito.any());
173-
174-
dataCommunicator.collapse(new Item("Item 0"));
175-
}
176167
}

flow-data/src/test/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalDataCommunicatorViewportTest.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ public void setViewportRange_toggleItemWithPreExpandedChildren_rangeItemsUpdated
256256
}
257257

258258
@Test
259-
public void setViewportRange_toggleItemOutsideRange_flatSizeNotUpdated() {
259+
public void setViewportRange_toggleItemOutsideViewport_flatSizeNotUpdated() {
260260
populateTreeData(treeData, 100, 2, 2);
261261
dataCommunicator.setViewportRange(0, 10);
262262
fakeClientCommunication();
@@ -275,7 +275,7 @@ public void setViewportRange_toggleItemOutsideRange_flatSizeNotUpdated() {
275275
}
276276

277277
@Test
278-
public void setViewportRange_expandItemOutsideRange_adjustRangeToIncludeItem_flatSizeUpdated() {
278+
public void setViewportRange_expandItemOutsideViewport_moveItemIntoViewport_flatSizeUpdated() {
279279
populateTreeData(treeData, 100, 2, 2);
280280
dataCommunicator.setViewportRange(0, 6);
281281
fakeClientCommunication();
@@ -293,6 +293,28 @@ public void setViewportRange_expandItemOutsideRange_adjustRangeToIncludeItem_fla
293293
assertArrayUpdateSize(102);
294294
}
295295

296+
@Test
297+
public void setViewportRange_expandItem_moveItemOutOfViewport_collapseItem_flatSizeUpdated() {
298+
populateTreeData(treeData, 100, 2, 2);
299+
dataCommunicator.expand(new Item("Item 0"));
300+
dataCommunicator.setViewportRange(0, 6);
301+
fakeClientCommunication();
302+
assertArrayUpdateSize(102);
303+
304+
Mockito.clearInvocations(arrayUpdater, arrayUpdate);
305+
306+
dataCommunicator.setViewportRange(101, 1);
307+
fakeClientCommunication();
308+
dataCommunicator.confirmUpdate(captureArrayUpdateId());
309+
fakeClientCommunication();
310+
311+
Mockito.clearInvocations(arrayUpdater, arrayUpdate);
312+
313+
dataCommunicator.collapse(new Item("Item 0"));
314+
fakeClientCommunication();
315+
assertArrayUpdateSize(100);
316+
}
317+
296318
@Test
297319
public void setViewportRangeWithStartBeyondFlatSize_rangeReset() {
298320
populateTreeData(treeData, 100, 10);

0 commit comments

Comments
 (0)