Package Exports
- doddle
Readme
Doddle
Doddle is a tiny yet feature-packed library for working with sync and async iterables. It’s inspired by LINQ, lodash, and rxjs.
Here are its features:
- 🤏 Tiny bundle size, without compromising user experience.
- 🔥 Packed with operators from the best APIs in software.
- 🤗 Strongly typed and extensively validated. Throws meaningful errors too.
- 🪞 One consistent API shared between sync and async iterables.
- 🔍 Produces concise, readable stack traces.
Get it now:
# yarn
yarn add doddle
# npm
npm install doddleThe Doddle
The Doddle is the library’s lazy primitive. It represents a delayed computation, just like a function.
When a Doddle is pulled, it’s evaluated and the result is cached. Evaluation only happens once.
The Doddle is designed like a Promise. It chains, flattens, and supports several useful operators. Its key method is pull(). It’s designed for both sync and async computations.
You can create one using the doddle function, passing it a function that will produce the value. This function can return a Promise.
import { doddle } from "doddle"
const d = doddle(() => {
console.log("evaluated when pulled")
return 5
})
d.pull() // 5Doddles are used throughout the sequence API, but they really come in handy outside it too. Read more about them here!
Seq
This wrapper unifies iterables and generator functions. You create one using the seq function.
You can pass this function an Iterable, like an array:
seq([1, 2, 3])Or a generator function:
seq(function* () {
yield 1
yield 2
})seq is a lot more flexible than that, though. You can even pass it a function that returns an array instead of a generator:
seq(() => [1, 2, 3])You can pass it a Doddle that returns an Iterable too:
const doddle1 = doddle(() => 1)
const doddle123 = doddle(() => [1, 2, 3])
seq(doddle(() => [1, 2, 3]))You can also pass it an array-like object:
const s3 = seq({
0: 1,
1: 1,
2: 3,
length: 3
}) // {1, 2, 3}But you can’t pass it a string!
Although strings are iterable, they’re rarely used that way, and treating them as collections just causes lots of bugs.
seq doesn’t accept strings and trying to pass one will error both during compilation and at runtime:
// ‼️ DoddleError: Strings not allowed
seq("this will error")
// TypeScript: Type `string` is not assignable to type ...Operators
The Seq wrapper comes with a comprehensive set of operators. These are all instance methods, making them easy to discover and call.
They never iterate more than they need to and they produce legible stack traces (there is almost always one entry per operator).
They're also Lazy. That means they return one of two things:
- Another Seq, which needs to be iterated for anything to happen.
- A Doddle, which needs to be pulled.
This lets you control exactly when the operation is computed.
ASeq
This wrapper unifies async iterables and async generator functions, while also supporting any input that Seq supports. You create one using the aseq function.
You can pass it an async generator:
aseq(async function* () {
yield 1
yield 2
})An array:
aseq([1, 2, 3])An async iterable:
aseq(aseq([1]))An async function that returns an array:
aseq(async () => [1, 2, 3])Or even an async function that returns an async Iterable:
aseq(async () => aseq([1, 2, 3]))Operators
The ASeq wrapper has the same API as Seq, except that all inputs can be async. That means:
- You can pass async iterables instead of regular ones.
- You can pass functions that return promises.
ASeq wrapper comes with a carbon copy of all the operators defined on Seq, but tweaked for inputs that can be async.
So, for example, ASeq.map accepts async functions and automatically flattens the resulting promise:
const example = aseq([1, 2, 3]).map(async x => x + 1)
for await (const x of example) {
// 1, 2, 3
console.log(x)
}Using in async code
You’ll often find yourself using aseq inside an async function, after awaiting something.
Here is an example:
async function example() {
const strings = await getStrings()
return aseq(strings).map(x => x.toUpperCase())
}But this code returns a Promise<ASeq<string>>, which is really annoying to work with.
There is a better way, though! You can just put the await inside the definition of the sequence, like this:
function example() {
return aseq(async () => {
const strings = await getStrings()
return aseq(strings).map(x => x.toUpperCase())
})
}The aseq function will flatten the entire thing, giving you a simple ASeq<string>.
Sequential
Lots of ASeq operators, like ASeq.map and ASeq.each, support functions that return promises.
When ASeq encounters one, it will await the resulting promise before continuing to iterate over the input. This happens even if the return value of the function isn’t used, like with the each operator:
import { setTimeout } from "timers/promises"
aseq([1, 2, 3]).each(async x => {
await setTimeout(100)
})This makes ASeq much easier to work with than observables, since stack traces will always point to where the error occurred and elements will always be yielded in the same order.
However, it does mean that ASeq is bad at async processing with I/O that can be parallelized, like this:
import { got } from "got"
aseq([url1, url2, url3]).map(url => got(url))