From 1371d102e75f477aca73ca6c87413dcda0cdaf56 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sat, 1 Nov 2025 22:33:45 +0100 Subject: [PATCH 01/12] refactor: consistent naming Signed-off-by: vmladenov --- index.d.ts | 36 +++++++++---------- src/creation-async.ts | 8 ++--- src/creation.ts | 6 ++-- src/fluent-async.ts | 64 +++++++++++++++++----------------- src/iterables/select.ts | 2 +- src/iterables/set-iterators.ts | 2 +- src/iterables/skip-while.ts | 2 +- src/iterables/skip.ts | 2 +- src/iterables/take-while.ts | 2 +- src/iterables/take.ts | 2 +- src/utils.ts | 2 +- 11 files changed, 64 insertions(+), 64 deletions(-) diff --git a/index.d.ts b/index.d.ts index e221d5a..0c6df94 100644 --- a/index.d.ts +++ b/index.d.ts @@ -437,52 +437,52 @@ declare module 'fluent-iter' { key: TKey; } - export interface FluentIterableAsync extends AsyncIterable { + export interface FluentAsyncIterable extends AsyncIterable { /** * Filters the iterable using predicate function typed overload * @param predicate */ - where(predicate: (item: TValue) => item is TSubValue): FluentIterableAsync; + where(predicate: (item: TValue) => item is TSubValue): FluentAsyncIterable; /** * Filters the iterable using predicate function * @param predicate */ - where(predicate: (item: TValue) => boolean): FluentIterableAsync; + where(predicate: (item: TValue) => boolean): FluentAsyncIterable; /** * Maps the iterable items * @param map map function */ - select(map: (item: TValue) => TOutput): FluentIterableAsync; + select(map: (item: TValue) => TOutput): FluentAsyncIterable; /** * Take first N items from iterable */ - take(count: number): FluentIterableAsync; + take(count: number): FluentAsyncIterable; /** * Return items while condition return true * @param condition */ - takeWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync; + takeWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable; /** * Skip first N items from iterable * @param count */ - skip(count: number): FluentIterableAsync; + skip(count: number): FluentAsyncIterable; /** * Skip items while condition return true, get the rest * @param condition */ - skipWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync; + skipWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable; /** * Return distinct items. Can specify optional item comparer * @param keySelector function to get key for comparison. */ - distinct(keySelector?: (item: TValue) => TKey): FluentIterableAsync; + distinct(keySelector?: (item: TValue) => TKey): FluentAsyncIterable; /** * Group items @@ -490,10 +490,10 @@ declare module 'fluent-iter' { */ groupBy(keySelector: (item: TValue, index: number) => TKey): [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] - ? FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> - : FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterableAsync; + ? FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + : FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; /** * Return a promise to an array. @@ -518,8 +518,8 @@ declare module 'fluent-iter' { toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Promise>; } - export interface FluentIterableAsyncPromise extends FluentIterableAsync> { - groupByStatus(): FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; + export interface FluentAsyncIterablePromise extends FluentAsyncIterable> { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; toStatusMap(): Promise>; } @@ -558,9 +558,9 @@ declare module 'fluent-iter' { export function fromObject(value: TValue): FluentIterable<{ key: string, value: TValue[TKey] }>; export function fromObject(value: TValue, resultCreator: (key: TKey, value: TValue[TKey]) => TResult): FluentIterable; - export function fromEvent(target: TTarget, event: TEvent): FluentIterableAsync; - export function fromTimer(interval: number, delay?: number): FluentIterableAsync; - export function fromPromises(...promises: Promise[]): FluentIterableAsyncPromise; + export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable; + export function fromTimer(interval: number, delay?: number): FluentAsyncIterable; + export function fromPromises(...promises: Promise[]): FluentAsyncIterablePromise; export function isFulfilled(result: PromiseResult): result is FulfilledPromiseResult; export function isRejected(result: PromiseResult): result is RejectedPromiseResult; } \ No newline at end of file diff --git a/src/creation-async.ts b/src/creation-async.ts index 0bfa366..9fe914a 100644 --- a/src/creation-async.ts +++ b/src/creation-async.ts @@ -1,19 +1,19 @@ -import type { FluentIterableAsync, FluentIterableAsyncPromise } from 'fluent-iter'; +import type { FluentAsyncIterable, FluentAsyncIterablePromise } from 'fluent-iter'; import FluentAsync, {FluentAsyncPromise} from "./fluent-async.js"; import fromEventAsync from "./generators/from-event.ts"; import fromTimerAsync from "./generators/from-timer.js"; import {fromPromisesIterable} from "./generators/promises.ts"; -export function fromEvent(target: TTarget, event: TEvent): FluentIterableAsync { +export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable { return new FluentAsync(fromEventAsync(target, event)); } -export function fromTimer(interval: number, delay?: number): FluentIterableAsync { +export function fromTimer(interval: number, delay?: number): FluentAsyncIterable { return new FluentAsync(fromTimerAsync(interval, delay)); } export function fromPromises( ...promises: Promise[] -): FluentIterableAsyncPromise { +): FluentAsyncIterablePromise { return new FluentAsyncPromise(fromPromisesIterable(...promises)); } diff --git a/src/creation.ts b/src/creation.ts index 141aae8..0abd1ac 100644 --- a/src/creation.ts +++ b/src/creation.ts @@ -1,4 +1,4 @@ -import type { FluentIterable, FluentIterableAsync } from 'fluent-iter'; +import type { FluentIterable, FluentAsyncIterable } from 'fluent-iter'; import Fluent from "./fluent.ts"; import arrayLikeIterator from "./generators/array-like.ts"; import objectIterator from "./generators/object.ts"; @@ -10,7 +10,7 @@ export function fromIterable(iterable: Iterable): FluentIterable return new Fluent(iterable); } -export function fromAsyncIterable(iterable: AsyncIterable): FluentIterableAsync { +export function fromAsyncIterable(iterable: AsyncIterable): FluentAsyncIterable { return new FluentAsync(iterable); } @@ -36,7 +36,7 @@ export function repeat(value: TValue, times: number): FluentIterable(iterable: AsyncIterable): FluentIterableAsync; +export function from(iterable: AsyncIterable): FluentAsyncIterable; export function from(iterable: Iterable | ArrayLike): FluentIterable; export function from(value: TValue): FluentIterable<{ key: string, value: TValue[TKey] }>; export function from(source: Iterable | ArrayLike | TValue | AsyncIterable) { diff --git a/src/fluent-async.ts b/src/fluent-async.ts index 6869c3a..d7e48ff 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -1,7 +1,7 @@ import type { FluentIterable, - FluentIterableAsync, - FluentIterableAsyncPromise, + FluentAsyncIterable, + FluentAsyncIterablePromise, FulfilledPromiseResult, IGrouping, PromiseMap, @@ -10,54 +10,54 @@ import type { } from 'fluent-iter'; import type {Mapper, Predicate} from "./interfaces.js"; import {whereAsyncIterator} from "./iterables/where.js"; -import {selectIteratorAsync} from "./iterables/select.js"; -import takeIteratorAsync from "./iterables/take.js"; +import {selectAsyncIterator} from "./iterables/select.js"; +import takeAsyncIterator from "./iterables/take.js"; import {toArrayAsyncCollector, toMapAsyncCollector} from "./finalizers/to-array.js"; import {groupByAsyncIterator} from "./iterables/group.ts"; -import {takeWhileIteratorAsync} from "./iterables/take-while.ts"; -import {skipIteratorAsync} from "./iterables/skip.ts"; -import {skipWhileIteratorAsync} from "./iterables/skip-while.ts"; -import {distinctIteratorAsync} from "./iterables/set-iterators.ts"; +import {takeWhileAsyncIterator} from "./iterables/take-while.ts"; +import {skipAsyncIterator} from "./iterables/skip.ts"; +import {skipWhileAsyncIterator} from "./iterables/skip-while.ts"; +import {distinctAsyncIterator} from "./iterables/set-iterators.ts"; -export default class FluentAsync implements FluentIterableAsync { +export default class FluentAsync implements FluentAsyncIterable { readonly #source: AsyncIterable; constructor(source: AsyncIterable) { this.#source = source; } - where(predicate: (item: TValue) => item is TSubValue): FluentIterableAsync; - where(predicate: Predicate): FluentIterableAsync; - where(predicate: Predicate): FluentIterableAsync | FluentIterableAsync { + where(predicate: (item: TValue) => item is TSubValue): FluentAsyncIterable; + where(predicate: Predicate): FluentAsyncIterable; + where(predicate: Predicate): FluentAsyncIterable | FluentAsyncIterable { return new FluentAsync(whereAsyncIterator(this, predicate)); } - select(map: Mapper): FluentIterableAsync { - return new FluentAsync(selectIteratorAsync(this, map)); + select(map: Mapper): FluentAsyncIterable { + return new FluentAsync(selectAsyncIterator(this, map)); } - take(count: number): FluentIterableAsync { - return new FluentAsync(takeIteratorAsync(this, count)); + take(count: number): FluentAsyncIterable { + return new FluentAsync(takeAsyncIterator(this, count)); } - takeWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync { - return new FluentAsync(takeWhileIteratorAsync(this, condition)); + takeWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable { + return new FluentAsync(takeWhileAsyncIterator(this, condition)); } - skip(count: number): FluentIterableAsync { - return new FluentAsync(skipIteratorAsync(this, count)); + skip(count: number): FluentAsyncIterable { + return new FluentAsync(skipAsyncIterator(this, count)); } - skipWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync { - return new FluentAsync(skipWhileIteratorAsync(this, condition)); + skipWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable { + return new FluentAsync(skipWhileAsyncIterator(this, condition)); } - distinct(keySelector?: (item: TValue) => TKey): FluentIterableAsync { - return new FluentAsync(distinctIteratorAsync(this, keySelector)); + distinct(keySelector?: (item: TValue) => TKey): FluentAsyncIterable { + return new FluentAsync(distinctAsyncIterator(this, keySelector)); } groupBy(keySelector: (item: TValue, index: number) => TKey): [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] ? - FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> - : FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterableAsync; + FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + : FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector?: (item: TValue, index: number) => TElement, - resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentIterableAsync | IGrouping | TResult> { + resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable | IGrouping | TResult> { return new FluentAsync(groupByAsyncIterator(this, keySelector, elementSelector, resultCreator)); } toArray(): Promise; @@ -80,9 +80,9 @@ export default class FluentAsync implements FluentIterableAsync } } -export class FluentAsyncPromise extends FluentAsync> implements FluentIterableAsyncPromise { - groupByStatus(): FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> { - return super.groupBy(x => x.status) as FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; +export class FluentAsyncPromise extends FluentAsync> implements FluentAsyncIterablePromise { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> { + return super.groupBy(x => x.status) as FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; } toStatusMap(): Promise> { diff --git a/src/iterables/select.ts b/src/iterables/select.ts index bfab9b8..28e5823 100644 --- a/src/iterables/select.ts +++ b/src/iterables/select.ts @@ -14,7 +14,7 @@ function* selectGenerator(input: Iterable, map: Mapper< } } -export function selectIteratorAsync(input: AsyncIterable, map: Mapper): AsyncIterable { +export function selectAsyncIterator(input: AsyncIterable, map: Mapper): AsyncIterable { return { [Symbol.asyncIterator]: async function* () { for await (const item of input) { diff --git a/src/iterables/set-iterators.ts b/src/iterables/set-iterators.ts index 17057c0..aa1832f 100644 --- a/src/iterables/set-iterators.ts +++ b/src/iterables/set-iterators.ts @@ -16,7 +16,7 @@ export function distinctIterator(source: Iterable, }; } -export function distinctIteratorAsync(source: AsyncIterable, keySelector?: (item: TValue) => TKey): AsyncIterable { +export function distinctAsyncIterator(source: AsyncIterable, keySelector?: (item: TValue) => TKey): AsyncIterable { const keySelectorFunc = keySelector ?? defaultKeySelector; return { [Symbol.asyncIterator]: async function* (){ diff --git a/src/iterables/skip-while.ts b/src/iterables/skip-while.ts index 9f52624..6ca6c70 100644 --- a/src/iterables/skip-while.ts +++ b/src/iterables/skip-while.ts @@ -20,7 +20,7 @@ function* skipWhileGenerator(input: Iterable, condition: (item: } } -export function skipWhileIteratorAsync(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { +export function skipWhileAsyncIterator(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { return { [Symbol.asyncIterator]: async function* (){ let flag = false; diff --git a/src/iterables/skip.ts b/src/iterables/skip.ts index 3fa5df7..85dd631 100644 --- a/src/iterables/skip.ts +++ b/src/iterables/skip.ts @@ -18,7 +18,7 @@ function* skipGenerator(input: Iterable, count: number): Generat } } -export function skipIteratorAsync(input: AsyncIterable, count: number): AsyncIterable { +export function skipAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { return { [Symbol.asyncIterator]: async function* (){ let skipped = 0; diff --git a/src/iterables/take-while.ts b/src/iterables/take-while.ts index 55b6778..cc850db 100644 --- a/src/iterables/take-while.ts +++ b/src/iterables/take-while.ts @@ -18,7 +18,7 @@ function* takeWhileGenerator(input: Iterable, condition: (item: } } -export function takeWhileIteratorAsync(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { +export function takeWhileAsyncIterator(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { return { [Symbol.asyncIterator]: async function* (){ let index = 0; diff --git a/src/iterables/take.ts b/src/iterables/take.ts index 6c393ca..a889041 100644 --- a/src/iterables/take.ts +++ b/src/iterables/take.ts @@ -17,7 +17,7 @@ export function takeIterator(input: Iterable, count: number): It } } -export default function takeIteratorAsync(input: AsyncIterable, count: number): AsyncIterable { +export default function takeAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { return { [Symbol.asyncIterator]: async function* () { let fetched = 0; diff --git a/src/utils.ts b/src/utils.ts index c0e353c..4a4fc3e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import {Comparer} from "./interfaces.ts"; import {from, fromIterable} from "./creation.js"; -import { FluentIterable, FluentIterableAsync} from "fluent-iter"; +import { FluentIterable, FluentAsyncIterable} from "fluent-iter"; /** * Helper function to be use to access Symbol.iterator of iterable From f4212cc8606213a74943487d56a0e0777d28fbc7 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sat, 1 Nov 2025 22:58:45 +0100 Subject: [PATCH 02/12] feat: add page async operator Signed-off-by: vmladenov --- index.d.ts | 6 +++ src/fluent-async.ts | 4 ++ src/fluent.ts | 103 ++++++++++++++++++++++++++++++++--------- src/iterables/page.ts | 20 +++++++- test/unit/page.spec.ts | 31 ++++++++++++- 5 files changed, 139 insertions(+), 25 deletions(-) diff --git a/index.d.ts b/index.d.ts index 0c6df94..1cd1023 100644 --- a/index.d.ts +++ b/index.d.ts @@ -495,6 +495,12 @@ declare module 'fluent-iter' { groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; + /** + * Create a paging + * @param pageSize + */ + page(pageSize: number): FluentAsyncIterable; + /** * Return a promise to an array. */ diff --git a/src/fluent-async.ts b/src/fluent-async.ts index d7e48ff..3f65d94 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -18,6 +18,7 @@ import {takeWhileAsyncIterator} from "./iterables/take-while.ts"; import {skipAsyncIterator} from "./iterables/skip.ts"; import {skipWhileAsyncIterator} from "./iterables/skip-while.ts"; import {distinctAsyncIterator} from "./iterables/set-iterators.ts"; +import {pageAsyncIterator} from "./iterables/page.ts"; export default class FluentAsync implements FluentAsyncIterable { readonly #source: AsyncIterable; @@ -60,6 +61,9 @@ export default class FluentAsync implements FluentAsyncIterable resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable | IGrouping | TResult> { return new FluentAsync(groupByAsyncIterator(this, keySelector, elementSelector, resultCreator)); } + page(pageSize: number): FluentAsyncIterable { + return new FluentAsync(pageAsyncIterator(this, pageSize)); + } toArray(): Promise; toArray(map: Mapper): Promise; toArray(map?: Mapper): Promise<(TValue|TResult)[]> { diff --git a/src/fluent.ts b/src/fluent.ts index 5919515..6e2d969 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -1,11 +1,11 @@ -import { whereIterator } from "./iterables/where.ts"; -import { selectIterator } from "./iterables/select.ts"; -import { selectManyIterator, flatIterator, flatMapIterator } from "./iterables/select-many.ts"; -import { takeIterator } from "./iterables/take.ts"; -import { skipIterator } from "./iterables/skip.ts"; -import { toArrayCollector } from "./finalizers/to-array.ts"; -import { takeWhileIterator } from "./iterables/take-while.ts"; -import { skipWhileIterator } from "./iterables/skip-while.ts"; +import {whereIterator} from "./iterables/where.ts"; +import {selectIterator} from "./iterables/select.ts"; +import {selectManyIterator, flatIterator, flatMapIterator} from "./iterables/select-many.ts"; +import {takeIterator} from "./iterables/take.ts"; +import {skipIterator} from "./iterables/skip.ts"; +import {toArrayCollector} from "./finalizers/to-array.ts"; +import {takeWhileIterator} from "./iterables/take-while.ts"; +import {skipWhileIterator} from "./iterables/skip-while.ts"; import takeLastIterator from "./iterables/take-last.ts"; import skipLastIterator from "./iterables/skip-last.ts"; import {allAndEveryCollector, allCollector} from "./finalizers/all.ts"; @@ -20,7 +20,7 @@ import {single, singleOrDefault} from "./finalizers/single.ts"; import {last, lastIndex, lastOrDefault, lastOrThrow} from "./finalizers/last.ts"; import {first, firstIndex, firstOrDefault, firstOrThrow} from "./finalizers/first.ts"; import reverseIterator from "./iterables/reverse.ts"; -import pageIterator from "./iterables/page.ts"; +import {pageIterator} from "./iterables/page.ts"; import { diffIterator, distinctIterator, @@ -36,7 +36,7 @@ import groupJoinIterator from "./iterables/group-join.ts"; import zipIterable from "./iterables/zip.js"; import type {Action, Comparer, Mapper, Predicate} from "./interfaces.ts"; -import type {FlatFluentIterable, FluentIterable, IGrouping } from 'fluent-iter'; +import type {FlatFluentIterable, FluentIterable, IGrouping} from 'fluent-iter'; export default class Fluent implements FluentIterable { readonly #source: Iterable; @@ -50,50 +50,67 @@ export default class Fluent implements FluentIterable { where(predicate: Predicate): FluentIterable | FluentIterable { return new Fluent(whereIterator(this, predicate)); } + select(map: Mapper): FluentIterable { return new Fluent(selectIterator(this, map)); } + flat(depth: number = 1): FlatFluentIterable { return new Fluent(flatIterator(this, depth)) as any; } + flatMap(mapper: (value: TValue) => TResult | ReadonlyArray): FluentIterable { return new Fluent(flatMapIterator(this, mapper)); } + selectMany(innerSelector: (item: TValue) => TInner[], resultCreator?: (outer: TValue, inner: TInner) => TResult): FluentIterable { if (typeof resultCreator !== 'undefined') { return new Fluent(selectManyIterator(this, innerSelector, resultCreator)); } return new Fluent(selectManyIterator(this, innerSelector)); } + take(count: number): FluentIterable { return new Fluent(takeIterator(this, count)); } + takeWhile(condition: (item: TValue, index: number) => boolean): FluentIterable { return new Fluent(takeWhileIterator(this, condition)); } + takeLast(count: number): FluentIterable { return new Fluent(takeLastIterator(this, count)); } + skip(count: number): FluentIterable { return new Fluent(skipIterator(this, count)); } + skipWhile(condition: (item: TValue, index: number) => boolean): FluentIterable { return new Fluent(skipWhileIterator(this, condition)); } + skipLast(count: number): FluentIterable { return new Fluent(skipLastIterator(this, count)); } + distinct(keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(distinctIterator(this, keySelector)); } - ofType(type: 'string'|'number'|'boolean'|'undefined'|'function'|'object'|'symbol'|((item: TValue) => item is TOutput)): FluentIterable { + + ofType(type: 'string' | 'number' | 'boolean' | 'undefined' | 'function' | 'object' | 'symbol' | ((item: TValue) => item is TOutput)): FluentIterable { const filter = typeof type === 'function' ? type : (item: TValue) => typeof item === type; return new Fluent(whereIterator(this, filter) as Iterable); } - ofClass(type: { new (...args: any[]): TOutput, prototype: TOutput }): FluentIterable { + + ofClass(type: { + new(...args: any[]): TOutput, + prototype: TOutput + }): FluentIterable { const filter = (item: TValue) => item instanceof type; return new Fluent(whereIterator(this, filter) as Iterable); } + groupBy(keySelector: (item: TValue, index: number) => TKey): FluentIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterable; @@ -102,18 +119,22 @@ export default class Fluent implements FluentIterable { resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentIterable | IGrouping | TResult> { return new Fluent(groupByIterator(this, keySelector, elementSelector, resultCreator)); } + orderBy(keySelector: (item: TValue) => TKey, comparer?: Comparer): FluentIterable { return new Fluent(sortAscendingIterator(this, keySelector, comparer)); } + orderByDescending(keySelector: (item: TValue) => TKey, comparer?: Comparer): FluentIterable { return new Fluent(sortDescendingIterator(this, keySelector, comparer)); } + groupJoin(joinIterable: Iterable, sourceKeySelector: (item: TValue) => TKey, joinIterableKeySelector: (item: TInner, index: number) => TKey, resultCreator: (outer: TValue, inner: FluentIterable & TInner[]) => TResult): FluentIterable { return new Fluent(groupJoinIterator(this, joinIterable, sourceKeySelector, joinIterableKeySelector, resultCreator as any)); } + join(separator: string): string; join(joinIterable: Iterable, sourceKeySelector: (item: TValue) => TKey, @@ -128,96 +149,125 @@ export default class Fluent implements FluentIterable { } return new Fluent(joinIterator(this, firstArgument, sourceKeySelector!, joinIterableKeySelector!, resultCreator!)); } + concat(secondIterable: Iterable): FluentIterable { return new Fluent(concatIterator(this, secondIterable)); } - union(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + union(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(unionIterator(this, secondIterable, keySelector)); } - intersect(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + intersect(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(intersectIterator(this, secondIterable, keySelector)); } - difference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + difference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(diffIterator(this, secondIterable, keySelector)); } - symmetricDifference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + symmetricDifference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(symmetricDiffIterator(this, secondIterable, keySelector)); } + page(pageSize: number): FluentIterable { return new Fluent(pageIterator(this, pageSize)); } + reverse(): FluentIterable { return new Fluent(reverseIterator(this)); } + toArray(): TValue[]; toArray(map: Mapper): TResult[]; toArray(map?: Mapper): TValue[] | TResult[] { return toArrayCollector(this, map); } + zip(second: Iterable): FluentIterable<[TValue, TOuter]> { return new Fluent(zipIterable(this, second)); } + toMap(keySelector: (item: TValue) => TKey): Map; toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Map; - toMap(keySelector: (item: TValue) => TKey, elementSelector?: (item: TValue) => TElement): Map { + toMap(keySelector: (item: TValue) => TKey, elementSelector?: (item: TValue) => TElement): Map { return new Map(this.select(item => [keySelector(item), elementSelector ? elementSelector(item) : item])); } + toSet(): Set { return new Set(this); } + first(predicate?: Predicate): TValue | undefined { return first(this, predicate); } + firstOrDefault(def: TValue, predicate?: Predicate): TValue { return firstOrDefault(this, def, predicate); } + firstOrThrow(predicate?: Predicate): TValue | never { return firstOrThrow(this, predicate); } + firstIndex(predicate: Predicate): number { return firstIndex(this, predicate); } + last(predicate?: Predicate): TValue | undefined { return last(this, predicate); } + lastOrDefault(def: TValue, predicate?: Predicate): TValue { return lastOrDefault(this, def, predicate); } + lastOrThrow(predicate?: Predicate): TValue | never { return lastOrThrow(this, predicate); } + lastIndex(predicate: (item: TValue) => boolean): number { return lastIndex(this, predicate); } + single(predicate?: Predicate): TValue | never { return single(this, predicate); } + singleOrDefault(def: TValue, predicate?: Predicate): TValue | never { return singleOrDefault(this, def, predicate); } + all(predicate: (item: TValue) => boolean): boolean { return allCollector(this, predicate); } + allAndEvery(predicate: (item: TValue) => boolean): boolean { return allAndEveryCollector(this, predicate); } + any(predicate?: ((item: TValue) => boolean) | undefined): boolean { return anyCollector(this, predicate); } + count(predicate?: ((item: TValue) => boolean) | undefined): number { return countCollector(this, predicate); } + aggregate(accumulator: (result: TValue, item: TValue, index: number) => TValue): TValue | never; - aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; - aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial?: TResult): TResult | TValue | never { + aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; + aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial?: TResult): TResult | TValue | never { return aggregateCollector(this, accumulator, initial); } + sum(): TValue { return aggregateCollector(this, (a, i) => (a as any) + (i as any) as any); } + product(): TValue { return aggregateCollector(this, (a, i) => (a as any) * (i as any) as any); } + min(comparer?: Comparer): TValue | never { const compare = comparer ?? defaultSortComparer; return aggregateCollector(this, (a, b) => { @@ -225,6 +275,7 @@ export default class Fluent implements FluentIterable { return comp < 0 ? a : (comp > 0 ? b : a); }); } + max(comparer?: Comparer): TValue | never { const compare = typeof comparer === 'undefined' ? defaultSortComparer : comparer; return aggregateCollector(this, (a, b) => { @@ -232,22 +283,27 @@ export default class Fluent implements FluentIterable { return comp < 0 ? b : (comp > 0 ? a : b); }); } + elementAt(index: number): TValue | undefined { return elementAtCollector(this, index); } + forEach(action: Action): void { return forEachCollector(this, action); } + isEqual(iterable: Iterable): boolean; - isEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; - isEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { + isEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; + isEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { return sequenceEqual(this, second, comparer); } + isElementsEqual(iterable: Iterable): boolean; - isElementsEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; - isElementsEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { + isElementsEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; + isElementsEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { return isElementsEqual(this, second, comparer); } + [Symbol.iterator](): Iterator { return this.#source[Symbol.iterator](); } @@ -255,6 +311,7 @@ export default class Fluent implements FluentIterable { export class Grouping extends Fluent implements IGrouping { readonly #key: TKey; + constructor(key: TKey, items: Iterable) { super(items); this.#key = key; diff --git a/src/iterables/page.ts b/src/iterables/page.ts index 50f1c3f..868ca85 100644 --- a/src/iterables/page.ts +++ b/src/iterables/page.ts @@ -1,6 +1,6 @@ import {createIterable} from "../utils.ts"; -export default function pageIterator(source: Iterable, pageSize: number): Iterable { +export function pageIterator(source: Iterable, pageSize: number): Iterable { return createIterable(() => pageGenerator(source, pageSize)); } @@ -17,3 +17,21 @@ function* pageGenerator(source: Iterable, pageSize: number): Gen yield page; } } + +export function pageAsyncIterator(source: AsyncIterable, pageSize: number): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + let page: TValue[] = []; + for await (const item of source) { + page.push(item); + if (page.length === pageSize) { + yield page; + page = []; + } + } + if (page.length > 0) { + yield page; + } + } + } +} diff --git a/test/unit/page.spec.ts b/test/unit/page.spec.ts index a87ba6a..9aa05c5 100644 --- a/test/unit/page.spec.ts +++ b/test/unit/page.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; -import { from, range } from "../../src/index.ts"; +import {from, fromTimer, range} from "../../src/index.ts"; +import {emptyAsyncIterable} from "../test-utils.ts"; describe('page iteration tests', () => { [ @@ -34,3 +35,31 @@ describe('page iteration tests', () => { expect(res1.length).toBe(2); }); }); + +describe('page async tests', () => { + it('should page async iterable', async () => { + let i = 0; + for await (const page of fromTimer(1).take(10).page(4)) { + switch (i) { + case 0: { + expect(page).toStrictEqual([0, 1, 2, 3]); + break; + } + case 1: { + expect(page).toStrictEqual([4, 5, 6, 7]); + break; + } + case 2: { + expect(page).toStrictEqual([8, 9]) + } + } + i++; + } + expect(i).toBe(3); + }); + + it('should return empty array when no items', async () => { + const res = await from(emptyAsyncIterable).page(4).toArray(); + expect(res).toStrictEqual([]); + }) +}); \ No newline at end of file From c6bcc03ea4932b7ca5db23e1183991dbda8238ee Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sat, 1 Nov 2025 23:33:58 +0100 Subject: [PATCH 03/12] feat: use builtin types Signed-off-by: vmladenov --- index.d.ts | 34 ++++++++++------------------------ src/fluent-async.ts | 17 +++++++---------- src/generators/promises.ts | 12 ++++++------ 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1cd1023..e374222 100644 --- a/index.d.ts +++ b/index.d.ts @@ -489,8 +489,8 @@ declare module 'fluent-iter' { * @param keySelector group key selector */ groupBy(keySelector: (item: TValue, index: number) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] - ? FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseSettledResult] + ? FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>> : FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; @@ -512,7 +512,7 @@ declare module 'fluent-iter' { * @param keySelector - key selector - keys should be unique, otherwise last keys will override first. */ toMap(keySelector: (item: TValue) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>] + [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>] ? Promise> : Promise>; @@ -524,28 +524,14 @@ declare module 'fluent-iter' { toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Promise>; } - export interface FluentAsyncIterablePromise extends FluentAsyncIterable> { - groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; + export interface FluentAsyncIterablePromise extends FluentAsyncIterable> { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>>; toStatusMap(): Promise>; } - type PromiseResult = FulfilledPromiseResult | RejectedPromiseResult; - - interface FulfilledPromiseResult { - index: number; - status: 'fulfilled'; - value: T; - } - - interface RejectedPromiseResult { - index: number; - status: 'rejected'; - reason: any; - } - - interface PromiseMap extends Map<'fulfilled'|'rejected', FluentIterable>> { - get(key: 'fulfilled'): FluentIterable> | undefined; - get(key: 'rejected'): FluentIterable | undefined; + interface PromiseMap extends Map<'fulfilled'|'rejected', FluentIterable>> { + get(key: 'fulfilled'): FluentIterable> | undefined; + get(key: 'rejected'): FluentIterable | undefined; } type FlatFluentIterable = Value extends ReadonlyArray @@ -567,6 +553,6 @@ declare module 'fluent-iter' { export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable; export function fromTimer(interval: number, delay?: number): FluentAsyncIterable; export function fromPromises(...promises: Promise[]): FluentAsyncIterablePromise; - export function isFulfilled(result: PromiseResult): result is FulfilledPromiseResult; - export function isRejected(result: PromiseResult): result is RejectedPromiseResult; + export function isFulfilled(result: PromiseSettledResult): result is PromiseFulfilledResult; + export function isRejected(result: PromiseSettledResult): result is PromiseRejectedResult; } \ No newline at end of file diff --git a/src/fluent-async.ts b/src/fluent-async.ts index 3f65d94..c88dd79 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -2,11 +2,8 @@ import type { FluentIterable, FluentAsyncIterable, FluentAsyncIterablePromise, - FulfilledPromiseResult, IGrouping, - PromiseMap, - PromiseResult, - RejectedPromiseResult + PromiseMap } from 'fluent-iter'; import type {Mapper, Predicate} from "./interfaces.js"; import {whereAsyncIterator} from "./iterables/where.js"; @@ -51,8 +48,8 @@ export default class FluentAsync implements FluentAsyncIterable return new FluentAsync(distinctAsyncIterator(this, keySelector)); } groupBy(keySelector: (item: TValue, index: number) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] ? - FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseSettledResult] ? + FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>> : FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; @@ -71,7 +68,7 @@ export default class FluentAsync implements FluentAsyncIterable } toMap(keySelector: (item: TValue) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>] + [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>] ? Promise> : Promise>; toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Promise>; @@ -84,9 +81,9 @@ export default class FluentAsync implements FluentAsyncIterable } } -export class FluentAsyncPromise extends FluentAsync> implements FluentAsyncIterablePromise { - groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> { - return super.groupBy(x => x.status) as FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; +export class FluentAsyncPromise extends FluentAsync> implements FluentAsyncIterablePromise { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>> { + return super.groupBy(x => x.status) as FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>>; } toStatusMap(): Promise> { diff --git a/src/generators/promises.ts b/src/generators/promises.ts index db94cb4..6c347d0 100644 --- a/src/generators/promises.ts +++ b/src/generators/promises.ts @@ -1,16 +1,16 @@ -import {FulfilledPromiseResult, PromiseResult, RejectedPromiseResult } from "fluent-iter"; import {createAsyncIterable} from "../utils.ts"; export function fromPromisesIterable( ...promises: Promise[] -): AsyncIterable> { +): AsyncIterable> { return createAsyncIterable(() => fromPromisesGenerator(...promises)); } +type IndexedPromiseSettledResult = PromiseSettledResult & { index: number }; async function* fromPromisesGenerator( ...promises: Promise[] -): AsyncGenerator> { - const pending = new Map>>( +): AsyncGenerator> { + const pending = new Map>>( promises.map((p, index) => [ index, p.then( @@ -27,10 +27,10 @@ async function* fromPromisesGenerator( } } -export function isFulfilled(result: PromiseResult): result is FulfilledPromiseResult { +export function isFulfilled(result: PromiseSettledResult): result is PromiseFulfilledResult { return result.status === 'fulfilled'; } -export function isRejected(result: PromiseResult): result is RejectedPromiseResult { +export function isRejected(result: PromiseSettledResult): result is PromiseRejectedResult { return result.status === 'rejected'; } From f5154f8144e8aeb207d76541368427c549a4f1d4 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 11:21:23 +0100 Subject: [PATCH 04/12] feat: improve subjects --- src/creation-async.ts | 4 +-- src/fluent-async.ts | 10 +++---- src/fluent.ts | 6 ++--- src/iterables/group.ts | 2 +- src/iterables/skip-last.ts | 55 +++++++++++++++----------------------- src/iterables/take-last.ts | 50 ++++++++++++++++------------------ src/iterables/zip.ts | 21 ++++++++++++++- src/utils.ts | 15 ++++++++++- test/unit/to-array.spec.ts | 2 +- test/unit/union.spec.ts | 2 +- 10 files changed, 91 insertions(+), 76 deletions(-) diff --git a/src/creation-async.ts b/src/creation-async.ts index 9fe914a..c5c5265 100644 --- a/src/creation-async.ts +++ b/src/creation-async.ts @@ -1,7 +1,7 @@ import type { FluentAsyncIterable, FluentAsyncIterablePromise } from 'fluent-iter'; -import FluentAsync, {FluentAsyncPromise} from "./fluent-async.js"; +import FluentAsync, {FluentAsyncPromise} from "./fluent-async.ts"; import fromEventAsync from "./generators/from-event.ts"; -import fromTimerAsync from "./generators/from-timer.js"; +import fromTimerAsync from "./generators/from-timer.ts"; import {fromPromisesIterable} from "./generators/promises.ts"; export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable { diff --git a/src/fluent-async.ts b/src/fluent-async.ts index c88dd79..17a1cbb 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -5,11 +5,11 @@ import type { IGrouping, PromiseMap } from 'fluent-iter'; -import type {Mapper, Predicate} from "./interfaces.js"; -import {whereAsyncIterator} from "./iterables/where.js"; -import {selectAsyncIterator} from "./iterables/select.js"; -import takeAsyncIterator from "./iterables/take.js"; -import {toArrayAsyncCollector, toMapAsyncCollector} from "./finalizers/to-array.js"; +import type {Mapper, Predicate} from "./interfaces.ts"; +import {whereAsyncIterator} from "./iterables/where.ts"; +import {selectAsyncIterator} from "./iterables/select.ts"; +import takeAsyncIterator from "./iterables/take.ts"; +import {toArrayAsyncCollector, toMapAsyncCollector} from "./finalizers/to-array.ts"; import {groupByAsyncIterator} from "./iterables/group.ts"; import {takeWhileAsyncIterator} from "./iterables/take-while.ts"; import {skipAsyncIterator} from "./iterables/skip.ts"; diff --git a/src/fluent.ts b/src/fluent.ts index 6e2d969..bf692c7 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -6,8 +6,8 @@ import {skipIterator} from "./iterables/skip.ts"; import {toArrayCollector} from "./finalizers/to-array.ts"; import {takeWhileIterator} from "./iterables/take-while.ts"; import {skipWhileIterator} from "./iterables/skip-while.ts"; -import takeLastIterator from "./iterables/take-last.ts"; -import skipLastIterator from "./iterables/skip-last.ts"; +import {takeLastIterator} from "./iterables/take-last.ts"; +import {skipLastIterator} from "./iterables/skip-last.ts"; import {allAndEveryCollector, allCollector} from "./finalizers/all.ts"; import anyCollector from "./finalizers/any.ts"; import countCollector from "./finalizers/count.ts"; @@ -33,7 +33,7 @@ import {sortAscendingIterator, sortDescendingIterator} from "./iterables/order.t import {groupByIterator} from "./iterables/group.ts"; import joinIterator from "./iterables/join.ts"; import groupJoinIterator from "./iterables/group-join.ts"; -import zipIterable from "./iterables/zip.js"; +import {zipIterable} from "./iterables/zip.ts"; import type {Action, Comparer, Mapper, Predicate} from "./interfaces.ts"; import type {FlatFluentIterable, FluentIterable, IGrouping} from 'fluent-iter'; diff --git a/src/iterables/group.ts b/src/iterables/group.ts index 88455c5..9de3413 100644 --- a/src/iterables/group.ts +++ b/src/iterables/group.ts @@ -1,6 +1,6 @@ import type {FluentIterable, IGrouping} from "fluent-iter"; import {createAsyncIterable, createIterable, group, groupAsync} from "../utils.ts"; -import {Grouping} from "../fluent.js"; +import {Grouping} from "../fluent.ts"; export function groupByIterator( source: Iterable, diff --git a/src/iterables/skip-last.ts b/src/iterables/skip-last.ts index 7b3620c..f3fc3e4 100644 --- a/src/iterables/skip-last.ts +++ b/src/iterables/skip-last.ts @@ -1,43 +1,30 @@ /** * Return skip last N elements from sequence */ -import {doneValue, getIterator, iteratorResultCreator} from "../utils.ts"; - -export default function skipLastIterator(input: Iterable, count: number): Iterable { - return new SkipLastIterable(input, count); -} - -export class SkipLastIterable implements Iterable { - readonly #source: Iterable; - readonly #count: number; - /** - * - * @param {Iterable} source - * @param {number} count - */ - constructor(source: Iterable, count: number) { - this.#source = source; - this.#count = count <= 0 ? 0 : count; +export function skipLastIterator(input: Iterable, count: number): Iterable { + return { + [Symbol.iterator]: function* () { + const keep: TValue[] = []; + for (const item of input) { + keep.push(item); + if (keep.length > count) { + yield keep.shift() as TValue; + } + } + } } +} - [Symbol.iterator]() { - const iterator = getIterator(this.#source); - const count = this.#count; - const keep: TValue[] = []; - let next: IteratorResult = iteratorResultCreator(void 0 as TValue); - return { - next() { - while (!next.done && keep.length <= count) { - next = iterator.next(); - if (!next.done) { - keep.push(next.value); - } - } - if (next.done) { - return doneValue(); +export function skipLastAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + const keep: TValue[] = []; + for await (const item of input) { + keep.push(item); + if (keep.length > count) { + yield keep.shift() as TValue; } - return iteratorResultCreator(keep.shift() as TValue); } - }; + } } } diff --git a/src/iterables/take-last.ts b/src/iterables/take-last.ts index 2723608..71898f0 100644 --- a/src/iterables/take-last.ts +++ b/src/iterables/take-last.ts @@ -1,36 +1,32 @@ /** * Take last N elements */ -import {createIterable} from "../utils.ts"; - -export default function takeLastIterator(input: Iterable, count: number): Iterable { - return createIterable(() => takeLastGenerator(input, count)); -} - -function* takeLastGenerator(input: Iterable, count: number): Generator { - const queue = new LimitedQueue(count); - for (const item of input) { - queue.push(item); +export function takeLastIterator(input: Iterable, count: number): Iterable { + return { + [Symbol.iterator]: function* () { + const keep: TValue[] = []; + for (const item of input) { + keep.push(item); + if (keep.length > count) { + keep.shift(); + } + } + yield* keep; + } } - yield* queue; } -class LimitedQueue implements Iterable { - readonly #container: TItem[]; - readonly #limit: number; - constructor(limit: number) { - this.#container = []; - this.#limit = limit; - } - - push(item: TItem) { - this.#container.push(item); - if (this.#container.length > this.#limit) { - this.#container.shift(); +export function takeLastAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + const keep: TValue[] = []; + for await (const item of input) { + keep.push(item); + if (keep.length > count) { + keep.shift(); + } + } + yield* keep; } } - - [Symbol.iterator]() { - return this.#container[Symbol.iterator](); - } } diff --git a/src/iterables/zip.ts b/src/iterables/zip.ts index fd28044..3f06489 100644 --- a/src/iterables/zip.ts +++ b/src/iterables/zip.ts @@ -1,5 +1,7 @@ // combines two iterables -export default function zipIterable(first: Iterable, second: Iterable): Iterable<[TThis, TOuter]> { +import {isFulfilled} from "../generators/promises.ts"; + +export function zipIterable(first: Iterable, second: Iterable): Iterable<[TThis, TOuter]> { return { [Symbol.iterator]: function* () { const firstIterator = first[Symbol.iterator](); @@ -15,3 +17,20 @@ export default function zipIterable(first: Iterable, secon } } } + +export function zipAsyncIterable(first: AsyncIterable, second: AsyncIterable): AsyncIterable<[TThis, TOuter]> { + return { + [Symbol.asyncIterator]: async function* () { + const firstIterator = first[Symbol.asyncIterator](); + const secondIterator = second[Symbol.asyncIterator](); + while (true) { + const [first, second] = await Promise.allSettled([firstIterator.next(), secondIterator.next()]); + if (isFulfilled(first) && isFulfilled(second) && !first.value.done && !second.value.done) { + yield [first.value.value, second.value.value]; + } else { + break; + } + } + } + } +} diff --git a/src/utils.ts b/src/utils.ts index 4a4fc3e..67765cd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import {Comparer} from "./interfaces.ts"; -import {from, fromIterable} from "./creation.js"; +import {from, fromIterable} from "./creation.ts"; import { FluentIterable, FluentAsyncIterable} from "fluent-iter"; /** @@ -159,6 +159,19 @@ export function emptyIterator(): Iterator { }; } + +function extracted(keySelector: (item: TValue, index: number) => TKey, item: TValue, i: number, elementSelector: (item: TValue, index: number) => TElement, map: Map) { + const key = keySelector(item, i); + if ((key !== null && typeof key === 'object') || typeof key === "function") { + throw new TypeError('groupBy method does not support keys to be objects or functions'); + } + const element = elementSelector(item, i); + const value = map.get(key) || []; + value.push(element); + map.set(key, value); + i++; +} + export function group( iterable: Iterable, keySelector: (item: TValue, index: number) => TKey, diff --git a/test/unit/to-array.spec.ts b/test/unit/to-array.spec.ts index 5a43afe..e546e8c 100644 --- a/test/unit/to-array.spec.ts +++ b/test/unit/to-array.spec.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from "vitest"; -import {fromTimer} from "../../src/index.js"; +import {fromTimer} from "../../src/index.ts"; describe('to array', () => { it('should get an array from async iterable', async () => { diff --git a/test/unit/union.spec.ts b/test/unit/union.spec.ts index a1a6817..be94ff8 100644 --- a/test/unit/union.spec.ts +++ b/test/unit/union.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { from, range } from "../../src/index.ts"; -import {Person} from "./models.js"; +import {Person} from "./models.ts"; describe('union tests', () => { [ From 7aeb9a42e60656c86b12915141404508d9fec3eb Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 11:50:36 +0100 Subject: [PATCH 05/12] feat: Add alias functions for better compatible with Array Alias functions added: - filter - map - find - findIndex - findLast - findLastIndex - every - some - reduce - at --- index.d.ts | 89 ++++++++++++++++++++++++++++++++++++++ src/finalizers/to-array.ts | 7 +-- src/fluent.ts | 19 ++++++++ test/unit/where.spec.ts | 12 ++++- 4 files changed, 123 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index e374222..e128ed6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,18 +6,39 @@ declare module 'fluent-iter' { */ where(predicate: (item: TValue) => item is TSubValue): FluentIterable; + /** + * Filters the iterable using predicate function typed overload. Alias to where. + * @see where + * @param predicate + */ + filter(predicate: (item: TValue) => item is TSubValue): FluentIterable; + /** * Filters the iterable using predicate function * @param predicate */ where(predicate: (item: TValue) => boolean): FluentIterable; + /** + * Filters the iterable using predicate function. Alias to where. + * @see where + * @param predicate + */ + filter(predicate: (item: TValue) => boolean): FluentIterable; + /** * Maps the iterable items * @param map map function */ select(map: (item: TValue) => TOutput): FluentIterable; + /** + * Maps the iterable items. Alias to select. + * @see select + * @param map map function + */ + map(map: (item: TValue) => TOutput): FluentIterable; + /** * Flat Iterable of collections up to N levels, default is 1 level. * @param depth @@ -274,6 +295,13 @@ declare module 'fluent-iter' { */ first(predicate?: (item: TValue) => boolean): TValue | undefined; + /** + * Get first item of iterable satisfiying the predicate. Alias for first. + * @see first + * @param predicate predicate for the item + */ + find(predicate: (item: TValue) => boolean): TValue | undefined; + /** * Get first item of iterable, if does not contain any return default * @param def @@ -293,12 +321,27 @@ declare module 'fluent-iter' { */ firstIndex(predicate: (item: TValue) => boolean): number; + /** + * Get index of first found item in sequence. Alias for firstIndex. + * @see firstIndex + * @param predicate predicate for the item + * @return index of item, when not found -1 + */ + findIndex(predicate: (item: TValue) => boolean): number; + /** * Get last item of iterable * @param predicate optional predicate for the item */ last(predicate?: (item: TValue) => boolean): TValue | undefined; + /** + * Get last item of iterable satisfiying the predicate. Alias for last + * @see last + * @param predicate optional predicate for the item + */ + findLast(predicate: (item: TValue) => boolean): TValue | undefined; + /** * Get last item of iterable, if does not contain any return default * @param def @@ -319,6 +362,14 @@ declare module 'fluent-iter' { */ lastIndex(predicate: (item: TValue) => boolean): number; + /** + * Get index of last found item in sequence satisfiying the predicate. Alias for lastIndex. + * @see lastIndex + * @param predicate predicate for the item + * @return index of item, when not found -1 + */ + findLastIndex(predicate: (item: TValue) => boolean): number; + /** * Checks if iterable has only one item and returns it. * @throws TypeError when no or multiple elements @@ -338,6 +389,13 @@ declare module 'fluent-iter' { */ all(predicate: (item: TValue) => boolean): boolean; + /** + * Returns if all items satisfy the predicate. It returns true if no items. Alias for all. + * @see all + * @param predicate + */ + every(predicate: (item: TValue) => boolean): boolean; + /** * Returns if all items satisfy the predicate. It returns false if no items. * @param predicate @@ -350,6 +408,13 @@ declare module 'fluent-iter' { */ any(predicate?: (item: TValue) => boolean): boolean; + /** + * Returns if any items satisfy the predicate. Alias for any. + * @see any + * @param predicate + */ + some(predicate?: (item: TValue) => boolean): boolean; + /** * Return count of items. * @param predicate if predicate is supplied then return the count of items that return true. @@ -363,6 +428,14 @@ declare module 'fluent-iter' { */ aggregate(accumulator: (result: TValue, item: TValue, index: number) => TValue): TValue | never; + /** + * Produce single value form sequence values. The initial value is first element. Alias for aggregate. + * @see aggregate + * @param accumulator function which produces the result. + * @throws TypeError when no elements + */ + reduce(accumulator: (result: TValue, item: TValue, index: number) => TValue): TValue | never; + /** * Produce single value form sequence values. The initial value is the second argument. * @param accumulator function which produces the result. @@ -370,6 +443,14 @@ declare module 'fluent-iter' { */ aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; + /** + * Produce single value form sequence values. The initial value is the second argument. Alias for aggregate. + * @see aggregate + * @param accumulator function which produces the result. + * @param initial initial value + */ + reduce(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; + /** * Produce a sum of sequence values * @throws {TypeError} if not items in sequence @@ -409,6 +490,14 @@ declare module 'fluent-iter' { */ elementAt(index: number): TValue | undefined; + /** + * Return element at specific index. Alias for elementAt. + * @see elementAt + * @param index index of requested element. + * @return undefined when no index out of range. + */ + at(index: number): TValue | undefined; + /** * do action over every item in the sequence * @param action diff --git a/src/finalizers/to-array.ts b/src/finalizers/to-array.ts index bf43394..47f2742 100644 --- a/src/finalizers/to-array.ts +++ b/src/finalizers/to-array.ts @@ -9,12 +9,13 @@ export function toArrayCollector(source: Iterable, map?: Mapper): } export async function toArrayAsyncCollector(source: AsyncIterable, map?: Mapper): Promise<(T|R)[]> { - const result: (T|R)[] = []; + if (!map) { + return Array.fromAsync(source); + } + const result: R[] = []; for await (const item of source) { if (map) { result.push(map(item)); - } else { - result.push(item); } } return result; diff --git a/src/fluent.ts b/src/fluent.ts index bf692c7..de218dd 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -50,11 +50,14 @@ export default class Fluent implements FluentIterable { where(predicate: Predicate): FluentIterable | FluentIterable { return new Fluent(whereIterator(this, predicate)); } + filter = this.where; select(map: Mapper): FluentIterable { return new Fluent(selectIterator(this, map)); } + map = this.select; + flat(depth: number = 1): FlatFluentIterable { return new Fluent(flatIterator(this, depth)) as any; } @@ -202,6 +205,8 @@ export default class Fluent implements FluentIterable { return first(this, predicate); } + find = this.first; + firstOrDefault(def: TValue, predicate?: Predicate): TValue { return firstOrDefault(this, def, predicate); } @@ -214,10 +219,14 @@ export default class Fluent implements FluentIterable { return firstIndex(this, predicate); } + findIndex = this.firstIndex; + last(predicate?: Predicate): TValue | undefined { return last(this, predicate); } + findLast = this.last; + lastOrDefault(def: TValue, predicate?: Predicate): TValue { return lastOrDefault(this, def, predicate); } @@ -230,6 +239,8 @@ export default class Fluent implements FluentIterable { return lastIndex(this, predicate); } + findLastIndex = this.lastIndex; + single(predicate?: Predicate): TValue | never { return single(this, predicate); } @@ -242,6 +253,8 @@ export default class Fluent implements FluentIterable { return allCollector(this, predicate); } + every = this.all; + allAndEvery(predicate: (item: TValue) => boolean): boolean { return allAndEveryCollector(this, predicate); } @@ -250,6 +263,8 @@ export default class Fluent implements FluentIterable { return anyCollector(this, predicate); } + some = this.any; + count(predicate?: ((item: TValue) => boolean) | undefined): number { return countCollector(this, predicate); } @@ -260,6 +275,8 @@ export default class Fluent implements FluentIterable { return aggregateCollector(this, accumulator, initial); } + reduce = this.aggregate; + sum(): TValue { return aggregateCollector(this, (a, i) => (a as any) + (i as any) as any); } @@ -288,6 +305,8 @@ export default class Fluent implements FluentIterable { return elementAtCollector(this, index); } + at = this.elementAt; + forEach(action: Action): void { return forEachCollector(this, action); } diff --git a/test/unit/where.spec.ts b/test/unit/where.spec.ts index a918fec..c91add3 100644 --- a/test/unit/where.spec.ts +++ b/test/unit/where.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { from, fromIterable } from "../../src/index.ts"; +import {from, fromIterable, range} from "../../src/index.ts"; describe("where tests", () => { [[1, 2, 3, 4, 5, 6, 7], new Set([1, 2, 3, 4, 5, 6, 7])].forEach( @@ -47,4 +47,14 @@ describe("where tests", () => { .toArray(); expect(res.length).toBe(2); }); + + it('should filter sequence', () => { + const res = from(range(0, 10)).filter(x => !(x & 1)).toArray(); + expect(res).toStrictEqual([0, 2, 4, 6, 8]); + }); + + it('should filter: typecheck sequence', () => { + const res = from([1, 2, '3', '4']).filter(x => typeof x === 'string').toArray(); + expect(res).toStrictEqual(['3', '4']); + }); }); From 4241f6520e97498b6a4a10ec0add70f593100ba0 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 11:57:18 +0100 Subject: [PATCH 06/12] docs: update README.md with new and updated functions --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7f0deb9..6bad7bb 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,15 @@ Operators: Transformers - where : filter iterable by predicate +- filter : alias for where - select : transform iterable using mapping function +- map : alias for select - selectMany : flatten an iterable and can transform it to another iterable +- flat: flatten an iterable up to N levels, default is 1 level +- flatMap: flatten an iterable one level and maps inner elements - ofType : produces typed iterable, used to filter by type - ofClass: filters iterable for elements of a given class +- groupBy: group items by a key - orderBy : Order iterable ascending by a key - orderByDescending : Order iterable descending by a key - groupJoin: Do a group join (left join) between current and external iterable. For each item of current sequence get array of items from external sequence. @@ -73,7 +78,6 @@ Combinations: - union: Produce a union of two iterables where the result is distinct values from both. - intersect: Return an intersection of two iterables where the result is distinct values. - difference: Return elements from the first iterable that are not in the second. Only distinct values are returned. -- except: Return elements from the first iterable that are not in the second. (TODO) - symmetricDifference: Return a symmetric difference ( distinct values except intersection ) of two iterables where the result is distinct values. Aggregators: @@ -81,28 +85,36 @@ Aggregators: - toMap(): Create a map object from sequence - toSet(): Creates a set from current sequence - first : first element in iterable or first element that satisfies predicate +- find: alias for first - firstOrDefault: like first but with default value -- firstOrThrows: like first but throws if no element is found +- firstOrThrow: like first but throws if no element is found - firstIndex: like first but returns index of element +- findIndex: alias for firstIndex - last: last element in iterable or last element that satisfies predicate +- findLast: alias for last - lastOrDefault: like last but with default value -- lastOrThrows: like last but throws if no element is found +- lastOrThrow: like last but throws if no element is found - lastIndex: like last but returns index of element +- findLastIndex: alias for lastIndex - single: single element in iterable or single element that satisfies predicate. Throws if more than one element is found. - singleOrDefault: like single but with default value when nothing found. Throws if more than one element is found. - all: check if all elements in iterable satisfy predicate +- every: alias for all - allAndEvery: like all but requires to have at least one element - any: check if at least one element in iterable satisfies predicate +- some: alias for any - count: count elements in iterable or count of elements that satisfy predicate - aggregate: Produce single value form sequence values. The initial value is the second argument. +- reduce: alias for aggregate - sum: Sum all elements in iterable - product: Multiply all elements in iterable - min: Get minimum value in iterable - max: Get maximum value in iterable - join: join all items from iterable with string concatenation - elementAt: get element at index +- at: alias for elementAt Misc: - forEach: iterate over iterable and execute a function for each element - isEqual: check if two iterables are equal. Same elements and order. -- isElementsEqual: check if two iterables are equal. Same elements but order can be different. +- isElementsEqual: check if two iterables are equal. Same elements but order can be different. \ No newline at end of file From 3f586a38afba4a97d1caa2a78981cff9cb1c7650 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 16:20:15 +0100 Subject: [PATCH 07/12] chore: update README with the FluentAsyncIterable functions Signed-off-by: vmladenov --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6bad7bb..e11d202 100644 --- a/README.md +++ b/README.md @@ -117,4 +117,33 @@ Aggregators: Misc: - forEach: iterate over iterable and execute a function for each element - isEqual: check if two iterables are equal. Same elements and order. -- isElementsEqual: check if two iterables are equal. Same elements but order can be different. \ No newline at end of file +- isElementsEqual: check if two iterables are equal. Same elements but order can be different. + +FluentAsyncIterable +------- +FluentAsyncIterable is a library that allows you to work with async iterables in a fluent way. + +Operators: + +Transformers +- where : filter iterable by predicate +- select : transform iterable using mapping function +- take : take first n elements +- takeWhile : take elements while predicate is true +- skip : skip first n elements +- skipWhile : skip elements while predicate is true +- distinct: removes duplicate elements +- groupBy: group items by a key +- page: Split iterable into chunks of a given size. + +Aggregators: +- toArray(): convert iterable to array +- toMap(): Create a map object from sequence + +FluentAsyncPromise +------- +FluentAsyncPromise is a special type of FluentAsyncIterable that is returned when working with promises. + +Operators: +- groupByStatus: Groups promises by their status (`fulfilled` or `rejected`). +- toStatusMap: Creates a map with promise statuses as keys. \ No newline at end of file From de8dfb51909015dcb365475521743c282a349e3c Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 16:26:15 +0100 Subject: [PATCH 08/12] feat: update aliases to be properties Signed-off-by: vmladenov --- README.md | 2 ++ index.d.ts | 22 ++++++++++++++++++++++ src/fluent-async.ts | 9 +++++++++ src/fluent.ts | 20 ++++++++++---------- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e11d202..107e57c 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,9 @@ Operators: Transformers - where : filter iterable by predicate +- filter : alias for where - select : transform iterable using mapping function +- map : alias for select - take : take first n elements - takeWhile : take elements while predicate is true - skip : skip first n elements diff --git a/index.d.ts b/index.d.ts index e128ed6..4525dbf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -538,12 +538,34 @@ declare module 'fluent-iter' { * @param predicate */ where(predicate: (item: TValue) => boolean): FluentAsyncIterable; + + /** + * Filters the iterable using predicate function typed overload. Alias to where. + * @see where + * @param predicate + */ + filter(predicate: (item: TValue) => item is TSubValue): FluentAsyncIterable; + + /** + * Filters the iterable using predicate function. Alias to where. + * @see where + * @param predicate + */ + filter(predicate: (item: TValue) => boolean): FluentAsyncIterable; + /** * Maps the iterable items * @param map map function */ select(map: (item: TValue) => TOutput): FluentAsyncIterable; + /** + * Maps the iterable items. Alias to select. + * @see select + * @param map map function + */ + map(map: (item: TValue) => TOutput): FluentAsyncIterable; + /** * Take first N items from iterable */ diff --git a/src/fluent-async.ts b/src/fluent-async.ts index 17a1cbb..d53ee16 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -29,9 +29,18 @@ export default class FluentAsync implements FluentAsyncIterable where(predicate: Predicate): FluentAsyncIterable | FluentAsyncIterable { return new FluentAsync(whereAsyncIterator(this, predicate)); } + + get filter() { + return this.where; + } + select(map: Mapper): FluentAsyncIterable { return new FluentAsync(selectAsyncIterator(this, map)); } + + get map() { + return this.select; + } take(count: number): FluentAsyncIterable { return new FluentAsync(takeAsyncIterator(this, count)); } diff --git a/src/fluent.ts b/src/fluent.ts index de218dd..af3b264 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -50,13 +50,13 @@ export default class Fluent implements FluentIterable { where(predicate: Predicate): FluentIterable | FluentIterable { return new Fluent(whereIterator(this, predicate)); } - filter = this.where; + get filter() { return this.where; } select(map: Mapper): FluentIterable { return new Fluent(selectIterator(this, map)); } - map = this.select; + get map() { return this.select; } flat(depth: number = 1): FlatFluentIterable { return new Fluent(flatIterator(this, depth)) as any; @@ -205,7 +205,7 @@ export default class Fluent implements FluentIterable { return first(this, predicate); } - find = this.first; + get find() { return this.first; } firstOrDefault(def: TValue, predicate?: Predicate): TValue { return firstOrDefault(this, def, predicate); @@ -219,13 +219,13 @@ export default class Fluent implements FluentIterable { return firstIndex(this, predicate); } - findIndex = this.firstIndex; + get findIndex() { return this.firstIndex; } last(predicate?: Predicate): TValue | undefined { return last(this, predicate); } - findLast = this.last; + get findLast() { return this.last; } lastOrDefault(def: TValue, predicate?: Predicate): TValue { return lastOrDefault(this, def, predicate); @@ -239,7 +239,7 @@ export default class Fluent implements FluentIterable { return lastIndex(this, predicate); } - findLastIndex = this.lastIndex; + get findLastIndex() { return this.lastIndex; } single(predicate?: Predicate): TValue | never { return single(this, predicate); @@ -253,7 +253,7 @@ export default class Fluent implements FluentIterable { return allCollector(this, predicate); } - every = this.all; + get every() { return this.all; } allAndEvery(predicate: (item: TValue) => boolean): boolean { return allAndEveryCollector(this, predicate); @@ -263,7 +263,7 @@ export default class Fluent implements FluentIterable { return anyCollector(this, predicate); } - some = this.any; + get some() { return this.any; } count(predicate?: ((item: TValue) => boolean) | undefined): number { return countCollector(this, predicate); @@ -275,7 +275,7 @@ export default class Fluent implements FluentIterable { return aggregateCollector(this, accumulator, initial); } - reduce = this.aggregate; + get reduce() { return this.aggregate; } sum(): TValue { return aggregateCollector(this, (a, i) => (a as any) + (i as any) as any); @@ -305,7 +305,7 @@ export default class Fluent implements FluentIterable { return elementAtCollector(this, index); } - at = this.elementAt; + get at() { return this.elementAt; } forEach(action: Action): void { return forEachCollector(this, action); From 93f0c622bafcaf78ac1dc76eced422a241be6242 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 16:49:58 +0100 Subject: [PATCH 09/12] chore: update vitest and add coverage-v8 package --- bun.lock | 138 ++++++--------------------------------------------- package.json | 6 +-- 2 files changed, 18 insertions(+), 126 deletions(-) diff --git a/bun.lock b/bun.lock index 8b5d489..e00c677 100644 --- a/bun.lock +++ b/bun.lock @@ -5,10 +5,10 @@ "name": "modern-linq", "devDependencies": { "@types/benchmark": "^2.1.5", - "@vitest/coverage-v8": "^3.2.4", + "@vitest/coverage-v8": "^4.0.6", "benchmark": "^2.1.4", "typescript": "^5", - "vitest": "^3.2.4", + "vitest": "^4.0.6", }, "peerDependencies": { "typescript": "^5", @@ -16,8 +16,6 @@ }, }, "packages": { - "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], @@ -80,20 +78,12 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="], - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="], @@ -138,6 +128,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@types/benchmark": ["@types/benchmark@2.1.5", "", {}, "sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w=="], "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], @@ -146,56 +138,30 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="], - - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.6", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.6", "ast-v8-to-istanbul": "^0.3.5", "debug": "^4.4.3", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.3.5", "std-env": "^3.9.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.6", "vitest": "4.0.6" }, "optionalPeers": ["@vitest/browser"] }, "sha512-cv6pFXj9/Otk7q1Ocoj8k3BUVVwnFr3jqcqpwYrU5LkKClU9DpaMEdX+zptx/RyIJS+/VpoxMWmfISXchmVDPQ=="], - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + "@vitest/expect": ["@vitest/expect@4.0.6", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.6", "@vitest/utils": "4.0.6", "chai": "^6.0.1", "tinyrainbow": "^3.0.3" } }, "sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg=="], - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + "@vitest/mocker": ["@vitest/mocker@4.0.6", "", { "dependencies": { "@vitest/spy": "4.0.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.19" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA=="], - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.6", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g=="], - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + "@vitest/runner": ["@vitest/runner@4.0.6", "", { "dependencies": { "@vitest/utils": "4.0.6", "pathe": "^2.0.3" } }, "sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og=="], - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "@vitest/snapshot": ["@vitest/snapshot@4.0.6", "", { "dependencies": { "@vitest/pretty-format": "4.0.6", "magic-string": "^0.30.19", "pathe": "^2.0.3" } }, "sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g=="], - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "@vitest/spy": ["@vitest/spy@4.0.6", "", {}, "sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "@vitest/utils": ["@vitest/utils@4.0.6", "", { "dependencies": { "@vitest/pretty-format": "4.0.6", "tinyrainbow": "^3.0.3" } }, "sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A=="], "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.30", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-9SdXjNheSiE8bALAQCQQuT6fgQaoxJh7IRYrRGZ8/9nv8WhJeC1aXAwN8TbaOssGOukUvyvnkgD9+Yuykvl1aA=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "benchmark": ["benchmark@2.1.4", "", { "dependencies": { "lodash": "^4.17.4", "platform": "^1.3.3" } }, "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ=="], - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "chai": ["chai@6.2.0", "", {}, "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], @@ -206,20 +172,12 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], @@ -228,40 +186,22 @@ "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -274,78 +214,30 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "vitest": ["vitest@4.0.6", "", { "dependencies": { "@vitest/expect": "4.0.6", "@vitest/mocker": "4.0.6", "@vitest/pretty-format": "4.0.6", "@vitest/runner": "4.0.6", "@vitest/snapshot": "4.0.6", "@vitest/spy": "4.0.6", "@vitest/utils": "4.0.6", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.19", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.6", "@vitest/browser-preview": "4.0.6", "@vitest/browser-webdriverio": "4.0.6", "@vitest/ui": "4.0.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package.json b/package.json index 719cb79..344ce02 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,10 @@ ], "devDependencies": { "@types/benchmark": "^2.1.5", - "@vitest/coverage-v8": "^3.2.4", + "@vitest/coverage-v8": "^4.0.6", "benchmark": "^2.1.4", - "vitest": "^3.2.4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.6" }, "peerDependencies": { "typescript": "^5" From 01d906c54a8e4e7fe3dff1e957ab6a73cd7c8d84 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 17:08:41 +0100 Subject: [PATCH 10/12] feat: add zip, takeLast and skipLast async iterable versions Signed-off-by: vmladenov --- README.md | 5 +++++ index.d.ts | 17 +++++++++++++++ src/fluent-async.ts | 16 ++++++++++++++ src/index.ts | 2 +- test/test-utils.ts | 7 ++++++ test/unit/skip-last.spec.ts | 43 ++++++++++++++++++++++++++++++++++++- test/unit/take-last.spec.ts | 25 ++++++++++++++++++++- test/unit/zip.spec.ts | 39 ++++++++++++++++++++++++++++++++- vitest.config.ts | 5 ++++- 9 files changed, 154 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 107e57c..7ab0014 100644 --- a/README.md +++ b/README.md @@ -132,12 +132,17 @@ Transformers - map : alias for select - take : take first n elements - takeWhile : take elements while predicate is true +- takeLast : take last n elements - skip : skip first n elements - skipWhile : skip elements while predicate is true +- skipLast : skip last n elements - distinct: removes duplicate elements - groupBy: group items by a key - page: Split iterable into chunks of a given size. +Combinations: +- zip: zip two iterables together, where the result is an iterable of tuples, finishes when one of the iterables is finished. + Aggregators: - toArray(): convert iterable to array - toMap(): Create a map object from sequence diff --git a/index.d.ts b/index.d.ts index 4525dbf..02e1474 100644 --- a/index.d.ts +++ b/index.d.ts @@ -612,6 +612,23 @@ declare module 'fluent-iter' { */ page(pageSize: number): FluentAsyncIterable; + /** + * zip two iterables together, where the result is an iterable of tuples, finishes when one of the iterables is finished. + */ + zip(second: AsyncIterable): FluentAsyncIterable<[TValue, TOuter]>; + + /** + * Take last N items from iterable + * @param count + */ + takeLast(count: number): FluentAsyncIterable; + + /** + * Skip last N items from iterable + * @param count + */ + skipLast(count: number): FluentAsyncIterable; + /** * Return a promise to an array. */ diff --git a/src/fluent-async.ts b/src/fluent-async.ts index d53ee16..9afb32e 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -16,6 +16,9 @@ import {skipAsyncIterator} from "./iterables/skip.ts"; import {skipWhileAsyncIterator} from "./iterables/skip-while.ts"; import {distinctAsyncIterator} from "./iterables/set-iterators.ts"; import {pageAsyncIterator} from "./iterables/page.ts"; +import {zipAsyncIterable} from "./iterables/zip.ts"; +import {takeLastAsyncIterator} from "./iterables/take-last.ts"; +import {skipLastAsyncIterator} from "./iterables/skip-last.ts"; export default class FluentAsync implements FluentAsyncIterable { readonly #source: AsyncIterable; @@ -70,6 +73,19 @@ export default class FluentAsync implements FluentAsyncIterable page(pageSize: number): FluentAsyncIterable { return new FluentAsync(pageAsyncIterator(this, pageSize)); } + + zip(second: AsyncIterable): FluentAsyncIterable<[TValue, TOuter]> { + return new FluentAsync(zipAsyncIterable(this, second)); + } + + takeLast(count: number): FluentAsyncIterable { + return new FluentAsync(takeLastAsyncIterator(this, count)); + } + + skipLast(count: number): FluentAsyncIterable { + return new FluentAsync(skipLastAsyncIterator(this, count)); + } + toArray(): Promise; toArray(map: Mapper): Promise; toArray(map?: Mapper): Promise<(TValue|TResult)[]> { diff --git a/src/index.ts b/src/index.ts index 829a371..7c6d590 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export { fromIterable, fromObject, fromArrayLike, range, from, repeat } from './creation.ts'; +export { fromIterable, fromAsyncIterable, fromObject, fromArrayLike, range, from, repeat } from './creation.ts'; export { fromEvent, fromTimer, fromPromises } from './creation-async.ts'; export { isFulfilled, isRejected } from './generators/promises.ts'; diff --git a/test/test-utils.ts b/test/test-utils.ts index 14c1849..3e70060 100644 --- a/test/test-utils.ts +++ b/test/test-utils.ts @@ -1,4 +1,6 @@ import {doneValue} from "../src/utils.ts"; +import {fromTimer} from "../src/index.ts"; +import { FluentAsyncIterable } from "fluent-iter"; export function wait(ms: number, result: T): Promise { return new Promise((resolve) => { @@ -21,3 +23,8 @@ export const emptyAsyncIterable: AsyncIterable = ({ next: () => Promise.resolve(doneValue()) }), }); + +export const testAsyncIterable = (cnt: number): FluentAsyncIterable => { + return fromTimer(1).take(cnt); +} + diff --git a/test/unit/skip-last.spec.ts b/test/unit/skip-last.spec.ts index d9f5f65..57530c0 100644 --- a/test/unit/skip-last.spec.ts +++ b/test/unit/skip-last.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; -import { from, range } from "../../src/index.ts"; +import {from, fromAsyncIterable, range} from "../../src/index.ts"; +import {testAsyncIterable} from "../test-utils.ts"; describe('skip last tests', () => { [ @@ -71,3 +72,43 @@ describe('skip last tests', () => { }); }); }); + +describe('skip last async tests', () => { + it('should skip last n numbers', async () => { + const output = await testAsyncIterable(5).skipLast(2).toArray(); + expect(output).toEqual([0, 1, 2]); + }); + + it('should skip last n numbers after another operation', async () => { + const output = await testAsyncIterable(6).where(_ => _ > 2).skipLast(2).toArray(); + expect(output).toEqual([3]); + }); + + it('should be able to continue with another operator', async () => { + const output = await testAsyncIterable(6) + .where(_ => _ > 2) + .skipLast(1) + .select(x => `item_${x}`) + .toArray(); + expect(output).toEqual(['item_3', 'item_4']); + }); + + it('should return nothing if count is less', async () => { + const res = await testAsyncIterable(2) + .skipLast(3) + .toArray(); + expect(res).toEqual([]); + }); + + it('should return empty from empty collection', async () => { + const output = await testAsyncIterable(0) + .skipLast(2) + .toArray(); + expect(output).toEqual([]); + }); + + it('should return all when none to be skipped', async () => { + expect(await testAsyncIterable(5).skipLast(0).toArray()).toEqual([0, 1, 2, 3, 4]); + expect(await testAsyncIterable(5).skipLast(-1).toArray()).toEqual([0, 1, 2, 3, 4]); + }); +}); diff --git a/test/unit/take-last.spec.ts b/test/unit/take-last.spec.ts index f400aa1..a0e47d1 100644 --- a/test/unit/take-last.spec.ts +++ b/test/unit/take-last.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; -import {from, range, repeat} from "../../src/index.ts"; +import {from, fromAsyncIterable, range, repeat} from "../../src/index.ts"; +import {testAsyncIterable} from "../test-utils.ts"; describe('take last tests', () => { [ @@ -45,3 +46,25 @@ describe('take last tests', () => { expect(Array.from(output)).toEqual([14, 16, 18]); }); }); + +describe('take last async tests', () => { + it('should take last 2 elements', async () => { + const output = await testAsyncIterable(5).takeLast(2).toArray(); + expect(output).toEqual([3, 4]); + }); + + it('should return empty when source is empty', async () => { + const output = await testAsyncIterable(0).takeLast(3).toArray(); + expect(output).toEqual([]); + }); + + it('should return empty when none to be taken', async () => { + expect(await testAsyncIterable(5).takeLast(0).toArray()).toEqual([]); + expect(await testAsyncIterable(5).takeLast(-1).toArray()).toEqual([]); + }); + + it('should able to continue the query', async () => { + const output = await testAsyncIterable(10).takeLast(3).select(_ => _ * 2).toArray(); + expect(output).toEqual([14, 16, 18]) + }); +}); diff --git a/test/unit/zip.spec.ts b/test/unit/zip.spec.ts index 94cff04..21fbace 100644 --- a/test/unit/zip.spec.ts +++ b/test/unit/zip.spec.ts @@ -1,5 +1,6 @@ import {describe, expect, it} from "vitest"; -import {range} from "../../src/index.ts"; +import {fromAsyncIterable, range} from "../../src/index.ts"; +import {testAsyncIterable} from "../test-utils.ts"; describe('zip iterable tests', () => { it('should combine two iterables', () => { @@ -21,3 +22,39 @@ describe('zip iterable tests', () => { ]); }); }); + +describe('zip async iterable tests', () => { + it('should combine two iterables', async () => { + const result = await testAsyncIterable(5) + .zip(testAsyncIterable(5)) + .toArray(); + expect(result).toEqual([ + [0, 0], [1, 1], [2, 2], [3, 3], [4, 4] + ]); + }); + + it('should combine two iterables from different type', async () => { + const result = await fromAsyncIterable(testAsyncIterable(5)) + .zip(testAsyncIterable(5).map(x => x.toString())) + .toArray(); + expect(result).toEqual([ + [0, '0'], [1, '1'], [2, '2'], [3, '3'], [4, '4'] + ]); + }); + + it('should stop as soon one fishes', async () => { + const result1 = await fromAsyncIterable(testAsyncIterable(5)) + .zip(testAsyncIterable(4)) + .toArray(); + expect(result1).toEqual([ + [0, 0], [1, 1], [2, 2], [3, 3] + ]); + + const result2 = await fromAsyncIterable(testAsyncIterable(4)) + .zip(testAsyncIterable(5)) + .toArray(); + expect(result2).toEqual([ + [0, 0], [1, 1], [2, 2], [3, 3] + ]); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 46bb071..aaf95ba 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,11 @@ -import {defineConfig} from "vite"; +import {defineConfig} from "vitest/config"; export default defineConfig({ test: { include: ["test/**/*.spec.{ts,js}"], globals: true, + coverage: { + provider: "v8", + } }, }); \ No newline at end of file From 569fa402053f11e55eb9b5f6527f2219d49f79eb Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 18:27:13 +0100 Subject: [PATCH 11/12] feat: improve code coverage for alias methods --- src/fluent-async-subject.ts | 5 +++-- test/unit/aggregate.spec.ts | 15 +++++++++++++-- test/unit/all.spec.ts | 10 ++++++++-- test/unit/any.spec.ts | 7 +++++++ test/unit/element-at.spec.ts | 10 ++++++++++ test/unit/first.spec.ts | 11 +++++++++++ test/unit/last.spec.ts | 10 ++++++++++ test/unit/select.spec.ts | 5 +++++ 8 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/fluent-async-subject.ts b/src/fluent-async-subject.ts index 1b85292..0650a9e 100644 --- a/src/fluent-async-subject.ts +++ b/src/fluent-async-subject.ts @@ -22,8 +22,9 @@ export class ReplaySubjectAsyncIterator implements SubjectIterator { return Promise.reject(this.error); } - if (this.queue.length > 0) { - return Promise.resolve(iteratorResultCreator(this.queue.shift()!)); + const nextValue = this.queue.shift(); + if (nextValue) { + return Promise.resolve(iteratorResultCreator(nextValue)); } if (this.closed) { diff --git a/test/unit/aggregate.spec.ts b/test/unit/aggregate.spec.ts index 9feacc9..058d7d5 100644 --- a/test/unit/aggregate.spec.ts +++ b/test/unit/aggregate.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { fromIterable, range } from "../../src/index.ts"; +import {from, fromIterable, range} from "../../src/index.ts"; describe('aggregate tests', () => { [ @@ -128,4 +128,15 @@ describe('aggregate tests', () => { const res = fromIterable([1, 2, 1, 3, 3]).max(); expect(res).toBe(3); }); -}); \ No newline at end of file + + it('should work alias', () => { + const res = from([1, 2, 3, 4]).reduce((a, b) => a + b); + expect(res).toBe(10); + const res1 = from([1, 2, 3, 4]).reduce((a, b) => a + b, 2); + expect(res1).toBe(12); + const res2 = from([]).reduce((a, b) => a + b, 2); + expect(res2).toBe(2); + const res3 = () => from([] as number[]).reduce((a, b) => a + b); + expect(res3).toThrowError(TypeError); + }); +}); diff --git a/test/unit/all.spec.ts b/test/unit/all.spec.ts index 411a176..42bb98d 100644 --- a/test/unit/all.spec.ts +++ b/test/unit/all.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { fromIterable } from "../../src/index.ts"; +import {from, fromIterable} from "../../src/index.ts"; describe("all tests", () => { [["a", "b", "c"], new Set(["a", "b", "c"])].forEach((source, indx) => { @@ -36,7 +36,7 @@ describe("all tests", () => { indx, () => { const input0 = fromIterable(source); - expect(input0.all((_) => typeof _ === "string")).toBe(false); + expect(input0.allAndEvery((_) => typeof _ === "string")).toBe(false); }, ); }); @@ -47,4 +47,10 @@ describe("all tests", () => { expect(input.allAndEvery((_) => typeof _ === "string")).toBe(false); }); }); + + it('should work alias', () => { + expect(from([1, 2, 3]).every(x => x > 0)).toBe(true); + expect(from([1, 2, 3]).every(x => x > 2)).toBe(false); + expect(from([]).every(x => x > 2)).toBe(true); + }); }); diff --git a/test/unit/any.spec.ts b/test/unit/any.spec.ts index 945c592..4ccb314 100644 --- a/test/unit/any.spec.ts +++ b/test/unit/any.spec.ts @@ -29,4 +29,11 @@ describe("any tests", () => { expect(from(new Set([1])).any()).toBe(true); expect(from(new Set()).any()).toBe(false); }); + + it('should work alias', () => { + expect(from([1, 2, 3]).some()).toBe(true); + expect(from([]).some()).toBe(false); + expect(from([1, 2, 3]).some(x => x > 2)).toBe(true); + expect(from([1, 2, 3]).some(x => x > 3)).toBe(false); + }); }); diff --git a/test/unit/element-at.spec.ts b/test/unit/element-at.spec.ts index e626f86..14ff461 100644 --- a/test/unit/element-at.spec.ts +++ b/test/unit/element-at.spec.ts @@ -15,4 +15,14 @@ describe('element at tests', () => { expect(iterable.elementAt(11)).toBe(undefined); }); }); + + it('should work alias', () => { + const source = range(0, 10).at(2); + expect(source).toBe(2); + }); + + it('should return undefined for out of range index', () => { + const source = range(0, 5); + expect(source.at(10)).toBe(undefined); + }); }); diff --git a/test/unit/first.spec.ts b/test/unit/first.spec.ts index c712561..9484595 100644 --- a/test/unit/first.spec.ts +++ b/test/unit/first.spec.ts @@ -44,6 +44,11 @@ describe('first finalizer', () => { }); }); + it('should work alias: find', () => { + const res = range(1, 5).find(x => x === 2); + expect(res).toBe(2); + }); + // firstOrDefault [ @@ -160,4 +165,10 @@ describe('first finalizer', () => { expect(from(source).firstIndex(_ => _ === 2)).toBe(0); }); }); + + it('should work alias: findIndex', () => { + const res = range(1, 5).findIndex(x => x === 3); + // [1 (0), 2 (1), 3 (2), 4] 3 is at index 2 + expect(res).toBe(2); + }); }); diff --git a/test/unit/last.spec.ts b/test/unit/last.spec.ts index 4c65779..a63bdc6 100644 --- a/test/unit/last.spec.ts +++ b/test/unit/last.spec.ts @@ -44,6 +44,11 @@ describe('last finalizer', () => { }); }); + it('should work alias: findLast', () => { + const res = range(1, 5).findLast(x => x === 2); + expect(res).toBe(2); + }); + // lastOrDefault [ @@ -166,4 +171,9 @@ describe('last finalizer', () => { it('should return last index when multiple times', () => { expect(from([1, 2, 3, 1, 2, 3]).lastIndex(_ => _ === 2)).toBe(4); }); + + it('should work alias: findLastIndex', () => { + const res = range(1,5).findLastIndex(x => x === 3); + expect(res).toBe(2); + }); }); diff --git a/test/unit/select.spec.ts b/test/unit/select.spec.ts index c924c13..ca8d35c 100644 --- a/test/unit/select.spec.ts +++ b/test/unit/select.spec.ts @@ -44,4 +44,9 @@ describe('select tests', () => { expect(Array.from(numbers)).toEqual([2, 4, 6, 8, 10, 12, 14]); }); }); + + it('should work alias: map', () => { + const res = range(1, 5).map(x => '' + x).toArray(); + expect(res).toEqual(['1', '2', '3', '4']); + }); }); From 41e49196ba9bb4b086fd98b0756eb6ea3b3612e7 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 18:29:49 +0100 Subject: [PATCH 12/12] feat: add AbortController to the fromTimer --- src/generators/from-timer.ts | 38 +++++++++++++++++------------ src/utils.ts | 45 ++++++++++++++++++++++------------- test/unit/from-timer.spec.ts | 46 ++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 32 deletions(-) diff --git a/src/generators/from-timer.ts b/src/generators/from-timer.ts index 8c8f0e6..09fc579 100644 --- a/src/generators/from-timer.ts +++ b/src/generators/from-timer.ts @@ -1,28 +1,36 @@ import {delay as sleep} from "../utils.ts"; -export default function fromTimerAsync(interval: number, delay?: number): AsyncIterable & AsyncDisposable { - let done = false; +export default function fromTimerAsync(interval: number, delay?: number): AsyncIterable & AsyncDisposable & Disposable { + const abortController = new AbortController(); + const {signal} = abortController; return { [Symbol.asyncIterator]: async function* () { - let i = 0; - if (delay) { - await sleep(delay); - if (done) { - return; + try { + let i = 0; + if (delay) { + await sleep(delay, signal); + if (signal.aborted) { + return; + } + yield i++; } - yield i++; - } - while (true) { - if (done) { - break; + while (true) { + if (signal.aborted) { + break; + } + await sleep(interval, signal); + yield i++; } - await sleep(interval); - yield i++; + } catch { + // ignore aborted errors } }, [Symbol.asyncDispose]() { - done = true; + abortController.abort('fromTimerAsync disposed async'); return Promise.resolve(); + }, + [Symbol.dispose]() { + abortController.abort('fromTimerAsync disposed'); } } } diff --git a/src/utils.ts b/src/utils.ts index 67765cd..69108d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import {Comparer} from "./interfaces.ts"; import {from, fromIterable} from "./creation.ts"; -import { FluentIterable, FluentAsyncIterable} from "fluent-iter"; +import { FluentIterable } from "fluent-iter"; /** * Helper function to be use to access Symbol.iterator of iterable @@ -159,19 +159,6 @@ export function emptyIterator(): Iterator { }; } - -function extracted(keySelector: (item: TValue, index: number) => TKey, item: TValue, i: number, elementSelector: (item: TValue, index: number) => TElement, map: Map) { - const key = keySelector(item, i); - if ((key !== null && typeof key === 'object') || typeof key === "function") { - throw new TypeError('groupBy method does not support keys to be objects or functions'); - } - const element = elementSelector(item, i); - const value = map.get(key) || []; - value.push(element); - map.set(key, value); - i++; -} - export function group( iterable: Iterable, keySelector: (item: TValue, index: number) => TKey, @@ -228,8 +215,32 @@ export function createAsyncIterable(generator: () => AsyncGenerator): Asyn } } -export function delay(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); +function abortableTimeout(callback: () => void, delay: number, signal: AbortSignal) { + const timeoutId = setTimeout(() => { + if (!signal.aborted) { + callback(); + } + }, delay); + + signal.addEventListener('abort', () => { + clearTimeout(timeoutId); + }); + + return timeoutId; +} + +export function delay(ms: number, abortSignal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (abortSignal) { + if (abortSignal.aborted) { + reject(new Error('Aborted')); + } + abortSignal.addEventListener('abort', () => { + reject(new Error('Aborted')); + }); + abortableTimeout(resolve, ms, abortSignal); + } else { + setTimeout(resolve, ms); + } }); } diff --git a/test/unit/from-timer.spec.ts b/test/unit/from-timer.spec.ts index aa85627..d436e9e 100644 --- a/test/unit/from-timer.spec.ts +++ b/test/unit/from-timer.spec.ts @@ -1,5 +1,7 @@ import {describe, expect, it} from "vitest"; import {fromTimer} from "../../src/index.ts"; +import {delay} from "../../src/utils.ts"; +import fromTimerAsync from "../../src/generators/from-timer.ts"; describe('fromTimer', () => { it('should return iterable of number every x milliseconds', async () => { @@ -25,4 +27,48 @@ describe('fromTimer', () => { } expect(numbers).toEqual([ 0, 4, 8 ]); }); + + it('should start with a delay', async () => { + const startTime = performance.now(); + const numbersStream = fromTimer(10, 20); + const numbers: number[] = []; + for await (const n of numbersStream) { + numbers.push(n); + if (numbers.length === 1) { + break; + } + } + const endTime = performance.now(); + expect(numbers).toEqual([0]); + expect(endTime - startTime).toBeGreaterThanOrEqual(20); + }); + + it('should stop the timer when iteration breaks', async () => { + let counter = 0; + const numbersStream = fromTimer(10); + for await (const _ of numbersStream) { + counter++; + if (counter === 2) { + break; + } + } + // Wait a bit to ensure no further emissions + await delay(100); + expect(counter).toEqual(2); + }); + + it('should stop if disposed is called', async () => { + const numbersStream = fromTimerAsync(10, 50); + const iterator = numbersStream[Symbol.asyncIterator](); + const dispose = numbersStream[Symbol.dispose]; + const numbers: number[] = []; + iterator.next().then(x => { + if (!x.done) { + numbers.push(x.value); + } + }); + dispose(); + await delay(100); // wait to check if something is not emitted. + expect(numbers).toStrictEqual([]); + }); });