Query Batching
Write createBatcher(fetchMany). It returns a function that takes a single id and returns a promise for that id's value.
Every id requested before the next microtask is collected and passed to fetchMany as one array. fetchMany(ids) resolves to an array of values in the same order, and each caller receives only its own.
Examples
const load = createBatcher(async (ids) => {
console.log('one request for', ids);
return ids.map((id) => 'user-' + id);
});
await Promise.all([load(1), load(2), load(3)]);one request for [1, 2, 3]
['user-1', 'user-2', 'user-3']Constraints
- fetchMany resolves to an array the same length as its input
Notes
- This is what DataLoader does, and how a GraphQL server avoids the N+1 problem.
- Swap the queue for a fresh array before awaiting, or later calls join a batch that has already gone.
Hints
Query Batching (reference solution)
One way to do it. The Editorial tab walks through why it is written this way.
Solution
1function createBatcher(fetchMany) {
2 let queue = [];
3 return (id) => new Promise((resolve, reject) => {
4 queue.push({ id, resolve, reject });
5 if (queue.length > 1) return;
6 Promise.resolve().then(() => {
7 const batch = queue;
8 queue = [];
9 fetchMany(batch.map((item) => item.id)).then(
10 (values) => batch.forEach((item, i) => item.resolve(values[i])),
11 (error) => batch.forEach((item) => item.reject(error))
12 );
13 });
14 });
15}Editorial: Query Batching
One request instead of fifty
A list of fifty rows, each asking for its own author, produces fifty requests. Batching collapses them: collect every id requested in the same tick, ask once, and hand each caller its slice.
Approach
A queue plus a scheduled flush. The first push schedules; later pushes in the same tick just join.
Implementation
function createBatcher(fetchMany) { let queue = []; return (id) => new Promise((resolve, reject) => { queue.push({ id, resolve, reject }); if (queue.length > 1) return; Promise.resolve().then(() => { const batch = queue; queue = []; fetchMany(batch.map((item) => item.id)).then( (values) => batch.forEach((item, i) => item.resolve(values[i])), (error) => batch.forEach((item) => item.reject(error)) ); }); }); }
Worth knowing
Promise.resolve().then(...) — a microtask — runs after the current synchronous block but before any timer, so everything requested "in the same tick" is caught without adding a millisecond of latency.
Swapping queue for a fresh array before awaiting matters: calls made while the request is in flight must start a new batch rather than joining one that has already left. This is what DataLoader does, and how a GraphQL server avoids the N+1 problem.