UNPKG

iterates

Version:

Iterator and AsyncIterator helper functions with typings

691 lines (609 loc) 16 kB
import { asIterable as asSyncIterable } from './sync.js'; import { autoCurry, curry2, curry2WithOptions } from './utils.js'; if (Symbol.asyncIterator === undefined) { ; Symbol.asyncIterator = Symbol(); } /** @internal */ export const isIterator = iterator => { return typeof iterator[Symbol.asyncIterator] === 'undefined'; }; /** @internal */ export const asIterable = asyncIterator => { if (isIterator(asyncIterator)) { return { [Symbol.asyncIterator]: () => asyncIterator }; } else { return asyncIterator; } }; /** * Turns a syncronous Iterable or Iterator to an asyncronous Iterable. */ export async function* asAsyncIterable(iterator) { for (const value of asSyncIterable(iterator)) { yield value; } } /** * Collect all values of the iterator in an Array. */ export async function asArray(iterator) { const array = []; for await (const value of asIterable(iterator)) { array.push(value); } return array; } /** * Creates in iterable that yields and then ends when the promise resolves */ export async function* fromPromise(promise) { yield await promise; } class Subscriber { items = []; onValue = () => {}; constructor(dispose) { this.dispose = dispose; let lastNext = Promise.resolve(); this.iterator = { next: async () => { await lastNext; return lastNext = new Promise((resolve, reject) => { if (this.items.length > 0) { const item = this.items.shift(); if (item.isError) { reject(item.error); } else { resolve({ done: item.done, value: item.value }); } return; } this.onValue = () => { const item = this.items.shift(); if (item.isError) { reject(item.error); } else { resolve({ done: item.done, value: item.value }); } }; }); } }; } pushNext(item) { this.items.push({ done: false, isError: false, value: item }); this.onValue(); } pushThrow(error) { this.items.push({ done: false, isError: true, error }); this.dispose(); this.onValue(); } done() { this.items.push({ done: true, isError: false }); this.dispose(); this.onValue(); } } /** * A Subject is an AsyncIterable which yields values that are pushed to the Subject. * * The Subject can be seen as an EventEmitter, allowing a producer to * push values to one or more consumers * * ## Example * ```typescript * const timestamps = new Subject<Date>() * * setInterval(() => timestamps.next(new Date()), 1000) * * for await (const timestamp of timestamps) { * console.log(timestamp); // Will log the current time every second * } * ``` */ export class Subject { _subscribers = new Set(); _isDisposed = false; _checkDisposed() { if (this._isDisposed) throw Error('The Subject have been disposed'); } /** * Push an item into the subject to yield to iterators */ next(item) { this._checkDisposed(); for (const subscriber of this._subscribers) { subscriber.pushNext(item); } } /** * Throw an error to the iterators */ throw(error) { this._checkDisposed(); for (const subscriber of this._subscribers) { subscriber.pushThrow(error); } this._subscribers.clear(); } /** * Ends the iterators * * Efter calling done the Subject is dispased and can no longer be used */ done() { this._checkDisposed(); for (const subscriber of this._subscribers) { subscriber.done(); } this._subscribers.clear(); this._isDisposed = true; } [Symbol.asyncIterator]() { const subscriber = new Subscriber(() => { this._subscribers.delete(subscriber); }); this._subscribers.add(subscriber); return subscriber.iterator; } } /** * Creates an iterator which gives the current iteration count * as well as the next value. * * The iterator returned yields objects of {index, item}, where index is the * current index of iteration and item is the value returned by the iterator. * * ## Example * ```typescript * [...enumerate(['a', 'b', 'c'])] * // [{index: 0, item: 'a'}, {index: 1, item: 'b'}, {index: 2, item: 'c'}] * ``` */ export function enumerate(asyncIterator) { let index = 0; return map(item => ({ index: index++, item }), asyncIterator); } /** * Calls fn for every item in the iterator to produce an iterator * with the results of fn(item) * * ## Example * ```typescript * [...map(e => e*e, [1, 2, 3])] // [1, 4, 9] * ``` */ export const map = curry2(async function* map(fn, asyncIterator) { for await (const item of asIterable(asyncIterator)) { yield await fn(item); } }); /** * Like map but filter out undefined results * * ## Example * ```typescript * [...flatMap(e => e % 2 === 0 ? undefined : e*e, [1, 2, 3])] // [1, 9] * ``` * * ## Why not map(filter())? * filterMap is functionally equivalent to map(filter()) but can give * cleaner code and higher performance for cases where filtering is applied * to the mapped value and not the input value. */ export const filterMap = curry2(async function* filterMap(fn, asyncIterator) { for await (const item of asIterable(asyncIterator)) { const innerItem = await fn(item); if (innerItem !== undefined) { yield innerItem; } } }); /** * Like map but flattens the result a single level * * ## Example * ```typescript * [...flatMap(e => [e, e*e], [1, 2, 3])] // [1, 1, 2, 4, 3, 9] * ``` */ export const flatMap = curry2(async function* flatMap(fn, asyncIterator) { for await (const item of asIterable(asyncIterator)) { for await (const innerItem of asIterable(fn(item))) { yield await innerItem; } } }); /** * Flattens an iterator a single level * * ## Example * ```typescript * [...flatten([[1, 2], [], [3]])] // [1, 2, 3] * ``` */ export const flatten = async function* flatten(asyncIterator) { for await (const item of asIterable(asyncIterator)) { for await (const innerItem of asIterable(item)) { yield innerItem; } } }; /** * Calls predicate for every item in the iterator to produce an iterator * with only the items for which predicate returns true * * ## Example * ```typescript * [...filter(e => e > 2, [1, 2, 3])] // [2, 3] * ``` */ export const filter = curry2(async function* filter(fn, asyncIterator) { for await (const item of asIterable(asyncIterator)) { if (fn(item)) { yield item; } } }); /** * Reduces an iterator to a single value by iteratively combining each * item of the iterator with an existing value * * Uses initialValue as the initial value, then iterates through the elements * and updates the value with each element using the combine function. * * If the iterator is empty, the initialValue is returned. * * ## Example * ```typescript * fold(0, (sum, item) => sum + item, [1, 2, 3]) // 6 * ``` */ export const fold = autoCurry(async function fold(initialValue, combine, asyncIterator) { let value = initialValue; for await (const item of asIterable(asyncIterator)) { value = combine(value, await item); } return value; }); /** * Like fold, scan iteratively combines each item of the iterator with * an existing value. Scan does however yield each intermediate value. * * If the iterator is empty, no value is yielded. * * ## Example * ```typescript * scan(0, (sum, item) => sum + item, [1, 2, 3]) // [1, 3, 6] * ``` */ export const scan = autoCurry(async function* scan(initialValue, combine, asyncIterator) { let value = initialValue; for await (const item of asIterable(asyncIterator)) { value = combine(value, await item); yield value; } return value; }); /** * Transforms an iterator into a Map. * * Calls `fn` for every item in the iterator. `fn` should return a tuple of `[key, value]` * for that item. * * If multiple items returns the same key the latter will overwrite the former. * This behavior can be changed by passing `merge` in the options object. * * `merge` takes the current value, the new value and the key and should return a combined * value. It can also throw to dissallow multiple items returning the same key. * * ## Example * ```typescript * collect(e => [e, e*e], [1, 2, 3]) // Map {1 => 1, 2 => 4, 3 => 9} * ``` * * ### Using merge * ```typescript * collect( * e => [e % 2 === 0 ? 'even' : 'odd', [e]], * [1, 2, 3], * {merge: (a, b) => a.concat(b)} * ) * // Map {'odd' => [1, 3], 'even' => [2]} * ``` */ export const collect = curry2WithOptions(async function collect(fn, iterator, { merge } = {}) { const collection = new Map(); for await (const item of asIterable(iterator)) { const [key, value] = await fn(item); if (merge !== undefined && collection.has(key)) { collection.set(key, merge(collection.get(key), value, key)); } else { collection.set(key, value); } } return collection; }); /** * Transforms an iterator into an Object. * * Calls `fn` for every item in the iterator. `fn` should return a tuple of `[key, value]` * for that item. * * If multiple items returns the same key the latter will overwrite the former. * This behavior can be changed by passing `merge` in the options object. * * `merge` takes the current value, the new value and the key and should return a combined * value. It can also throw to dissallow multiple items returning the same key. * * ## Example * ```typescript * collectRecord(e => [e, e*e], [1, 2, 3]) // {1: 1, 2: 4, 3: 9} * ``` * * ### Using merge * ```typescript * collectRecord( * e => [e % 2 === 0 ? 'even' : 'odd', [e]], * [1, 2, 3], * {merge: (a, b) => a.concat(b)} * ) * // {odd: [1, 3], even: [2]} * ``` */ export const collectRecord = curry2WithOptions(async function collect(fn, iterator, { merge } = {}) { const record = {}; for await (const item of asIterable(iterator)) { const [key, value] = await fn(item); if (merge !== undefined && key in record) { record[key] = merge(record[key], value, key); } else { record[key] = value; } } return record; }); /** * Zips two iterators by taking the next value of each iterator as a tuple * * If the two iterators have a different length, it will zip until the first iterator ends * and then end with a return value of the longer iterator. * * ## Example * ```typescript * [...zip([1, 2, 3], ['a', 'b', 'c']) // [[1, 'a'], [2, 'b'], [3, 'c']] * ``` */ export const zip = curry2(async function* zip(a, b) { const iterableA = asIterable(a); const iteratorB = asIterable(b)[Symbol.asyncIterator](); for await (const itemA of iterableA) { const { done, value: itemB } = await iteratorB.next(); if (done) { return a; } else { yield [await itemA, await itemB]; } } const { done } = await iteratorB.next(); if (!done) return b; }); /** * Creates an iterator that only yields the first item yielded by the source * iterator during a window lasting the specified duration * * ## Example * ```typescript * const timestamps = new Subject<Date>() * const throttledTimestamps = throttle(2000, timestamps) * * setInterval(() => timestamps.next(new Date()), 1000) * * for await (const timestamp of throttledTimestamps) { * console.log(timestamp); // Will log the current time every other second * } * ``` */ export const throttle = curry2(async function* throttle(duration, asyncIterator) { let hasItem = false; let lastItem = undefined; let lastYield = 0; for await (const item of asIterable(asyncIterator)) { const now = Date.now(); if (lastYield === 0 || now - lastYield >= duration) { hasItem = false; lastItem = undefined; lastYield = now; yield item; } else { lastItem = item; hasItem = true; } } if (hasItem) { yield lastItem; } }); /** * Returns true if predicate returns true for every item in the iterator * * Returns true if the iterator is empty * * ## Example * ```typescript * await all(e => e > 1, [1, 2, 3]) // false * await all(e => e > 0, [1, 2, 3]) // true * await all(e => e > 1, []) // true * ``` */ export const all = curry2(async function all(predicate, asyncIterator) { for await (const item of asIterable(asyncIterator)) { if (!(await predicate(item))) { return false; } } return true; }); /** * Returns true if predicate returns true for any item in the iterator * * Returns false if the iterator is empty * * ## Example * ```typescript * await any(e => e > 1, [1, 2, 3]) // true * await any(e => e > 3, [1, 2, 3]) // false * await any(e => e > 1, []) // false * ``` */ // tslint:disable-next-line:variable-name export const any = curry2(async function any(predicate, asyncIterator) { for await (const item of asIterable(asyncIterator)) { if (await predicate(item)) { return true; } } return false; }); /** * Calls fn for every item and returns the first item for which fn returns true * * Returns undefined if fn return fals for all items or if the iterator is empty * * ## Example * ```typescript * await find(e => e > 1, [1, 2, 3]) // 2 * ``` */ export const find = curry2(async function find(fn, asyncIterator) { for await (const item of asIterable(asyncIterator)) { if (await fn(item)) { return item; } } }); /** * Consumes an iterator, creating two Arrays from it * * The predicate passed to partition() can return true, or false. * partition() returns a pair, all of the elements for which it returned * true, and all of the elements for which it returned false. * * ## Example * ```typescript * const [even, odd] = await partition(e => e % 2 === 0, [1, 2, 3]) * * expect(even).toEqual([2]) * expect(odd).toEqual([1, 3]) * ``` */ export const partition = curry2(async function partition(fn, iterator) { const passing = []; const failing = []; for await (const item of asIterable(iterator)) { if (fn(item)) { passing.push(item); } else { failing.push(item); } } return [passing, failing]; }); /** * Returns the first value of the iterator or undefined if it's empty * * ## Example * ```typescript * await first([1, 2, 3]) // 1 * ``` */ export async function first(asyncIterator) { for await (const item of asIterable(asyncIterator)) { return item; } } /** * Returns an iterator with the first count values of the iterator * * The returned iterator may hold fewer than `count` values if the * iterator contains less items than `count` * * ## Example * ```typescript * await take(2, [1, 2, 3]) // [1, 2] * ``` */ export const take = curry2(async function* find(count, asyncIterator) { if (count <= 0) return; let index = 0; for await (const item of asIterable(asyncIterator)) { yield item; index++; if (index >= count) break; } }); /** * Yields values until the notifier iterable yields a value */ export const takeUntil = curry2(async function* find(notifier, asyncIterator) { let didResolve = false; const returnValue = asIterable(notifier)[Symbol.asyncIterator]().next().then(returnValue => { didResolve = true; return returnValue; }); for await (const item of asIterable(asyncIterator)) { if (didResolve) break; yield item; } if (didResolve) { return await returnValue; } }); /** * Returns the last value of the iterator or undefined if it's empty * * ## Example * ```typescript * await last([1, 2, 3]) // 3 * ``` */ export async function last(asyncIterator) { let lastItem; for await (const item of asIterable(asyncIterator)) { lastItem = item; } return lastItem; }