From bab19bcd37252f9483e361e46a7e005149658ede Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sat, 18 Oct 2025 23:46:37 +0200 Subject: [PATCH 1/4] feat: add fromTimer and fromEvent async creators --- src/creation.ts | 2 ++ src/generators/from-event.ts | 51 ++++++++++++++++++++++++++++++++++++ src/generators/from-timer.ts | 28 ++++++++++++++++++++ src/utils.ts | 6 +++++ test/unit/from-event.spec.ts | 20 ++++++++++++++ test/unit/from-timer.spec.ts | 16 +++++++++++ tsconfig.json | 5 +++- 7 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/generators/from-event.ts create mode 100644 src/generators/from-timer.ts create mode 100644 test/unit/from-event.spec.ts create mode 100644 test/unit/from-timer.spec.ts diff --git a/src/creation.ts b/src/creation.ts index 991739a..f5c3c77 100644 --- a/src/creation.ts +++ b/src/creation.ts @@ -43,6 +43,8 @@ export function from(source: Iterable | ArrayLike | TVal return fromObject(source as object); } + + function isIterable(o: any): o is Iterable { const iterator = o[Symbol.iterator]; return typeof iterator === 'function'; diff --git a/src/generators/from-event.ts b/src/generators/from-event.ts new file mode 100644 index 0000000..f6d9426 --- /dev/null +++ b/src/generators/from-event.ts @@ -0,0 +1,51 @@ +import {doneValue, iteratorResultCreator} from "../utils.ts"; + +export function fromEvent(target: TTarget, event: TEvent): AsyncIterable & AsyncDisposable { + const eventQueue: HTMLElementEventMap[TEvent][] = []; + const resolverQueue: ((result: IteratorResult) => void)[] = []; + + const eventHandler = (e: Event) => { + const eventValue = e as HTMLElementEventMap[TEvent]; + + const nextResolver = resolverQueue.shift(); + if (nextResolver) { + nextResolver(iteratorResultCreator(eventValue)); + } else { + eventQueue.push(eventValue); + } + }; + + target.addEventListener(event, eventHandler); + return { + [Symbol.asyncIterator]() { + return { + next(): Promise> { + const nextEvent = eventQueue.shift(); + if (nextEvent) { + return Promise.resolve(iteratorResultCreator(nextEvent)); + } + + return new Promise((resolve) => { + resolverQueue.push(resolve); + }); + }, + + return(): Promise> { + target.removeEventListener(event, eventHandler); + resolverQueue.length = 0; + return Promise.resolve(doneValue()); + }, + throw(error: any) { + target.removeEventListener(event, eventHandler); + resolverQueue.length = 0; + return Promise.reject(error); + } + }; + }, + [Symbol.asyncDispose]: () => { + target.removeEventListener(event, eventHandler); + resolverQueue.length = 0; + return Promise.resolve(); + } + }; +} \ No newline at end of file diff --git a/src/generators/from-timer.ts b/src/generators/from-timer.ts new file mode 100644 index 0000000..ea7b62d --- /dev/null +++ b/src/generators/from-timer.ts @@ -0,0 +1,28 @@ +import {delay as sleep} from "../utils.ts"; + +export function fromTimer(interval: number, delay?: number): AsyncIterable & AsyncDisposable { + let done = false; + return { + [Symbol.asyncIterator]: async function* () { + let i = 0; + if (delay) { + await sleep(delay); + if (done) { + return; + } + yield i++; + } + while (true) { + if (done) { + break; + } + await sleep(interval); + yield i++; + } + }, + [Symbol.asyncDispose]() { + done = true; + return Promise.resolve(); + } + } +} diff --git a/src/utils.ts b/src/utils.ts index c1da2fd..a3b2334 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -182,3 +182,9 @@ export function createIterable(generator: () => Generator): Iterable { [Symbol.iterator]: generator, } } + +export function delay(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/test/unit/from-event.spec.ts b/test/unit/from-event.spec.ts new file mode 100644 index 0000000..95e121e --- /dev/null +++ b/test/unit/from-event.spec.ts @@ -0,0 +1,20 @@ +import {describe, expect, it} from "vitest"; +import {fromEvent} from "../../src/generators/from-event.js"; + +describe('fromEvent', () => { + it('should get events', async () => { + const target = new EventTarget(); + await using eventsStream = fromEvent(target, 'click'); + setTimeout(() => target.dispatchEvent(new Event('click')), 10); + setTimeout(() => target.dispatchEvent(new Event('click')), 20); + setTimeout(() => target.dispatchEvent(new Event('click')), 30); + const events: Event[] = []; + for await (const event of eventsStream) { + events.push(event); + if (events.length === 3) { + break; + } + } + expect(events.length).toEqual(3); + }); +}); diff --git a/test/unit/from-timer.spec.ts b/test/unit/from-timer.spec.ts new file mode 100644 index 0000000..d9bb19e --- /dev/null +++ b/test/unit/from-timer.spec.ts @@ -0,0 +1,16 @@ +import {describe, expect, it} from "vitest"; +import {fromTimer} from "../../src/generators/from-timer.ts"; + +describe('fromTimer', () => { + it('should return iterable of number every x milliseconds', async () => { + await using numbersStream = fromTimer(100); + const numbers: number[] = []; + for await (const n of numbersStream) { + numbers.push(n); + if (numbers.length === 3) { + break; + } + } + expect(numbers.length).toEqual(3); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 965ed76..9bbbe5e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,10 @@ "module": "nodenext", "target": "esnext", "lib": [ - "esnext" + "esnext", + "ESNext.AsyncIterable", + "DOM", + "DOM.AsyncIterable" ], "declaration": true, "strict": true, From a759b2e7857c1a5ac251961bf0fb45159ab019e0 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 19 Oct 2025 11:43:21 +0200 Subject: [PATCH 2/4] feat: update grouping to produce FluentIterable instead of Iterable results Signed-off-by: vmladenov --- index.d.ts | 6 +++--- src/fluent.ts | 21 ++++++++++++++++----- src/iterables/group-join.ts | 8 +++++--- src/iterables/group.ts | 28 ++++++---------------------- src/utils.ts | 6 ++++-- test/benchmark/start.ts | 6 +++--- test/integration/tests.spec.ts | 4 ++-- test/unit/group-join.spec.ts | 3 ++- test/unit/group.spec.ts | 24 ++++++++++++------------ test/unit/page.spec.ts | 2 +- 10 files changed, 54 insertions(+), 54 deletions(-) diff --git a/index.d.ts b/index.d.ts index e25edf3..2ddbeca 100644 --- a/index.d.ts +++ b/index.d.ts @@ -136,7 +136,7 @@ declare module 'fluent-iter' { 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: Iterable) => TResult): FluentIterable; + resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterable; /** * Order iterable ascending by a key @@ -162,7 +162,7 @@ declare module 'fluent-iter' { groupJoin(joinIterable: Iterable, sourceKeySelector: (item: TValue) => TKey, joinIterableKeySelector: (item: TInner, index: number) => TKey, - resultCreator: (outer: TValue, inner: TInner[]) => TResult): FluentIterable; + resultCreator: (outer: TValue, inner: FluentIterable) => TResult): FluentIterable; /** * Do an inner join between current and external sequence. For each item of current sequence get a item from external sequence. @@ -420,7 +420,7 @@ declare module 'fluent-iter' { isElementsEqual(iterable: Iterable, comparer: (a: TValue, b: TAnotherValue) => boolean): boolean; } - export interface IGrouping extends Iterable { + export interface IGrouping extends FluentIterable { key: TKey; } diff --git a/src/fluent.ts b/src/fluent.ts index 52c03cc..8469f0e 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -38,7 +38,6 @@ import zipIterable from "./iterables/zip.js"; import type {Action, Comparer, Mapper, Predicate} from "./interfaces.ts"; import type { FluentIterable, IGrouping } from 'fluent-iter'; - export default class Fluent implements FluentIterable { readonly #source: Iterable; @@ -88,10 +87,10 @@ export default class Fluent implements FluentIterable { } 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: Iterable) => TResult): FluentIterable; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterable; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector?: (item: TValue, index: number) => TElement, - resultCreator?: (key: TKey, items: Iterable) => TResult): FluentIterable | IGrouping | TResult> { + 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 { @@ -103,8 +102,8 @@ export default class Fluent implements FluentIterable { groupJoin(joinIterable: Iterable, sourceKeySelector: (item: TValue) => TKey, joinIterableKeySelector: (item: TInner, index: number) => TKey, - resultCreator: (outer: TValue, inner: TInner[]) => TResult): FluentIterable { - return new Fluent(groupJoinIterator(this, joinIterable, sourceKeySelector, joinIterableKeySelector, resultCreator)); + 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, @@ -244,3 +243,15 @@ export default class Fluent implements FluentIterable { return this.#source[Symbol.iterator](); } } + +export class Grouping extends Fluent implements IGrouping { + readonly #key: TKey; + constructor(key: TKey, items: Iterable) { + super(items); + this.#key = key; + } + + public get key() { + return this.#key; + } +} diff --git a/src/iterables/group-join.ts b/src/iterables/group-join.ts index 0101072..e9f2635 100644 --- a/src/iterables/group-join.ts +++ b/src/iterables/group-join.ts @@ -1,11 +1,13 @@ +import { FluentIterable } from "fluent-iter"; import {createIterable, defaultElementSelector, group} from "../utils.ts"; +import {from} from "../creation.ts"; export default function groupJoinIterator( source: Iterable, joinIterable: Iterable, sourceKeySelector: (sourceItem: TThis) => TKey, joinIterableKeySelector: (otherItem: TOther, index: number) => TKey, - resultCreator: (first: TThis, second: TOther[]) => TResult): Iterable { + resultCreator: (first: TThis, second: FluentIterable) => TResult): Iterable { return createIterable(() => groupJoinGenerator(source, joinIterable, sourceKeySelector, joinIterableKeySelector, resultCreator)); } @@ -14,11 +16,11 @@ function* groupJoinGenerator( joinIterable: Iterable, sourceKeySelector: (sourceItem: TThis) => TKey, joinIterableKeySelector: (otherItem: TOther, index: number) => TKey, - resultCreator: (first: TThis, second: TOther[]) => TResult): Generator { + resultCreator: (first: TThis, second: FluentIterable) => TResult): Generator { const innerMap = group(joinIterable, joinIterableKeySelector, defaultElementSelector); for (const outerItem of source) { const key = sourceKeySelector(outerItem); - const innerItems = innerMap.get(key) || []; + const innerItems = innerMap.get(key) || from([]); yield resultCreator(outerItem, innerItems); } } diff --git a/src/iterables/group.ts b/src/iterables/group.ts index 17c3868..463d2fc 100644 --- a/src/iterables/group.ts +++ b/src/iterables/group.ts @@ -1,11 +1,12 @@ -import type {IGrouping} from "fluent-iter"; -import {createIterable, getIterator, group} from "../utils.ts"; +import type {FluentIterable, IGrouping} from "fluent-iter"; +import {createIterable, group} from "../utils.ts"; +import {Grouping} from "../fluent.js"; export function groupByIterator( source: Iterable, keySelector: (item: TValue, index: number) => TKey, elementSelector?: (item: TValue, index: number) => TElement, - resultCreator?: (key: TKey, items: Iterable) => TResult): Iterable | IGrouping | TResult> { + resultCreator?: (key: TKey, items: FluentIterable) => TResult): Iterable | IGrouping | TResult> { return createIterable(() => groupByGenerator(source, keySelector, elementSelector, resultCreator)); } @@ -13,9 +14,9 @@ function* groupByGenerator( source: Iterable, keySelector: (item: TValue, index: number) => TKey, elementSelector?: (item: TValue, index: number) => TElement, - resultCreator?: (key: TKey, items: Iterable) => TResult): Generator | IGrouping | TResult> { + resultCreator?: (key: TKey, items: FluentIterable) => TResult): Generator | IGrouping | TResult> { const elementSelect: (item: TValue, index: number) => TElement = elementSelector ?? ((item) => item as unknown as TElement); - const resultCreate: (key: TKey, items: Iterable) => TResult = resultCreator ?? + const resultCreate: (key: TKey, items: FluentIterable) => TResult = resultCreator ?? ((key, items) => new Grouping(key, items) as unknown as TResult); const groups = group(source, keySelector, elementSelect); @@ -23,20 +24,3 @@ function* groupByGenerator( yield resultCreate(key, items); } } - -export class Grouping implements IGrouping { - readonly #key: TKey; - readonly #items: Iterable; - constructor(key: TKey, items: Iterable) { - this.#key = key; - this.#items = items; - } - - public get key() { - return this.#key; - } - - [Symbol.iterator]() { - return getIterator(this.#items); - } -} diff --git a/src/utils.ts b/src/utils.ts index a3b2334..47321de 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,6 @@ import {Comparer} from "./interfaces.ts"; +import {from, fromIterable} from "./creation.js"; +import { FluentIterable } from "fluent-iter"; /** * Helper function to be use to access Symbol.iterator of iterable @@ -160,7 +162,7 @@ export function emptyIterator(): Iterator { export function group( iterable: Iterable, keySelector: (item: TValue, index: number) => TKey, - elementSelector: (item: TValue, index: number) => TElement): Map { + elementSelector: (item: TValue, index: number) => TElement): Map> { const map = new Map(); let i = 0; for (const item of iterable) { @@ -174,7 +176,7 @@ export function group( map.set(key, value); i++; } - return map; + return fromIterable(map).toMap(([key, _]) => key as TKey, ([_, value]) => from(value)); } export function createIterable(generator: () => Generator): Iterable { diff --git a/test/benchmark/start.ts b/test/benchmark/start.ts index f3e8ac6..43ed8a7 100644 --- a/test/benchmark/start.ts +++ b/test/benchmark/start.ts @@ -37,7 +37,7 @@ suit.on("complete", function (this: Benchmark[]) { .groupBy( (_, i) => i, (_) => _.reverse().join(""), - (_, items) => from(items).first(), + (_, items) => items.first(), ) .reverse() .join(","); @@ -72,13 +72,13 @@ suit.on("complete", function (this: Benchmark[]) { (_) => _.category, (_) => _, (key, items) => { - const fastestBench = from(items).max( + const fastestBench = items.max( (a, b) => a.bench.hz - b.bench.hz, ); return { name: key, fastest: getBenchData(fastestBench), - benches: from(items) + benches: items .orderByDescending((_) => _.bench.hz) .select(getBenchData), }; diff --git a/test/integration/tests.spec.ts b/test/integration/tests.spec.ts index a847f2a..2f3c9fa 100644 --- a/test/integration/tests.spec.ts +++ b/test/integration/tests.spec.ts @@ -50,7 +50,7 @@ describe("typescript tests", () => { .groupBy((_) => _.owner.age) .select((gr) => ({ age: gr.key, - countPets: from(gr).count(), + countPets: gr.count(), })) .orderBy((_) => _.age) .toMap( @@ -383,7 +383,7 @@ describe("typescript tests", () => { .groupBy(i => i.odd) .select(_ => ({ key: _.key, - items: from(_).orderByDescending(_ => _.num).toArray() + items: _.orderByDescending(_ => _.num).toArray() })) .orderBy(_ => _.key); diff --git a/test/unit/group-join.spec.ts b/test/unit/group-join.spec.ts index 4084f92..1e304bc 100644 --- a/test/unit/group-join.spec.ts +++ b/test/unit/group-join.spec.ts @@ -25,7 +25,8 @@ describe('groupJoin tests', () => { { persons: persons, pets: new Set(pets) }, ].forEach((source, indx) => { it('should join arrays: ' + indx, () => { - const res = from(source.persons).groupJoin(source.pets, s => s.name, i => i.owner, (person, pets) => ({ person, pets })).toArray(); + const res = from(source.persons) + .groupJoin(source.pets, s => s.name, i => i.owner, (person, pets) => ({ person, pets: pets.toArray() })).toArray(); expect(res).toEqual([ { person: persons[0], pets: [ pets[1] ] }, { person: persons[1], pets: [ pets[2], pets[3] ] }, diff --git a/test/unit/group.spec.ts b/test/unit/group.spec.ts index ed3ede9..3ea3fb9 100644 --- a/test/unit/group.spec.ts +++ b/test/unit/group.spec.ts @@ -23,15 +23,15 @@ describe('groupBy tests', () => { for (const gr of res) { switch (gr.key) { case 10: { - expect(Array.from(gr)).toEqual([ input[0], input[2], input[4] ]); + expect(gr.toArray()).toEqual([ input[0], input[2], input[4] ]); break; } case 20: { - expect(Array.from(gr)).toEqual([ input[1], input[5] ]); + expect(gr.toArray()).toEqual([ input[1], input[5] ]); break; } case 30: { - expect(Array.from(gr)).toEqual([ input[3] ]); + expect(gr.toArray()).toEqual([ input[3] ]); break; } default: @@ -51,18 +51,18 @@ describe('groupBy tests', () => { for (const gr of res) { switch (gr.key) { case 10: { - expect(from(gr).count()).toBe(3); - expect(from(gr).toArray()).toEqual(['A', 'C', 'E']); + expect(gr.count()).toBe(3); + expect(gr.toArray()).toEqual(['A', 'C', 'E']); break; } case 20: { - expect(from(gr).count()).toBe(2); - expect(from(gr).toArray()).toEqual(['B', 'F']); + expect(gr.count()).toBe(2); + expect(gr.toArray()).toEqual(['B', 'F']); break; } case 30: { - expect(from(gr).count()).toBe(1); - expect(from(gr).toArray()).toEqual(['D']); + expect(gr.count()).toBe(1); + expect(gr.toArray()).toEqual(['D']); break; } default: @@ -77,14 +77,14 @@ describe('groupBy tests', () => { setInput ].forEach((source, indx) => { it('should group collection and convert result: ' + indx, () => { - const res = fromIterable(source).groupBy(_ => _.age, _ => _.name, (key, elms) => `${key}:${Array.from(elms).join(',')}`).toArray(); + const res = fromIterable(source).groupBy(_ => _.age, _ => _.name, (key, elms) => `${key}:${elms.join(',')}`).toArray(); expect(res.length).toBe(3); expect(res).toEqual(['10:A,C,E', '20:B,F', '30:D']); }); }); it('should be able to work with strings', () => { - const res = from('abcdeabcdebbacc').groupBy(_ => _).where(g => from(g).count() < 4).select(g => g.key).join(''); + const res = from('abcdeabcdebbacc').groupBy(_ => _).where(g => g.count() < 4).select(g => g.key).join(''); expect(res).toBe('ade'); }); @@ -100,7 +100,7 @@ describe('groupBy tests', () => { it('should not throw if group by null or undefined', () => { const res = from('abcdeabcdebbacc') - .groupBy(_ => _ === 'a' ? null : (_ === 'b' ? undefined : _), _ => _, (key, group) => ({ key: key, group: Array.from(group) })) + .groupBy(_ => _ === 'a' ? null : (_ === 'b' ? undefined : _), _ => _, (key, group) => ({ key: key, group: group.toArray() })) .toArray(); expect(res).toEqual([ { key: null, group: ['a', 'a', 'a'] }, diff --git a/test/unit/page.spec.ts b/test/unit/page.spec.ts index 2eeed8a..a87ba6a 100644 --- a/test/unit/page.spec.ts +++ b/test/unit/page.spec.ts @@ -22,7 +22,7 @@ describe('page iteration tests', () => { it('should page string', () => { const res = from('abcdefd').page(3) - .groupBy((arr, i) => i, _ => _, (key, items) => from(items).firstOrThrow().join('')) + .groupBy((arr, i) => i, _ => _, (key, items) => items.firstOrThrow().join('')) .join(','); expect(res).toBe('abc,def,d'); }); From 33e4c8107af4d99d43ba7fc47ae9341258a8d343 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 19 Oct 2025 11:48:38 +0200 Subject: [PATCH 3/4] feat: fluent async Introdution of fluent async funtionality - fromEvent method: AsyncIterable returning events - fromTimer method: AsyncIterable returning numbers sequence based on interval Signed-off-by: vmladenov --- index.d.ts | 22 ++++++++++++++++++++++ index.ts | 4 ++++ src/creation-async.ts | 12 ++++++++++++ src/fluent-async.ts | 24 ++++++++++++++++++++++++ src/fluent.ts | 4 ++-- src/generators/from-event.ts | 2 +- src/generators/from-timer.ts | 2 +- src/index.ts | 1 + src/iterables/select.ts | 13 ++++++++++++- src/iterables/where.ts | 14 +++++++++++++- test/unit/from-event.spec.ts | 2 +- test/unit/from-timer.spec.ts | 16 ++++++++++++++-- 12 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 src/creation-async.ts create mode 100644 src/fluent-async.ts diff --git a/index.d.ts b/index.d.ts index 2ddbeca..0675efd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -424,6 +424,25 @@ declare module 'fluent-iter' { key: TKey; } + export interface FluentIterableAsync extends AsyncIterable { + /** + * Filters the iterable using predicate function typed overload + * @param predicate + */ + where(predicate: (item: TValue) => item is TSubValue): FluentIterableAsync; + + /** + * Filters the iterable using predicate function + * @param predicate + */ + where(predicate: (item: TValue) => boolean): FluentIterableAsync; + /** + * Maps the iterable items + * @param map map function + */ + select(map: (item: TValue) => TOutput): FluentIterableAsync; + } + export function from(iterable: Iterable | ArrayLike): FluentIterable; export function from(value: TValue): FluentIterable<{ key: string, value: TValue[TKey] }>; @@ -435,4 +454,7 @@ 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; } \ No newline at end of file diff --git a/index.ts b/index.ts index faccaae..be40e62 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,12 @@ export { + // sync fromIterable, fromObject, fromArrayLike, range, from, repeat, + // async + fromEvent, + fromTimer, } from "./src/index.ts"; diff --git a/src/creation-async.ts b/src/creation-async.ts new file mode 100644 index 0000000..09dbe85 --- /dev/null +++ b/src/creation-async.ts @@ -0,0 +1,12 @@ +import type { FluentIterableAsync } from 'fluent-iter'; +import FluentAsync from "./fluent-async.js"; +import fromEventAsync from "./generators/from-event.ts"; +import fromTimerAsync from "./generators/from-timer.js"; + +export function fromEvent(target: TTarget, event: TEvent): FluentIterableAsync { + return new FluentAsync(fromEventAsync(target, event)); +} + +export function fromTimer(interval: number, delay?: number): FluentIterableAsync { + return new FluentAsync(fromTimerAsync(interval, delay)); +} diff --git a/src/fluent-async.ts b/src/fluent-async.ts new file mode 100644 index 0000000..07a0bdf --- /dev/null +++ b/src/fluent-async.ts @@ -0,0 +1,24 @@ +import type { FluentIterableAsync } from 'fluent-iter'; +import type {Mapper, Predicate} from "./interfaces.js"; +import {whereAsyncIterator} from "./iterables/where.js"; +import {selectIteratorAsync} from "./iterables/select.js"; + +export default class FluentAsync implements FluentIterableAsync { + 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 { + return new FluentAsync(whereAsyncIterator(this, predicate)); + } + select(map: Mapper): FluentIterableAsync { + return new FluentAsync(selectIteratorAsync(this, map)); + } + [Symbol.asyncIterator](): AsyncIterator { + return this.#source[Symbol.asyncIterator](); + } +} diff --git a/src/fluent.ts b/src/fluent.ts index 8469f0e..2581ed2 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -1,5 +1,5 @@ -import whereIterator from "./iterables/where.ts"; -import selectIterator from "./iterables/select.ts"; +import { whereIterator } from "./iterables/where.ts"; +import { selectIterator } from "./iterables/select.ts"; import selectManyIterator from "./iterables/select-many.ts"; import takeIterator from "./iterables/take.ts"; import skipIterator from "./iterables/skip.ts"; diff --git a/src/generators/from-event.ts b/src/generators/from-event.ts index f6d9426..f3bdb8f 100644 --- a/src/generators/from-event.ts +++ b/src/generators/from-event.ts @@ -1,6 +1,6 @@ import {doneValue, iteratorResultCreator} from "../utils.ts"; -export function fromEvent(target: TTarget, event: TEvent): AsyncIterable & AsyncDisposable { +export default function fromEventAsync(target: TTarget, event: TEvent): AsyncIterable & AsyncDisposable { const eventQueue: HTMLElementEventMap[TEvent][] = []; const resolverQueue: ((result: IteratorResult) => void)[] = []; diff --git a/src/generators/from-timer.ts b/src/generators/from-timer.ts index ea7b62d..8c8f0e6 100644 --- a/src/generators/from-timer.ts +++ b/src/generators/from-timer.ts @@ -1,6 +1,6 @@ import {delay as sleep} from "../utils.ts"; -export function fromTimer(interval: number, delay?: number): AsyncIterable & AsyncDisposable { +export default function fromTimerAsync(interval: number, delay?: number): AsyncIterable & AsyncDisposable { let done = false; return { [Symbol.asyncIterator]: async function* () { diff --git a/src/index.ts b/src/index.ts index 66f80aa..094071e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export { fromIterable, fromObject, fromArrayLike, range, from, repeat } from './creation.ts'; +export { fromEvent, fromTimer } from './creation-async.ts'; \ No newline at end of file diff --git a/src/iterables/select.ts b/src/iterables/select.ts index aced8fa..bfab9b8 100644 --- a/src/iterables/select.ts +++ b/src/iterables/select.ts @@ -4,7 +4,7 @@ import {createIterable} from "../utils.ts"; /** * Return mapped array [1, 2, 3].select(x => x * 2) === [2, 4, 6] */ -export default function selectIterator(input: Iterable, map: Mapper): Iterable { +export function selectIterator(input: Iterable, map: Mapper): Iterable { return createIterable(() => selectGenerator(input, map)); } @@ -13,3 +13,14 @@ function* selectGenerator(input: Iterable, map: Mapper< yield map(item); } } + +export function selectIteratorAsync(input: AsyncIterable, map: Mapper): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + for await (const item of input) { + yield map(item); + } + } + } +} + diff --git a/src/iterables/where.ts b/src/iterables/where.ts index 4f1a80d..8389f91 100644 --- a/src/iterables/where.ts +++ b/src/iterables/where.ts @@ -3,7 +3,7 @@ import type {Predicate} from "../interfaces.ts"; /** * Return filtered array [1, 2, 3, 4].where(x => x % 2 === 0) === [2, 4] */ -export default function whereIterator(input: Iterable, predicate: Predicate): Iterable { +export function whereIterator(input: Iterable, predicate: Predicate): Iterable { return { [Symbol.iterator]: function* () { for (const item of input) { @@ -14,3 +14,15 @@ export default function whereIterator(input: Iterable, predicate } } } + +export function whereAsyncIterator(input: AsyncIterable, predicate: Predicate): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + for await (const item of input) { + if (predicate(item)) { + yield item; + } + } + } + } +} diff --git a/test/unit/from-event.spec.ts b/test/unit/from-event.spec.ts index 95e121e..3840b10 100644 --- a/test/unit/from-event.spec.ts +++ b/test/unit/from-event.spec.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from "vitest"; -import {fromEvent} from "../../src/generators/from-event.js"; +import fromEvent from "../../src/generators/from-event.ts"; describe('fromEvent', () => { it('should get events', async () => { diff --git a/test/unit/from-timer.spec.ts b/test/unit/from-timer.spec.ts index d9bb19e..aa85627 100644 --- a/test/unit/from-timer.spec.ts +++ b/test/unit/from-timer.spec.ts @@ -1,9 +1,9 @@ import {describe, expect, it} from "vitest"; -import {fromTimer} from "../../src/generators/from-timer.ts"; +import {fromTimer} from "../../src/index.ts"; describe('fromTimer', () => { it('should return iterable of number every x milliseconds', async () => { - await using numbersStream = fromTimer(100); + const numbersStream = fromTimer(100); const numbers: number[] = []; for await (const n of numbersStream) { numbers.push(n); @@ -13,4 +13,16 @@ describe('fromTimer', () => { } expect(numbers.length).toEqual(3); }); + + it('should return iterable of number every x milliseconds', async () => { + const numbersStream = fromTimer(2).where(n => n % 2 === 0).select(n => n * 2); + const numbers: number[] = []; + for await (const n of numbersStream) { + numbers.push(n); + if (numbers.length === 3) { + break; + } + } + expect(numbers).toEqual([ 0, 4, 8 ]); + }); }); From 18543f94090c54bf5e2769fb349126e3ba61166a Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 19 Oct 2025 12:06:41 +0200 Subject: [PATCH 4/4] feat: add really useful take and toArray methods - take: read N values from AsyncIterable - toArray: returns a promise to an array Signed-off-by: vmladenov --- index.d.ts | 11 +++++++++++ src/finalizers/to-array.ts | 14 +++++++++++++- src/fluent-async.ts | 10 ++++++++++ src/fluent.ts | 4 ++-- src/iterables/take.ts | 38 ++++++++++++++++++++++++++------------ test/unit/to-array.spec.ts | 10 ++++++++++ 6 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 test/unit/to-array.spec.ts diff --git a/index.d.ts b/index.d.ts index 0675efd..8193086 100644 --- a/index.d.ts +++ b/index.d.ts @@ -441,6 +441,17 @@ declare module 'fluent-iter' { * @param map map function */ select(map: (item: TValue) => TOutput): FluentIterableAsync; + + /** + * Take first N items from iterable + */ + take(count: number): FluentIterableAsync; + + /** + * Return a promise to an array. + */ + toArray(): Promise; + toArray(map: (item: TValue) => TResult): Promise; } export function from(iterable: Iterable | ArrayLike): FluentIterable; diff --git a/src/finalizers/to-array.ts b/src/finalizers/to-array.ts index 97f1d36..0a82e5d 100644 --- a/src/finalizers/to-array.ts +++ b/src/finalizers/to-array.ts @@ -1,9 +1,21 @@ import type {Mapper} from "../interfaces.ts"; -export default function toArrayCollector(source: Iterable, map?: Mapper): T[] | R[] { +export function toArrayCollector(source: Iterable, map?: Mapper): T[] | R[] { if (!map) { return Array.from(source); } else { return Array.from(source).map(map); } } + +export async function toArrayAsyncCollector(source: AsyncIterable, map?: Mapper): Promise<(T|R)[]> { + const result: (T|R)[] = []; + for await (const item of source) { + if (map) { + result.push(map(item)); + } else { + result.push(item); + } + } + return result; +} diff --git a/src/fluent-async.ts b/src/fluent-async.ts index 07a0bdf..5e45c47 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -2,6 +2,8 @@ import type { FluentIterableAsync } from 'fluent-iter'; import type {Mapper, Predicate} from "./interfaces.js"; import {whereAsyncIterator} from "./iterables/where.js"; import {selectIteratorAsync} from "./iterables/select.js"; +import takeIteratorAsync, {takeIterator} from "./iterables/take.js"; +import {toArrayAsyncCollector} from "./finalizers/to-array.js"; export default class FluentAsync implements FluentIterableAsync { readonly #source: AsyncIterable; @@ -18,6 +20,14 @@ export default class FluentAsync implements FluentIterableAsync select(map: Mapper): FluentIterableAsync { return new FluentAsync(selectIteratorAsync(this, map)); } + take(count: number): FluentIterableAsync { + return new FluentAsync(takeIteratorAsync(this, count)); + } + toArray(): Promise; + toArray(map: Mapper): Promise; + toArray(map?: Mapper): Promise<(TValue|TResult)[]> { + return toArrayAsyncCollector(this, map); + } [Symbol.asyncIterator](): AsyncIterator { return this.#source[Symbol.asyncIterator](); } diff --git a/src/fluent.ts b/src/fluent.ts index 2581ed2..0f7e232 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -1,9 +1,9 @@ import { whereIterator } from "./iterables/where.ts"; import { selectIterator } from "./iterables/select.ts"; import selectManyIterator from "./iterables/select-many.ts"; -import takeIterator from "./iterables/take.ts"; +import { takeIterator } from "./iterables/take.ts"; import skipIterator from "./iterables/skip.ts"; -import toArrayCollector from "./finalizers/to-array.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"; diff --git a/src/iterables/take.ts b/src/iterables/take.ts index f0f02c2..6c393ca 100644 --- a/src/iterables/take.ts +++ b/src/iterables/take.ts @@ -1,20 +1,34 @@ -import {createIterable} from "../utils.ts"; - /** * Return first N numbers of source */ -export default function takeIterator(input: Iterable, count: number): Iterable { - return createIterable(() => takeGenerator(input, count)); +export function takeIterator(input: Iterable, count: number): Iterable { + return { + [Symbol.iterator]: function* () { + let fetched = 0; + for (const item of input) { + if (fetched < count) { + yield item; + fetched++; + } else { + break; + } + } + } + } } -function* takeGenerator(input: Iterable, count: number): Generator { - let fetched = 0; - for (const item of input) { - if (fetched < count) { - yield item; - fetched++; - } else { - break; +export default function takeIteratorAsync(input: AsyncIterable, count: number): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + let fetched = 0; + for await (const item of input) { + if (fetched < count) { + yield item; + fetched++; + } else { + break; + } + } } } } diff --git a/test/unit/to-array.spec.ts b/test/unit/to-array.spec.ts new file mode 100644 index 0000000..a2ffa67 --- /dev/null +++ b/test/unit/to-array.spec.ts @@ -0,0 +1,10 @@ +import {describe, expect, it} from "vitest"; +import {fromTimer} from "../../src/index.js"; + +describe('to array', () => { + it('should get an array from async iterable', async () => { + const arr = await fromTimer(5, 0).take(3).toArray(); + + expect(arr).toEqual([0, 1, 2]); + }); +});