Repository navigation
Browse: a proxy's or group's package page answers from the cache, with Refresh from upstream - #841
Merged
Merged
Conversation
The web UI's package page went through the read path clients use: on a
proxy, and on each proxy member of a group, a listing past the metadata
TTL was fetched from the upstream on the request, then the upstream
metadata, the tags and the latest manifest, one step and one member
after another. The page was slow, and opening it confirmed the package's
rows, so the browse list grew as a side effect of looking.
The page now reads a proxy, and a group's proxy members, cached only
(ProxyRepo::cached_only, AppState::cached_read_ops_for): the cached
version rows, the stored upstream documents whatever their age, the
stored tags and the downloaded packages, as an offline proxy serves
them, without an upstream request and without discovering anything. The
stored copies are read past the per-instance caches, so a stale copy is
never cached there for the reads that ask the upstream, and a fetch
under way there is not waited for.
Each upstream listing records when it was answered, in a new table
proxy_package_listing. The page says how old it is (cached_at), and once
that is past the metadata TTL or older than the repository's last edit
or invalidation, it refreshes the package off the request: one refresh
per proxy and package at a time, four per instance at most, none again
within a minute, and never for a package the proxy holds nothing of.
POST /browse/packages/{repo}/{id}/refresh lets any reader ask the
upstream now, as a client's install does; the UI offers it as Refresh
from upstream, also on the "not cached" state that a package the proxy
holds nothing of now shows instead of being fetched by looking.
The tests drive a mock NuGet upstream that holds its requests: the page
answers while it does, a stale page starts one refresh, an uncached id
is neither asked for nor recorded, Refresh needs read access only, and a
group reads and refreshes its proxy member the same way.
Fixes #836
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJXNBkjivKSznagvTyC
This was referenced Nov 3, 2026
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.
Fixes #836. This is part 1 of #834; the other parts are #837 to #840.
What changes
The web UI's package page (
GET /browse/packages/{repo}/{id}) used to go through the read path clients use. On a proxy, and on each proxy member of a group, a listing pastmetadata_ttl_secondswas fetched from the upstream on the request. Then came the upstream metadata, the tags and the latest manifest, one step and one member after another. The page was slow, and opening it added the package to the browse list.The page now reads what Forge holds.
ProxyRepo::cached_only(throughAppState::cached_read_ops_for). That means the cached version rows, the stored upstream documents whatever their age, the stored tags and the downloaded packages, as an offline proxy serves them.It says how old the data is, and refreshes in the background (the option chosen in #834).
proxy_package_listing(migration0018). Repointing the proxy deletes its rows; deleting the repository cascades.cached_at. For a group, this is the oldest listing among the proxies serving the package.refreshing: true. Limits: one refresh per proxy and package at a time, four per instance, and no new one within a minute of the last. A package the proxy holds nothing of is never refreshed this way.Refresh from upstream (every reader, as chosen in #834).
POST /browse/packages/{repo}/{id}/refreshasks the upstream through the client read path, then returns the refreshed detail. It needs read access on the repository.Web UI
Not in this PR
The other parts of #834 are their own issues:
NuGet proxies still show no description for versions not downloaded yet, because the registration summary carries only dependencies. That gap was already there; #838 covers it.
Tests
crates/server/tests/browse.rs, using a mock NuGet upstream that can hold its requests:PackageDetail.test.tsx(cache age, Refresh, polling during a background refresh, the not-cached state, no Refresh on an offline proxy) andlib/age.test.ts.forge-server: browse, package_delay, proxy_search, npm, plugin_contract, proxy_cache, group_proxy, dispatch;forge-core: lib, group_read, proxy_documents, proxy_read, retention_apply, retention_worker;forge-db: all;tsc;cargo clippy --workspace --all-targets -D warnings,cargo fmt --check,cargo xtask changelog --check..sqlxis regenerated: three new queries.Generated by Claude Code