Skip to content

Commit b11b5a5

Browse files
authored
fix(useAsyncIterState): rapidly updating state yields the first update instead of the last update's value (#44)
1 parent 6dd5ac5 commit b11b5a5

File tree

3 files changed

+62
-6
lines changed

3 files changed

+62
-6
lines changed

spec/tests/useAsyncIterState.spec.tsx

+43-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { renderHook, cleanup as cleanupMountedReactTrees, act } from '@testing-l
55
import { useAsyncIterState } from '../../src/index.js';
66
import { asyncIterToArray } from '../utils/asyncIterToArray.js';
77
import { asyncIterTake } from '../utils/asyncIterTake.js';
8+
import { asyncIterTakeFirst } from '../utils/asyncIterTakeFirst.js';
89
import { checkPromiseState } from '../utils/checkPromiseState.js';
910
import { pipe } from '../utils/pipe.js';
1011

@@ -13,12 +14,49 @@ afterEach(() => {
1314
});
1415

1516
describe('`useAsyncIterState` hook', () => {
17+
it(gray('Updating states iteratively with the returned setter works correctly'), async () => {
18+
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
19+
20+
const rounds = 3;
21+
22+
const yieldsPromise = pipe(values, asyncIterTake(rounds), asyncIterToArray);
23+
const currentValues = [values.value.current];
24+
25+
for (let i = 0; i < rounds; ++i) {
26+
await act(() => {
27+
setValue(i);
28+
currentValues.push(values.value.current);
29+
});
30+
}
31+
32+
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
33+
expect(await yieldsPromise).toStrictEqual([0, 1, 2]);
34+
});
35+
36+
it(
37+
gray('Updating states as rapidly as possible with the returned setter works correctly'),
38+
async () => {
39+
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
40+
41+
const yieldPromise = pipe(values, asyncIterTakeFirst());
42+
const currentValues = [values.value.current];
43+
44+
for (let i = 0; i < 3; ++i) {
45+
setValue(i);
46+
currentValues.push(values.value.current);
47+
}
48+
49+
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
50+
expect(await yieldPromise).toStrictEqual(2);
51+
}
52+
);
53+
1654
it(gray('The returned iterable can be async-iterated upon successfully'), async () => {
1755
const [values, setValue] = renderHook(() => useAsyncIterState<string>()).result.current;
1856

1957
const valuesToSet = ['a', 'b', 'c'];
2058

21-
const collectPromise = pipe(values, asyncIterTake(valuesToSet.length), asyncIterToArray);
59+
const yieldsPromise = pipe(values, asyncIterTake(valuesToSet.length), asyncIterToArray);
2260
const currentValues = [values.value.current];
2361

2462
for (const value of valuesToSet) {
@@ -28,7 +66,7 @@ describe('`useAsyncIterState` hook', () => {
2866
});
2967
}
3068

31-
expect(await collectPromise).toStrictEqual(['a', 'b', 'c']);
69+
expect(await yieldsPromise).toStrictEqual(['a', 'b', 'c']);
3270
expect(currentValues).toStrictEqual([undefined, 'a', 'b', 'c']);
3371
});
3472

@@ -130,7 +168,7 @@ describe('`useAsyncIterState` hook', () => {
130168

131169
it(
132170
gray(
133-
"The returned iterable's values are each shared between all its parallel consumers so that each receives all the values that will yield after the start of its consumption"
171+
"The returned iterable's values are each shared between all its parallel consumers so that each will receives all values that will yield from the time it started consuming"
134172
),
135173
async () => {
136174
const [values, setValue] = renderHook(() => useAsyncIterState<string>()).result.current;
@@ -140,9 +178,11 @@ describe('`useAsyncIterState` hook', () => {
140178

141179
for (const [i, value] of ['a', 'b', 'c'].entries()) {
142180
consumeStacks[i] = [];
181+
143182
(async () => {
144183
for await (const v of values) consumeStacks[i].push(v);
145184
})();
185+
146186
await act(() => {
147187
setValue(value);
148188
currentValues.push(values.value.current);

spec/utils/asyncIterTakeFirst.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export { asyncIterTakeFirst };
2+
3+
function asyncIterTakeFirst<T>(): (src: AsyncIterable<T>) => Promise<T | undefined> {
4+
return async sourceIter => {
5+
const iterator = sourceIter[Symbol.asyncIterator]();
6+
try {
7+
const first = await iterator.next();
8+
return first.done ? undefined : first.value;
9+
} finally {
10+
await iterator.return?.();
11+
}
12+
};
13+
}

src/useAsyncIterState/IterableChannel.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ class IterableChannel<T> {
1010

1111
put(value: T): void {
1212
if (!this.#isClosed) {
13-
this.#currentValue = value;
14-
this.#nextIteration.resolve({ done: false, value });
15-
this.#nextIteration = promiseWithResolvers();
13+
(async () => {
14+
this.#currentValue = value;
15+
await undefined; // Deferring to the next microtick so that an attempt to pull the a value before making multiple rapid synchronous calls to `put()` will make that pull ultimately yield only the last value that was put - instead of the first one as were if this otherwise wasn't deferred.
16+
this.#nextIteration.resolve({ done: false, value: this.#currentValue });
17+
this.#nextIteration = promiseWithResolvers();
18+
})();
1619
}
1720
}
1821

0 commit comments

Comments
 (0)