From fc7ef0658680d5fd2c2a02909aec3f5f4d1968d8 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Thu, 2 Jan 2025 16:25:15 +0200 Subject: [PATCH 1/8] implmentations and tests for a new `useAsyncIterMemo` hook --- spec/tests/useAsyncIterMemo.spec.ts | 213 ++++++++++++++++++++++++ spec/utils/IterableChannelTestHelper.ts | 33 ++++ spec/utils/asyncIterOf.ts | 9 + spec/utils/asyncIterTickSeparatedOf.ts | 14 ++ src/index.ts | 2 + src/useAsyncIterMemo/index.ts | 71 ++++++++ 6 files changed, 342 insertions(+) create mode 100644 spec/tests/useAsyncIterMemo.spec.ts create mode 100644 spec/utils/IterableChannelTestHelper.ts create mode 100644 spec/utils/asyncIterOf.ts create mode 100644 spec/utils/asyncIterTickSeparatedOf.ts create mode 100644 src/useAsyncIterMemo/index.ts diff --git a/spec/tests/useAsyncIterMemo.spec.ts b/spec/tests/useAsyncIterMemo.spec.ts new file mode 100644 index 0000000..98058e2 --- /dev/null +++ b/spec/tests/useAsyncIterMemo.spec.ts @@ -0,0 +1,213 @@ +import { nextTick } from 'node:process'; +import { it, describe, expect, afterEach } from 'vitest'; +import { gray } from 'colorette'; +import { renderHook, cleanup as cleanupMountedReactTrees } from '@testing-library/react'; +import { useAsyncIterMemo, iterateFormatted } from '../../src/index.js'; +import { pipe } from '../utils/pipe.js'; +import { asyncIterToArray } from '../utils/asyncIterToArray.js'; +import { asyncIterTake } from '../utils/asyncIterTake.js'; +import { asyncIterOf } from '../utils/asyncIterOf.js'; +import { asyncIterTickSeparatedOf } from '../utils/asyncIterTickSeparatedOf.js'; +import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js'; + +afterEach(() => { + cleanupMountedReactTrees(); +}); + +describe('`useAsyncIterMemo` hook', () => { + it(gray('___ ___ ___ 1'), async () => { + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo((...deps) => deps, [val1, val2, iter1, iter2]), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: asyncIterOf('a', 'b', 'c'), + iter2: asyncIterOf('d', 'e', 'f'), + }, + } + ); + + const [resVal1, resVal2, resIter1, resIter2] = renderedHook.result.current; + + expect(resVal1).toStrictEqual('a'); + expect(resVal2).toStrictEqual('b'); + expect(await asyncIterToArray(resIter1)).toStrictEqual(['a', 'b', 'c']); + expect(await asyncIterToArray(resIter2)).toStrictEqual(['d', 'e', 'f']); + }); + + it(gray('___ ___ ___ 2'), async () => { + const channel1 = new IterableChannelTestHelper(); + const channel2 = new IterableChannelTestHelper(); + let timesRerun = 0; + + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo( + (...deps) => { + timesRerun++; + return deps; + }, + [val1, val2, iter1, iter2] + ), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: iterateFormatted(channel1, v => `${v}_formatted_1st_time`), + iter2: iterateFormatted(channel2, v => `${v}_formatted_1st_time`), + }, + } + ); + + const hookFirstResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(1); + + const [, , resIter1, resIter2] = hookFirstResult; + + feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_1st_time', + 'b_formatted_1st_time', + 'c_formatted_1st_time', + ]); + + feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + + expect(resIter2Values).toStrictEqual([ + 'd_formatted_1st_time', + 'e_formatted_1st_time', + 'f_formatted_1st_time', + ]); + } + + renderedHook.rerender({ + val1: 'a', + val2: 'b', + iter1: iterateFormatted(channel1, v => `${v}_formatted_2nd_time`), + iter2: iterateFormatted(channel2, v => `${v}_formatted_2nd_time`), + }); + + const hookSecondResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(1); + expect(hookFirstResult).toStrictEqual(hookSecondResult); + + const [, , resIter1, resIter2] = hookSecondResult; + + feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_2nd_time', + 'b_formatted_2nd_time', + 'c_formatted_2nd_time', + ]); + + feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_2nd_time', + 'e_formatted_2nd_time', + 'f_formatted_2nd_time', + ]); + } + }); + + it(gray('___ ___ ___ 3'), async () => { + const iter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); + const iter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); + let timesRerun = 0; + + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo( + (...deps) => { + timesRerun++; + return deps; + }, + [val1, val2, iter1, iter2] + ), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: iterateFormatted(iter1, v => `${v}_formatted_1st_time`), + iter2: iterateFormatted(iter2, v => `${v}_formatted_1st_time`), + }, + } + ); + + const hookFirstResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(1); + + const [, , resIter1, resIter2] = hookFirstResult; + + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_1st_time', + 'b_formatted_1st_time', + 'c_formatted_1st_time', + ]); + + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_1st_time', + 'e_formatted_1st_time', + 'f_formatted_1st_time', + ]); + } + + const differentIter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); + const differentIter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); + + renderedHook.rerender({ + val1: 'a', + val2: 'b', + iter1: iterateFormatted(differentIter1, v => `${v}_formatted_2nd_time`), + iter2: iterateFormatted(differentIter2, v => `${v}_formatted_2nd_time`), + }); + + const hookSecondResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(2); + + expect(hookFirstResult[0]).toStrictEqual(hookSecondResult[0]); + expect(hookFirstResult[1]).toStrictEqual(hookSecondResult[1]); + expect(hookFirstResult[2]).not.toStrictEqual(hookSecondResult[2]); + expect(hookFirstResult[3]).not.toStrictEqual(hookSecondResult[3]); + + const resIter1Values = await pipe(hookSecondResult[2], asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_2nd_time', + 'b_formatted_2nd_time', + 'c_formatted_2nd_time', + ]); + + const resIter2Values = await pipe(hookSecondResult[3], asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_2nd_time', + 'e_formatted_2nd_time', + 'f_formatted_2nd_time', + ]); + } + }); +}); + +async function feedChannelAcrossTicks( + channel: IterableChannelTestHelper, + values: T[] +): Promise { + for (const value of values) { + await new Promise(resolve => nextTick(resolve)); + channel.put(value); + } +} diff --git a/spec/utils/IterableChannelTestHelper.ts b/spec/utils/IterableChannelTestHelper.ts new file mode 100644 index 0000000..9186c44 --- /dev/null +++ b/spec/utils/IterableChannelTestHelper.ts @@ -0,0 +1,33 @@ +export { IterableChannelTestHelper }; + +class IterableChannelTestHelper implements AsyncIterable { + #isClosed = false; + #nextIteration = Promise.withResolvers>(); + + put(value: T): void { + if (!this.#isClosed) { + this.#nextIteration.resolve({ done: false, value }); + this.#nextIteration = Promise.withResolvers(); + } + } + + close(): void { + this.#isClosed = true; + this.#nextIteration.resolve({ done: true, value: undefined }); + } + + [Symbol.asyncIterator]() { + const whenIteratorClosed = Promise.withResolvers>(); + + return { + next: (): Promise> => { + return Promise.race([this.#nextIteration.promise, whenIteratorClosed.promise]); + }, + + return: async (): Promise> => { + whenIteratorClosed.resolve({ done: true, value: undefined }); + return { done: true, value: undefined }; + }, + }; + } +} diff --git a/spec/utils/asyncIterOf.ts b/spec/utils/asyncIterOf.ts new file mode 100644 index 0000000..c1a733f --- /dev/null +++ b/spec/utils/asyncIterOf.ts @@ -0,0 +1,9 @@ +export { asyncIterOf }; + +function asyncIterOf(...values: T[]) { + return { + async *[Symbol.asyncIterator]() { + yield* values; + }, + }; +} diff --git a/spec/utils/asyncIterTickSeparatedOf.ts b/spec/utils/asyncIterTickSeparatedOf.ts new file mode 100644 index 0000000..1f07ada --- /dev/null +++ b/spec/utils/asyncIterTickSeparatedOf.ts @@ -0,0 +1,14 @@ +import { nextTick } from 'node:process'; + +export { asyncIterTickSeparatedOf }; + +function asyncIterTickSeparatedOf(...values: T[]): { + [Symbol.asyncIterator](): AsyncGenerator; +} { + return { + async *[Symbol.asyncIterator]() { + await new Promise(resolve => nextTick(resolve)); + yield* values; + }, + }; +} diff --git a/src/index.ts b/src/index.ts index e608a5d..74cfbd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { useAsyncIter, type IterationResult } from './useAsyncIter/index.js'; import { Iterate, type IterateProps } from './Iterate/index.js'; import { iterateFormatted, type FixedRefFormattedIterable } from './iterateFormatted/index.js'; import { useAsyncIterState, type AsyncIterStateResult } from './useAsyncIterState/index.js'; +import { useAsyncIterMemo } from './useAsyncIterMemo/index.js'; import { type MaybeAsyncIterable } from './MaybeAsyncIterable/index.js'; export { @@ -15,4 +16,5 @@ export { useAsyncIterState, type AsyncIterStateResult, type MaybeAsyncIterable, + useAsyncIterMemo, }; diff --git a/src/useAsyncIterMemo/index.ts b/src/useAsyncIterMemo/index.ts new file mode 100644 index 0000000..cfb2c59 --- /dev/null +++ b/src/useAsyncIterMemo/index.ts @@ -0,0 +1,71 @@ +import { useMemo } from 'react'; +import { type FixedRefFormattedIterable } from '../iterateFormatted/index.js'; +import { + reactAsyncIterSpecialInfoSymbol, + type ReactAsyncIterSpecialInfo, +} from '../common/reactAsyncIterSpecialInfoSymbol.js'; +import { useLatest } from '../common/hooks/useLatest.js'; +import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js'; +import { type ExtractAsyncIterValue } from '../common/ExtractAsyncIterValue.js'; + +export { useAsyncIterMemo }; + +const useAsyncIterMemo: { + ( + factory: (...depsWithWrappedAsyncIters: DepsWithReactAsyncItersWrapped) => TRes, + deps: TDeps + ): TRes; + + (factory: () => TRes, deps: []): TRes; +} = ( + factory: (...depsWithWrappedAsyncIters: DepsWithReactAsyncItersWrapped) => TRes, + deps: TDeps +) => { + const latestDepsRef = useLatest(deps); + + const depsWithFormattedItersAccountedFor = latestDepsRef.current.map(dep => + isReactAsyncIterable(dep) ? dep[reactAsyncIterSpecialInfoSymbol].origSource : dep + ); + + const result = useMemo(() => { + const depsWithWrappedFormattedIters = latestDepsRef.current.map((dep, i) => { + const specialInfo = isReactAsyncIterable(dep) + ? dep[reactAsyncIterSpecialInfoSymbol] + : undefined; + + return !specialInfo + ? dep + : (() => { + let iterationIdx = 0; + + return asyncIterSyncMap( + specialInfo.origSource, + value => + (latestDepsRef.current[i] as FixedRefFormattedIterable)[ + reactAsyncIterSpecialInfoSymbol + ].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? + ); + })(); + }) as DepsWithReactAsyncItersWrapped; + + return factory(...depsWithWrappedFormattedIters); + }, depsWithFormattedItersAccountedFor); + + return result; +}; + +type DepsWithReactAsyncItersWrapped = { + [I in keyof TDeps]: TDeps[I] extends { + [Symbol.asyncIterator](): AsyncIterator; + [reactAsyncIterSpecialInfoSymbol]: ReactAsyncIterSpecialInfo; + } + ? AsyncIterable> + : TDeps[I]; +}; + +function isReactAsyncIterable( + input: T +): input is T & FixedRefFormattedIterable { + const inputAsAny = input as any; + return !!inputAsAny?.[reactAsyncIterSpecialInfoSymbol]; +} From 61b4ab9c468435aebb497b830b04f6e8b4f45010 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Thu, 2 Jan 2025 16:25:15 +0200 Subject: [PATCH 2/8] implementations and tests for a new `useAsyncIterMemo` hook --- spec/tests/useAsyncIterMemo.spec.ts | 213 ++++++++++++++++++++++++ spec/utils/IterableChannelTestHelper.ts | 33 ++++ spec/utils/asyncIterOf.ts | 9 + spec/utils/asyncIterTickSeparatedOf.ts | 14 ++ src/index.ts | 2 + src/useAsyncIterMemo/index.ts | 71 ++++++++ 6 files changed, 342 insertions(+) create mode 100644 spec/tests/useAsyncIterMemo.spec.ts create mode 100644 spec/utils/IterableChannelTestHelper.ts create mode 100644 spec/utils/asyncIterOf.ts create mode 100644 spec/utils/asyncIterTickSeparatedOf.ts create mode 100644 src/useAsyncIterMemo/index.ts diff --git a/spec/tests/useAsyncIterMemo.spec.ts b/spec/tests/useAsyncIterMemo.spec.ts new file mode 100644 index 0000000..98058e2 --- /dev/null +++ b/spec/tests/useAsyncIterMemo.spec.ts @@ -0,0 +1,213 @@ +import { nextTick } from 'node:process'; +import { it, describe, expect, afterEach } from 'vitest'; +import { gray } from 'colorette'; +import { renderHook, cleanup as cleanupMountedReactTrees } from '@testing-library/react'; +import { useAsyncIterMemo, iterateFormatted } from '../../src/index.js'; +import { pipe } from '../utils/pipe.js'; +import { asyncIterToArray } from '../utils/asyncIterToArray.js'; +import { asyncIterTake } from '../utils/asyncIterTake.js'; +import { asyncIterOf } from '../utils/asyncIterOf.js'; +import { asyncIterTickSeparatedOf } from '../utils/asyncIterTickSeparatedOf.js'; +import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js'; + +afterEach(() => { + cleanupMountedReactTrees(); +}); + +describe('`useAsyncIterMemo` hook', () => { + it(gray('___ ___ ___ 1'), async () => { + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo((...deps) => deps, [val1, val2, iter1, iter2]), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: asyncIterOf('a', 'b', 'c'), + iter2: asyncIterOf('d', 'e', 'f'), + }, + } + ); + + const [resVal1, resVal2, resIter1, resIter2] = renderedHook.result.current; + + expect(resVal1).toStrictEqual('a'); + expect(resVal2).toStrictEqual('b'); + expect(await asyncIterToArray(resIter1)).toStrictEqual(['a', 'b', 'c']); + expect(await asyncIterToArray(resIter2)).toStrictEqual(['d', 'e', 'f']); + }); + + it(gray('___ ___ ___ 2'), async () => { + const channel1 = new IterableChannelTestHelper(); + const channel2 = new IterableChannelTestHelper(); + let timesRerun = 0; + + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo( + (...deps) => { + timesRerun++; + return deps; + }, + [val1, val2, iter1, iter2] + ), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: iterateFormatted(channel1, v => `${v}_formatted_1st_time`), + iter2: iterateFormatted(channel2, v => `${v}_formatted_1st_time`), + }, + } + ); + + const hookFirstResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(1); + + const [, , resIter1, resIter2] = hookFirstResult; + + feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_1st_time', + 'b_formatted_1st_time', + 'c_formatted_1st_time', + ]); + + feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + + expect(resIter2Values).toStrictEqual([ + 'd_formatted_1st_time', + 'e_formatted_1st_time', + 'f_formatted_1st_time', + ]); + } + + renderedHook.rerender({ + val1: 'a', + val2: 'b', + iter1: iterateFormatted(channel1, v => `${v}_formatted_2nd_time`), + iter2: iterateFormatted(channel2, v => `${v}_formatted_2nd_time`), + }); + + const hookSecondResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(1); + expect(hookFirstResult).toStrictEqual(hookSecondResult); + + const [, , resIter1, resIter2] = hookSecondResult; + + feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_2nd_time', + 'b_formatted_2nd_time', + 'c_formatted_2nd_time', + ]); + + feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_2nd_time', + 'e_formatted_2nd_time', + 'f_formatted_2nd_time', + ]); + } + }); + + it(gray('___ ___ ___ 3'), async () => { + const iter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); + const iter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); + let timesRerun = 0; + + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo( + (...deps) => { + timesRerun++; + return deps; + }, + [val1, val2, iter1, iter2] + ), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: iterateFormatted(iter1, v => `${v}_formatted_1st_time`), + iter2: iterateFormatted(iter2, v => `${v}_formatted_1st_time`), + }, + } + ); + + const hookFirstResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(1); + + const [, , resIter1, resIter2] = hookFirstResult; + + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_1st_time', + 'b_formatted_1st_time', + 'c_formatted_1st_time', + ]); + + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_1st_time', + 'e_formatted_1st_time', + 'f_formatted_1st_time', + ]); + } + + const differentIter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); + const differentIter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); + + renderedHook.rerender({ + val1: 'a', + val2: 'b', + iter1: iterateFormatted(differentIter1, v => `${v}_formatted_2nd_time`), + iter2: iterateFormatted(differentIter2, v => `${v}_formatted_2nd_time`), + }); + + const hookSecondResult = renderedHook.result.current; + + { + expect(timesRerun).toStrictEqual(2); + + expect(hookFirstResult[0]).toStrictEqual(hookSecondResult[0]); + expect(hookFirstResult[1]).toStrictEqual(hookSecondResult[1]); + expect(hookFirstResult[2]).not.toStrictEqual(hookSecondResult[2]); + expect(hookFirstResult[3]).not.toStrictEqual(hookSecondResult[3]); + + const resIter1Values = await pipe(hookSecondResult[2], asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_2nd_time', + 'b_formatted_2nd_time', + 'c_formatted_2nd_time', + ]); + + const resIter2Values = await pipe(hookSecondResult[3], asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_2nd_time', + 'e_formatted_2nd_time', + 'f_formatted_2nd_time', + ]); + } + }); +}); + +async function feedChannelAcrossTicks( + channel: IterableChannelTestHelper, + values: T[] +): Promise { + for (const value of values) { + await new Promise(resolve => nextTick(resolve)); + channel.put(value); + } +} diff --git a/spec/utils/IterableChannelTestHelper.ts b/spec/utils/IterableChannelTestHelper.ts new file mode 100644 index 0000000..9186c44 --- /dev/null +++ b/spec/utils/IterableChannelTestHelper.ts @@ -0,0 +1,33 @@ +export { IterableChannelTestHelper }; + +class IterableChannelTestHelper implements AsyncIterable { + #isClosed = false; + #nextIteration = Promise.withResolvers>(); + + put(value: T): void { + if (!this.#isClosed) { + this.#nextIteration.resolve({ done: false, value }); + this.#nextIteration = Promise.withResolvers(); + } + } + + close(): void { + this.#isClosed = true; + this.#nextIteration.resolve({ done: true, value: undefined }); + } + + [Symbol.asyncIterator]() { + const whenIteratorClosed = Promise.withResolvers>(); + + return { + next: (): Promise> => { + return Promise.race([this.#nextIteration.promise, whenIteratorClosed.promise]); + }, + + return: async (): Promise> => { + whenIteratorClosed.resolve({ done: true, value: undefined }); + return { done: true, value: undefined }; + }, + }; + } +} diff --git a/spec/utils/asyncIterOf.ts b/spec/utils/asyncIterOf.ts new file mode 100644 index 0000000..c1a733f --- /dev/null +++ b/spec/utils/asyncIterOf.ts @@ -0,0 +1,9 @@ +export { asyncIterOf }; + +function asyncIterOf(...values: T[]) { + return { + async *[Symbol.asyncIterator]() { + yield* values; + }, + }; +} diff --git a/spec/utils/asyncIterTickSeparatedOf.ts b/spec/utils/asyncIterTickSeparatedOf.ts new file mode 100644 index 0000000..1f07ada --- /dev/null +++ b/spec/utils/asyncIterTickSeparatedOf.ts @@ -0,0 +1,14 @@ +import { nextTick } from 'node:process'; + +export { asyncIterTickSeparatedOf }; + +function asyncIterTickSeparatedOf(...values: T[]): { + [Symbol.asyncIterator](): AsyncGenerator; +} { + return { + async *[Symbol.asyncIterator]() { + await new Promise(resolve => nextTick(resolve)); + yield* values; + }, + }; +} diff --git a/src/index.ts b/src/index.ts index e608a5d..74cfbd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { useAsyncIter, type IterationResult } from './useAsyncIter/index.js'; import { Iterate, type IterateProps } from './Iterate/index.js'; import { iterateFormatted, type FixedRefFormattedIterable } from './iterateFormatted/index.js'; import { useAsyncIterState, type AsyncIterStateResult } from './useAsyncIterState/index.js'; +import { useAsyncIterMemo } from './useAsyncIterMemo/index.js'; import { type MaybeAsyncIterable } from './MaybeAsyncIterable/index.js'; export { @@ -15,4 +16,5 @@ export { useAsyncIterState, type AsyncIterStateResult, type MaybeAsyncIterable, + useAsyncIterMemo, }; diff --git a/src/useAsyncIterMemo/index.ts b/src/useAsyncIterMemo/index.ts new file mode 100644 index 0000000..cfb2c59 --- /dev/null +++ b/src/useAsyncIterMemo/index.ts @@ -0,0 +1,71 @@ +import { useMemo } from 'react'; +import { type FixedRefFormattedIterable } from '../iterateFormatted/index.js'; +import { + reactAsyncIterSpecialInfoSymbol, + type ReactAsyncIterSpecialInfo, +} from '../common/reactAsyncIterSpecialInfoSymbol.js'; +import { useLatest } from '../common/hooks/useLatest.js'; +import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js'; +import { type ExtractAsyncIterValue } from '../common/ExtractAsyncIterValue.js'; + +export { useAsyncIterMemo }; + +const useAsyncIterMemo: { + ( + factory: (...depsWithWrappedAsyncIters: DepsWithReactAsyncItersWrapped) => TRes, + deps: TDeps + ): TRes; + + (factory: () => TRes, deps: []): TRes; +} = ( + factory: (...depsWithWrappedAsyncIters: DepsWithReactAsyncItersWrapped) => TRes, + deps: TDeps +) => { + const latestDepsRef = useLatest(deps); + + const depsWithFormattedItersAccountedFor = latestDepsRef.current.map(dep => + isReactAsyncIterable(dep) ? dep[reactAsyncIterSpecialInfoSymbol].origSource : dep + ); + + const result = useMemo(() => { + const depsWithWrappedFormattedIters = latestDepsRef.current.map((dep, i) => { + const specialInfo = isReactAsyncIterable(dep) + ? dep[reactAsyncIterSpecialInfoSymbol] + : undefined; + + return !specialInfo + ? dep + : (() => { + let iterationIdx = 0; + + return asyncIterSyncMap( + specialInfo.origSource, + value => + (latestDepsRef.current[i] as FixedRefFormattedIterable)[ + reactAsyncIterSpecialInfoSymbol + ].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? + ); + })(); + }) as DepsWithReactAsyncItersWrapped; + + return factory(...depsWithWrappedFormattedIters); + }, depsWithFormattedItersAccountedFor); + + return result; +}; + +type DepsWithReactAsyncItersWrapped = { + [I in keyof TDeps]: TDeps[I] extends { + [Symbol.asyncIterator](): AsyncIterator; + [reactAsyncIterSpecialInfoSymbol]: ReactAsyncIterSpecialInfo; + } + ? AsyncIterable> + : TDeps[I]; +}; + +function isReactAsyncIterable( + input: T +): input is T & FixedRefFormattedIterable { + const inputAsAny = input as any; + return !!inputAsAny?.[reactAsyncIterSpecialInfoSymbol]; +} From f86a313ef0212b2bc59c591694d0d187e196129a Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Fri, 31 Jan 2025 19:39:46 +0200 Subject: [PATCH 3/8] more done --- src/useAsyncIterMemo/index.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/useAsyncIterMemo/index.ts b/src/useAsyncIterMemo/index.ts index cfb2c59..0efd5e9 100644 --- a/src/useAsyncIterMemo/index.ts +++ b/src/useAsyncIterMemo/index.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react'; -import { type FixedRefFormattedIterable } from '../iterateFormatted/index.js'; import { reactAsyncIterSpecialInfoSymbol, + type ReactAsyncIterable, type ReactAsyncIterSpecialInfo, -} from '../common/reactAsyncIterSpecialInfoSymbol.js'; +} from '../common/ReactAsyncIterable.js'; import { useLatest } from '../common/hooks/useLatest.js'; import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js'; -import { type ExtractAsyncIterValue } from '../common/ExtractAsyncIterValue.js'; +import { type DeasyncIterized } from '../common/DeasyncIterized.js'; export { useAsyncIterMemo }; @@ -41,7 +41,7 @@ const useAsyncIterMemo: { return asyncIterSyncMap( specialInfo.origSource, value => - (latestDepsRef.current[i] as FixedRefFormattedIterable)[ + (latestDepsRef.current[i] as ReactAsyncIterable)[ reactAsyncIterSpecialInfoSymbol ].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? ); @@ -59,13 +59,11 @@ type DepsWithReactAsyncItersWrapped = { [Symbol.asyncIterator](): AsyncIterator; [reactAsyncIterSpecialInfoSymbol]: ReactAsyncIterSpecialInfo; } - ? AsyncIterable> + ? AsyncIterable> : TDeps[I]; }; -function isReactAsyncIterable( - input: T -): input is T & FixedRefFormattedIterable { +function isReactAsyncIterable(input: T): input is T & ReactAsyncIterable { const inputAsAny = input as any; return !!inputAsAny?.[reactAsyncIterSpecialInfoSymbol]; } From a9530a810e0462d8df29c8ad167e30804d0aef83 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Sat, 1 Feb 2025 14:09:10 +0200 Subject: [PATCH 4/8] fix test --- spec/tests/useAsyncIterMemo.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tests/useAsyncIterMemo.spec.ts b/spec/tests/useAsyncIterMemo.spec.ts index 98058e2..4579efe 100644 --- a/spec/tests/useAsyncIterMemo.spec.ts +++ b/spec/tests/useAsyncIterMemo.spec.ts @@ -2,7 +2,7 @@ import { nextTick } from 'node:process'; import { it, describe, expect, afterEach } from 'vitest'; import { gray } from 'colorette'; import { renderHook, cleanup as cleanupMountedReactTrees } from '@testing-library/react'; -import { useAsyncIterMemo, iterateFormatted } from '../../src/index.js'; +import { useAsyncIterMemo, iterateFormatted } from '../libEntrypoint.js'; import { pipe } from '../utils/pipe.js'; import { asyncIterToArray } from '../utils/asyncIterToArray.js'; import { asyncIterTake } from '../utils/asyncIterTake.js'; From ac6a21897d21cabe1828f8abf9c8ed2fcba07c57 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Sat, 1 Feb 2025 23:36:57 +0200 Subject: [PATCH 5/8] up --- spec/tests/useAsyncIterMemo.spec.ts | 319 +++++++++++++-------------- spec/utils/feedChannelAcrossTicks.ts | 14 ++ src/useAsyncIterMemo/index.ts | 2 +- 3 files changed, 174 insertions(+), 161 deletions(-) create mode 100644 spec/utils/feedChannelAcrossTicks.ts diff --git a/spec/tests/useAsyncIterMemo.spec.ts b/spec/tests/useAsyncIterMemo.spec.ts index 4579efe..36c5892 100644 --- a/spec/tests/useAsyncIterMemo.spec.ts +++ b/spec/tests/useAsyncIterMemo.spec.ts @@ -1,21 +1,21 @@ -import { nextTick } from 'node:process'; import { it, describe, expect, afterEach } from 'vitest'; import { gray } from 'colorette'; import { renderHook, cleanup as cleanupMountedReactTrees } from '@testing-library/react'; import { useAsyncIterMemo, iterateFormatted } from '../libEntrypoint.js'; import { pipe } from '../utils/pipe.js'; +import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js'; +import { feedChannelAcrossTicks } from '../utils/feedChannelAcrossTicks.js'; import { asyncIterToArray } from '../utils/asyncIterToArray.js'; import { asyncIterTake } from '../utils/asyncIterTake.js'; import { asyncIterOf } from '../utils/asyncIterOf.js'; import { asyncIterTickSeparatedOf } from '../utils/asyncIterTickSeparatedOf.js'; -import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js'; afterEach(() => { cleanupMountedReactTrees(); }); describe('`useAsyncIterMemo` hook', () => { - it(gray('___ ___ ___ 1'), async () => { + it(gray('When given mixed iterable and plain values, will work correctly'), async () => { const renderedHook = renderHook( ({ val1, val2, iter1, iter2 }) => useAsyncIterMemo((...deps) => deps, [val1, val2, iter1, iter2]), @@ -37,177 +37,176 @@ describe('`useAsyncIterMemo` hook', () => { expect(await asyncIterToArray(resIter2)).toStrictEqual(['d', 'e', 'f']); }); - it(gray('___ ___ ___ 2'), async () => { - const channel1 = new IterableChannelTestHelper(); - const channel2 = new IterableChannelTestHelper(); - let timesRerun = 0; - - const renderedHook = renderHook( - ({ val1, val2, iter1, iter2 }) => - useAsyncIterMemo( - (...deps) => { - timesRerun++; - return deps; + it( + gray( + 'When updated consecutively with formatted iterables of the same source iterables each time, will work correctly and not re-run factory function' + ), + async () => { + const channel1 = new IterableChannelTestHelper(); + const channel2 = new IterableChannelTestHelper(); + let timesRerun = 0; + + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo( + (...deps) => { + timesRerun++; + return deps; + }, + [val1, val2, iter1, iter2] + ), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: iterateFormatted(channel1, v => `${v}_formatted_1st_time`), + iter2: iterateFormatted(channel2, v => `${v}_formatted_1st_time`), }, - [val1, val2, iter1, iter2] - ), - { - initialProps: { - val1: 'a', - val2: 'b', - iter1: iterateFormatted(channel1, v => `${v}_formatted_1st_time`), - iter2: iterateFormatted(channel2, v => `${v}_formatted_1st_time`), - }, - } - ); - - const hookFirstResult = renderedHook.result.current; - - { - expect(timesRerun).toStrictEqual(1); + } + ); - const [, , resIter1, resIter2] = hookFirstResult; + const hookFirstResult = renderedHook.result.current; - feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); - const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_1st_time', - 'b_formatted_1st_time', - 'c_formatted_1st_time', - ]); + { + expect(timesRerun).toStrictEqual(1); + + const [, , resIter1, resIter2] = hookFirstResult; + + feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_1st_time', + 'b_formatted_1st_time', + 'c_formatted_1st_time', + ]); + + feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_1st_time', + 'e_formatted_1st_time', + 'f_formatted_1st_time', + ]); + } - feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); - const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + renderedHook.rerender({ + val1: 'a', + val2: 'b', + iter1: iterateFormatted(channel1, v => `${v}_formatted_2nd_time`), + iter2: iterateFormatted(channel2, v => `${v}_formatted_2nd_time`), + }); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_1st_time', - 'e_formatted_1st_time', - 'f_formatted_1st_time', - ]); - } + const hookSecondResult = renderedHook.result.current; - renderedHook.rerender({ - val1: 'a', - val2: 'b', - iter1: iterateFormatted(channel1, v => `${v}_formatted_2nd_time`), - iter2: iterateFormatted(channel2, v => `${v}_formatted_2nd_time`), - }); - - const hookSecondResult = renderedHook.result.current; - - { - expect(timesRerun).toStrictEqual(1); - expect(hookFirstResult).toStrictEqual(hookSecondResult); - - const [, , resIter1, resIter2] = hookSecondResult; - - feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); - const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_2nd_time', - 'b_formatted_2nd_time', - 'c_formatted_2nd_time', - ]); - - feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); - const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_2nd_time', - 'e_formatted_2nd_time', - 'f_formatted_2nd_time', - ]); + { + expect(timesRerun).toStrictEqual(1); + expect(hookFirstResult).toStrictEqual(hookSecondResult); + + const [, , resIter1, resIter2] = hookSecondResult; + + feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_2nd_time', + 'b_formatted_2nd_time', + 'c_formatted_2nd_time', + ]); + + feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_2nd_time', + 'e_formatted_2nd_time', + 'f_formatted_2nd_time', + ]); + } } - }); + ); + + it( + gray( + 'When updated consecutively with formatted iterables of different source iterables each time, will work correctly and re-run factory function' + ), + async () => { + const iter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); + const iter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); + let timesRerun = 0; + + const renderedHook = renderHook( + ({ val1, val2, iter1, iter2 }) => + useAsyncIterMemo( + (...deps) => { + timesRerun++; + return deps; + }, + [val1, val2, iter1, iter2] + ), + { + initialProps: { + val1: 'a', + val2: 'b', + iter1: iterateFormatted(iter1, v => `${v}_formatted_1st_time`), + iter2: iterateFormatted(iter2, v => `${v}_formatted_1st_time`), + }, + } + ); - it(gray('___ ___ ___ 3'), async () => { - const iter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); - const iter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); - let timesRerun = 0; + const hookFirstResult = renderedHook.result.current; - const renderedHook = renderHook( - ({ val1, val2, iter1, iter2 }) => - useAsyncIterMemo( - (...deps) => { - timesRerun++; - return deps; - }, - [val1, val2, iter1, iter2] - ), { - initialProps: { - val1: 'a', - val2: 'b', - iter1: iterateFormatted(iter1, v => `${v}_formatted_1st_time`), - iter2: iterateFormatted(iter2, v => `${v}_formatted_1st_time`), - }, + expect(timesRerun).toStrictEqual(1); + + const [, , resIter1, resIter2] = hookFirstResult; + + const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_1st_time', + 'b_formatted_1st_time', + 'c_formatted_1st_time', + ]); + + const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_1st_time', + 'e_formatted_1st_time', + 'f_formatted_1st_time', + ]); } - ); - - const hookFirstResult = renderedHook.result.current; - { - expect(timesRerun).toStrictEqual(1); + const differentIter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); + const differentIter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); - const [, , resIter1, resIter2] = hookFirstResult; + renderedHook.rerender({ + val1: 'a', + val2: 'b', + iter1: iterateFormatted(differentIter1, v => `${v}_formatted_2nd_time`), + iter2: iterateFormatted(differentIter2, v => `${v}_formatted_2nd_time`), + }); - const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_1st_time', - 'b_formatted_1st_time', - 'c_formatted_1st_time', - ]); - - const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_1st_time', - 'e_formatted_1st_time', - 'f_formatted_1st_time', - ]); - } + const hookSecondResult = renderedHook.result.current; - const differentIter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); - const differentIter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); - - renderedHook.rerender({ - val1: 'a', - val2: 'b', - iter1: iterateFormatted(differentIter1, v => `${v}_formatted_2nd_time`), - iter2: iterateFormatted(differentIter2, v => `${v}_formatted_2nd_time`), - }); - - const hookSecondResult = renderedHook.result.current; - - { - expect(timesRerun).toStrictEqual(2); - - expect(hookFirstResult[0]).toStrictEqual(hookSecondResult[0]); - expect(hookFirstResult[1]).toStrictEqual(hookSecondResult[1]); - expect(hookFirstResult[2]).not.toStrictEqual(hookSecondResult[2]); - expect(hookFirstResult[3]).not.toStrictEqual(hookSecondResult[3]); - - const resIter1Values = await pipe(hookSecondResult[2], asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_2nd_time', - 'b_formatted_2nd_time', - 'c_formatted_2nd_time', - ]); - - const resIter2Values = await pipe(hookSecondResult[3], asyncIterTake(3), asyncIterToArray); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_2nd_time', - 'e_formatted_2nd_time', - 'f_formatted_2nd_time', - ]); + { + expect(timesRerun).toStrictEqual(2); + + expect(hookFirstResult[0]).toStrictEqual(hookSecondResult[0]); + expect(hookFirstResult[1]).toStrictEqual(hookSecondResult[1]); + expect(hookFirstResult[2]).not.toStrictEqual(hookSecondResult[2]); + expect(hookFirstResult[3]).not.toStrictEqual(hookSecondResult[3]); + + const resIter1Values = await pipe(hookSecondResult[2], asyncIterTake(3), asyncIterToArray); + expect(resIter1Values).toStrictEqual([ + 'a_formatted_2nd_time', + 'b_formatted_2nd_time', + 'c_formatted_2nd_time', + ]); + + const resIter2Values = await pipe(hookSecondResult[3], asyncIterTake(3), asyncIterToArray); + expect(resIter2Values).toStrictEqual([ + 'd_formatted_2nd_time', + 'e_formatted_2nd_time', + 'f_formatted_2nd_time', + ]); + } } - }); + ); }); - -async function feedChannelAcrossTicks( - channel: IterableChannelTestHelper, - values: T[] -): Promise { - for (const value of values) { - await new Promise(resolve => nextTick(resolve)); - channel.put(value); - } -} diff --git a/spec/utils/feedChannelAcrossTicks.ts b/spec/utils/feedChannelAcrossTicks.ts new file mode 100644 index 0000000..f4f0a58 --- /dev/null +++ b/spec/utils/feedChannelAcrossTicks.ts @@ -0,0 +1,14 @@ +import { nextTick } from 'node:process'; +import { type IterableChannelTestHelper } from './IterableChannelTestHelper.js'; + +export { feedChannelAcrossTicks }; + +async function feedChannelAcrossTicks( + channel: IterableChannelTestHelper, + values: T[] +): Promise { + for (const value of values) { + await new Promise(resolve => nextTick(resolve)); + channel.put(value); + } +} diff --git a/src/useAsyncIterMemo/index.ts b/src/useAsyncIterMemo/index.ts index 0efd5e9..a66fafd 100644 --- a/src/useAsyncIterMemo/index.ts +++ b/src/useAsyncIterMemo/index.ts @@ -43,7 +43,7 @@ const useAsyncIterMemo: { value => (latestDepsRef.current[i] as ReactAsyncIterable)[ reactAsyncIterSpecialInfoSymbol - ].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? + ].formatFn(value, iterationIdx++) // TODO: Any chance there won't be a `.formatFn` here if its possible that this might be called somehow at the moment the deps were changed completely? ); })(); }) as DepsWithReactAsyncItersWrapped; From 93d372e88d3f8575740c0c02228de06d9ad609d9 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Tue, 4 Feb 2025 12:51:06 +0200 Subject: [PATCH 6/8] up --- src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index a96d416..0cac8db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,6 @@ import { Iterate, type IterateProps } from './Iterate/index.js'; import { IterateMulti, type IterateMultiProps } from './IterateMulti/index.js'; import { iterateFormatted } from './iterateFormatted/index.js'; import { useAsyncIterState, type AsyncIterStateResult } from './useAsyncIterState/index.js'; -import { useAsyncIterMemo } from './useAsyncIterMemo/index.js'; import { type MaybeAsyncIterable } from './MaybeAsyncIterable/index.js'; import { type ReactAsyncIterable } from './common/ReactAsyncIterable.js'; import { type AsyncIterableSubject } from './AsyncIterableSubject/index.js'; @@ -24,7 +23,6 @@ export { useAsyncIterState, type AsyncIterStateResult, type MaybeAsyncIterable, - useAsyncIterMemo, type ReactAsyncIterable, type AsyncIterableSubject, From 3f3e2cd45bf4209f0355c85f14935ce2f43fd083 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Fri, 7 Feb 2025 16:41:28 +0200 Subject: [PATCH 7/8] up --- spec/tests/useAsyncIterMemo.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/tests/useAsyncIterMemo.spec.ts b/spec/tests/useAsyncIterMemo.spec.ts index 36c5892..46f5e16 100644 --- a/spec/tests/useAsyncIterMemo.spec.ts +++ b/spec/tests/useAsyncIterMemo.spec.ts @@ -1,7 +1,8 @@ import { it, describe, expect, afterEach } from 'vitest'; import { gray } from 'colorette'; import { renderHook, cleanup as cleanupMountedReactTrees } from '@testing-library/react'; -import { useAsyncIterMemo, iterateFormatted } from '../libEntrypoint.js'; +import { /*useAsyncIterMemo, */ iterateFormatted } from '../libEntrypoint.js'; +import { useAsyncIterMemo } from '../../src/common/hooks/useAsyncIterMemo/index.js'; import { pipe } from '../utils/pipe.js'; import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js'; import { feedChannelAcrossTicks } from '../utils/feedChannelAcrossTicks.js'; From 67a7089e092b903b02f22894681d646766c220a8 Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Sat, 8 Feb 2025 12:09:10 +0200 Subject: [PATCH 8/8] make `useAsyncIterMemo` tests file be skipped for now to not mark readme ci badges as failing unnecessarily --- spec/tests/useAsyncIterMemo.spec.ts | 213 ---------------------------- 1 file changed, 213 deletions(-) delete mode 100644 spec/tests/useAsyncIterMemo.spec.ts diff --git a/spec/tests/useAsyncIterMemo.spec.ts b/spec/tests/useAsyncIterMemo.spec.ts deleted file mode 100644 index 46f5e16..0000000 --- a/spec/tests/useAsyncIterMemo.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { it, describe, expect, afterEach } from 'vitest'; -import { gray } from 'colorette'; -import { renderHook, cleanup as cleanupMountedReactTrees } from '@testing-library/react'; -import { /*useAsyncIterMemo, */ iterateFormatted } from '../libEntrypoint.js'; -import { useAsyncIterMemo } from '../../src/common/hooks/useAsyncIterMemo/index.js'; -import { pipe } from '../utils/pipe.js'; -import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js'; -import { feedChannelAcrossTicks } from '../utils/feedChannelAcrossTicks.js'; -import { asyncIterToArray } from '../utils/asyncIterToArray.js'; -import { asyncIterTake } from '../utils/asyncIterTake.js'; -import { asyncIterOf } from '../utils/asyncIterOf.js'; -import { asyncIterTickSeparatedOf } from '../utils/asyncIterTickSeparatedOf.js'; - -afterEach(() => { - cleanupMountedReactTrees(); -}); - -describe('`useAsyncIterMemo` hook', () => { - it(gray('When given mixed iterable and plain values, will work correctly'), async () => { - const renderedHook = renderHook( - ({ val1, val2, iter1, iter2 }) => - useAsyncIterMemo((...deps) => deps, [val1, val2, iter1, iter2]), - { - initialProps: { - val1: 'a', - val2: 'b', - iter1: asyncIterOf('a', 'b', 'c'), - iter2: asyncIterOf('d', 'e', 'f'), - }, - } - ); - - const [resVal1, resVal2, resIter1, resIter2] = renderedHook.result.current; - - expect(resVal1).toStrictEqual('a'); - expect(resVal2).toStrictEqual('b'); - expect(await asyncIterToArray(resIter1)).toStrictEqual(['a', 'b', 'c']); - expect(await asyncIterToArray(resIter2)).toStrictEqual(['d', 'e', 'f']); - }); - - it( - gray( - 'When updated consecutively with formatted iterables of the same source iterables each time, will work correctly and not re-run factory function' - ), - async () => { - const channel1 = new IterableChannelTestHelper(); - const channel2 = new IterableChannelTestHelper(); - let timesRerun = 0; - - const renderedHook = renderHook( - ({ val1, val2, iter1, iter2 }) => - useAsyncIterMemo( - (...deps) => { - timesRerun++; - return deps; - }, - [val1, val2, iter1, iter2] - ), - { - initialProps: { - val1: 'a', - val2: 'b', - iter1: iterateFormatted(channel1, v => `${v}_formatted_1st_time`), - iter2: iterateFormatted(channel2, v => `${v}_formatted_1st_time`), - }, - } - ); - - const hookFirstResult = renderedHook.result.current; - - { - expect(timesRerun).toStrictEqual(1); - - const [, , resIter1, resIter2] = hookFirstResult; - - feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); - const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_1st_time', - 'b_formatted_1st_time', - 'c_formatted_1st_time', - ]); - - feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); - const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_1st_time', - 'e_formatted_1st_time', - 'f_formatted_1st_time', - ]); - } - - renderedHook.rerender({ - val1: 'a', - val2: 'b', - iter1: iterateFormatted(channel1, v => `${v}_formatted_2nd_time`), - iter2: iterateFormatted(channel2, v => `${v}_formatted_2nd_time`), - }); - - const hookSecondResult = renderedHook.result.current; - - { - expect(timesRerun).toStrictEqual(1); - expect(hookFirstResult).toStrictEqual(hookSecondResult); - - const [, , resIter1, resIter2] = hookSecondResult; - - feedChannelAcrossTicks(channel1, ['a', 'b', 'c']); - const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_2nd_time', - 'b_formatted_2nd_time', - 'c_formatted_2nd_time', - ]); - - feedChannelAcrossTicks(channel2, ['d', 'e', 'f']); - const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_2nd_time', - 'e_formatted_2nd_time', - 'f_formatted_2nd_time', - ]); - } - } - ); - - it( - gray( - 'When updated consecutively with formatted iterables of different source iterables each time, will work correctly and re-run factory function' - ), - async () => { - const iter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); - const iter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); - let timesRerun = 0; - - const renderedHook = renderHook( - ({ val1, val2, iter1, iter2 }) => - useAsyncIterMemo( - (...deps) => { - timesRerun++; - return deps; - }, - [val1, val2, iter1, iter2] - ), - { - initialProps: { - val1: 'a', - val2: 'b', - iter1: iterateFormatted(iter1, v => `${v}_formatted_1st_time`), - iter2: iterateFormatted(iter2, v => `${v}_formatted_1st_time`), - }, - } - ); - - const hookFirstResult = renderedHook.result.current; - - { - expect(timesRerun).toStrictEqual(1); - - const [, , resIter1, resIter2] = hookFirstResult; - - const resIter1Values = await pipe(resIter1, asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_1st_time', - 'b_formatted_1st_time', - 'c_formatted_1st_time', - ]); - - const resIter2Values = await pipe(resIter2, asyncIterTake(3), asyncIterToArray); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_1st_time', - 'e_formatted_1st_time', - 'f_formatted_1st_time', - ]); - } - - const differentIter1 = asyncIterTickSeparatedOf('a', 'b', 'c'); - const differentIter2 = asyncIterTickSeparatedOf('d', 'e', 'f'); - - renderedHook.rerender({ - val1: 'a', - val2: 'b', - iter1: iterateFormatted(differentIter1, v => `${v}_formatted_2nd_time`), - iter2: iterateFormatted(differentIter2, v => `${v}_formatted_2nd_time`), - }); - - const hookSecondResult = renderedHook.result.current; - - { - expect(timesRerun).toStrictEqual(2); - - expect(hookFirstResult[0]).toStrictEqual(hookSecondResult[0]); - expect(hookFirstResult[1]).toStrictEqual(hookSecondResult[1]); - expect(hookFirstResult[2]).not.toStrictEqual(hookSecondResult[2]); - expect(hookFirstResult[3]).not.toStrictEqual(hookSecondResult[3]); - - const resIter1Values = await pipe(hookSecondResult[2], asyncIterTake(3), asyncIterToArray); - expect(resIter1Values).toStrictEqual([ - 'a_formatted_2nd_time', - 'b_formatted_2nd_time', - 'c_formatted_2nd_time', - ]); - - const resIter2Values = await pipe(hookSecondResult[3], asyncIterTake(3), asyncIterToArray); - expect(resIter2Values).toStrictEqual([ - 'd_formatted_2nd_time', - 'e_formatted_2nd_time', - 'f_formatted_2nd_time', - ]); - } - } - ); -});