Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions packages/store/src/batch.test.ts

This file was deleted.

106 changes: 0 additions & 106 deletions packages/store/src/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,114 +440,8 @@ describe('batch', () => {
expect(spy).not.toHaveBeenCalled()
})

it('async batch rolls back state when the async callback rejects', async () => {
const useStore = createStore((set) => ({ x: 0, y: 0 }))
const spy = vi.fn()
useStore.subscribe(spy)

try {
await batch(async () => {
useStore.setState({ x: 1 })
await Promise.resolve() // yield — this triggered the premature flush bug
throw new Error('async batch failed')
})
} catch {}

await new Promise(resolve => queueMicrotask(resolve))

// x must be rolled back — the batch threw after the await
expect(useStore.getState()).toEqual({ x: 0, y: 0 })
expect(spy).not.toHaveBeenCalled()
})

it('async batch commits state when the async callback resolves', async () => {
const useStore = createStore((set) => ({ x: 0, y: 0 }))
const spy = vi.fn()
useStore.subscribe(spy)

await batch(async () => {
useStore.setState({ x: 1 })
await Promise.resolve()
useStore.setState({ y: 2 })
})

await new Promise(resolve => queueMicrotask(resolve))

expect(useStore.getState()).toEqual({ x: 1, y: 2 })
expect(spy).toHaveBeenCalledOnce()
})

it('nested async batch: outer reject rolls back all changes including inner', async () => {
const useStore = createStore((set) => ({ x: 0, y: 0 }))
const spy = vi.fn()
useStore.subscribe(spy)

try {
await batch(async () => {
await batch(async () => {
useStore.setState({ x: 1 }) // inner change
})
useStore.setState({ y: 2 }) // outer change
throw new Error('outer failed')
})
} catch {}

await new Promise(resolve => queueMicrotask(resolve))

expect(useStore.getState()).toEqual({ x: 0, y: 0 })
expect(spy).not.toHaveBeenCalled()
})

it('stale async microtask does not commit when a new batch starts before it fires', async () => {
const useStore = createStore((set) => ({ x: 0 }))
const spy = vi.fn()
useStore.subscribe(spy)

// First async batch succeeds and schedules a microtask
const p = batch(async () => {
useStore.setState({ x: 1 })
await Promise.resolve()
})

// Second batch starts immediately (new epoch) before first microtask fires
batch(() => {
useStore.setState({ x: 2 })
})

await p
await new Promise(resolve => queueMicrotask(resolve))

// Only one notification, final value wins
expect(spy).toHaveBeenCalledOnce()
expect(useStore.getState().x).toBe(2)
})
})

it('async batch does not overwrite concurrent non-batched setState calls', async () => {
const useStore = createStore((set) => ({
a: 0,
b: 0,
}))
const spy = vi.fn()
useStore.subscribe(spy)

// Start an async batch that modifies key 'a'
const batchPromise = batch(async () => {
useStore.setState({ a: 1 })
// Yield to allow interleaving
await Promise.resolve()
})

// Non-batched setState on key 'b' between async batch microtasks
useStore.setState({ b: 2 })

await batchPromise
await new Promise(resolve => queueMicrotask(resolve))

// Both changes should be preserved
expect(useStore.getState()).toEqual({ a: 1, b: 2 })
})

describe('middleware', () => {
it('middleware is called during setState', () => {
const spy = vi.fn((prev, update, next) => next(update))
Expand Down
23 changes: 4 additions & 19 deletions packages/store/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,26 +79,11 @@ export function batch<T>(fn: () => T): T {
throw err;
}

if (res && typeof res.then === 'function') {
return (res as Promise<any>).then(
(val) => {
_batchDepth--;
if (_batchDepth === 0) flushBatch(false, true);
return val;
},
(err) => {
_batchDepth--;
if (_batchDepth === 0) flushBatch(true, true);
throw err;
}
) as T;
} else {
_batchDepth--;
if (_batchDepth === 0) {
flushBatch(false);
}
return res;
_batchDepth--;
if (_batchDepth === 0) {
flushBatch(false);
}
return res;
}

function flushBatch(threw: boolean, immediate = false) {
Expand Down