← run

js-09-pool

0.857
6/7 tests· concurrency
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
```js
export async function pool(thunks, concurrency) {
  const results = new Array(thunks.length);
  let nextIndex = 0;
  let activeCount = 0;
  
  return new Promise((resolve, reject) => {
    // Handle empty array case
    if (thunks.length === 0) {
      resolve([]);
      return;
    }
    
    // Start initial batch up to concurrency limit
    for (let i = 0; i < Math.min(concurrency, thunks.length); i++) {
      startThunk(i);
    }
    
    function startThunk(index) {
      activeCount++;
      
      try {
        const thunkResult = thunks[index]();
        
        thunkResult
          .then((result) => {
            results[index] = result;
            activeCount--;
            
            // Start next thunk if available
            if (nextIndex < thunks.length) {
              startThunk(nextIndex);
              nextIndex++;
            }
            
            // Check if all tasks are completed
            if (activeCount === 0 && nextIndex >= thunks.length) {
              resolve(results);
            }
          })
          .catch((error) => {
            reject(error);
          });
      } catch (error) {
        reject(error);
      }
    }
  });
}
```

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: 36.89522
  type: 'test'
  ...
# Subtest: peak concurrency never exceeds the limit
ok 2 - peak concurrency never exceeds the limit
  ---
  duration_ms: 52.688403
  type: 'test'
  ...
# Subtest: concurrency of 1 runs strictly sequentially
ok 3 - concurrency of 1 runs strictly sequentially
  ---
  duration_ms: 42.573469
  type: 'test'
  ...
# Subtest: empty thunks -> empty array
ok 4 - empty thunks -> empty array
  ---
  duration_ms: 0.275951
  type: 'test'
  ...
# Subtest: concurrency larger than number of thunks
ok 5 - concurrency larger than number of thunks
  ---
  duration_ms: 10.220403
  type: 'test'
  ...
# Subtest: all thunks actually run exactly once
not ok 6 - all thunks actually run exactly once
  ---
  duration_ms: 9.295481
  type: 'test'
  location: '/tmp/llmlab-js-09-pool-8v3kx_w5/solution.test.js:59:1'
  failureType: 'testCodeFailure'
  error: |-
    Expected values to be strictly equal:
    
    8 !== 6
    
  code: 'ERR_ASSERTION'
  name: 'AssertionError'
  expected: 6
  actual: 8
  operator: 'strictEqual'
  stack: |-
    TestContext.<anonymous> (file:///tmp/llmlab-js-09-pool-8v3kx_w5/solution.test.js:71:10)
    async Test.run (node:internal/test_runner/test:1313:7)
    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)
  ...
# Subtest: pool keeps the slots full as tasks finish
ok 7 - pool keeps the slots full as tasks finish
  ---
  duration_ms: 50.318654
  type: 'test'
  ...
1..7
# tests 7
# suites 0
# pass 6
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 227.584925
Self-repair attempts
[
  {
    "total": 7,
    "answer": "```js\nexport async function pool(thunks, concurrency) {\n  const results = new Array(thunks.length);\n  let nextIndex = 0;\n  let activeCount = 0;\n  \n  return new Promise((resolve, reject) => {\n    // Handle empty array case\n    if (thunks.length === 0) {\n      resolve([]);\n      return;\n    }\n    \n    // Start initial batch up to concurrency limit\n    for (let i = 0; i < Math.min(concurrency, thunks.length); i++) {\n      startThunk(i);\n    }\n    \n    function startThunk(index) {\n      activeCount++;\n      \n      try {\n        const thunkResult = thunks[index]();\n        \n        thunkResult\n          .then((result) => {\n            results[index] = result;\n            activeCount--;\n            \n            // Start next thunk if available\n            if (nextIndex < thunks.length) {\n              startThunk(nextIndex);\n              nextIndex++;\n            }\n            \n            // Check if all tasks are completed\n            if (activeCount === 0 && nextIndex >= thunks.length) {\n              resolve(results);\n            }\n          })\n          .catch((error) => {\n            reject(error);\n          });\n      } catch (error) {\n        reject(error);\n      }\n    }\n  });\n}\n```",
    "passed": 6,
    "test_error": "TAP version 13\n# Subtest: results are returned in original order, not completion order\nok 1 - results are returned in original order, not completion order\n  ---\n  duration_ms: 36.89522\n  type: 'test'\n  ...\n# Subtest: peak concurrency never exceeds the limit\nok 2 - peak concurrency never exceeds the limit\n  ---\n  duration_ms: 52.688403\n  type: 'test'\n  ...\n# Subtest: concurrency of 1 runs strictly sequentially\nok 3 - concurrency of 1 runs strictly sequentially\n  ---\n  duration_ms: 42.573469\n  type: 'test'\n  ...\n# Subtest: empty thunks -> empty array\nok 4 - empty thunks -> empty array\n  ---\n  duration_ms: 0.275951\n  type: 'test'\n  ...\n# Subtest: concurrency larger than number of thunks\nok 5 - concurrency larger than number of thunks\n  ---\n  duration_ms: 10.220403\n  type: 'test'\n  ...\n# Subtest: all thunks actually run exactly once\nnot ok 6 - all thunks actually run exactly once\n  ---\n  duration_ms: 9.295481\n  type: 'test'\n  location: '/tmp/llmlab-js-09-pool-8v3kx_w5/solution.test.js:59:1'\n  failureType: 'testCodeFailure'\n  error: |-\n    Expected values to be strictly equal:\n    \n    8 !== 6\n    \n  code: 'ERR_ASSERTION'\n  name: 'AssertionError'\n  expected: 6\n  actual: 8\n  operator: 'strictEqual'\n  stack: |-\n    TestContext.<anonymous> (file:///tmp/llmlab-js-09-pool-8v3kx_w5/solution.test.js:71:10)\n    async Test.run (node:internal/test_runner/test:1313:7)\n    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)\n  ...\n# Subtest: pool keeps the slots full as tasks finish\nok 7 - pool keeps the slots full as tasks finish\n  ---\n  duration_ms: 50.318654\n  type: 'test'\n  ...\n1..7\n# tests 7\n# suites 0\n# pass 6\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 227.584925"
  },
  {
    "total": 7,
    "answer": "Looking at the test failure, it seems that some thunks are being executed more than once (8 executions instead of 6), which suggests there's a bug in my logic where thunks might be getting started multiple times.\n\nThe issue is likely in how I'm tracking the `nextIndex`. Let me analyze the problem:\n\n1. I start the first `min(concurrency, thunks.length)` thunks with indices 0 to `concurrency-1`\n2. Then in `startThunk`, I call `startThunk(nextIndex)` and increment `nextIndex`\n3. But if a thunk fails or there's some race condition, this could cause issues\n\nActually, looking more carefully: the problem is that when I start the initial batch, I'm using a for loop with index `i`, but then in `startThunk` I'm using `nextIndex`. The issue is that `nextIndex` starts at 0, and after starting the first batch, it might still be 0 or have incorrect value.\n\nLet me fix this by properly initializing the state and ensuring each thunk is only started once:\n\n```js\nexport async function pool(thunks, concurrency) {\n  const results = new Array(thunks.length);\n  let currentIndex = 0;\n  let activeCount = 0;\n  \n  return new Promise((resolve, reject) => {\n    // Handle empty array case\n    if (thunks.length === 0) {\n      resolve([]);\n      return;\n    }\n    \n    function startNext() {\n      // Start the next thunk if available and we have capacity\n      while (activeCount < concurrency && currentIndex < thunks.length) {\n        const index = currentIndex++;\n        activeCount++;\n        \n        try {\n          const thunkResult = thunks[index]();\n          \n          thunkResult\n            .then((result) => {\n              results[index] = result;\n              activeCount--;\n              \n              // Check if all tasks are completed\n              if (activeCount === 0 && currentIndex >= thunks.length) {\n                resolve(results);\n              } else {\n                // Try to start more thunks\n                startNext();\n              }\n            })\n            .catch((error) => {\n              reject(error);\n            });\n        } catch (error) {\n          reject(error);\n        }\n      }\n      \n      // If no more thunks to start and all active ones have finished\n      if (currentIndex >= thunks.length && activeCount === 0) {\n        resolve(results);\n      }\n    }\n    \n    // Start the initial batch\n    startNext();\n  });\n}\n```",
    "passed": 7,
    "test_error": ""
  }
]