JSPM

  • Created
  • Published
  • Downloads 1004
  • Score
    100M100P100Q84842F
  • License MIT

The nicest way to work with (async) iterables.

Package Exports

  • doddle

Readme

Doddle

Doddle workflow status Doddle package version Doddle Coveralls Doddle minified size(gzip)

Doddle is a tiny yet feature-packed library for iteration and lazy evaluation, inspired by lodash, LINQ, and rxjs.

  • 🤏 Tiny bundle size — without compromising user experience.

  • 🧰 Packed with operators from the best APIs in software.

  • 📜 Clear and detailed error messages.

  • 🔍 Built for debuggability, with readable stack traces and navigable source code.

  • 🧪 With over 1000 test cases, ensuring the consistency of both runtime code and type declarations.

Get it now:

yarn add doddle

Doddle

The library’s flagship lazy primitive. Commonly used throughout the API, but designed to be as convenient as possible.

Represents a computation that may not have happened yet. To make it produce a value you call its pull method.

import { doddle } from "doddle"

const d = doddle(() => {
    console.log("evaluated when pulled")
    return 5
})

d.pull() // 5

More Info...

Seq

The Seq wrapper unifies synchronous iterables and generator functions. You create one using the seq constructor function.

This function accepts lots of different inputs that can be interpreted as an Iterable. For example:

import { seq } from "doddle"

// # Array
const s1 = seq([1, 2, 3]) // {1, 2, 3}

// # Generator function
const s2 = seq(function* () {
    yield 1
    yield 2
    yield 3
}) // {1, 2}

// # Array-like object
const s3 = seq({
    0: 1,
    1: 1,
    2: 3,
    length: 3
}) // {1, 2, 3}

Wrapping something using seq has no side-effects.

Operators

The Seq wrapper comes with a comprehensive set of operators. These operators all have the same traits:

  • Instance: They’re instance methods, making them easier to use and discover.
  • Careful: They never iterate more than you tell them to.
  • Flexible: Accept flexible inputs, the same way as seq. They also interop seamlessly with the Doddle lazy primitive.
  • Debuggable: Produce legible stack traces; written in debug-friendly code.

In addition, all operators are Lazy. The return one of two things:

  • Another Seq, which has to be iterated for anything to happen.
  • A Doddle, which must be pulled explicitly to compute the operation.

This separates defining a computation from executing it. It also means that many operators work just fine with infinite inputs.

⚠️ string inputs

A pretty common bug happens when a string gets passed where a collection is expected. This usually doesn't cause an error, and instead you get s,t,u,f,f, ,l,i,k,e, ,t,h,i,s.

Doddle doesn't do this. Both its type declarations and runtime logic exclude string inputs.

If you want this behavior, convert the string into an array first. One of these should work:

seq("hello world".split(""))
seq([..."abc"])

ASeq

This wrapper is an async version of Seq, built for asynchronous iterables and similar objects. You create one using the aseq constructor function.

This function also accepts everything that seq accepts, plus async variations on those things. That includes:

import { aseq, seq } from "doddle"

// An array
aseq([1, 2, 3]) // {1, 2, 3}

// Generator function
aseq(function* () {
    yield* [1, 2, 3]
}) // {1, 2, 3}

// Async generator function
aseq(async function* () {
    yield* [1, 2, 3]
}) // {1, 2, 3}

// Function returning an array
aseq(() => [1, 2, 3])

// Async function returning an array
aseq(async () => [1, 2, 3])

Operators

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, flattening them into the resulting async Iterable:

aseq([1, 2, 3]).map(async x => x + 1) // {2, 3, 4}

These operators have the same names and functionality, with the bonus of accepting async variations.

Non-concurrent

ASeq will always await all promises before continuing the iteration. This means that something like this will wait 100ms before each element is yielded:

function sleep(t: number) {
    return new Promise(resolve => setTimeout(resolve, t))
}
await aseq([1, 2, 3])
    .map(async x => {
        await sleep(100)
        return x
    })
    .toArray()
    .pull()

As opposed to something like this, which will wait only 100ms in total:

await Promise.all(
    [1, 2, 3].map(async x => {
        await sleep(100)
        return x
    })
)

This is by design, since it has several advantages:

  • Elements will never be yielded out of order.
  • It doesn’t require a cache to keep concurrent promises.
  • The logic of each operator is identical to its sync counterpart, enabling code reuse.
  • It results in legible stack traces and debuggable code.

That does mean that aseq can’t be used for concurrent processing. That functionality is saved for a future out-of-order, highly concurrent version of aseq that's currently in the works.