js-09-pool
1.000
Challenge · difficulty 5/5
# Concurrency-limited async pool
Implement an ES module **`solution.js`** (no external libraries):
```js
export async function pool(thunks, concurrency) { /* ... */ }
```
`thunks` is an array of zero-argument functions, each returning a promise (async thunks).
Run them with **at most `concurrency` running at the same time**, and resolve with an array
of their results **in the original order of `thunks`** (not completion order).
Requirements:
- The result at index `i` must be the resolved value of `thunks[i]`.
- At no point may more than `concurrency` thunks be in flight simultaneously.
- As soon as one thunk settles, the next pending thunk should start (keep the pool full).
- An empty `thunks` array resolves to `[]`.
- You may assume `concurrency >= 1`.
Notes:
- Do not simply run everything via `Promise.all(thunks.map(...))` — that ignores the limit.
- You do not need to handle rejections specially (a rejecting thunk may reject the pool).
Example:
```js
const order = [];
const make = (id, ms) => () =>
new Promise((res) => setTimeout(() => { order.push(id); res(id); }, ms));
await pool([make("a", 30), make("b", 10), make("c", 20)], 2);
// => ["a", "b", "c"] (results in original order, regardless of finish order)
```
tests/solution.test.js
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { pool } from "./solution.js";
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
// Build instrumented thunks that track peak concurrency.
function makeTracker() {
const state = { active: 0, peak: 0, starts: [] };
const thunk = (id, ms) => async () => {
state.active++;
state.peak = Math.max(state.peak, state.active);
state.starts.push(id);
await sleep(ms);
state.active--;
return id;
};
return { state, thunk };
}
test("results are returned in original order, not completion order", async () => {
const { thunk } = makeTracker();
// 'a' is slowest but must still come first in the results.
const out = await pool([thunk("a", 30), thunk("b", 5), thunk("c", 15)], 3);
assert.deepEqual(out, ["a", "b", "c"]);
});
test("peak concurrency never exceeds the limit", async () => {
const { state, thunk } = makeTracker();
const thunks = [];
for (let i = 0; i < 10; i++) thunks.push(thunk(i, 10));
const out = await pool(thunks, 3);
assert.deepEqual(out, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
assert.ok(state.peak <= 3, `peak was ${state.peak}, expected <= 3`);
assert.equal(state.peak, 3); // pool should actually reach the limit
});
test("concurrency of 1 runs strictly sequentially", async () => {
const { state, thunk } = makeTracker();
const thunks = [thunk("x", 10), thunk("y", 10), thunk("z", 10)];
const out = await pool(thunks, 1);
assert.deepEqual(out, ["x", "y", "z"]);
assert.equal(state.peak, 1);
});
test("empty thunks -> empty array", async () => {
const out = await pool([], 4);
assert.deepEqual(out, []);
});
test("concurrency larger than number of thunks", async () => {
const { state, thunk } = makeTracker();
const thunks = [thunk(1, 5), thunk(2, 5)];
const out = await pool(thunks, 10);
assert.deepEqual(out, [1, 2]);
assert.equal(state.peak, 2);
});
test("all thunks actually run exactly once", async () => {
let count = 0;
const thunks = [];
for (let i = 0; i < 6; i++) {
thunks.push(async () => {
count++;
await sleep(2);
return i * i;
});
}
const out = await pool(thunks, 2);
assert.deepEqual(out, [0, 1, 4, 9, 16, 25]);
assert.equal(count, 6);
});
test("pool keeps the slots full as tasks finish", async () => {
// Mix of fast and slow tasks; with limit 2, a fast finisher should let a new
// task start while a slow one is still running -> peak hits 2 repeatedly.
const { state, thunk } = makeTracker();
const thunks = [
thunk("a", 40),
thunk("b", 5),
thunk("c", 5),
thunk("d", 5),
];
const out = await pool(thunks, 2);
assert.deepEqual(out, ["a", "b", "c", "d"]);
assert.equal(state.peak, 2);
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.Proposed solution
```javascript
export async function pool(thunks, concurrency) {
const results = new Array(thunks.length);
let nextIndex = 0;
let running = 0;
let completed = 0;
let rejected = false;
return new Promise((resolve, reject) => {
const run = () => {
while (running < concurrency && nextIndex < thunks.length) {
const i = nextIndex++;
running++;
const p = Promise.resolve(thunks[i]());
p.then(value => {
results[i] = value;
})
.catch(err => {
if (!rejected) {
rejected = true;
reject(err);
}
})
.finally(() => {
running--;
completed++;
if (completed === thunks.length) {
if (!rejected) {
resolve(results);
}
} else if (!rejected) {
run();
}
});
}
};
run();
if (completed === thunks.length && !rejected) {
resolve(results);
}
});
}
```Test output (stdout)
TAP version 13
# Subtest: results are returned in original order, not completion order
ok 1 - results are returned in original order, not completion order
---
duration_ms: 30.708866
type: 'test'
...
# Subtest: peak concurrency never exceeds the limit
ok 2 - peak concurrency never exceeds the limit
---
duration_ms: 42.712081
type: 'test'
...
# Subtest: concurrency of 1 runs strictly sequentially
ok 3 - concurrency of 1 runs strictly sequentially
---
duration_ms: 32.004771
type: 'test'
...
# Subtest: empty thunks -> empty array
ok 4 - empty thunks -> empty array
---
duration_ms: 0.241041
type: 'test'
...
# Subtest: concurrency larger than number of thunks
ok 5 - concurrency larger than number of thunks
---
duration_ms: 5.102988
type: 'test'
...
# Subtest: all thunks actually run exactly once
ok 6 - all thunks actually run exactly once
---
duration_ms: 5.736519
type: 'test'
...
# Subtest: pool keeps the slots full as tasks finish
ok 7 - pool keeps the slots full as tasks finish
---
duration_ms: 40.657991
type: 'test'
...
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 184.322933
Reasoning (chain-of-thought)
The user wants an ES module `solution.js` that exports an async function `pool(thunks, concurrency)`.
Key requirements:
1. `thunks` is an array of functions that return promises.
2. Limit concurrent execution to `concurrency`.
3. Return results in the same order as `thunks`.
4. Start new thunks as soon as slots free up.
5. Handle empty array.
6. No external libraries.
Algorithm:
- Create an array `results` of the same length as `thunks` to store results by index.
- Maintain a queue of pending thunks (indices).
- Maintain a count of currently running thunks.
- Start up to `concurrency` thunks immediately.
- When a thunk finishes, store its result in `results[index]`, decrement running count, and start the next one from the queue if available.
- Wait until all thunks are done.
Implementation details:
- Use a pointer `nextIndex` to track the next thunk to start.
- Use a loop or recursive function to manage the pool.
- Since we need to wait for all to finish, we can use `Promise.all` on the results array, or just wait until `results` is fully populated.
Let's refine the logic:
```javascript
export async function pool(thunks, concurrency) {
const results = new Array(thunks.length);
let nextIndex = 0;
let running = 0;
const runNext = () => {
while (running < concurrency && nextIndex < thunks.length) {
const index = nextIndex++;
running++;
thunks[index]()
.then(value => {
results[index] = value;
})
.finally(() => {
running--;
runNext();
});
}
};
runNext();
// Wait for all results to be filled
// Since we store by index, we can just wait until results has no undefined
// But Promise.all is easier if we map thunks to promises that resolve to results[index]
// However, we need to ensure the promises are created and tracked.
// Alternative: collect all promises in an array and await Promise.all(promises)
// But we need to control concurrency.
// Let's stick to the queue approach and wait for completion.
// We can create a promise that resolves when all are done.
return new Promise((resolve, reject) => {
let completed = 0;
const checkDone = () => {
if (completed === thunks.length) {
resolve(results);
}
};
const runNext = () => {
while (running < concurrency && nextIndex < thunks.length) {
const index = nextIndex++;
running++;
thunks[index]()
.then(value => {
results[index] = value;
})
.catch(err => {
// Propagate rejection? The prompt says "a rejecting thunk may reject the pool".
// So we should reject the main promise.
reject(err);
})
.finally(() => {
running--;
completed++;
if (!rejected) { // Need to track if already rejected
runNext();
checkDone();
}
});
}
};
let rejected = false;
runNext();
});
}
```
Wait, the prompt says "You do not need to handle rejections specially (a rejecting thunk may reject the pool)."
This implies if one rejects, the whole pool can reject. But we still need to ensure we don't crash or leave things hanging.
Also, the example shows successful resolution.
Let's simplify. We can use an async function and `Promise.all` on a mapped array of promises that are executed with concurrency control.
Another approach:
```javascript
export async function pool(thunks, concurrency) {
const results = [];
const executing = [];
let index = 0;
const next = () => {
while (executing.length < concurrency && index < thunks.length) {
const i = index++;
const p = thunks[i]().then(res => {
results[i] = res;
executing.splice(executing.indexOf(p), 1);
next();
});
executing.push(p);
}
};
next();
return Promise.all(results); // Wait for all results to be set
}
```
Wait, `Promise.all(results)` waits for the values in `results` to be promises? No, `results` is an array of values. `Promise.all` on an array of values just resolves immediately.
We need to wait for the promises in `executing` to finish, and ensure `results` is fully populated.
Actually, if we store the promise itself in `results` initially, then `Promise.all(results)` works.
But we need to execute them with concurrency limit.
Let's try this:
```javascript
export async function pool(thunks, concurrency) {
const results = new Array(thunks.length);
let nextIndex = 0;
let running = 0;
const run = () => {
while (running < concurrency && nextIndex < thunks.length) {
const i = nextIndex++;
running++;
const p = thunks[i]();
results[i] = p.then(res => res); // Store the promise, resolve to value
p.finally(() => {
running--;
run();
});
}
};
run();
return Promise.all(results);
}
```
Does this work?
1. `results[i]` is assigned a promise that resolves to the value of `thunks[i]()`.
2. `Promise.all(results)` waits for all these promises to resolve.
3. The `run` function ensures at most `concurrency` are started.
4. When one finishes (`finally`), `running` decreases, and `run` is called again to start more.
5. Order is preserved because `results[i]` corresponds to `thunks[i]`.
Edge case: `thunks` is empty.
`nextIndex` = 0, `thunks.length` = 0. Loop doesn't run. `results` is `[]`. `Promise.all([])` resolves to `[]`. Correct.
Edge case: `concurrency` >= 1. Assumed.
Rejection handling:
If `thunks[i]()` rejects, `results[i]` becomes a rejected promise. `Promise.all(results)` will reject immediately with that error. This matches "a rejecting thunk may reject the pool".
The `finally` block still runs, so `running` decreases and `run` continues. This is good, it doesn't stop the pool from processing others, but the overall result rejects.
One detail: `results[i] = p.then(res => res);`
If `p` rejects, `results[i]` is a rejected promise. `Promise.all` sees it and rejects.
Is there any risk of `run` being called recursively too deep?
With `concurre
…[25093 chars elided]…
s it.
If `catch` throws, `finally` runs, then error propagates?
But `catch` only sets `rejected` and calls `reject`. It doesn't throw.
So it's fine.
I'm confident.
```javascript
export async function pool(thunks, concurrency) {
const results = new Array(thunks.length);
let nextIndex = 0;
let running = 0;
let completed = 0;
let rejected = false;
return new Promise((resolve, reject) => {
const run = () => {
while (running < concurrency && nextIndex < thunks.length) {
const i = nextIndex++;
running++;
const p = Promise.resolve(thunks[i]());
p.then(value => {
results[i] = value;
})
.catch(err => {
if (!rejected) {
rejected = true;
reject(err);
}
})
.finally(() => {
running--;
completed++;
if (completed === thunks.length) {
if (!rejected) {
resolve(results);
}
} else if (!rejected) {
run();
}
});
}
};
run();
if (completed === thunks.length && !rejected) {
resolve(results);
}
});
}
```
Wait, `Promise.resolve(thunks[i]())`
If `thunks[i]` is not a function? Prompt says "array of zero-argument functions".
So it's fine.
One last thought: `run` is called in `finally`.
If `rejected` is true, we skip `run()`.
But `running` is decremented.
So `running` becomes 0 eventually.
But we don't start new ones.
This is fine.
I'll provide this solution.
One tiny thing: `export async function pool`
The `async` keyword makes the function return a promise automatically.
We are returning `new Promise(...)`.
So it's wrapped in another promise?
No, `async` function returns the value of the `return` statement wrapped in `Promise.resolve`.
If we `return new Promise(...)`, it returns that promise.
So it's fine.
Actually, `async function` is just syntactic sugar.
`return new Promise(...)` is correct.
I'll output the code now.