Skip to content

Comments

perf(middleware-stack): optimize cloneTo, cache middleware list, fix .reverse() mutation#1883

Open
TrevorBurnham wants to merge 1 commit intosmithy-lang:mainfrom
TrevorBurnham:middleware-stack-optimization
Open

perf(middleware-stack): optimize cloneTo, cache middleware list, fix .reverse() mutation#1883
TrevorBurnham wants to merge 1 commit intosmithy-lang:mainfrom
TrevorBurnham:middleware-stack-optimization

Conversation

@TrevorBurnham
Copy link
Contributor

Problem

Every client.send() call rebuilds the middleware stack from scratch — concat() clones all entries via cloneTo, then resolve() calls getMiddlewareList() which normalizes, sorts, and builds a closure chain.

Additionally, expandRelativeMiddlewareList calls .reverse() on the after array, which mutates it in place. If getMiddlewareList is ever called twice on the same stack (e.g. via identify() then resolve()), the mutation corrupts the ordering on the second call.

Changes

1. _addBulk fast path in cloneTo

cloneTo copies entries from one stack to a new (typically empty) stack. Previously it called toStack.add() for each entry, which runs duplicate-name checks, override logic, getAllAliases() allocations, and object spread reconstruction — all unnecessary when the target is empty.

The new _addBulk internal method skips validation for entries with no name conflicts and falls back to the full add()/addRelativeTo() path when a duplicate is detected, preserving override semantics for non-empty targets (i.e. applyToStack on a populated stack).

Cross-version compatibility is maintained: cloneTo checks for _addBulk on the target and falls back to the public API if absent.

2. Cache getMiddlewareList result

The sorted/expanded middleware list is now cached after the first resolve() call. The cache is invalidated on any mutation (add, addRelativeTo, remove, removeByName, removeByReference, removeByTag, _addBulk). A shallow copy is returned from the cache to prevent external consumers from corrupting it.

3. flatMap instead of map + reduce

Replaces the sort().map(expand).reduce(push) chain with sort().flatMap(expand): single pass, no intermediate arrays, equivalent semantics.

4. Reverse iteration instead of .reverse()

  • expandRelativeMiddlewareList: replaces from.after.reverse().forEach(...) with a reverse for loop. This fixes a latent bug where .reverse() mutates the after array, corrupting results on repeated calls to getMiddlewareList.
  • resolve(): replaces .map(e => e.middleware).reverse() with a reverse for loop, avoiding two intermediate array allocations.

….reverse() mutation

- Add _addBulk internal fast path for cloneTo to skip validation on empty stacks,
  with fallback to add()/addRelativeTo() for duplicate/override handling on non-empty targets
- Cache getMiddlewareList result, invalidate on any mutation, return shallow copy
- Replace map+reduce with flatMap in getMiddlewareList
- Replace .reverse() calls with reverse iteration in expandRelativeMiddlewareList and resolve()
  to avoid array mutation and intermediate allocations
- Add comprehensive tests for bulk add, cache invalidation, and expand stability
@TrevorBurnham TrevorBurnham requested a review from a team as a code owner February 21, 2026 17:01
@kuhe
Copy link
Contributor

kuhe commented Feb 24, 2026

what is the performance comparison of control, this PR, and new Client({ cacheMiddleware: true })?

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.

2 participants