[=] batchkit |

batchkit

Automatic batching for async operations.

After installation, create a batcher like this:

import { batch } from 'batchkit'

const users = batch(
  (ids) => db.users.findMany({ where: { id: { in: ids } } }),
  'id'
)

// All calls in the same tick become one db query
await Promise.all([
  users.get(1),
  users.get(2),
  users.get(3),
])

Try it in the playground →

Creating a Batcher

const users = batch(fn, match, options?)

fn is your batch function. It receives an array of keys and should return the corresponding results:

const users = batch(
  async (ids: number[]) => {
    return db.users.findMany({ where: { id: { in: ids } } })
  },
  'id'
)

The function also receives an AbortSignal as a second argument, which you can pass to fetch or other cancellable APIs:

const users = batch(
  async (ids, signal) => {
    return fetch(`/api/users?ids=${ids}`, { signal }).then(r => r.json())
  },
  'id'
)

Matching Results

match tells batchkit how to connect each result back to the key that requested it.

By field name

The most common case. If your results have an id field:

batch(fn, 'id')

batchkit finds the result where result.id === requestedKey.

By index (object responses)

When your API returns an object keyed by ID:

{ "1": { "name": "Alice" }, "2": { "name": "Bob" } }

Use indexed:

import { batch, indexed } from 'batchkit'

batch(fn, indexed)

Custom matching

For complex cases, pass a function:

batch(fn, (results, key) => {
  return results.find(r => r.externalId === key.id)
})

How Batching Works

When you call .get(), the request is queued. At the end of the current microtask, all queued requests are dispatched together.

// These three calls...
users.get(1)
users.get(2)
users.get(3)

// ...become one call to your batch function:
fn([1, 2, 3], signal)

Duplicate keys within the same batch are automatically deduplicated. If you call users.get(1) twice, the key 1 is only fetched once, but both promises resolve to the same value.

Timing Control

Delay dispatching

By default, batches dispatch immediately (next microtask). Use wait to collect requests over a time window:

batch(fn, 'id', { wait: 50 })  // Wait 50ms before dispatching

This is useful when requests arrive over time (user scrolling, animations) rather than all at once.

Custom schedulers

For advanced control:

import { batch, onAnimationFrame, onIdle } from 'batchkit'

// Sync with browser rendering
batch(fn, 'id', { schedule: onAnimationFrame })

// Low-priority background work
batch(fn, 'id', { schedule: onIdle({ timeout: 100 }) })

Batch Size Limits

Some APIs limit how many items you can request at once. Use max to split large batches:

batch(fn, 'id', { max: 100 })

If 250 keys are queued, batchkit makes three calls: 100, 100, then 50.

Cancellation

Cancel everything

users.abort()

All pending and in-flight requests reject with an AbortError. The signal passed to your batch function is also aborted.

Cancel one request

Pass an AbortSignal to .get():

const controller = new AbortController()
const promise = users.get(1, { signal: controller.signal })

controller.abort()  // This request rejects, others continue

If all requests in a batch are aborted, the underlying fetch is aborted too.

Force dispatch

Skip the scheduler and dispatch immediately:

users.flush()

Debugging

Client DevTools

Batchkit offers a user interface for debugging your batchers in real-time with native packages for React and Svelte.

Learn more →

Server-side tracing

For non-browser environments, you can manually trace your batchers with the trace option.

const users = batch(fn, 'id', {
  name: 'users',
  trace: (event) => console.log(event.type, event)
})

Trace propagates the following events:

EventDescription
getA key was requested via get(key)
dedupRequest was deduplicated (same key already pending)
scheduleA batch was scheduled for dispatch
dispatchBatch started executing with its collected keys
resolveBatch completed successfully
errorBatch failed with an error
abortBatch was aborted via signal or abort()