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', - ]); - } - } - ); -}); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy