Skip to main content

Rollback Scope

The Rollback Scope utility (from v0.13.0) provides transactional semantics for state mutations. It allows you to wrap multiple state changes in an atomic operation that can be automatically rolled back on failure.


Overview

createRollbackScope creates a transactional wrapper around any BaseOriginator or DiffOriginator instance. The begin() method executes a callback within a transactional boundary:

  • On success: All changes are committed
  • On rollback/error: All changes are reverted to the original state
import { createRollbackScope, rollback } from '@dxbox/use-less-react/classes';

const scope = createRollbackScope(myOriginator)
.withBatchedNotifications();

await scope.begin(async () => {
myOriginator.value = 'new value';

if (someCondition) {
rollback(); // Revert all changes
}
});

API Reference

createRollbackScope(originator)

Factory function that creates a RollbackScope instance.

ParameterTypeDescription
originatorBaseOriginator | DiffOriginatorThe originator instance to wrap

Returns: RollbackScope<TOriginator>

The factory automatically detects the originator type and uses the appropriate caretaker:

  • DiffOriginatorDiffCaretaker
  • BaseOriginatorBaseCaretaker
// With BaseOriginator
const baseScope = createRollbackScope(myBaseOriginator);

// With DiffOriginator
const diffScope = createRollbackScope(myDiffOriginator);

Fluent API for Notification Configuration

After creating a scope, you must configure how notifications are handled using one of these methods:

.withBatchedNotifications(options?)

Configures the scope to batch all notifications during transaction execution. Useful for optimistic updates.

const scope = createRollbackScope(myOriginator)
.withBatchedNotifications();

await scope.begin(async () => {
// All notifications are batched - single render
myOriginator.field1 = 'a';
myOriginator.field2 = 'b';
}); // Single notification sent here
.withStandardNotifications()

Configures the scope to send notifications immediately as changes occur during the transaction.

const scope = createRollbackScope(myOriginator)
.withStandardNotifications();

await scope.begin(async () => {
// Each mutation triggers immediate notification
myOriginator.field1 = 'a'; // Notification
myOriginator.field2 = 'b'; // Notification
});

Note: You must call one of these methods before calling begin(). The fluent API ensures type-safe configuration.


RollbackScope.begin(callback)

Executes the callback within a transactional boundary.

ParameterTypeDescription
callback() => Promise<void> | voidThe operations to execute transactionally

Returns: Promise<void>

Behavior:

  1. Captures a memento (state snapshot) before execution
  2. Handles notifications according to the configured mode:
    • .withBatchedNotifications(): Batches all notifications, sends single notification on success
    • .withStandardNotifications(): Sends notifications immediately as changes occur
  3. On success: commits changes
  4. On RollbackError or any exception: restores state, re-throws error
await scope.begin(async () => {
// Multiple mutations...
originator.field1 = 'value1';
originator.field2 = 'value2';

await someAsyncOperation();

originator.field3 = 'value3';
}); // All changes committed atomically

rollback()

Helper function that throws a RollbackError to trigger a rollback.

import { rollback } from '@dxbox/use-less-react/classes';

await scope.begin(async () => {
originator.status = 'processing';

const result = await apiCall();

if (!result.success) {
rollback(); // Throws RollbackError
}
});

Note: rollback() throws an exception, so code after the rollback() call will not execute.


RollbackError

Custom error class used to signal rollback operations.

import { RollbackError } from '@dxbox/use-less-react/classes';

try {
await scope.begin(async () => {
// ...
rollback();
});
} catch (error) {
if (error instanceof RollbackError) {
console.log('Transaction was rolled back');
}
}

Rollback Triggers

There are two ways to trigger a rollback:

1. Explicit Rollback

Use the rollback() helper when a condition requires aborting:

await scope.begin(async () => {
order.status = 'confirmed';

if (!validateOrder(order)) {
rollback();
}
});

2. Automatic Rollback on Exceptions

Any thrown exception triggers an automatic rollback:

await scope.begin(async () => {
account.balance -= amount;

// If this throws, balance is restored
await paymentService.charge(amount);
});

Nested Rollback Scopes

RollbackScope supports nested rollback scopes with independent rollback behavior:

await outerScope.begin(async () => {
parent.value = 'outer';

try {
await innerScope.begin(async () => {
child.value = 'inner';
rollback(); // Only inner rolls back
});
} catch {
// Inner rolled back, outer continues
}

parent.status = 'done';
}); // Outer commits

Nested Transaction Rules

ScenarioResult
Inner rollbackInner changes reverted, outer continues
Outer rollbackAll changes reverted (including committed inner)
Inner exception (uncaught)Propagates to outer, both roll back

Notification Handling

Notification behavior during rollback scopes is controlled by the fluent API configuration:

With Batched Notifications

When using .withBatchedNotifications(), all mutations are wrapped in batchNotifications():

  • No intermediate renders during the transaction
  • Single notification when the transaction commits
  • Zero notifications on rollback
const scope = createRollbackScope(originator)
.withBatchedNotifications();

await scope.begin(async () => {
// These don't trigger individual notifications
originator.field1 = 'a';
originator.field2 = 'b';
originator.field3 = 'c';
}); // Single notification here

With Standard Notifications

When using .withStandardNotifications(), notifications are sent immediately for each mutation:

const scope = createRollbackScope(originator)
.withStandardNotifications();

await scope.begin(async () => {
originator.field1 = 'a'; // Notification sent
originator.field2 = 'b'; // Notification sent
originator.field3 = 'c'; // Notification sent
});

Best Practices

  1. Keep Rollback Scopes focused — Include only related mutations in a single transaction

  2. Combine with async/await — The API is designed for async operations

  3. Consider nested scopes carefully — Understand the rollback propagation behavior