Skip to content

Commit ba8ea41

Browse files
authored
feat!: Split computed into separate cached and direct versions (#23822)
* Split computed into separate cached and direct versions Previously, the `computed` method was just one out of many ways of creating a computed signal with the distinction that this computed signal was also caching. This made it difficult to understand when to use the method. Fix this discrepancy by changing `computed` to create a non-cached computed signal and introducing a separate `cached` method that creates a cached signal out of any other signal. * Address review comments * Clarify error message when inner signal is computed and reads no other signals * Document difference between direct lambda and Signal.computed() * Format
1 parent 5ad2558 commit ba8ea41

File tree

4 files changed

+172
-102
lines changed

4 files changed

+172
-102
lines changed

flow-server/src/main/java/com/vaadin/flow/signals/Signal.java

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import com.vaadin.flow.signals.function.SignalMapper;
3030
import com.vaadin.flow.signals.function.TransactionTask;
3131
import com.vaadin.flow.signals.function.ValueSupplier;
32-
import com.vaadin.flow.signals.impl.ComputedSignal;
32+
import com.vaadin.flow.signals.impl.CachedSignal;
3333
import com.vaadin.flow.signals.impl.Effect;
3434
import com.vaadin.flow.signals.impl.Transaction;
3535
import com.vaadin.flow.signals.impl.Transaction.Type;
@@ -48,8 +48,7 @@
4848
* effect without registering a dependency.
4949
* <p>
5050
* This interface can be used for creating simple computed signals as a lambda
51-
* function that uses other signals. This kind of signal is more limited than
52-
* {@link #computed(SignalComputation)} since it doesn't cache its value.
51+
* function that uses other signals.
5352
*
5453
* @param <T>
5554
* the signal value type
@@ -68,11 +67,11 @@ public interface Signal<T extends @Nullable Object> extends Serializable {
6867
* signal value is changed concurrently.
6968
* <p>
7069
* Reading the value inside an {@link #unboundEffect(EffectAction)} or
71-
* {@link #computed(SignalComputation)} callback sets up that effect or
72-
* computed signal to depend on the signal.
70+
* {@link #cached(Signal)} callback sets up that effect or cached signal to
71+
* depend on the signal.
7372
* <p>
7473
* This method must only be called within a reactive context such as an
75-
* effect, a computed signal, an explicit {@link #untracked(ValueSupplier)}
74+
* effect, cached signal, an explicit {@link #untracked(ValueSupplier)}
7675
* block, or a {@link #runInTransaction(TransactionTask) transaction}.
7776
* Calling it outside such a context throws an
7877
* {@link IllegalStateException}. Use {@link #peek()} for one-time reads
@@ -87,7 +86,7 @@ public interface Signal<T extends @Nullable Object> extends Serializable {
8786
/**
8887
* Reads the value without setting up any dependencies. This method returns
8988
* the same value as {@link #get()} but without creating a dependency when
90-
* used inside a transaction, effect or computed signal.
89+
* used inside a transaction, effect or cached signal.
9190
* <p>
9291
* Unlike {@link #get()}, this method can be called outside a reactive
9392
* context and is the recommended way to read a signal value for one-time
@@ -108,12 +107,6 @@ default T peek() {
108107
* passed the value of this signal. If the mapper function accesses other
109108
* signal values, then the computed signal will also depend on those
110109
* signals.
111-
* <p>
112-
* The computed signal does not perform any caching but will instead run the
113-
* callback every time the signal value is read. Use
114-
* {@link #computed(SignalComputation)} to create a computed signal that
115-
* caches the result of running the callback until the value of any
116-
* dependency changes.
117110
*
118111
* @param <C>
119112
* the computed signal type
@@ -228,16 +221,22 @@ static Registration unboundEffect(EffectAction action) {
228221
/**
229222
* Creates a new computed signal with the given computation callback. A
230223
* computed signal behaves like a regular signal except that the value is
231-
* not directly set but instead computed from other signals. The computed
232-
* signal is automatically updated if any of the used signals are updated.
233-
* The computation is lazy so that it only runs when its value is accessed
234-
* and only if the previously computed value might have been invalidated by
235-
* dependent signal changes. If the computation callback throws a
236-
* {@link RuntimeException}, then that exception will be re-thrown when
237-
* accessing the signal value. An {@link Signal#unboundEffect(EffectAction)
238-
* effect} or computed signal that uses the value from a computed signal
239-
* will not be invalidated if the computation is run again but produces the
240-
* same value as before.
224+
* not directly set but instead computed from other signals. The value is
225+
* computed again every time the value is read. Use {@link #cached(Signal)}
226+
* to create a signal that computes a new value only if any of the dependent
227+
* signals might have changed.
228+
* <p>
229+
* A computed signal can also be defined directly as a lambda expression.
230+
* Using this method enables type inference in cases where the target type
231+
* isn't explicitly defined.
232+
*
233+
* <pre>
234+
* // Type must be explicitly defined for a direct lambda expression
235+
* Signal&lt;Integer&gt; signal = () -&gt; stringSignal.get().length();
236+
*
237+
* // Type can be inferred by wrapping in computed()
238+
* var signal = Signal.computed(() -&gt; stringSignal.get().length());
239+
* </pre>
241240
*
242241
* @param <T>
243242
* the signal type
@@ -247,7 +246,33 @@ static Registration unboundEffect(EffectAction action) {
247246
*/
248247
static <T extends @Nullable Object> Signal<T> computed(
249248
SignalComputation<T> computation) {
250-
return new ComputedSignal<>(computation);
249+
return Objects.requireNonNull(computation)::compute;
250+
}
251+
252+
/**
253+
* Creates a new cached signal based on the given inner signal. The inner
254+
* signal is typically a computed signal performing an expensive computation
255+
* based on other signal values. When getting the value of this signal, a
256+
* previously cached value will be used if available as long as that value
257+
* is still valid. The cached value remains valid until the value changes
258+
* for any of the inner signals.
259+
* <p>
260+
* If a {@link RuntimeException} is thrown when getting the value of the
261+
* inner signal, then that exception will be re-thrown when accessing the
262+
* value of this signal. An {@link Signal#unboundEffect(EffectAction)
263+
* effect} or outer cached signal that uses the value from a cached signal
264+
* will not be invalidated if the inner signal is invalidated but holds the
265+
* same value as before invalidation.
266+
*
267+
* @param <T>
268+
* the signal type
269+
* @param innerSignal
270+
* the inner signal, not <code>null</code>
271+
* @return the cached signal not <code>null</code>
272+
*/
273+
static <T extends @Nullable Object> Signal<T> cached(
274+
Signal<T> innerSignal) {
275+
return new CachedSignal<>(innerSignal);
251276
}
252277

253278
/**

flow-server/src/main/java/com/vaadin/flow/signals/function/SignalComputation.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
* <p>
2828
* Dependencies are automatically tracked - any signal whose value is accessed
2929
* during the computation becomes a dependency. The computation is lazy and only
30-
* runs when the signal value is accessed and the previous value might have been
31-
* invalidated by dependent signal changes.
30+
* runs when the signal value is accessed.
3231
*
3332
* @param <T>
3433
* the computed value type

flow-server/src/main/java/com/vaadin/flow/signals/impl/ComputedSignal.java renamed to flow-server/src/main/java/com/vaadin/flow/signals/impl/CachedSignal.java

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -30,78 +30,79 @@
3030
import com.vaadin.flow.signals.Signal;
3131
import com.vaadin.flow.signals.SignalCommand;
3232
import com.vaadin.flow.signals.function.EffectAction;
33-
import com.vaadin.flow.signals.function.SignalComputation;
3433
import com.vaadin.flow.signals.impl.UsageTracker.Usage;
3534
import com.vaadin.flow.signals.shared.AbstractSharedSignal;
3635
import com.vaadin.flow.signals.shared.SharedNodeSignal;
3736
import com.vaadin.flow.signals.shared.impl.SynchronousSignalTree;
3837

3938
/**
40-
* A signal with a value that is computed based on the value of other signals.
41-
* The signal value will be lazily re-computed when needed after the value has
42-
* changed for any of the signals that were used when computing the previous
43-
* value. If the computation callback throws a {@link RuntimeException}, then
44-
* that exception will be re-thrown when accessing the value of this signal. An
45-
* {@link Signal#unboundEffect(EffectAction) effect} or computed signal that
46-
* uses the value from a computed signal will not be invalidated if the
47-
* computation is run again but produces the same value as before.
39+
* A signal that caches the value of an inner signal. The inner signal is
40+
* typically a computed signal performing an expensive computation that should
41+
* be run again only if any of its dependencies has changed.
42+
* <p>
43+
* If a {@link RuntimeException} is thrown when getting the value of the inner
44+
* signal, then that exception will be re-thrown when accessing the value of
45+
* this signal. An {@link Signal#unboundEffect(EffectAction) effect} or outer
46+
* cached signal that uses the value from a cached signal will not be
47+
* invalidated if the computation is run again but produces the same value as
48+
* before.
4849
*
4950
* @param <T>
5051
* the value type
5152
*/
52-
public class ComputedSignal<T extends @Nullable Object>
53+
public class CachedSignal<T extends @Nullable Object>
5354
extends AbstractSharedSignal<T> {
5455

5556
/*
5657
* This state is never supposed to be synchronized across a cluster or to
5758
* Hilla clients.
5859
*/
59-
private record ComputedState(@Nullable Object value,
60+
private record CacheState(@Nullable Object value,
6061
@Nullable RuntimeException exception,
6162
Usage dependencies) implements Serializable {
6263
}
6364

64-
private final SignalComputation<T> computation;
65+
private final Signal<T> inner;
6566

6667
private int dependentCount = 0;
6768
private @Nullable Registration dependencyRegistration;
6869

6970
/**
70-
* Creates a new computed signal with the provided compute callback.
71+
* Creates a new cached signal with the provided inner signal.
7172
*
72-
* @param computation
73-
* a callback that returns the computed value, not null
73+
* @param inner
74+
* a signal to wrap, not null
7475
*/
75-
public ComputedSignal(SignalComputation<T> computation) {
76+
public CachedSignal(Signal<T> inner) {
7677
super(new SynchronousSignalTree(true), Id.ZERO, ANYTHING_GOES);
77-
this.computation = Objects.requireNonNull(computation);
78+
this.inner = Objects.requireNonNull(inner);
7879
}
7980

8081
/**
81-
* As long as nobody listens to changes to this computed signal, we can just
82-
* re-compute on demand every time the value is read. But when there's at
82+
* As long as nobody listens to changes to this cached signal, we can just
83+
* re-validate on demand every time the value is read. But when there's at
8384
* least one active listener, we need to actively listen to changes in our
8485
* dependencies.
8586
* <p>
8687
* Whenever a dependency changes, we run {@link #getValidState(Data)}. This
87-
* causes the compute callback to run again. If that leads to a new value in
88+
* causes the inner signal to be read again. If that leads to a new value in
8889
* the tree, then the {@link Usage} from the super class will be triggered
8990
* which in notifies out external listeners.
9091
* <p>
9192
* We have just a single set of internal listeners on our dependencies even
92-
* if there are multiple external listeners so that the compute callback is
93-
* run only once. We keep track of how many active external listeners we
94-
* have so that our internal listener is active if and only if there's at
95-
* least one active external listener.
93+
* if there are multiple external listeners so that the inner signal is read
94+
* only once. We keep track of how many active external listeners we have so
95+
* that our internal listener is active if and only if there's at least one
96+
* active external listener.
9697
*/
9798
private synchronized void revalidateAndListen() {
9899
// Clear listeners on old dependencies
99100
if (dependencyRegistration != null) {
100101
dependencyRegistration.remove();
101102
}
102103

103-
// Run compute callback to get new dependencies
104-
ComputedState state = getValidState(data(Transaction.getCurrent()));
104+
// Read inner value to get new dependencies
105+
CacheState state = getValidState(data(Transaction.getCurrent()));
105106

106107
// avoid lambda to allow proper deserialization
107108
TransientListener usageListener = new TransientListener() {
@@ -131,7 +132,7 @@ private synchronized Registration countActiveExternalListener() {
131132

132133
return () -> {
133134
if (!removed.getAndSet(true)) {
134-
synchronized (ComputedSignal.this) {
135+
synchronized (CachedSignal.this) {
135136
/*
136137
* Decrease the number of active external listeners and stop
137138
* listening to our dependencies if there are no more
@@ -194,38 +195,38 @@ public boolean invoke(boolean immediate) {
194195
};
195196
}
196197

197-
private ComputedState getValidState(@Nullable Data data) {
198-
ComputedState state = readState(data);
198+
private CacheState getValidState(@Nullable Data data) {
199+
CacheState state = readState(data);
199200

200201
if (state == null || state.dependencies.hasChanges()) {
201202
@Nullable
202203
Object[] holder = new @Nullable Object[2];
203204
Usage dependencies = UsageTracker.track(() -> {
204205
try {
205-
holder[0] = computation.compute();
206+
holder[0] = inner.get();
206207
} catch (RuntimeException e) {
207208
holder[1] = e;
208209
}
209210
});
210211
if (dependencies == UsageTracker.NO_USAGE) {
211212
throw new MissingSignalUsageException(
212-
"Signal computation must read at least one signal value.");
213+
"A computing inner signal must read at least one other signal value.");
213214
}
214215
@Nullable
215216
Object value = holder[0];
216217
@Nullable
217218
RuntimeException exception = (RuntimeException) holder[1];
218219

219-
state = new ComputedState(value, exception, dependencies);
220+
state = new CacheState(value, exception, dependencies);
220221

221222
submit(new SignalCommand.SetCommand(Id.random(), id(),
222-
new ComputedPOJONode(state)));
223+
new CachedPOJONode(state)));
223224
}
224225

225226
return state;
226227
}
227228

228-
private static @Nullable ComputedState readState(@Nullable Data data) {
229+
private static @Nullable CacheState readState(@Nullable Data data) {
229230
if (data == null) {
230231
return null;
231232
}
@@ -238,22 +239,22 @@ private ComputedState getValidState(@Nullable Data data) {
238239
return extractState(value);
239240
}
240241

241-
private static ComputedState extractState(JsonNode json) {
242-
ComputedPOJONode pojoNode = (ComputedPOJONode) json;
243-
return (ComputedState) pojoNode.getPojo();
242+
private static CacheState extractState(JsonNode json) {
243+
CachedPOJONode pojoNode = (CachedPOJONode) json;
244+
return (CacheState) pojoNode.getPojo();
244245
}
245246

246-
private static class ComputedPOJONode extends POJONode
247+
private static class CachedPOJONode extends POJONode
247248
implements Serializable {
248249

249-
public ComputedPOJONode(Object v) {
250+
public CachedPOJONode(Object v) {
250251
super(v);
251252
}
252253
}
253254

254255
@Override
255256
protected @Nullable T extractValue(@Nullable Data data) {
256-
ComputedState state = getValidState(data);
257+
CacheState state = getValidState(data);
257258

258259
if (state.exception != null) {
259260
throw state.exception;
@@ -275,13 +276,12 @@ public ComputedPOJONode(Object v) {
275276

276277
@Override
277278
public T peekConfirmed() {
278-
throw new UnsupportedOperationException(
279-
"Cannot peek a computed signal");
279+
throw new UnsupportedOperationException("Cannot peek a cached signal");
280280
}
281281

282282
@Override
283283
public SharedNodeSignal asNode() {
284284
throw new UnsupportedOperationException(
285-
"Cannot use a computed signal as a node signal");
285+
"Cannot use a cached signal as a node signal");
286286
}
287287
}

0 commit comments

Comments
 (0)