Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 61b4ab9

Browse files
committed
implementations and tests for a new useAsyncIterMemo hook
1 parent 20af9b0 commit 61b4ab9

File tree

6 files changed

+342
-0
lines changed

6 files changed

+342
-0
lines changed

‎spec/tests/useAsyncIterMemo.spec.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { nextTick } from 'node:process';
2+
import { it, describe, expect, afterEach } from 'vitest';
3+
import { gray } from 'colorette';
4+
import { renderHook, cleanup as cleanupMountedReactTrees } from '@testing-library/react';
5+
import { useAsyncIterMemo, iterateFormatted } from '../../src/index.js';
6+
import { pipe } from '../utils/pipe.js';
7+
import { asyncIterToArray } from '../utils/asyncIterToArray.js';
8+
import { asyncIterTake } from '../utils/asyncIterTake.js';
9+
import { asyncIterOf } from '../utils/asyncIterOf.js';
10+
import { asyncIterTickSeparatedOf } from '../utils/asyncIterTickSeparatedOf.js';
11+
import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js';
12+
13+
afterEach(() => {
14+
cleanupMountedReactTrees();
15+
});
16+
17+
describe('`useAsyncIterMemo` hook', () => {
18+
it(gray('___ ___ ___ 1'), async () => {
19+
const renderedHook = renderHook(
20+
({ val1, val2, iter1, iter2 }) =>
21+
useAsyncIterMemo((...deps) => deps, [val1, val2, iter1, iter2]),
22+
{
23+
initialProps: {
24+
val1: 'a',
25+
val2: 'b',
26+
iter1: asyncIterOf('a', 'b', 'c'),
27+
iter2: asyncIterOf('d', 'e', 'f'),
28+
},
29+
}
30+
);
31+
32+
const [resVal1, resVal2, resIter1, resIter2] = renderedHook.result.current;
33+
34+
expect(resVal1).toStrictEqual('a');
35+
expect(resVal2).toStrictEqual('b');
36+
expect(await asyncIterToArray(resIter1)).toStrictEqual(['a', 'b', 'c']);
37+
expect(await asyncIterToArray(resIter2)).toStrictEqual(['d', 'e', 'f']);
38+
});
39+
40+
it(gray('___ ___ ___ 2'), async () => {
41+
const channel1 = new IterableChannelTestHelper<string>();
42+
const channel2 = new IterableChannelTestHelper<string>();
43+
let timesRerun = 0;
44+
45+
const renderedHook = renderHook(
46+
({ val1, val2, iter1, iter2 }) =>
47+
useAsyncIterMemo(
48+
(...deps) => {
49+
timesRerun++;
50+
return deps;
51+
},
52+
[val1, val2, iter1, iter2]
53+
),
54+
{
55+
initialProps: {
56+
val1: 'a',
57+
val2: 'b',
58+
iter1: iterateFormatted(channel1, v => `${v}_formatted_1st_time`),
59+
iter2: iterateFormatted(channel2, v => `${v}_formatted_1st_time`),
60+
},
61+
}
62+
);
63+
64+
const hookFirstResult = renderedHook.result.current;
65+
66+
{
67+
expect(timesRerun).toStrictEqual(1);
68+
69+
const [, , resIter1, resIter2] = hookFirstResult;
70+
71+
feedChannelAcrossTicks(channel1, ['a', 'b', 'c']);
72+
const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray);
73+
expect(resIter1Values).toStrictEqual([
74+
'a_formatted_1st_time',
75+
'b_formatted_1st_time',
76+
'c_formatted_1st_time',
77+
]);
78+
79+
feedChannelAcrossTicks(channel2, ['d', 'e', 'f']);
80+
const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray);
81+
82+
expect(resIter2Values).toStrictEqual([
83+
'd_formatted_1st_time',
84+
'e_formatted_1st_time',
85+
'f_formatted_1st_time',
86+
]);
87+
}
88+
89+
renderedHook.rerender({
90+
val1: 'a',
91+
val2: 'b',
92+
iter1: iterateFormatted(channel1, v => `${v}_formatted_2nd_time`),
93+
iter2: iterateFormatted(channel2, v => `${v}_formatted_2nd_time`),
94+
});
95+
96+
const hookSecondResult = renderedHook.result.current;
97+
98+
{
99+
expect(timesRerun).toStrictEqual(1);
100+
expect(hookFirstResult).toStrictEqual(hookSecondResult);
101+
102+
const [, , resIter1, resIter2] = hookSecondResult;
103+
104+
feedChannelAcrossTicks(channel1, ['a', 'b', 'c']);
105+
const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray);
106+
expect(resIter1Values).toStrictEqual([
107+
'a_formatted_2nd_time',
108+
'b_formatted_2nd_time',
109+
'c_formatted_2nd_time',
110+
]);
111+
112+
feedChannelAcrossTicks(channel2, ['d', 'e', 'f']);
113+
const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray);
114+
expect(resIter2Values).toStrictEqual([
115+
'd_formatted_2nd_time',
116+
'e_formatted_2nd_time',
117+
'f_formatted_2nd_time',
118+
]);
119+
}
120+
});
121+
122+
it(gray('___ ___ ___ 3'), async () => {
123+
const iter1 = asyncIterTickSeparatedOf('a', 'b', 'c');
124+
const iter2 = asyncIterTickSeparatedOf('d', 'e', 'f');
125+
let timesRerun = 0;
126+
127+
const renderedHook = renderHook(
128+
({ val1, val2, iter1, iter2 }) =>
129+
useAsyncIterMemo(
130+
(...deps) => {
131+
timesRerun++;
132+
return deps;
133+
},
134+
[val1, val2, iter1, iter2]
135+
),
136+
{
137+
initialProps: {
138+
val1: 'a',
139+
val2: 'b',
140+
iter1: iterateFormatted(iter1, v => `${v}_formatted_1st_time`),
141+
iter2: iterateFormatted(iter2, v => `${v}_formatted_1st_time`),
142+
},
143+
}
144+
);
145+
146+
const hookFirstResult = renderedHook.result.current;
147+
148+
{
149+
expect(timesRerun).toStrictEqual(1);
150+
151+
const [, , resIter1, resIter2] = hookFirstResult;
152+
153+
const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray);
154+
expect(resIter1Values).toStrictEqual([
155+
'a_formatted_1st_time',
156+
'b_formatted_1st_time',
157+
'c_formatted_1st_time',
158+
]);
159+
160+
const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray);
161+
expect(resIter2Values).toStrictEqual([
162+
'd_formatted_1st_time',
163+
'e_formatted_1st_time',
164+
'f_formatted_1st_time',
165+
]);
166+
}
167+
168+
const differentIter1 = asyncIterTickSeparatedOf('a', 'b', 'c');
169+
const differentIter2 = asyncIterTickSeparatedOf('d', 'e', 'f');
170+
171+
renderedHook.rerender({
172+
val1: 'a',
173+
val2: 'b',
174+
iter1: iterateFormatted(differentIter1, v => `${v}_formatted_2nd_time`),
175+
iter2: iterateFormatted(differentIter2, v => `${v}_formatted_2nd_time`),
176+
});
177+
178+
const hookSecondResult = renderedHook.result.current;
179+
180+
{
181+
expect(timesRerun).toStrictEqual(2);
182+
183+
expect(hookFirstResult[0]).toStrictEqual(hookSecondResult[0]);
184+
expect(hookFirstResult[1]).toStrictEqual(hookSecondResult[1]);
185+
expect(hookFirstResult[2]).not.toStrictEqual(hookSecondResult[2]);
186+
expect(hookFirstResult[3]).not.toStrictEqual(hookSecondResult[3]);
187+
188+
const resIter1Values = await pipe(hookSecondResult[2], asyncIterTake(3), asyncIterToArray);
189+
expect(resIter1Values).toStrictEqual([
190+
'a_formatted_2nd_time',
191+
'b_formatted_2nd_time',
192+
'c_formatted_2nd_time',
193+
]);
194+
195+
const resIter2Values = await pipe(hookSecondResult[3], asyncIterTake(3), asyncIterToArray);
196+
expect(resIter2Values).toStrictEqual([
197+
'd_formatted_2nd_time',
198+
'e_formatted_2nd_time',
199+
'f_formatted_2nd_time',
200+
]);
201+
}
202+
});
203+
});
204+
205+
async function feedChannelAcrossTicks<const T>(
206+
channel: IterableChannelTestHelper<T>,
207+
values: T[]
208+
): Promise<void> {
209+
for (const value of values) {
210+
await new Promise(resolve => nextTick(resolve));
211+
channel.put(value);
212+
}
213+
}

‎spec/utils/IterableChannelTestHelper.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export { IterableChannelTestHelper };
2+
3+
class IterableChannelTestHelper<T> implements AsyncIterable<T, void, void> {
4+
#isClosed = false;
5+
#nextIteration = Promise.withResolvers<IteratorResult<T, void>>();
6+
7+
put(value: T): void {
8+
if (!this.#isClosed) {
9+
this.#nextIteration.resolve({ done: false, value });
10+
this.#nextIteration = Promise.withResolvers();
11+
}
12+
}
13+
14+
close(): void {
15+
this.#isClosed = true;
16+
this.#nextIteration.resolve({ done: true, value: undefined });
17+
}
18+
19+
[Symbol.asyncIterator]() {
20+
const whenIteratorClosed = Promise.withResolvers<IteratorReturnResult<undefined>>();
21+
22+
return {
23+
next: (): Promise<IteratorResult<T, void>> => {
24+
return Promise.race([this.#nextIteration.promise, whenIteratorClosed.promise]);
25+
},
26+
27+
return: async (): Promise<IteratorReturnResult<void>> => {
28+
whenIteratorClosed.resolve({ done: true, value: undefined });
29+
return { done: true, value: undefined };
30+
},
31+
};
32+
}
33+
}

‎spec/utils/asyncIterOf.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { asyncIterOf };
2+
3+
function asyncIterOf<const T>(...values: T[]) {
4+
return {
5+
async *[Symbol.asyncIterator]() {
6+
yield* values;
7+
},
8+
};
9+
}

‎spec/utils/asyncIterTickSeparatedOf.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { nextTick } from 'node:process';
2+
3+
export { asyncIterTickSeparatedOf };
4+
5+
function asyncIterTickSeparatedOf<const T>(...values: T[]): {
6+
[Symbol.asyncIterator](): AsyncGenerator<T, void, void>;
7+
} {
8+
return {
9+
async *[Symbol.asyncIterator]() {
10+
await new Promise(resolve => nextTick(resolve));
11+
yield* values;
12+
},
13+
};
14+
}

‎src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useAsyncIter, type IterationResult } from './useAsyncIter/index.js';
22
import { Iterate, type IterateProps } from './Iterate/index.js';
33
import { iterateFormatted, type FixedRefFormattedIterable } from './iterateFormatted/index.js';
44
import { useAsyncIterState, type AsyncIterStateResult } from './useAsyncIterState/index.js';
5+
import { useAsyncIterMemo } from './useAsyncIterMemo/index.js';
56
import { type MaybeAsyncIterable } from './MaybeAsyncIterable/index.js';
67

78
export {
@@ -15,4 +16,5 @@ export {
1516
useAsyncIterState,
1617
type AsyncIterStateResult,
1718
type MaybeAsyncIterable,
19+
useAsyncIterMemo,
1820
};

‎src/useAsyncIterMemo/index.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useMemo } from 'react';
2+
import { type FixedRefFormattedIterable } from '../iterateFormatted/index.js';
3+
import {
4+
reactAsyncIterSpecialInfoSymbol,
5+
type ReactAsyncIterSpecialInfo,
6+
} from '../common/reactAsyncIterSpecialInfoSymbol.js';
7+
import { useLatest } from '../common/hooks/useLatest.js';
8+
import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js';
9+
import { type ExtractAsyncIterValue } from '../common/ExtractAsyncIterValue.js';
10+
11+
export { useAsyncIterMemo };
12+
13+
const useAsyncIterMemo: {
14+
<TRes, const TDeps extends React.DependencyList>(
15+
factory: (...depsWithWrappedAsyncIters: DepsWithReactAsyncItersWrapped<TDeps>) => TRes,
16+
deps: TDeps
17+
): TRes;
18+
19+
<TRes>(factory: () => TRes, deps: []): TRes;
20+
} = <TRes, const TDeps extends React.DependencyList>(
21+
factory: (...depsWithWrappedAsyncIters: DepsWithReactAsyncItersWrapped<TDeps>) => TRes,
22+
deps: TDeps
23+
) => {
24+
const latestDepsRef = useLatest(deps);
25+
26+
const depsWithFormattedItersAccountedFor = latestDepsRef.current.map(dep =>
27+
isReactAsyncIterable(dep) ? dep[reactAsyncIterSpecialInfoSymbol].origSource : dep
28+
);
29+
30+
const result = useMemo(() => {
31+
const depsWithWrappedFormattedIters = latestDepsRef.current.map((dep, i) => {
32+
const specialInfo = isReactAsyncIterable(dep)
33+
? dep[reactAsyncIterSpecialInfoSymbol]
34+
: undefined;
35+
36+
return !specialInfo
37+
? dep
38+
: (() => {
39+
let iterationIdx = 0;
40+
41+
return asyncIterSyncMap(
42+
specialInfo.origSource,
43+
value =>
44+
(latestDepsRef.current[i] as FixedRefFormattedIterable<unknown, unknown>)[
45+
reactAsyncIterSpecialInfoSymbol
46+
].formatFn(value, iterationIdx++) // TODO: Any change there won't be a `.formatFn` here if its possible that this might be called somehow at the moment the deps were changed completely?
47+
);
48+
})();
49+
}) as DepsWithReactAsyncItersWrapped<TDeps>;
50+
51+
return factory(...depsWithWrappedFormattedIters);
52+
}, depsWithFormattedItersAccountedFor);
53+
54+
return result;
55+
};
56+
57+
type DepsWithReactAsyncItersWrapped<TDeps extends React.DependencyList> = {
58+
[I in keyof TDeps]: TDeps[I] extends {
59+
[Symbol.asyncIterator](): AsyncIterator<unknown>;
60+
[reactAsyncIterSpecialInfoSymbol]: ReactAsyncIterSpecialInfo<unknown, unknown>;
61+
}
62+
? AsyncIterable<ExtractAsyncIterValue<TDeps[I]>>
63+
: TDeps[I];
64+
};
65+
66+
function isReactAsyncIterable<T>(
67+
input: T
68+
): input is T & FixedRefFormattedIterable<unknown, unknown> {
69+
const inputAsAny = input as any;
70+
return !!inputAsAny?.[reactAsyncIterSpecialInfoSymbol];
71+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /