Skip to content

refactor: replace neo-async with a built-in async helper - #5

Open
anurag6569201 wants to merge 1 commit into
qa/agent-webpack-webpack/pr-05-21959/basefrom
qa/agent-webpack-webpack/pr-05-21959/head
Open

anurag6569201 wants to merge 1 commit into
qa/agent-webpack-webpack/pr-05-21959/basefrom
qa/agent-webpack-webpack/pr-05-21959/head

Conversation

@anurag6569201

Copy link
Copy Markdown

Summary

webpack used only four helpers from neo-async (each, eachLimit/forEachLimit, map, parallel), so this replaces the dependency with lib/util/async.js and drops it from package.json. That also removes the awkward IterableCollection/ErrorCallback casts its typings forced at four call sites.

Measured against neo-async@2.6.2 (median, interleaved arms, warmed): eachLimit is 2.7–20.8× faster on a synchronously-completing collection — it drains from its dispatch loop instead of deferring, which over 100k items is 0 process.nextTick calls against 49,999. each is ~1.1–1.4× faster; map/parallel are at parity (±7%). Requiring the module costs 0.7 ms / 26 KB instead of 4.3 ms / 336 KB, and the emitted bundle drops ~30 KB. On a full 2170-module build the wall-clock and peak-RSS differences sit inside run-to-run noise, so no build-time win is claimed.

Like neo-async, each item gets a callback that may only report once; without that a duplicate callback settles the run while a later item is still in flight.

What kind of change does this PR introduce?

refactor

Did you add tests for your changes?

Yes — test/Async.unittest.js (29 cases, 100% line and branch coverage of lib/util/async.js), covering arrays, iterables, Map entries, empty collections, error propagation, stack safety under synchronous iterators, and the duplicate-callback guard per helper. The existing suites cover the call sites; ConfigTestCases, ConfigCacheTestCases and StatsTestCases pass unchanged.

Does this PR introduce a breaking change?

No. lib/util/async.js is internal and the helpers keep neo-async's observable semantics, including each starting every item even after one fails — MultiWatching/MultiCompiler rely on that to close every watcher.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a — no public API or option changes.

Use of AI

Claude Code was used to write lib/util/async.js and its tests, migrate the call sites, and run the A/B measurements quoted above against neo-async. Every claim here comes from a measurement I ran and reviewed; the wall-clock build comparison is reported as inconclusive because its spreads overlapped. All output was reviewed before commit.

Summary by CodeRabbit

  • Improvements

    • Improved reliability of asynchronous processing while preserving compilation, loading, asset generation, and module-processing behavior.
    • Improved handling of arrays and other iterable inputs, including processing order and concurrency controls.
  • Bug Fixes

    • Prevented duplicate callbacks, late completions, and stack-related issues during asynchronous operations.
  • Tests

    • Expanded coverage for ordering, concurrency limits, error handling, empty inputs, iterable collections, and synchronous or asynchronous operations.

Source merge-base: 7cb6dd49a74909407d55faad27f75a3d8542cf7d
Source head: 9a637529266093cd113ffb65a64576bda9726954

@shipwright-agent

Copy link
Copy Markdown

⛔ Shipwright · Blocked

Recommendation: do not merge PR #5 · Tier T3
Checks: 0 total · 0 needing attention

Next step: resolve the blocking findings before merge.

Findings (8)

  • CRITICAL The 'once' wrapper throws when a callback is invoked twice. · lib/util/async.js:55
    • Fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.
  • CRITICAL The same 'each' dispatch loop continues iterating after an error because it only checks 'finished' at the top of the loop, not after each iterator invocation. · lib/util/async.js:55
    • Fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.
  • CRITICAL The 'once' wrapper throws an error if the callback is called more than once. · lib/util/async.js:55
    • Fix: Fix the review finding before release.
  • HIGH In 'each', when the collection is an empty array, the function returns 'callback(null)' without setting 'finished = true'. · lib/util/async.js:55
    • Fix: Fix the review finding before release.
  • HIGH The new 'each' implementation intentionally continues dispatching after an error, but this behavior is not documented in the function contract and differs from the previous 'neo-as · lib/util/async.js:55
    • Fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.
  • HIGH The 'map' implementation continues invoking the iterator for all remaining items after an error has already been reported. · lib/util/async.js:190
    • Fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.
  • MEDIUM In 'map', when the collection is an empty array, the function returns 'callback(null, results)' without setting 'finished = true'. · lib/util/async.js:190
    • Fix: Fix the review finding before release.
  • LOW In 'eachLimit', the 'done' callback decrements 'running' before checking 'finished'. · lib/util/async.js:120
    • Fix: Fix the review finding before release.

Fireworks usage: 42,824 input · 1,651 output · 44,475 total tokens · $0.0105 · 26s · 0 fix iteration(s)

Open the Shipwright check for full evidence and the audit bundle. Use /shipwright rerun to verify again.

Comment thread lib/util/async.js
if (--remaining === 0) {
finished = true;
callback(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · CRITICAL

The 'once' wrapper throws when a callback is invoked twice.

Impact: The 'once' wrapper throws when a callback is invoked twice. In 'each', the dispatch loop does not check 'finished' after each iterator call, so after a synchronous error sets 'finished = true', the loop continues invoking remaining iterators. If any of those iterators call their callback, 'once(done)' throws 'Callback was already called', producing an uncaught exception that can crash the process.

Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.

Comment thread lib/util/async.js
if (--remaining === 0) {
finished = true;
callback(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · CRITICAL

The same 'each' dispatch loop continues iterating after an error because it only checks 'finished' at the top of the loop, not after each iterator invocation.

Impact: The same 'each' dispatch loop continues iterating after an error because it only checks 'finished' at the top of the loop, not after each iterator invocation. The comment claims this is intentional, but it makes the 'once' guard throw for any later synchronous callback, turning a reported error into an uncaught exception.

Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.

Comment thread lib/util/async.js
if (--remaining === 0) {
finished = true;
callback(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · CRITICAL

The 'once' wrapper throws an error if the callback is called more than once.

Impact: The 'once' wrapper throws an error if the callback is called more than once. In 'each', the dispatch loop does not check 'finished' after each iterator call, so if an iterator calls its callback synchronously with an error, 'finished' is set to true, but the loop continues to call the iterator for the remaining items. If any of those remaining iterators call their callback, the 'once' wrapper will throw 'Callback wa…

Suggested fix: Fix the review finding before release.

Comment thread lib/util/async.js
if (--remaining === 0) {
finished = true;
callback(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · HIGH

In 'each', when the collection is an empty array, the function returns 'callback(null)' without setting 'finished = true'.

Impact: In 'each', when the collection is an empty array, the function returns 'callback(null)' without setting 'finished = true'. This is not a defect by itself because the function returns immediately, but it is inconsistent with the non-empty path. More importantly, for non-array iterables, 'remaining' starts at 1 and is incremented for each item; after the loop, 'done(null)' is called. If the iterable is empty, 'remaini…

Suggested fix: Fix the review finding before release.

Comment thread lib/util/async.js
if (--remaining === 0) {
finished = true;
callback(null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · HIGH

The new 'each' implementation intentionally continues dispatching after an error, but this behavior is not documented in the function contract and differs from the previous 'neo-as

Impact: The new 'each' implementation intentionally continues dispatching after an error, but this behavior is not documented in the function contract and differs from the previous 'neo-async' behavior. A future maintainer cannot tell from the signature or JSDoc that iterators may still be invoked after the first error, which is a likely source of subtle bugs.

Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.

Comment thread lib/util/async.js
if (err) {
finished = true;
callback(err, results);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · HIGH

The 'map' implementation continues invoking the iterator for all remaining items after an error has already been reported.

Impact: The 'map' implementation continues invoking the iterator for all remaining items after an error has already been reported. If an iterator has side effects or assumes it will not be called after failure, this can cause unexpected behavior or exceptions. The original 'neo-async' implementation stopped dispatching after an error, so this is a behavioral regression with potential security or correctness implications.

Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.

Comment thread lib/util/async.js
if (err) {
finished = true;
callback(err, results);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · MEDIUM

In 'map', when the collection is an empty array, the function returns 'callback(null, results)' without setting 'finished = true'.

Impact: In 'map', when the collection is an empty array, the function returns 'callback(null, results)' without setting 'finished = true'. This is not a defect because the function returns immediately. For non-array iterables, 'results' is initially an empty array, and after the loop, 'results.length = index' is set. If the iterable is empty, 'index' is 0, so 'results.length' remains 0, and 'settle(null)' is called. 'remain…

Suggested fix: Fix the review finding before release.

Comment thread lib/util/async.js
exhausted = true;
break;
}
item = next.value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shipwright · LOW

In 'eachLimit', the 'done' callback decrements 'running' before checking 'finished'.

Impact: In 'eachLimit', the 'done' callback decrements 'running' before checking 'finished'. If an iterator calls its callback synchronously with an error, 'finished' is set to true and 'callback(err)' is called, but 'running' has already been decremented. The dispatch loop then continues because 'dispatching' is true and 'finished' is checked at the top of the loop, so it breaks. However, if the error occurs after the disp…

Suggested fix: Fix the review finding before release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant