Skip to content

Refresh API Explorer pages on code and asset changes in serve - #4321

Merged
akira28 merged 2 commits into
mainfrom
dev/api-pages-hot-reload
Oct 6, 2026
Merged

akira28 merged 2 commits into
mainfrom
dev/api-pages-hot-reload

Conversation

@akira28

@akira28 akira28 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

docs-builder serve under dotnet watch now 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 serve without 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 .md and .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 watch applies 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 watches src/Elastic.Documentation.Site/_static and refreshes the browser when Parcel writes JS or CSS. /_static responses send Cache-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

dotnet test tests/Elastic.ApiExplorer.Tests/
# GenerateProducts_RegenerateWithShorterPage_TruncatesPreviousOutput
./dev.sh serve
# open http://localhost:3000/api/doc/elasticsearch/
# edit src/Elastic.ApiExplorer/Landing/LandingView.cshtml; the page refreshes with the change

🤖 Generated with Claude Code

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>
@akira28
akira28 marked this pull request as ready for review October 6, 2026 09:51
@akira28
akira28 requested a review from a team as a code owner October 6, 2026 09:51
@akira28
akira28 requested a review from Mpdreamz October 6, 2026 09:51
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

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;

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.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@akira28
akira28 added this pull request to stack #4323 October 6, 2026 10:34
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();

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.

[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?

@akira28
akira28 merged commit 64a16c1 into main Oct 6, 2026
37 checks passed
@akira28
akira28 deleted the dev/api-pages-hot-reload branch October 6, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants