Skip to content

Commit 435b30d

Browse files
Artur-tltv
andauthored
feat: add bulk insert methods to ListSignal and SharedListSignal (#24026)
Add insertAllLast, insertAllFirst, and insertAllAt to ListSignal that perform the entire batch with a single notification. Add insertAllLast, insertAllFirst and insertAllAt to SharedListSignal using transactions for atomicity. --------- Co-authored-by: Tomi Virtanen <tltv@vaadin.com>
1 parent cce6379 commit 435b30d

5 files changed

Lines changed: 408 additions & 0 deletions

File tree

flow-server/src/main/java/com/vaadin/flow/signals/local/ListSignal.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.vaadin.flow.signals.local;
1717

1818
import java.util.ArrayList;
19+
import java.util.Collection;
1920
import java.util.Collections;
2021
import java.util.List;
2122
import java.util.Objects;
@@ -201,6 +202,107 @@ private ValueSignal<T> insertAtInternal(int index, T value) {
201202
return entry;
202203
}
203204

205+
private List<ValueSignal<T>> createSignals(Collection<? extends T> values) {
206+
List<ValueSignal<T>> signals = new ArrayList<>(values.size());
207+
for (T value : values) {
208+
signals.add(new ValueSignal<>(value, equalityChecker));
209+
}
210+
return signals;
211+
}
212+
213+
/**
214+
* Inserts all values as the last entries in this list. All entries are
215+
* added with a single change notification.
216+
* <p>
217+
* Individual null values are permitted if the element type allows null.
218+
*
219+
* @param values
220+
* the values to insert, not <code>null</code>
221+
* @return an unmodifiable list of signals for the inserted entries
222+
*/
223+
public List<ValueSignal<T>> insertAllLast(Collection<? extends T> values) {
224+
Objects.requireNonNull(values, "Values must not be null");
225+
if (values.isEmpty()) {
226+
return List.of();
227+
}
228+
lock();
229+
try {
230+
checkPreconditions();
231+
return insertAllAtInternal(
232+
Objects.requireNonNull(getSignalValue()).size(), values);
233+
} finally {
234+
unlock();
235+
}
236+
}
237+
238+
/**
239+
* Inserts all values as the first entries in this list, preserving the
240+
* order of the provided collection. All entries are added with a single
241+
* change notification.
242+
* <p>
243+
* Individual null values are permitted if the element type allows null.
244+
*
245+
* @param values
246+
* the values to insert, not <code>null</code>
247+
* @return an unmodifiable list of signals for the inserted entries
248+
*/
249+
public List<ValueSignal<T>> insertAllFirst(Collection<? extends T> values) {
250+
return insertAllAt(0, values);
251+
}
252+
253+
/**
254+
* Inserts all values at the given index in this list, preserving the order
255+
* of the provided collection. All entries are added with a single change
256+
* notification.
257+
* <p>
258+
* Individual null values are permitted if the element type allows null.
259+
* <p>
260+
* <b>Note:</b> This method should only be used in non-concurrent cases
261+
* where the list structure is not being modified by other threads. The
262+
* index is sensitive to concurrent modifications and may lead to unexpected
263+
* results if the list is modified between determining the index and calling
264+
* this method.
265+
*
266+
* @param index
267+
* the index at which to insert (0 for first, size() for last)
268+
* @param values
269+
* the values to insert, not <code>null</code>
270+
* @return an unmodifiable list of signals for the inserted entries
271+
* @throws IndexOutOfBoundsException
272+
* if index is negative or greater than size()
273+
*/
274+
public List<ValueSignal<T>> insertAllAt(int index,
275+
Collection<? extends T> values) {
276+
Objects.requireNonNull(values, "Values must not be null");
277+
if (values.isEmpty()) {
278+
return List.of();
279+
}
280+
lock();
281+
try {
282+
checkPreconditions();
283+
List<ValueSignal<T>> currentEntries = Objects
284+
.requireNonNull(getSignalValue());
285+
if (index < 0 || index > currentEntries.size()) {
286+
throw new IndexOutOfBoundsException(
287+
"Index: " + index + ", Size: " + currentEntries.size());
288+
}
289+
return insertAllAtInternal(index, values);
290+
} finally {
291+
unlock();
292+
}
293+
}
294+
295+
private List<ValueSignal<T>> insertAllAtInternal(int index,
296+
Collection<? extends T> values) {
297+
assertLockHeld();
298+
List<ValueSignal<T>> created = createSignals(values);
299+
List<ValueSignal<T>> newEntries = new ArrayList<>(
300+
Objects.requireNonNull(getSignalValue()));
301+
newEntries.addAll(index, created);
302+
setSignalValue(Collections.unmodifiableList(newEntries));
303+
return Collections.unmodifiableList(created);
304+
}
305+
204306
/**
205307
* Removes the given entry from this list. Does nothing if the entry is not
206308
* in the list.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.signals.operations;
17+
18+
import java.util.List;
19+
import java.util.Objects;
20+
21+
import com.vaadin.flow.signals.Signal;
22+
23+
/**
24+
* An operation that inserts multiple child signals into a list as a single
25+
* atomic batch. Unlike using individual {@link InsertOperation} instances, this
26+
* operation tracks the entire batch with a single {@link #result()} future.
27+
*
28+
* @param <T>
29+
* the type of the newly inserted signals
30+
*/
31+
public class BulkInsertOperation<T extends Signal<?>>
32+
extends SignalOperation<Void> {
33+
34+
private final List<T> signals;
35+
36+
/**
37+
* Creates a new bulk insert operation with the given list of inserted
38+
* signal instances.
39+
*
40+
* @param signals
41+
* an unmodifiable list of the newly inserted signal instances,
42+
* not <code>null</code>
43+
*/
44+
public BulkInsertOperation(List<T> signals) {
45+
this.signals = Objects.requireNonNull(signals);
46+
}
47+
48+
/**
49+
* Gets the list of newly inserted signal instances. The instances can be
50+
* used immediately even in cases where the result of the operation is not
51+
* immediately confirmed.
52+
*
53+
* @return an unmodifiable list of the newly inserted signal instances, not
54+
* <code>null</code>
55+
*/
56+
public List<T> signals() {
57+
return signals;
58+
}
59+
}

flow-server/src/main/java/com/vaadin/flow/signals/shared/SharedListSignal.java

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

18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
1821
import java.util.List;
1922
import java.util.Objects;
2023
import java.util.stream.Collectors;
@@ -30,8 +33,11 @@
3033
import com.vaadin.flow.signals.SignalCommand;
3134
import com.vaadin.flow.signals.function.CommandValidator;
3235
import com.vaadin.flow.signals.function.TransactionTask;
36+
import com.vaadin.flow.signals.operations.BulkInsertOperation;
3337
import com.vaadin.flow.signals.operations.InsertOperation;
3438
import com.vaadin.flow.signals.operations.SignalOperation;
39+
import com.vaadin.flow.signals.operations.SignalOperation.Result;
40+
import com.vaadin.flow.signals.operations.TransactionOperation;
3541
import com.vaadin.flow.signals.shared.impl.LocalAsynchronousSignalTree;
3642
import com.vaadin.flow.signals.shared.impl.SignalTree;
3743

@@ -299,6 +305,88 @@ public InsertOperation<SharedValueSignal<T>> insertAt(T value,
299305
this::child);
300306
}
301307

308+
/**
309+
* Inserts all values as the last entries in this list. All inserts are
310+
* performed within a single transaction for atomicity.
311+
* <p>
312+
* Individual null values are permitted if the element type allows null.
313+
*
314+
* @param values
315+
* the values to insert, not <code>null</code>
316+
* @return a bulk insert operation containing the inserted signals and a
317+
* single result future for the entire batch
318+
*/
319+
public BulkInsertOperation<SharedValueSignal<T>> insertAllLast(
320+
Collection<? extends T> values) {
321+
return insertAllAt(values, ListPosition.last());
322+
}
323+
324+
/**
325+
* Inserts all values as the first entries in this list, preserving the
326+
* order of the provided collection. All inserts are performed within a
327+
* single transaction for atomicity.
328+
* <p>
329+
* Individual null values are permitted if the element type allows null.
330+
*
331+
* @param values
332+
* the values to insert, not <code>null</code>
333+
* @return a bulk insert operation containing the inserted signals and a
334+
* single result future for the entire batch
335+
*/
336+
public BulkInsertOperation<SharedValueSignal<T>> insertAllFirst(
337+
Collection<? extends T> values) {
338+
return insertAllAt(values, ListPosition.first());
339+
}
340+
341+
/**
342+
* Inserts all values at the given position in this list, preserving the
343+
* order of the provided collection. All inserts are performed within a
344+
* single transaction for atomicity.
345+
* <p>
346+
* Each element is inserted immediately after the previously inserted
347+
* element, so the final order matches the iteration order of the provided
348+
* collection.
349+
* <p>
350+
* Individual null values are permitted if the element type allows null.
351+
*
352+
* @param values
353+
* the values to insert, not <code>null</code>
354+
* @param at
355+
* the position at which to insert the first value, not
356+
* <code>null</code>
357+
* @return a bulk insert operation containing the inserted signals and a
358+
* single result future for the entire batch
359+
*/
360+
public BulkInsertOperation<SharedValueSignal<T>> insertAllAt(
361+
Collection<? extends T> values, ListPosition at) {
362+
Objects.requireNonNull(values, "Values must not be null");
363+
Objects.requireNonNull(at, "Position must not be null");
364+
if (values.isEmpty()) {
365+
BulkInsertOperation<SharedValueSignal<T>> op = new BulkInsertOperation<>(
366+
List.of());
367+
op.result().complete(new Result<>(null));
368+
return op;
369+
}
370+
TransactionOperation<List<SharedValueSignal<T>>> txOp = Signal
371+
.runInTransaction(() -> {
372+
List<SharedValueSignal<T>> signals = new ArrayList<>(
373+
values.size());
374+
ListPosition currentPos = at;
375+
for (T value : values) {
376+
InsertOperation<SharedValueSignal<T>> insertOp = insertAt(
377+
value, currentPos);
378+
signals.add(insertOp.signal());
379+
currentPos = ListPosition.after(insertOp.signal());
380+
}
381+
return Collections.unmodifiableList(signals);
382+
});
383+
BulkInsertOperation<SharedValueSignal<T>> bulkOp = new BulkInsertOperation<>(
384+
Objects.requireNonNull(txOp.returnValue()));
385+
txOp.result().thenAccept(
386+
resultOrError -> bulkOp.result().complete(resultOrError));
387+
return bulkOp;
388+
}
389+
302390
/**
303391
* Moves the given child signal to the given position in this list. The
304392
* operation fails if the child is not a child or if this list of if

flow-server/src/test/java/com/vaadin/flow/signals/local/ListSignalTest.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,100 @@ void getValues_iterateWithoutTracking_throws() {
702702
assertThrows(IllegalStateException.class, () -> stream.toList());
703703
}
704704

705+
@Test
706+
void insertAllLast_multipleValues_valuesAppended() {
707+
ListSignal<String> signal = new ListSignal<>();
708+
signal.insertLast("existing");
709+
710+
List<ValueSignal<String>> created = signal
711+
.insertAllLast(List.of("a", "b", "c"));
712+
713+
assertEquals(3, created.size());
714+
assertValues(signal, "existing", "a", "b", "c");
715+
}
716+
717+
@Test
718+
void insertAllLast_emptyCollection_noChange() {
719+
ListSignal<String> signal = new ListSignal<>();
720+
signal.insertLast("existing");
721+
722+
AtomicBoolean changed = new AtomicBoolean(false);
723+
Usage usage = UsageTracker.track(signal::get);
724+
usage.onNextChange(initial -> {
725+
changed.set(true);
726+
return false;
727+
});
728+
729+
List<ValueSignal<String>> created = signal.insertAllLast(List.of());
730+
731+
assertTrue(created.isEmpty());
732+
assertFalse(changed.get());
733+
assertValues(signal, "existing");
734+
}
735+
736+
@Test
737+
void insertAllLast_singleNotification() {
738+
ListSignal<String> signal = new ListSignal<>();
739+
740+
AtomicInteger changeCount = new AtomicInteger();
741+
Usage usage = UsageTracker.track(signal::get);
742+
usage.onNextChange(initial -> {
743+
changeCount.incrementAndGet();
744+
return true;
745+
});
746+
747+
signal.insertAllLast(List.of("a", "b", "c"));
748+
749+
assertEquals(1, changeCount.get());
750+
}
751+
752+
@Test
753+
void insertAllFirst_multipleValues_valuesAtStart() {
754+
ListSignal<String> signal = new ListSignal<>();
755+
signal.insertLast("existing");
756+
757+
List<ValueSignal<String>> created = signal
758+
.insertAllFirst(List.of("a", "b", "c"));
759+
760+
assertEquals(3, created.size());
761+
assertValues(signal, "a", "b", "c", "existing");
762+
}
763+
764+
@Test
765+
void insertAllAt_validIndex_valuesInserted() {
766+
ListSignal<String> signal = new ListSignal<>();
767+
signal.insertLast("first");
768+
signal.insertLast("last");
769+
770+
List<ValueSignal<String>> created = signal.insertAllAt(1,
771+
List.of("a", "b"));
772+
773+
assertEquals(2, created.size());
774+
assertValues(signal, "first", "a", "b", "last");
775+
}
776+
777+
@Test
778+
void insertAllAt_invalidIndex_throwsException() {
779+
ListSignal<String> signal = new ListSignal<>();
780+
signal.insertLast("existing");
781+
782+
assertThrows(IndexOutOfBoundsException.class,
783+
() -> signal.insertAllAt(5, List.of("a")));
784+
assertThrows(IndexOutOfBoundsException.class,
785+
() -> signal.insertAllAt(-1, List.of("a")));
786+
}
787+
788+
@Test
789+
void insertAllLast_insideExplicitTransaction_throwsException() {
790+
ListSignal<String> signal = new ListSignal<>();
791+
792+
assertThrows(IllegalStateException.class, () -> {
793+
Transaction.runInTransaction(() -> {
794+
signal.insertAllLast(List.of("a", "b"));
795+
});
796+
});
797+
}
798+
705799
private static void assertValues(ListSignal<String> signal,
706800
String... expectedValues) {
707801
List<String> values = signal.peek().stream().map(ValueSignal::peek)

0 commit comments

Comments
 (0)