Which package?
@termuijs/store
What happened?
batch() is documented to be atomic — if the callback throws, all state changes within it
should be rolled back. This holds for flat (non-nested) batches. But when batch() calls
are nested and the outer batch throws, inner batch changes are not rolled back.
Root cause: when the inner batch completes without throwing, _batchDepth drops back to 0
and flushBatch(false) is called, which schedules a queueMicrotask to commit inner changes.
This microtask is already enqueued before the outer batch throws. When the outer batch throws,
flushBatch(true) correctly rolls back outer changes, but has no way to cancel the already-
scheduled microtask that commits the inner batch's changes.
Expected: If the outer batch() throws, all state changes — including those from nested
inner batches — are rolled back completely.
Actual: Inner batch changes survive the outer batch throw and are committed.
Steps to reproduce
```typescript
import { createStore, batch } from '@termuijs/store';
const store = createStore(() => ({ x: 0, y: 0 }));
try {
batch(() => {
// Inner batch completes successfully and schedules a microtask commit
batch(() => {
store.setState({ x: 1 });
});
store.setState({ y: 2 });
throw new Error('outer batch failed');
});
} catch {}
// Allow microtask queue to flush
await Promise.resolve();
console.log(store.getState());
// Actual: { x: 1, y: 0 } ← inner change survived
// Expected: { x: 0, y: 0 } ← full rollback
```
The core issue is in flushBatch: when _batchDepth returns to 0 at the end of an inner
batch, it cannot tell it is nested inside an outer batch that may still fail.
Environment
- TermUI version: latest (main branch)
- Node.js 18+
- Any OS / terminal
GSSoC contributor?
Which package?
@termuijs/store
What happened?
batch()is documented to be atomic — if the callback throws, all state changes within itshould be rolled back. This holds for flat (non-nested) batches. But when
batch()callsare nested and the outer batch throws, inner batch changes are not rolled back.
Root cause: when the inner batch completes without throwing,
_batchDepthdrops back to 0and
flushBatch(false)is called, which schedules aqueueMicrotaskto commit inner changes.This microtask is already enqueued before the outer batch throws. When the outer batch throws,
flushBatch(true)correctly rolls back outer changes, but has no way to cancel the already-scheduled microtask that commits the inner batch's changes.
Expected: If the outer
batch()throws, all state changes — including those from nestedinner batches — are rolled back completely.
Actual: Inner batch changes survive the outer batch throw and are committed.
Steps to reproduce
```typescript
import { createStore, batch } from '@termuijs/store';
const store = createStore(() => ({ x: 0, y: 0 }));
try {
batch(() => {
// Inner batch completes successfully and schedules a microtask commit
batch(() => {
store.setState({ x: 1 });
});
} catch {}
// Allow microtask queue to flush
await Promise.resolve();
console.log(store.getState());
// Actual: { x: 1, y: 0 } ← inner change survived
// Expected: { x: 0, y: 0 } ← full rollback
```
The core issue is in
flushBatch: when_batchDepthreturns to 0 at the end of an innerbatch, it cannot tell it is nested inside an outer batch that may still fail.
Environment
GSSoC contributor?