@jenbuska/yielded
v2.1.0
Published
A TypeScript library for composing and transforming values from synchronous iterables and asynchronous generators through a uniform, lazy-evaluated pipeline API
Downloads
53
Maintainers
Readme
Yielded
A TypeScript library for composing and transforming values from synchronous iterables and asynchronous generators through a uniform, lazy-evaluated pipeline API.
Features
- 🔄 Lazy Evaluation - Operations don't process the entire sequence up front; each value flows through the pipeline one at a time
- 🚀 Parallel Processing - Built-in support for concurrent async operations with configurable concurrency limits
- 🔗 Unified API - Same familiar API for both sync iterables and async iterators
- 📦 Native Iterator Extension - Extends the native Iterator API with additional utilities
- 🛡️ Type-Safe - Full TypeScript support with comprehensive type inference
- 🎯 Cancelable - Integrated AbortSignal support for canceling async operations
- 🌊 Composable - Chain multiple operations together for complex data transformations
- 🪶 Zero Dependencies - No external runtime dependencies; installs nothing beyond the package itself
Compatibility
Node.js Requirements
This library requires Node.js 20.4.0 or newer (Node 20, 22, 24 LTS releases are all supported and tested in CI).
Why 20.4.0? The runtime cleanup mechanism relies on
Symbol.disposeandSymbol.asyncDispose, which were introduced in Node.js 20.4.0. The published bundle no longer uses theusingkeyword (it was replaced withtry/finally), so older versions of Node that didn't supportusingnow work fine.
Browser Support:
- ✅ Chrome 90+ (full ES2022+ support)
- ✅ Firefox 88+ (full ES2022+ support)
- ✅ Edge 90+ (full ES2022+ support)
- ✅ Safari — tested and working via WebKit (ES2022+ supported in modern Safari releases)
The published bundle targets ES2022 and has no runtime dependencies, so it works in all modern browsers without additional configuration.
TypeScript Configuration
No special TypeScript lib settings are needed specifically for this package. The table below shows what standard configs already cover everything:
| Environment | Recommended lib | Notes |
|-------------|-------------------|-------|
| Node.js | ["ES2022"] or ["ESNext"] | AbortSignal (for withSignal) is included via @types/node |
| Browser (React, Vue, etc.) | ["ES2022", "DOM"] | Standard browser config; AbortSignal comes from the DOM lib |
DOM.Iterableis NOT required by@jenbuska/yielded.
You may see it in theexamples/react-viteproject — that is standard boilerplate for Vite browser apps (it addsSymbol.iteratorsupport to DOM APIs such asNodeListandHTMLCollection) and has nothing to do with this library's types. Include it in your own config if your application code iterates over DOM collections directly.
Installation
npm install @jenbuska/yieldedQuick Start
import { Yielded } from "@jenbuska/yielded";
// Simple transformation pipeline
const result = Yielded.from([1, 2, 3, 4, 5])
.filter(n => n % 2 === 0)
.map(n => n * 2)
.toArray();
// => [4, 8]
// Async data processing with parallel operations
const customers = await Yielded.from(contractorIds)
.parallel(5) // Process up to 5 at a time
.flatMap(async id => await fetchCustomers(id))
.awaited() // Back to sequential processing
.filter(customer => customer.isActive)
.sorted((a, b) => a.name.localeCompare(b.name))
.take(10)
.toArray();Core Concepts
Lazy Evaluation
Operations in Yielded are lazy - they don't execute until you consume the result. This means you can chain multiple transformations efficiently without creating intermediate arrays:
Yielded.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.filter(n => n % 2 === 0) // Not executed yet
.map(n => n * 2) // Not executed yet
.take(3) // Not executed yet
.toArray(); // NOW all operations execute
// => [4, 8, 12]Creating Yielded Instances
// From arrays
Yielded.from([1, 2, 3])
// From any iterable
Yielded.from(new Set([1, 2, 3]))
Yielded.from("hello") // chars: 'h', 'e', 'l', 'l', 'o'
// From async iterables
Yielded.from(asyncGenerator())
// From promises
Yielded.from(Promise.resolve([1, 2, 3]))API Reference
Transformation Operations
Transform values as they flow through the pipeline:
map(mapper)
Transform each value with a mapping function.
Yielded.from([1, 2, 3])
.map(n => n * 2)
.toArray()
// => [2, 4, 6]filter(predicate)
Keep only values that pass the predicate test.
Yielded.from([1, 2, 3, 4, 5])
.filter(n => n % 2 === 0)
.toArray()
// => [2, 4]flatMap(mapper)
Map each value to an array/iterable and flatten the results.
Yielded.from([1, 2, 3])
.flatMap(n => [n, n * 2])
.toArray()
// => [1, 2, 2, 4, 3, 6]flat(depth?)
Flatten nested arrays up to the specified depth (default: 1).
Yielded.from([[1, 2], [3, [4, 5]]])
.flat()
.toArray()
// => [1, 2, 3, [4, 5]]Slicing Operations
Control which values pass through:
take(count)
Take only the first count values.
Yielded.from([1, 2, 3, 4, 5])
.take(3)
.toArray()
// => [1, 2, 3]drop(count)
Skip the first count values.
Yielded.from([1, 2, 3, 4, 5])
.drop(2)
.toArray()
// => [3, 4, 5]takeLast(count)
Take only the last count values.
Yielded.from([1, 2, 3, 4, 5])
.takeLast(2)
.toArray()
// => [4, 5]dropLast(count)
Drop the last count values.
Yielded.from([1, 2, 3, 4, 5])
.dropLast(2)
.toArray()
// => [1, 2, 3]takeWhile(predicate)
Take values while the predicate returns true, stop at the first false.
Yielded.from([2, 4, 6, 7, 8])
.takeWhile(n => n % 2 === 0)
.toArray()
// => [2, 4, 6]Sorting and Ordering
sorted(comparator?)
Sort all values using an optional comparator function.
Yielded.from([3, 1, 4, 1, 5])
.sorted()
.toArray()
// => [1, 1, 3, 4, 5]
Yielded.from(['banana', 'apple', 'cherry'])
.sorted((a, b) => a.localeCompare(b))
.toArray()
// => ['apple', 'banana', 'cherry']reversed()
Reverse the order of all values.
Yielded.from([1, 2, 3])
.reversed()
.toArray()
// => [3, 2, 1]Batching and Grouping
batch(size)
Group values into arrays of the specified size.
Yielded.from([1, 2, 3, 4, 5, 6, 7])
.batch(3)
.toArray()
// => [[1, 2, 3], [4, 5, 6], [7]]chunkBy(predicate)
Group consecutive values while the predicate returns the same key.
Yielded.from([1, 2, 2, 3, 3, 3, 4])
.chunkBy(n => n)
.toArray()
// => [[1], [2, 2], [3, 3, 3], [4]]groupBy(keySelector, groups?)
Group all values by the result of the key selector function.
Yielded.from([
{ type: 'fruit', name: 'apple' },
{ type: 'vegetable', name: 'carrot' },
{ type: 'fruit', name: 'banana' }
])
.groupBy(item => item.type)
// => {
// fruit: [{ type: 'fruit', name: 'apple' }, { type: 'fruit', name: 'banana' }],
// vegetable: [{ type: 'vegetable', name: 'carrot' }]
// }Async and Parallel Operations
parallel(concurrency)
Process async operations with limited concurrency. Returns a ParallelYielded instance.
Note: concurrency must be an integer between 1 and 50 (inclusive).
await Yielded.from([1, 2, 3, 4, 5])
.parallel(2) // Max 2 concurrent operations
.flatMap(async n => {
await delay(100);
return n * 2;
})
.awaited()
.toArray()
// => [2, 4, 6, 8, 10]awaited()
Convert parallel or async operations back to sequential async processing.
Yielded.from([Promise.resolve(1), Promise.resolve(2)])
.awaited()
.map(n => n * 2)
.toArray()Utility Operations
tap(callback)
Execute a side effect for each value without modifying the stream.
Yielded.from([1, 2, 3])
.tap(n => console.log('Processing:', n))
.map(n => n * 2)
.toArray()
// Logs: Processing: 1, Processing: 2, Processing: 3
// => [2, 4, 6]mapPairwise(mapper)
Transform consecutive pairs of values.
Yielded.from([1, 2, 3, 4])
.mapPairwise((prev, next) => next - prev)
.toArray()
// => [1, 1, 1]lift(middleware)
Apply a custom generator transformation.
Yielded.from([1, 2, 3])
.lift(function*(gen) {
for (const value of gen) {
yield value;
yield value * 10;
}
})
.toArray()
// => [1, 10, 2, 20, 3, 30]withSignal(signal)
Attach an AbortSignal to cancel async operations.
const controller = new AbortController();
const promise = Yielded.from(hugeDataset)
.parallel(10)
.flatMap(async item => await processItem(item))
.withSignal(controller.signal)
.toArray();
// Later: cancel the operation
controller.abort();Terminal Operations (Resolvers)
These operations consume the iterator and return a final result:
toArray()
Collect all values into an array.
Yielded.from([1, 2, 3]).toArray()
// => [1, 2, 3]toSet()
Collect all values into a Set.
Yielded.from([1, 2, 2, 3]).toSet()
// => Set { 1, 2, 3 }toSorted(comparator?)
Return a sorted array without modifying the original.
Yielded.from([3, 1, 2]).toSorted()
// => [1, 2, 3]toReversed()
Return a reversed array.
Yielded.from([1, 2, 3]).toReversed()
// => [3, 2, 1]reduce(reducer, initialValue?)
Reduce all values to a single value.
Note: Without initialValue, throws a TypeError if the iterator is empty.
Yielded.from([1, 2, 3, 4, 5])
.reduce((sum, n) => sum + n, 0)
// => 15
Yielded.from([1, 2, 3])
.reduce((max, n) => Math.max(max, n))
// => 3forEach(callback)
Execute a function for each value.
Yielded.from([1, 2, 3])
.forEach((n, index) => console.log(`${index}: ${n}`))
// Logs: 0: 1, 1: 2, 2: 3consume()
Consume all values without collecting them (useful for side effects).
Yielded.from([1, 2, 3])
.tap(n => saveToDatabase(n))
.consume()find(predicate)
Find the first value that passes the predicate.
Yielded.from([1, 2, 3, 4, 5])
.find(n => n > 3)
// => 4some(predicate)
Check if any value passes the predicate.
Yielded.from([1, 2, 3, 4, 5])
.some(n => n > 3)
// => trueevery(predicate)
Check if all values pass the predicate.
Yielded.from([2, 4, 6, 8])
.every(n => n % 2 === 0)
// => truefirst(defaultValue?)
Get the first value.
Note: Without defaultValue, throws a TypeError if the iterator is empty.
Yielded.from([1, 2, 3]).first()
// => 1
Yielded.from([]).first(0)
// => 0
Yielded.from([]).first()
// Throws TypeErrorlast(defaultValue?)
Get the last value.
Note: Without defaultValue, throws a TypeError if the iterator is empty.
Yielded.from([1, 2, 3]).last()
// => 3
Yielded.from([]).last(0)
// => 0
Yielded.from([]).last()
// Throws TypeErrorcount()
Count the total number of values.
Yielded.from([1, 2, 3, 4, 5]).count()
// => 5sumBy(selector?)
Sum all values, optionally using a selector function.
Yielded.from([1, 2, 3, 4, 5]).sumBy()
// => 15
Yielded.from([{x: 1}, {x: 2}, {x: 3}]).sumBy(obj => obj.x)
// => 6minBy(selector?, defaultValue?)
Find the minimum value, optionally using a selector function.
Note: Without defaultValue, throws a TypeError if the iterator is empty.
Yielded.from([3, 1, 4, 1, 5]).minBy(n => n)
// => 1
Yielded.from([{x: 3}, {x: 1}, {x: 2}]).minBy(obj => obj.x)
// => {x: 1}
Yielded.from<{x: number}>([]).minBy(obj => obj.x, 0)
// => 0
Yielded.from<{x: number}>([]).minBy(obj => obj.x)
// Throws TypeErrormaxBy(selector?, defaultValue?)
Find the maximum value, optionally using a selector function.
Note: Without defaultValue, throws a TypeError if the iterator is empty.
Yielded.from([3, 1, 4, 1, 5]).maxBy()
// => 5
Yielded.from([{x: 3}, {x: 1}, {x: 2}]).maxBy(obj => obj.x)
// => {x: 3}
Yielded.from<{x: number}>([]).maxBy(obj => obj.x, 0)
// => 0
Yielded.from<{x: number}>([]).maxBy((obj) => obj.x)
// Throws TypeErrorAdvanced Examples
Real-World Data Processing
import { Yielded } from "@jenbuska/yielded";
// Paginated API data fetching with parallel processing
async function getCustomersForOrganization(
organizationId: string,
pagination: PaginationArgs,
signal?: AbortSignal
) {
const { page, pageSize, sortBy, sortDirection } = pagination;
return Yielded.from(getContractors(organizationId))
// Allow up to 5 concurrent API calls
.parallel(5)
.flatMap(async (contractor) => {
const customers = await getContractorCustomers(
contractor.contractorId,
{ signal }
);
return customers.map(customer => ({
...customer,
contractorId: contractor.contractorId
}));
})
// Back to sequential processing
.awaited()
.filter(customer => customer.isActive)
.sorted(createComparator({ sortBy, sortDirection }))
.drop(page * pageSize)
.take(pageSize)
.withSignal(signal)
.toArray();
}Stream Processing with Error Handling
async function processLogFiles(files: string[]) {
return Yielded.from(files)
.parallel(3)
.flatMap(async (filename) => {
try {
const content = await readFile(filename);
return parseLogEntries(content);
} catch (error) {
console.error(`Error processing ${filename}:`, error);
return [];
}
})
.awaited()
.filter(entry => entry.level === 'ERROR')
.groupBy(entry => entry.timestamp.slice(0, 10)) // Group by date
.toArray();
}Infinite Sequences
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take only what you need from infinite sequences
const firstTenFibs = Yielded.from(fibonacci())
.take(10)
.toArray();
// => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]TypeScript Support
Yielded is written in TypeScript and provides full type inference:
import { Yielded, type IYielded, type IAsyncYielded } from "@jenbuska/yielded";
// Types are automatically inferred
const numbers: IYielded<number> = Yielded.from([1, 2, 3]);
const strings = numbers.map(n => n.toString()); // IYielded<string>
const evens = numbers.filter((n): n is 2 | 4 => n % 2 === 0); // Type narrowing works!
// Async types
const asyncNumbers: IAsyncYielded<number> = Yielded.from(asyncGenerator());Performance Considerations
- Lazy evaluation means operations only execute when needed, saving CPU cycles
- Memory efficient - processes one item at a time instead of creating intermediate arrays
- Parallel processing - leverage concurrency for I/O-bound async operations
- Native methods - uses native Iterator methods when available for better performance
When to Use Yielded
✅ Good use cases:
- Processing large datasets that don't fit in memory
- Async data pipelines with multiple transformation steps
- Stream processing with lazy evaluation
- Parallel async operations with concurrency control
❌ Consider alternatives:
- Simple array operations where you need all data immediately (use native array methods)
- Very small datasets where overhead isn't worth it
- When you need random access to elements
Browser and Node.js Support
Node.js (CI-tested on all active LTS releases):
- ✅ Node.js 20 (maintenance LTS, 20.4.0+ required for
Symbol.dispose) - ✅ Node.js 22 (active LTS)
- ✅ Node.js 24 (current)
Browsers (all tested with Playwright in CI):
- ✅ Chrome 90+
- ✅ Firefox 88+
- ✅ Edge 90+
- ✅ Safari — works out of the box (WebKit, no polyfills needed)
Contributing
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
License
MIT © jEnbuska
