Transactions
- Basic Transactions
- Transaction Benefits
- Verification Methods
- Operation Results
- Bypassing Transactions
- Best Practices
Transactions allow grouping multiple signal operations into a single atomic unit. All operations in a transaction either succeed or fail together, ensuring data consistency.
|
Note
|
Preview Feature
This is a preview version of Signals. You need to enable it with the feature flag |
Basic Transactions
Use Signal.runInTransaction() to execute multiple signal operations atomically:
Source code
Java
Signal.runInTransaction(() -> {
// All operations here will be committed atomically
firstNameSignal.value("John");
lastNameSignal.value("Doe");
ageSignal.value(30);
});If any operation fails, none of the changes are applied. Observers see either the complete update or nothing at all - never a partial update.
Transaction Benefits
Transactions provide several guarantees:
- Atomicity
-
All changes in a transaction succeed or fail together
- Consistency
-
No observer sees partial updates
- Isolation
-
Changes are not visible until the transaction commits
Verification Methods
Transactions support verification methods that ensure certain conditions are met before committing changes.
Verifying Expected Values
The verifyValue() method verifies that a signal has a specific expected value:
Source code
Java
Signal.runInTransaction(() -> {
// Transaction fails if current status is not "pending"
statusSignal.verifyValue("pending");
// Only executed if verification passes
statusSignal.value("processing");
processedBySignal.value(currentUser);
});Verifying List Item Position
The verifyPosition() method verifies that an item is at a specific position in a list signal:
Source code
Java
Signal.runInTransaction(() -> {
SharedValueSignal<Todo> firstItem = todoList.value().get(0);
// Verify the item we want to update is still first
todoList.verifyPosition(firstItem, ListPosition.first());
// Update only if verification passes
firstItem.value(new Todo("Updated first item", false));
});Verifying Child Exists
The verifyChild() method on SharedListSignal verifies that a child signal is still part of the list:
Source code
Java
Signal.runInTransaction(() -> {
SharedValueSignal<Todo> itemSignal = todoList.value().get(0);
// Verify the child signal still exists in the list before updating
todoList.verifyChild(itemSignal);
itemSignal.value(updatedValue);
});Operation Results
Signal operations return result objects that provide information about the operation and allow chaining.
SignalOperation
The base interface for all signal operations. Operations return a CompletableFuture that resolves when the operation completes:
Source code
Java
SignalOperation<String> op = stringSignal.value("new value");
// Get the result as a CompletableFuture
CompletableFuture<SignalOperation.ResultOrError<String>> result = op.result();
// Check if the operation has completed
if (result.isDone()) {
// Get the result (blocks if not yet complete)
SignalOperation.ResultOrError<String> resultOrError = result.get();
if (resultOrError.successful()) {
System.out.println("Operation completed successfully");
}
}CancelableOperation
Some operations that may retry (like update()) return a CancelableOperation that can be canceled:
Source code
Java
CancelableOperation<Integer> updateOp = counter.update(v -> v + 1);
// If needed, cancel the retry loop
updateOp.cancel();The update() operation uses a compare-and-set approach. If another change occurs concurrently, the operation retries with the new value. Calling cancel() stops this retry loop.
|
Note
|
A call to cancel() may not always be effective, as a succeeding operation might already be on its way to the server.
|
InsertOperation
List insert operations return an InsertOperation that provides immediate access to the inserted signal:
Source code
Java
SharedListSignal<Todo> todoList = new SharedListSignal<>(Todo.class);
InsertOperation<Todo> op = todoList.insertLast(new Todo("New task", false));
// Get the signal for the inserted item immediately
SharedValueSignal<Todo> insertedSignal = op.signal();
// You can start working with the signal right away
insertedSignal.update(todo -> new Todo(todo.text(), true));This is useful when you need to reference the newly inserted item immediately after insertion.
TransactionOperation
When running transactions, you can get a TransactionOperation that tracks the entire transaction. The returnValue() method returns a SignalOperation that can be used to check the transaction result:
Source code
Java
TransactionOperation txOp = Signal.runInTransaction(() -> {
firstNameSignal.value("John");
lastNameSignal.value("Doe");
});
// Get the underlying SignalOperation to check transaction status
SignalOperation<?> signalOp = txOp.returnValue();
CompletableFuture<?> result = signalOp.result();
if (result.isDone()) {
System.out.println("Transaction committed successfully");
}Bypassing Transactions
Effect and computed signal callbacks run inside a read-only transaction to prevent accidental changes. If you need to modify a signal from within an effect (and you’re certain there’s no risk of infinite loops), use runWithoutTransaction():
Source code
Java
ComponentEffect.effect(component, () -> {
String value = sourceSignal.value();
// WARNING: This might lead to infinite loops.
// Do this only if absolutely necessary.
Signal.runWithoutTransaction(() -> {
derivedSignal.value(transformValue(value));
});
});|
Warning
|
Using runWithoutTransaction() bypasses safety checks. Only use it when you’re certain the update won’t cause an infinite loop.
|
Best Practices
Use Transactions for Related Updates
When multiple signals should be updated together, use a transaction:
Source code
Java
// Good: Related updates in a transaction
Signal.runInTransaction(() -> {
orderStatusSignal.value("shipped");
shippedAtSignal.value(Instant.now());
trackingNumberSignal.value(tracking);
});
// Avoid: Separate updates that might leave inconsistent state
orderStatusSignal.value("shipped");
shippedAtSignal.value(Instant.now()); // What if this fails?
trackingNumberSignal.value(tracking);Use Verification for Conditional Updates
When updates depend on current state, use verification:
Source code
Java
Signal.runInTransaction(() -> {
// Ensure we're updating the expected state
statusSignal.verifyValue("pending");
quantitySignal.verifyValue(expectedQuantity);
statusSignal.value("confirmed");
quantitySignal.value(newQuantity);
});Prefer update() for Single-Signal Atomic Updates
For atomic updates based on current value, use update() instead of a transaction:
Source code
Java
// Preferred for single-signal updates
counter.update(v -> v + 1);
// Transaction is overkill for single updates
Signal.runInTransaction(() -> {
counter.value(counter.value() + 1);
});Keep Transactions Small
Transactions hold resources while executing. Keep them focused on the minimal set of related operations:
Source code
Java
// Good: Small, focused transaction
Signal.runInTransaction(() -> {
userSignal.value(updatedUser);
lastModifiedSignal.value(Instant.now());
});
// Avoid: Large transactions with unrelated operations
Signal.runInTransaction(() -> {
userSignal.value(updatedUser);
lastModifiedSignal.value(Instant.now());
// Don't include unrelated updates
analyticsCounterSignal.incrementBy(1);
logSignal.value(logMessage);
});