Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adjust increment api and remove add from NumberSignal #2704

Merged
merged 11 commits into from
Sep 11, 2024
Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.vaadin.hilla.signals;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.vaadin.hilla.signals.core.event.StateEvent;

/**
* A signal that holds a number value.
*/
Expand All @@ -24,4 +27,31 @@ public NumberSignal(Double defaultValue) {
public NumberSignal() {
this(0.0);
}

/**
* Processes the event and updates the signal value if needed. Note that
* this method is not thread-safe and should be called from a synchronized
* context.
*
* @param event
* the event to process
* @return <code>true</code> if the event was successfully processed and the
* signal value was updated, <code>false</code> otherwise.
*/
@Override
protected boolean processEvent(ObjectNode event) {
try {
var stateEvent = new StateEvent<>(event, Double.class);
if (!StateEvent.EventType.INCREMENT
.equals(stateEvent.getEventType())) {
return super.processEvent(event);
}
Double expectedValue = getValue();
Double newValue = expectedValue + stateEvent.getValue();
return super.compareAndSet(newValue, expectedValue);
} catch (StateEvent.InvalidEventTypeException e) {
throw new UnsupportedOperationException(
"Unsupported JSON: " + event, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,26 @@ private ObjectNode createStatusUpdateEvent(String eventId,
return snapshot.toJson();
}

private boolean processEvent(ObjectNode event) {
/**
* Processes the event and updates the signal value if needed. Note that
* this method is not thread-safe and should be called from a synchronized
* context.
*
* @param event
* the event to process
* @return <code>true</code> if the event was successfully processed and the
* signal value was updated, <code>false</code> otherwise.
*/
protected boolean processEvent(ObjectNode event) {
try {
var stateEvent = new StateEvent<>(event, valueType);
return switch (stateEvent.getEventType()) {
case SET -> {
this.value = stateEvent.getValue();
yield true;
}
case REPLACE -> compareAndSet(stateEvent);
case REPLACE -> compareAndSet(stateEvent.getValue(),
stateEvent.getExpected());
default -> throw new UnsupportedOperationException(
"Unsupported event: " + stateEvent.getEventType());
};
Expand All @@ -163,9 +174,21 @@ private boolean processEvent(ObjectNode event) {
}
}

private boolean compareAndSet(StateEvent<T> stateEvent) {
if (Objects.equals(this.value, stateEvent.getExpected())) {
this.value = stateEvent.getValue();
/**
* Compares the current value with the expected value and updates the signal
* value if they match. Note that this method is not thread-safe and should
* be called from a synchronized context.
*
* @param newValue
* the new value to set
* @param expectedValue
* the expected value
* @return <code>true</code> if the value was successfully updated,
* <code>false</code> otherwise
*/
protected boolean compareAndSet(T newValue, T expectedValue) {
if (Objects.equals(this.value, expectedValue)) {
this.value = newValue;
return true;
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static final class Field {
* Possible types of state events.
*/
public enum EventType {
SNAPSHOT, SET, REPLACE, REJECT
SNAPSHOT, SET, REPLACE, REJECT, INCREMENT
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ public void submit_eventWithUnknownCommand_throws() {
assertTrue(exception.getMessage().startsWith("Unsupported JSON: "));
}

@Test
public void submit_eventWithIncrementCommand_incrementsValue() {
NumberSignal signal = new NumberSignal(42.0);

signal.submit(createIncrementEvent("2"));
assertEquals(44.0, signal.getValue(), 0.0);

signal.submit(createIncrementEvent("-5.5"));
assertEquals(38.5, signal.getValue(), 0.0);
}

private ObjectNode createIncrementEvent(String value) {
var setEvent = new StateEvent<>(UUID.randomUUID().toString(),
StateEvent.EventType.INCREMENT, Double.parseDouble(value));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valueOf is slightly better than parseDouble here (IDE suggestion)

return setEvent.toJson();
}

private ObjectNode createSetEvent(String value) {
var setEvent = new StateEvent<>(UUID.randomUUID().toString(),
StateEvent.EventType.SET, Double.parseDouble(value));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public void constructor_withJsonInvalidEventType_shouldThrowInvalidEventTypeExce
StateEvent.InvalidEventTypeException.class,
() -> new StateEvent<>(json, String.class));

String expectedMessage = "Invalid event type invalidType. Type should be either of: [SNAPSHOT, SET, REPLACE, REJECT]";
String expectedMessage = "Invalid event type invalidType. Type should be either of: [SNAPSHOT, SET, REPLACE, REJECT, INCREMENT]";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"either of" is rather used with two options. Should be "one of".

String actualMessage = exception.getMessage();

assertTrue(actualMessage.contains(expectedMessage));
Expand All @@ -153,7 +153,7 @@ public void constructor_withJsonMissingEventType_shouldThrowInvalidEventTypeExce
StateEvent.InvalidEventTypeException.class,
() -> new StateEvent<>(json, String.class));

String expectedMessage = "Missing event type. Type is required, and should be either of: [SNAPSHOT, SET, REPLACE, REJECT]";
String expectedMessage = "Missing event type. Type is required, and should be either of: [SNAPSHOT, SET, REPLACE, REJECT, INCREMENT]";
String actualMessage = exception.getMessage();

assertTrue(actualMessage.contains(expectedMessage));
Expand Down
11 changes: 11 additions & 0 deletions packages/ts/react-signals/src/FullStackSignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ export abstract class FullStackSignal<T> extends DependencyTrackingSignal<T> {
this.#paused = false;
}

/**
* Sets the local value of the signal without sending any events to the server
* @param value - The new value.
* @internal
*/
protected setValueLocal(value: T): void {
this.#paused = true;
this.value = value;
this.#paused = false;
}

/**
* A method to update the server with the new value.
*
Expand Down
49 changes: 49 additions & 0 deletions packages/ts/react-signals/src/NumberSignal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createIncrementStateEvent } from './events.js';
import { $update } from './FullStackSignal.js';
import { ValueSignal } from './ValueSignal.js';

/**
* A signal that holds a number value. The underlying
* value of this signal is stored and updated as a
* shared value on the server.
*
* After obtaining the NumberSignal instance from
* a server-side service that returns one, the value
* can be updated using the `value` property,
* and it can be read with or without the
* `value` property (similar to a normal signal):
*
* @example
* ```tsx
* const counter = CounterService.counter();
*
* return (
* <Button onClick={() => counter.incrementBy(1)}>
* Click count: { counter }
* </Button>
* <Button onClick={() => counter.value = 0}>Reset</Button>
* );
* ```
*/
export class NumberSignal extends ValueSignal<number> {
/**
* Increments the value by the specified delta. The delta can be negative to
* decrease the value.
*
* This method differs from using the `++` or `+=` operators directly on the
* signal value. It performs an atomic operation to prevent conflicts from
* concurrent changes, ensuring that other users' modifications are not
* accidentally overwritten.
*
* @param delta - The delta to increment the value by. The delta can be
* negative.
*/
incrementBy(delta: number): void {
if (delta === 0) {
return;
}
this.setValueLocal(this.value + delta);
const event = createIncrementStateEvent(delta);
this[$update](event);
}
}
109 changes: 0 additions & 109 deletions packages/ts/react-signals/src/Signals.ts

This file was deleted.

17 changes: 16 additions & 1 deletion packages/ts/react-signals/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,22 @@ export function createReplaceStateEvent<T>(expected: T, value: T): ReplaceStateE
};
}

export type IncrementStateEvent = CreateStateEventType<number, 'increment'>;

export function createIncrementStateEvent(delta: number): IncrementStateEvent {
return {
id: nanoid(),
type: 'increment',
value: delta,
};
}

/**
* An object that describes the change of the signal state.
*/
export type StateEvent<T> = RejectStateEvent | ReplaceStateEvent<T> | SetStateEvent<T> | SnapshotStateEvent<T>;
export type StateEvent<T> =
| IncrementStateEvent
| RejectStateEvent
| ReplaceStateEvent<T>
| SetStateEvent<T>
| SnapshotStateEvent<T>;
2 changes: 1 addition & 1 deletion packages/ts/react-signals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import './polyfills.js';

// eslint-disable-next-line import/export
export * from './core.js';
export { NumberSignal } from './Signals.js';
export { NumberSignal } from './NumberSignal.js';
export { ValueSignal } from './ValueSignal.js';
export type { OperationSubscription } from './ValueSignal.js';
export { FullStackSignal } from './FullStackSignal.js';
Loading