Repository navigation
Refresh API Explorer pages on code and asset changes in serve - #4321
Conversation
API pages are generated once and kept, so C#, Razor, and Parcel changes never reached them without a restart. Hot reload now marks them stale, and a Parcel rebuild refreshes the browser. Versions of one product now render in parallel. Pages are overwritten in place, because deleting the in-memory tree took longer than rendering it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docs preview (local build)Handbook preview: https://docs-v3-preview.elastic.dev/elastic/docs-builder/pull/4321/ |
| if (HaveOpenApiSpecsChanged(config)) | ||
| _apiReferencesStale = false; | ||
| var force = _forceApiRegeneration; | ||
| _forceApiRegeneration = false; |
There was a problem hiding this comment.
[LOW] _forceApiRegeneration can lose a concurrent invalidation
EnsureApiReferencesAsync reads _forceApiRegeneration and then clears it in two separate operations:
var force = _forceApiRegeneration;
_forceApiRegeneration = false;If InvalidateApiReferences() runs between those two lines, its true write can be overwritten by the clear here. In that interleaving, a hot-reload update may be dropped and the next /api request can return without regenerating.
Using an atomic consume pattern for the flag (for example Interlocked.Exchange on an int-backed field) would avoid the lost-update window.
There was a problem hiding this comment.
Good catch, fixed in f085a42. The flag is now an int that InvalidateApiReferences sets with Interlocked.Exchange(ref _forceApiRegeneration, 1), and EnsureApiReferencesAsync consumes it with Interlocked.Exchange(ref _forceApiRegeneration, 0) == 1. An invalidation that lands mid-generation is either consumed by the current run or leaves _apiReferencesStale set for the next request.
Reading the flag and then clearing it left a window where a hot reload could set it in between. The clear then dropped that invalidation, and the next /api/ request skipped regeneration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // Serve writes to an in-memory file system where a recursive delete of the generated tree takes | ||
| // longer than regenerating it. Pages are overwritten in place instead; a page whose operation | ||
| // was removed from the spec stays reachable until the server restarts. | ||
| ApiPath.Create(); |
There was a problem hiding this comment.
[LOW] Stale API pages can survive spec removals during serve
Switching from recursive delete to in-place overwrites means pages that are no longer generated (for example, after removing an operation/path from a spec) remain on disk and keep being served until restart. That makes removed endpoints still appear valid in local API docs and can mask regressions while iterating.
Could we add a lightweight cleanup strategy during regeneration (for example, tracking generated outputs per run and deleting leftovers) so removed pages stop resolving without bringing back full-tree delete cost?
docs-builder serveunderdotnet watchnow shows C#, Razor, JS, and CSS changes on API Explorer pages without a restart. Regenerating the API pages after an edit takes about 12 s instead of 25–31 s.Affects: API reference
Prompt summary: The author wants to iterate on API Explorer pages in
./dev.sh servewithout restarting the container after each C#, HTML, or JS change. They also asked whether spec regeneration can run in parallel.Why
The dev server generates API pages once, on the first
/api/request, and regenerates them only when a spec or API Markdown file changes. A hot-reloaded code change refreshes the browser, but the browser gets the old pages. A Parcel rebuild does not refresh the browser at all, because live reload only watches.mdand.yml. Each regeneration also spends about 13 s deleting the previous output from the in-memory file system before it renders anything.What
Hot reload marks API pages stale
When
dotnet watchapplies a code change, the server marks the API pages for regeneration even if no spec changed. The browser refresh that follows gets freshly rendered pages. Flags and timestamps are now recorded before generation, so a change that lands mid-generation is not lost. A failed generation is retried on the next request.Parcel rebuilds refresh the browser
Under
dotnet watch, the server watchessrc/Elastic.Documentation.Site/_staticand refreshes the browser when Parcel writes JS or CSS./_staticresponses sendCache-Control: no-cache. Without that header, a refresh can reuse a cached bundle, because generated pages keep the asset hash from when they were rendered.Versions of one product render in parallel
Products were already generated in parallel. The versions of one product ran one after another, so Elasticsearch (3 versions) and Kibana (4 versions) set the total time. Each version has its own navigation, render context, and output folder, so they now render in parallel.
API pages are overwritten in place
The server no longer deletes the generated API tree before it regenerates it. A recursive delete on the in-memory file system took longer than the render itself. HTML pages are now written with
FileMode.Create, so a shorter page no longer keeps trailing bytes from the previous one.Out of scope: pages within one version still render sequentially, so the largest version (Elasticsearch
main, about 8 s) sets the floor. Specs are also downloaded and parsed again on every regeneration (about 3–5 s). A page whose operation is removed from a spec stays reachable until the server restarts.Verify
🤖 Generated with Claude Code