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),
])
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.
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:
| Event | Description |
|---|---|
get | A key was requested via get(key) |
dedup | Request was deduplicated (same key already pending) |
schedule | A batch was scheduled for dispatch |
dispatch | Batch started executing with its collected keys |
resolve | Batch completed successfully |
error | Batch failed with an error |
abort | Batch was aborted via signal or abort() |