Which package?
@termuijs/store
What happened?
The batch() function incorrectly attempts to span asynchronous functions, freezing the entire application's state updates during await.
When you pass an async function to batch(async () => { ... }), the batch implementation intercepts the returned Promise and delays decrementing _batchDepth until the Promise resolves.
However, JavaScript is single-threaded and batch relies on a global _batchDepth counter. While the async batch function is suspended (e.g., waiting for an await fetch() or a timeout), _batchDepth remains > 0.
Because of this, any unrelated store updates happening anywhere else in the application (like user input handlers, background timers, or incoming network events) will erroneously think they are part of the active batch.
Their state changes will be buffered into the internal _batchStores map, and their listeners will not be notified until the original async batch finally resolves. This causes the UI to completely freeze and swallow updates across the entire application for the duration of any awaited promise inside a batch().
batch (much like React's unstable_batchedUpdates) should only batch synchronous execution. It should not try to await returned promises.
Steps to reproduce
import { createStore, batch } from '@termuijs/store';
const store = createStore({ count: 0 });
async function sleep(ms: number) {
return new Promise(r => setTimeout(r, ms));
}
let updates = 0;
store.subscribe(() => updates++);
// Start a batch with an async function
batch(async () => {
store.setState({ count: 1 });
// _batchDepth stays > 0 during this await!
await sleep(500);
store.setState({ count: 2 });
});
// While the batch is sleeping, another unrelated part of the app updates the state
setTimeout(() => {
store.setState({ count: 99 });
console.log("Unrelated update completed. Current state:", store.getState());
console.log("Did listeners fire for the unrelated update?", updates > 0 ? "YES" : "NO (BUG!)");
}, 100);
Environment
- Bun version: 1.1.20
- OS: Windows 11
- Terminal emulator: PowerShell
- TermUI version:
main
Screenshots or terminal output
Unrelated update completed. Current state: { count: 99 }
Did listeners fire for the unrelated update? NO (BUG!)
GSSoC contributor?
Which package?
@termuijs/store
What happened?
The
batch()function incorrectly attempts to span asynchronous functions, freezing the entire application's state updates duringawait.When you pass an async function to
batch(async () => { ... }), thebatchimplementation intercepts the returned Promise and delays decrementing_batchDepthuntil the Promise resolves.However, JavaScript is single-threaded and
batchrelies on a global_batchDepthcounter. While the async batch function is suspended (e.g., waiting for anawait fetch()or a timeout),_batchDepthremains> 0.Because of this, any unrelated store updates happening anywhere else in the application (like user input handlers, background timers, or incoming network events) will erroneously think they are part of the active batch.
Their state changes will be buffered into the internal
_batchStoresmap, and their listeners will not be notified until the original async batch finally resolves. This causes the UI to completely freeze and swallow updates across the entire application for the duration of any awaited promise inside abatch().batch(much like React'sunstable_batchedUpdates) should only batch synchronous execution. It should not try to await returned promises.Steps to reproduce
Environment
mainScreenshots or terminal output
GSSoC contributor?