perf: parallelize getAllDescendantPages sibling fetches#113
Merged
Conversation
Sibling pages at each tree level are independent, but the previous implementation awaited each child's recursive fetch sequentially, producing N sequential API calls for an N-node tree. Fetch siblings in chunks of 10 with Promise.all. Wall-clock time drops from O(N) to roughly O(depth * ceil(width/10)); total API call count is unchanged. Public signature, result order, parentId attachment, and maxDepth behavior are all preserved.
The previous commit created SIBLING_CONCURRENCY inside every recursive frame, so the cap of 10 was enforced per parent, not per traversal. In a tree with branching factor B and depth D, concurrent getChildPages requests could grow to min(B, 10)^D rather than staying at 10. Replace the per-frame chunking with a single semaphore created at the top-level call and threaded through a private _collectDescendants helper. Every getChildPages request in the traversal now acquires against the same semaphore, giving a hard cap of 10 concurrent in-flight requests regardless of tree shape. Public signature, DFS pre-order result, parentId attachment, and maxDepth behavior are unchanged. Add a test that builds a 15-wide, 2-deep tree, counts concurrent getChildPages invocations, and asserts the observed peak stays at or below 10 — this would have failed on the previous per-parent cap (peak ~100) and now passes.
github-actions Bot
pushed a commit
that referenced
this pull request
Apr 22, 2026
## [1.31.1](v1.31.0...v1.31.1) (2026-04-22) ### Performance Improvements * parallelize getAllDescendantPages sibling fetches ([#113](#113)) ([bdee5de](bdee5de))
|
🎉 This PR is included in version 1.31.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request Template
Description
getAllDescendantPagespreviously awaited each child's recursive fetch sequentially, producing N sequential API calls for an N-node subtree (a classic N+1 pattern). For deep or wide page trees this dominates wall-clock time onexport,copy, and tree-traversal commands.This PR introduces a single semaphore shared across the entire traversal that caps concurrent
getChildPagesrequests at 10, regardless of tree shape.Revision after review (commit f6e637c): an earlier version of this PR placed the concurrency cap inside each recursive frame, so the limit only applied per parent. For a tree with branching factor B and depth D, concurrent requests could grow to
min(B, 10)^D— exactly the request flood the cap was meant to prevent. The fix threads a singlecreateSemaphore(10)instance through a private_collectDescendantshelper; everygetChildPagescall in the traversal acquires against that one semaphore, giving a hard global cap.Preserved behavior:
getAllDescendantPages(pageId, maxDepth, currentDepth))parentIdattachment on each descendantmaxDepthshort-circuitType of Change
Testing
Added
getAllDescendantPages caps concurrent getChildPages across the whole traversalintests/confluence-client.test.js: builds a 15-wide, 2-deep tree (240 descendants), instrumentsgetChildPageswith an in-flight counter, and asserts the observed peak stays ≤ 10. This test would have failed on the previous per-parent implementation (peak ~100) and passes on the current one.Full suite:
npx jest→ 236 tests pass.Checklist
Additional Context
Why a semaphore instead of a library like
p-limit?createSemaphoreis ~15 lines of zero-dependency code and is the only place in this codebase that needs a limiter. A dependency seemed like overreach.Why not switch to a single CQL
ancestor = Xquery? That's a larger behavior change (different pagination semantics, loss of per-levelparentIdattribution without a second pass) and deserves its own PR. This change is the minimal fix for the N+1 pattern.