From 7967c141ee7f0fcc0abf9647bb43f3d47baee372 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Thu, 13 Mar 2025 13:20:26 +0200 Subject: [PATCH] fix formatted iter index argument not accounting for a current value as if it is the first yield as expected --- spec/tests/Iterate.spec.tsx | 2 +- spec/tests/IterateMulti.spec.tsx | 2 +- spec/tests/useAsyncIter.spec.ts | 70 +++++++++++++------ spec/tests/useAsyncIterMulti.spec.ts | 2 +- src/common/useAsyncItersImperatively/index.ts | 22 +++--- src/useAsyncIter/index.ts | 36 +++++----- 6 files changed, 82 insertions(+), 52 deletions(-) diff --git a/spec/tests/Iterate.spec.tsx b/spec/tests/Iterate.spec.tsx index 6373a6e..38b362e 100644 --- a/spec/tests/Iterate.spec.tsx +++ b/spec/tests/Iterate.spec.tsx @@ -672,7 +672,7 @@ describe('`Iterate` component', () => { } expect(renderFn.mock.calls.flat()).toStrictEqual( - ['a_current', 'a', 'b_current_formatted_0', 'b_formatted_0'].map(value => ({ + ['a_current', 'a', 'b_current_formatted_0', 'b_formatted_1'].map(value => ({ value, pendingFirst: false, done: false, diff --git a/spec/tests/IterateMulti.spec.tsx b/spec/tests/IterateMulti.spec.tsx index 0bc09cd..f75079c 100644 --- a/spec/tests/IterateMulti.spec.tsx +++ b/spec/tests/IterateMulti.spec.tsx @@ -917,7 +917,7 @@ describe('`IterateMulti` hook', () => { { value: 'a', pendingFirst: false, done: false, error: undefined }, ], [ - { value: 'b_formatted_0', pendingFirst: false, done: false, error: undefined }, + { value: 'b_formatted_1', pendingFirst: false, done: false, error: undefined }, { value: 'a', pendingFirst: false, done: false, error: undefined }, ], ]); diff --git a/spec/tests/useAsyncIter.spec.ts b/spec/tests/useAsyncIter.spec.ts index e917454..4fc5934 100644 --- a/spec/tests/useAsyncIter.spec.ts +++ b/spec/tests/useAsyncIter.spec.ts @@ -434,7 +434,7 @@ describe('`useAsyncIter` hook', () => { gray( 'When given an iterable with a `.value.current` property at any point, uses that as the current value and skips the pending stage' ), - () => + () => { ([{ initialValue: undefined }, { initialValue: '_' }] as const).forEach( ({ initialValue }) => { it( @@ -454,28 +454,21 @@ describe('`useAsyncIter` hook', () => { const results: any[] = []; - for (const run of [ + for (const next of [ + () => renderedHook.rerender({ value: channel1 }), + () => channel1.put('a'), () => - act(() => - renderedHook.rerender({ - value: channel1, - }) - ), - () => act(() => channel1.put('a')), - () => - act(() => - renderedHook.rerender({ - value: iterateFormatted(channel2, (val, i) => `${val}_formatted_${i}`), - }) - ), - () => act(() => channel2.put('b')), + renderedHook.rerender({ + value: iterateFormatted(channel2, (val, i) => `${val}_formatted_${i}`), + }), + () => channel2.put('b'), ]) { - await run(); + await act(next); results.push(renderedHook.result.current); } expect(results).toStrictEqual( - ['a_current', 'a', 'b_current_formatted_0', 'b_formatted_0'].map(value => ({ + ['a_current', 'a', 'b_current_formatted_0', 'b_formatted_1'].map(value => ({ value, pendingFirst: false, done: false, @@ -485,7 +478,38 @@ describe('`useAsyncIter` hook', () => { } ); } - ) + ); + + it(gray('with a formatted iterable'), async () => { + let timesRerendered = 0; + const channel = Object.assign(new IteratorChannelTestHelper(), { + value: { current: 'a_current' }, + }); + + const renderedHook = await act(() => + renderHook(() => { + timesRerendered++; + return useAsyncIter(iterateFormatted(channel, (val, i) => `${val}_formatted_${i}`)); + }) + ); + expect(timesRerendered).toStrictEqual(1); + expect(renderedHook.result.current).toStrictEqual({ + value: 'a_current_formatted_0', + pendingFirst: false, + done: false, + error: undefined, + }); + + await act(() => channel.put('a_next')); + expect(timesRerendered).toStrictEqual(2); + expect(renderedHook.result.current).toStrictEqual({ + value: 'a_next_formatted_1', + pendingFirst: false, + done: false, + error: undefined, + }); + }); + } ); it(gray('When unmounted will close the last active iterator it held'), async () => { @@ -627,19 +651,19 @@ describe('`useAsyncIter` hook', () => { const renderedHook = await act(() => renderHook( - ({ formatInto }) => { + ({ formatTo }) => { timesRerendered++; - return useAsyncIter(iterateFormatted(channel, _ => formatInto)); + return useAsyncIter(iterateFormatted(channel, _ => formatTo)); }, { - initialProps: { formatInto: '' as string | null | undefined }, + initialProps: { formatTo: '' as string | null | undefined }, } ) ); await act(() => { channel.put('a'); - renderedHook.rerender({ formatInto: null }); + renderedHook.rerender({ formatTo: null }); }); expect(timesRerendered).toStrictEqual(3); expect(renderedHook.result.current).toStrictEqual({ @@ -651,7 +675,7 @@ describe('`useAsyncIter` hook', () => { await act(() => { channel.put('b'); - renderedHook.rerender({ formatInto: undefined }); + renderedHook.rerender({ formatTo: undefined }); }); expect(timesRerendered).toStrictEqual(5); expect(renderedHook.result.current).toStrictEqual({ diff --git a/spec/tests/useAsyncIterMulti.spec.ts b/spec/tests/useAsyncIterMulti.spec.ts index 73164dd..47abfa7 100644 --- a/spec/tests/useAsyncIterMulti.spec.ts +++ b/spec/tests/useAsyncIterMulti.spec.ts @@ -632,7 +632,7 @@ describe('`useAsyncIterMulti` hook', () => { { value: 'a', pendingFirst: false, done: false, error: undefined }, ], [ - { value: 'b_formatted_0', pendingFirst: false, done: false, error: undefined }, + { value: 'b_formatted_1', pendingFirst: false, done: false, error: undefined }, { value: 'a', pendingFirst: false, done: false, error: undefined }, ], ]); diff --git a/src/common/useAsyncItersImperatively/index.ts b/src/common/useAsyncItersImperatively/index.ts index 0720a53..9085e9e 100644 --- a/src/common/useAsyncItersImperatively/index.ts +++ b/src/common/useAsyncItersImperatively/index.ts @@ -120,22 +120,20 @@ const useAsyncItersImperatively: { return existingIterState.currState; } - const formattedIter: AsyncIterable = (() => { - let iterationIdx = 0; - return asyncIterSyncMap(baseIter, value => iterState.formatFn(value, iterationIdx++)); - })(); - const inputWithMaybeCurrentValue = input as typeof input & { value?: AsyncIterableSubject['value']; }; - let pendingFirst; + let iterationIdx: number; + let pendingFirst: boolean; let startingValue; if (inputWithMaybeCurrentValue.value) { + iterationIdx = 1; // If source has a current value, it should have been the "first iteration" already, so in that case the right up next one here is *the second* already (index of 1) pendingFirst = false; startingValue = inputWithMaybeCurrentValue.value.current; } else { + iterationIdx = 0; pendingFirst = true; startingValue = i < ref.current.currResults.length @@ -147,11 +145,17 @@ const useAsyncItersImperatively: { ); } + const formattedIter: AsyncIterable = asyncIterSyncMap(baseIter, value => + iterState.formatFn(value, iterationIdx++) + ); + const destroyFn = iterateAsyncIterWithCallbacks(formattedIter, startingValue, next => { iterState.currState = { pendingFirst: false, ...next }; - const newPrevResults = ref.current.currResults.slice(0); // Using `.slice(0)` in attempt to copy the array faster than `[...ref.current.currResults]` would - newPrevResults[i] = iterState.currState; - ref.current.currResults = newPrevResults as typeof ref.current.currResults; + ref.current.currResults = (() => { + const newResults = ref.current.currResults.slice(0); // Using `.slice(0)` in attempt to copy the array faster than `[...ref.current.currResults]` would + newResults[i] = iterState.currState; + return newResults as typeof ref.current.currResults; + })(); onYieldCb(ref.current.currResults); }); diff --git a/src/useAsyncIter/index.ts b/src/useAsyncIter/index.ts index 513bbfe..2ab3f24 100644 --- a/src/useAsyncIter/index.ts +++ b/src/useAsyncIter/index.ts @@ -11,6 +11,7 @@ import { } from '../common/ReactAsyncIterable.js'; import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCallbacks.js'; import { callOrReturn } from '../common/callOrReturn.js'; +import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js'; import { type Iterate } from '../Iterate/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars import { type iterateFormatted } from '../iterateFormatted/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars @@ -148,19 +149,19 @@ const useAsyncIter: { const iterSourceRefToUse = latestInputRef.current[reactAsyncIterSpecialInfoSymbol]?.origSource ?? latestInputRef.current; - useMemo((): void => { - const latestInputRefCurrent = latestInputRef.current!; + const latestInputRefCurrent = latestInputRef.current!; - let value; + useMemo((): void => { let pendingFirst; + let value; if (latestInputRefCurrent.value) { - value = latestInputRefCurrent.value.current; pendingFirst = false; + value = latestInputRefCurrent.value.current; } else { const prevSourceLastestVal = stateRef.current.value; - value = prevSourceLastestVal; pendingFirst = true; + value = prevSourceLastestVal; } stateRef.current = { @@ -172,22 +173,23 @@ const useAsyncIter: { }, [iterSourceRefToUse]); useEffect(() => { - let iterationIdx = 0; + const formattedIter = (() => { + let iterationIdx = latestInputRefCurrent.value ? 1 : 0; // If source has a current value, it should have been the "first iteration" already, so in that case the right up next one here is *the second* already (index of 1) - return iterateAsyncIterWithCallbacks(iterSourceRefToUse, stateRef.current.value, next => { - const possibleGivenFormatFn = - latestInputRef.current?.[reactAsyncIterSpecialInfoSymbol]?.formatFn; + return asyncIterSyncMap(iterSourceRefToUse, value => { + const possibleGivenFormatFn = + latestInputRef.current?.[reactAsyncIterSpecialInfoSymbol]?.formatFn; - const formattedValue = possibleGivenFormatFn - ? possibleGivenFormatFn(next.value, iterationIdx++) - : next.value; + const formattedValue = possibleGivenFormatFn + ? possibleGivenFormatFn(value, iterationIdx++) + : value; - stateRef.current = { - ...next, - pendingFirst: false, - value: formattedValue, - }; + return formattedValue; + }); + })(); + return iterateAsyncIterWithCallbacks(formattedIter, stateRef.current.value, next => { + stateRef.current = { ...next, pendingFirst: false }; rerender(); }); }, [iterSourceRefToUse]);