vaadin / hilla

Build better business applications, faster. No more juggling REST endpoints or deciphering GraphQL queries. Hilla seamlessly connects Spring Boot and React to accelerate application development.
https://hilla.dev
Apache License 2.0
920 stars 58 forks source link

[Full-stack Signals] add support for having single valued signals other than numbers #2623

Closed taefi closed 2 months ago

taefi commented 3 months ago

Describe your motivation

Full-stack signals library should offer signals that can hold a single shared value of types other than numbers, e.g. JSON-based types string and boolean.

Describe the solution you'd like

A generic type called ValueSignal<T> can be used for representing these types as of now. In the future, if there was a need for introducing atomic operations for these types as well, we can introduce more specific types.

The fact that NumberSignal is the designated signal type to handle atomic arithmetic operations should not prevent users from having ValueSignal<Integer> instances. For instance:

@AnonymousAllowed
@BrowserCallable
public class ShoppingCartService {
    private final ValueSignal<Integer> cartSize = new ValueSignal<>(0);

    public ValueSignal<Integer> size() {
        return cartSize;
    }
}
const cartSize = ShoppingCartService.size();

(Disclaimer: The above is just an example to show the possibility of using ValueSignal with numbers as well, not a recommendation for implementation.)

The suggested ValueSignal<T> supports three different write operations that all replace the current value with a new value with different approaches for dealing with concurrent changes (using the above example for brevity):

  1. Last-write-wins (overwrite):

    cartSize.value++; // this is the same as followings:
    // cartSize.value = cartSize.value + 1;
    // cartSize.set(cartSize.value + 1);
  2. Conditional replace (also known as "compare and set") - the change is applied only if the expected value matches the current value when the change is applied on the server:

    const expected = cartSize.value;
    cartSize.replace(expected, expected + 1);
  3. Runs the callback and tries to set the returned value as the signal value. In case of a concurrent change, the callback is run again with an updated input value. This is repeated until the result can be applied without concurrent changes:

    const updateOperation = cartSize.update(lastSeenValue => lastSeenValue + 1);
    // Abort if contention has prevented success after 1000 ms
    setTimeout(() => updateOperation.cancel(), 1000);

    Note that there's no guarantee that cancel() will be effective since a succeeding operation might already be on its way to the server.

Describe alternatives you've considered

No response

Additional context

No response

platosha commented 2 months ago

Includes #2625