fix: cache unsuccessful hosted Git URL parses by jeffwidman · Pull Request #347 · npm/hosted-git-info · GitHub
Skip to content

fix: cache unsuccessful hosted Git URL parses - #347

Open
jeffwidman wants to merge 1 commit into
npm:mainfrom
jeffwidman:fix/cache-failed-url-parses
Open

jeffwidman wants to merge 1 commit into
npm:mainfrom
jeffwidman:fix/cache-failed-url-parses

Conversation

@jeffwidman

Copy link
Copy Markdown
Member

Cache unsuccessful hosted Git URL parses without changing their public undefined return.

fromUrl() already tries to cache failures, but stores undefined, which lru-cache does not retain. Consequently, repeated non-Git inputs repeat parsing. This matters to npm because npm-package-arg tries hosted-Git classification while handling ordinary registry version specifications.

Change and compatibility

  • Store null as an internal failure sentinel; translate it back to undefined on return.
  • Reuse the existing 1,000-entry LRU, including recency updates and eviction. Negative and positive results share this bound; there is no new cache, dependency, or configuration.
  • Clear the cache when addHost() changes parser configuration, so an earlier failure does not hide a newly registered host.
  • Cover repeated failures, mixed positive/negative capacity, negative-hit recency, eviction/reparsing, successful-result reuse, and registration of a previously unrecognized host.
  • Correct the README's null wording to describe the existing public undefined return, not a new API behavior.

Measured effect

With only this fix applied to npm 11.17.0's hosted-git-info 9.0.3, using Node 24.22.0, Arborist 9.8.0, and lru-cache 11.5.1:

Native command shape Median CPU, before -> after Median wall time, before -> after
Lockfile writer 9.2365 -> 8.6159 s (6.7% lower) 8.0810 -> 7.4797 s (7.4% lower)
Compatibility checker 7.6624 -> 7.2901 s (4.9% lower) 6.6767 -> 6.2765 s (6.0% lower)

These are Dependabot-shaped npm install <target> --package-lock-only --ignore-scripts calls: the writer adds --force; the checker adds --dry-run=true. Both use --offline --audit=false --fund=false --timing.

The measurements use an isolated large workspace workload, warm npm caches, networking disabled, one excluded warmup per arm, and three measured pairs with alternating order. No instrumentation was included in timing samples. All 4,686 manifest/lockfile outputs plus .npmrc were byte-identical across all eight runs per command shape; npm output also matched after removing timing text.

A tiny project with one local dependency was effectively neutral over seven measured pairs: CPU 0.347578 -> 0.347536 s; wall 0.274601 -> 0.274895 s.

These are local native-command results, not complete Dependabot-job savings or a general performance guarantee. The large workload is not a public fixture. Negative entries also compete with successful results for the existing cache capacity.

Public reproduction of the avoided work

From a hosted-git-info checkout with dependencies installed:

node <<'JS'
const assert = require('node:assert/strict')
const parserPath = require.resolve('./lib/from-url.js')
const parse = require(parserPath)
let parserCalls = 0
require.cache[parserPath].exports = (...args) => {
  parserCalls++
  return parse(...args)
}
const HostedGit = require('./')
for (let i = 0; i < 20000; i++) {
  assert.equal(HostedGit.fromUrl(`^1.${i % 100}.0`), undefined)
}
console.log({ calls: 20000, distinctInputs: 100, parserCalls })
JS

Verified on upstream main and this branch: 20,000 -> 100 parser executions, with all calls still returning undefined. This counts work, rather than claiming a synthetic timing improvement.

Validation

Source validation is against hosted-git-info 10.1.1, based on main at 8c3bebb5e42142b7bce06b085b4c1bd1adba7371, using the repository's own development dependencies, lru-cache 11.5.3, and Node 24.22.0:

  • npm run test:cover — passed, 31 tests, 100% lines/functions/branches.
  • npm run eslint — passed across the repository.
  • npm test — all 31 tests and ESLint pass, but the subsequent template-oss-check hook fails on existing generated configuration drift. A clean checkout of the same upstream commit, using identical dependencies, has the same five template-file differences. No tooling/configuration changes are included here.
  • The public reproduction above and offline native-command output comparisons — passed.

References

Related to #343. This fix is independent of replacing the cache implementation; that proposal currently also discards undefined values and should preserve negative caching and host-registration invalidation if it moves forward.

Keep failed parses in the bounded LRU with an internal null sentinel, preserve undefined returns, and invalidate cached results when registering hosts. Cover negative-hit recency and mixed-result eviction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread README.md

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Clarification: this corrects the documentation, not the public return type. Before this PR, an unrecognized URL already returned undefined; the existing upstream tests explicitly assert that.

The new null value is only an internal cache sentinel. It is translated back to undefined on return, so callers observe undefined both before and after this change. No public return-value change from null to undefined is introduced.

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