Skip to content

perf: parallelize jsDelivr README fallback probes#2384

Open
trivikr wants to merge 5 commits intonpmx-dev:mainfrom
trivikr:fetch-readme-parallel
Open

perf: parallelize jsDelivr README fallback probes#2384
trivikr wants to merge 5 commits intonpmx-dev:mainfrom
trivikr:fetch-readme-parallel

Conversation

@trivikr
Copy link
Copy Markdown
Contributor

@trivikr trivikr commented Apr 5, 2026

🔗 Linked issue

N/A

🧭 Context

README fallback probing in server/utils/readme-loaders.ts was strictly sequential. On cache misses, packages with missing or nonstandard README filenames could pay for several failed jsDelivr requests before the loader found the right file or gave up, which made worst-case README render time noticeably slower than necessary.

📚 Description

This changes the jsDelivr README fallback path to reduce that long-tail latency while keeping the fallback behavior conservative.

  • Build a prioritized candidate list that tries the npm registry’s readmeFilename first when it exists.
  • Probe jsDelivr candidates in small parallel batches instead of one-by-one.
  • Preserve existing fallback semantics for missing, nonstandard, and truncated npm READMEs.
  • Add unit coverage for batched probing and candidate prioritization in test/unit/server/utils/readme-loaders.spec.ts.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 7, 2026 4:12am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 7, 2026 4:12am
npmx-lunaria Ignored Ignored Apr 7, 2026 4:12am

Request Review

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR updates server/utils/readme-loaders.ts to fetch jsDelivr README candidates in concurrent batches of three (via Promise.all) using a new constant JSDELIVR_README_FETCH_BATCH_SIZE. fetchReadmeFromJsdelivr now returns the first non-empty response from each batch (fetch errors or non-OK responses map to null). A new helper buildReadmeFetchCandidates excludes a provided readmeFilename from standard candidates. resolvePackageReadmeSource is changed to try the explicit readmeFilename first and, if that yields null, fall back to fetching the batched candidate list. Tests were added/expanded to verify batching, selective response.text() invocation, and the new candidate ordering; exported function signatures remain unchanged.

Possibly related PRs

Suggested reviewers

  • ghostdevv
  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is directly related to the changeset, explaining the performance improvement from sequential to batched-parallel README probing and detailing the specific changes made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@trivikr
Copy link
Copy Markdown
Contributor Author

trivikr commented Apr 5, 2026

Also updated PR to fetch provided readme before starting the fallback batch in 72bb0b2

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/utils/readme-loaders.ts (1)

115-125: Fix typoed inline comments

There are a couple of typos (unsucessful, sometihng) in the inline comments. Quick polish for readability.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 248c6759-6520-45f8-8aca-2e08376a991a

📥 Commits

Reviewing files that changed from the base of the PR and between 36fced5 and 4e00761.

📒 Files selected for processing (2)
  • server/utils/readme-loaders.ts
  • test/unit/server/utils/readme-loaders.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • test/unit/server/utils/readme-loaders.spec.ts

Comment on lines +47 to 67
const responses = await Promise.all(
batch.map(async filename => {
try {
const url = `https://cdn.jsdelivr.net/npm/${packageName}${versionSuffix}/${filename}`
const response = await fetch(url)
if (!response.ok) {
return null
}

return response
} catch {
return null
}
}),
)

for (const response of responses) {
const text = await response?.text()
if (text?.trim()) {
return text
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Node.js fetch (undici), should unread Response bodies be consumed or cancelled to promptly release underlying connections?

💡 Result:

Yes—don’t leave a Node.js/undici fetch() response body unread if you care about promptly releasing resources.

  • In Node (undici), relying on GC to eventually clean up an unconsumed body is not deterministic and can cause excessive connection usage, reduced keep‑alive reuse, and even stalls/deadlocks when you run out of connections. You should always either consume or cancel the response body. [1]
  • If you only needed headers, prefer method: 'HEAD' so there’s no body to drain/cancel. [1]
  • Node’s own docs/examples also call out that consuming the response body is mandatory in this context. [2]

Practical rule

  • If you want connection reuse: drain/consume the body (e.g., await res.arrayBuffer(), await res.text(), or read the stream to EOF). [1]
  • If you want to stop early: cancel the body (await res.body?.cancel()), which promptly frees the socket from being “owned” by that body stream (typically by closing that connection rather than reusing it). [1][3]

Example (don’t care about body):

const res = await fetch(url);
await res.body?.cancel(); // release promptly (no keep-alive reuse)

Sources: [1] [2] [3]


🏁 Script executed:

# First, let's locate and read the readme-loaders.ts file
find . -name "readme-loaders.ts" -type f

Repository: npmx-dev/npmx.dev

Length of output: 93


🏁 Script executed:

# Read the file to verify the code structure and lines around 47-67
cat -n server/utils/readme-loaders.ts | head -80

Repository: npmx-dev/npmx.dev

Length of output: 3034


Cancel unread batch responses before early return to avoid connection resource leaks

When a matching README is found, the loop returns immediately without consuming the remaining responses in the batch. In Node.js (undici), unread fetch() response bodies retain their underlying connections indefinitely until garbage collected, which is not deterministic. This can cause excessive connection usage and potential connection pool exhaustion in long-running runtimes.

Suggested fix
     for (const response of responses) {
       const text = await response?.text()
       if (text?.trim()) {
+        await Promise.all(
+          responses.map(other =>
+            other && other !== response && !other.bodyUsed
+              ? other.body?.cancel().catch(() => undefined)
+              : undefined,
+          ),
+        )
         return text
       }
     }
+
+    await Promise.all(
+      responses.map(response =>
+        response && !response.bodyUsed
+          ? response.body?.cancel().catch(() => undefined)
+          : undefined,
+      ),
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const responses = await Promise.all(
batch.map(async filename => {
try {
const url = `https://cdn.jsdelivr.net/npm/${packageName}${versionSuffix}/${filename}`
const response = await fetch(url)
if (!response.ok) {
return null
}
return response
} catch {
return null
}
}),
)
for (const response of responses) {
const text = await response?.text()
if (text?.trim()) {
return text
}
const responses = await Promise.all(
batch.map(async filename => {
try {
const url = `https://cdn.jsdelivr.net/npm/${packageName}${versionSuffix}/${filename}`
const response = await fetch(url)
if (!response.ok) {
return null
}
return response
} catch {
return null
}
}),
)
for (const response of responses) {
const text = await response?.text()
if (text?.trim()) {
await Promise.all(
responses.map(other =>
other && other !== response && !other.bodyUsed
? other.body?.cancel().catch(() => undefined)
: undefined,
),
)
return text
}
}
await Promise.all(
responses.map(response =>
response && !response.bodyUsed
? response.body?.cancel().catch(() => undefined)
: undefined,
),
)

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

Labels

needs review This PR is waiting for a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants