fix(preview): serve fresh HTML for project pages after .qmd edit#14548
fix(preview): serve fresh HTML for project pages after .qmd edit#14548cderv wants to merge 10 commits into
Conversation
In project preview mode, the persistent ProjectContext returned by watcher.project() owns a long-lived fileInformationCache populated at preview startup. When the watcher fires on a source edit, two render paths follow: the watcher itself dispatches to a render call that builds an ephemeral context (no stale cache), but the subsequent HTTP-handler render in serve.ts reuses the persistent context and reads the pre-edit expanded markdown back out of cache.fullMarkdown. The regenerated HTML mtime advances while the body content stays at the pre-edit revision (#10392). Invalidate cache entries for each changed input before the watcher's render dispatch, mirroring the renderForPreview pattern in src/command/preview/preview.ts. A companion commit adds a source-mtime and size fingerprint inside projectResolveFullMarkdownForFile so the cache contract self-validates even if a future caller forgets to invalidate; this surgical fix is the single-commit cherry-pick target for the v1.9 backport. Closes #10392.
The persistent ProjectContext used by website/book preview kept a fileInformationCache.fullMarkdown entry populated at startup with no freshness fingerprint, so subsequent renders fed Pandoc the pre-edit expanded markdown for the HTTP-handler renderProject() call site. Adds sourceMtime + sourceSize fields on FileInformation and re-reads when either differs from the cached value. Pairs with the watcher-side invalidation in the preceding commit; this layer closes the contract gap for any future caller that forgets to invalidate. Size is included alongside mtime to catch the edge case where an edit lands within a single mtime tick on a coarse-resolution filesystem but changes the byte count. Relates to #10392.
…#10392) Manual T1/T2/T3 (P1) plus T4-T6 (P2/P3) covering the project preview stale-render reproduction. Reproduces deterministically against the existing website fixture at tests/docs/manual/preview/project-preview/. Originally drafted on the debug/preview-cache-logging investigation branch; landing here so the spec accompanies the fix. Relates to #10392.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
…hness guard The preview architecture doc only described the single-file re-render path. It did not capture the project-preview two-invocation chain (watcher render plus HTTP-handler render reusing the persistent context) that #10392 lives on, nor the fullMarkdown mtime+size freshness guard added to defend it.
The single-input row in the Context Computation Count table counted +1 for the serve.ts HTTP-handler render, but that render reuses the persistent project context (renderProject(watcher.project(), ...)) and computes no new context. Counting +1 misstated context-creation cost in a table whose column tracks context computations. Changed to +0 and clarified the note that the row reflects two render invocations but only one new context computation.
…ix (#10392) The fix spans two surfaces: a project-preview-only invalidation call site and a freshness guard that runs on every render, inspect, and execute path. T8-T14 cover the wider surface so a future change can confirm the guard introduces no regression off the preview path and the new statSync per cache hit stays negligible at book scale.
Manual testing showed a sub-second save burst is coalesced by the watcher into a single render, so the final body can reflect the first read of the burst rather than the last keystroke. That is render-scheduling behavior, not cache staleness, and is tracked as a separate follow-up. T7 asserts transient-notebook safety under concurrency, which is what the fix governs.
Manual preview testingI ran the manual preview protocols against this branch (dev build, Windows), focusing on the two surfaces the fix touches: the project-preview-only Jupyter
|
When previewing a website or book project, editing a non-index
.qmdregenerated the HTML on disk but the preview server kept serving the pre-edit body. Stopping and restartingquarto previewcleared it for the first edit; the next edit reproduced the bug.Root Cause
watcher.project()returns a long-livedProjectContextwhosefileInformationCache.fullMarkdowncaches the expanded markdown per input. The HTTP-handler render insrc/project/serve/serve.tsreuses this context, so after a watcher-triggered re-render,projectResolveFullMarkdownForFilereturned the pre-edit expanded markdown. The regenerated HTML body therefore reflected the previous content.Fix
Two layers, motivated separately:
src/project/serve/watch.ts): invalidate the project context'sfileInformationCachefor each changed input inside thesubmitRendercallback. Inside the callback (not beforesubmitRender) so the cache mutation is serialized with any in-flight HTTP-handler render via the existing render queue —invalidateForFilemay delete a transient.quarto_ipynb, and running it before enqueue could race a concurrent read.src/project/project-shared.ts): guard thefullMarkdowncache entry by source filemtime + size. A stale entry is dropped on the next read even if a future caller forgets to invalidate. Size catches the case where filesystems with coarse mtime resolution (e.g. 2-second FAT/SMB) place a rapid edit in the same tick.Scope
This PR fixes the direct symptom for non-index project pages where the edit is to the
.qmdfile itself. Related cases raised in the issue thread are not covered here and are tracked separately:{{< include >}}— the includer's cache is not invalidated when the includee changes (Preview not updated on changes to included files #2795).template-partials:file (quarto preview does not re-render when a template-partial is edited #14561).Test Plan
Automated (
tests/unit/project/file-information-cache.test.ts): mtime guard, size guard, per-file invalidation.Manual spec:
tests/docs/manual/preview/10392-project-preview-stale-cache-after-edit.md..qmdin a website project — served HTML reflects the edit (T1)index.qmd— no regression on the index path (T3).quarto_ipynbaccumulation tests pass#11475 and #13755 report the same root cause on older release lines (1.6, 1.8); this PR addresses it on
mainfor 1.10.Fixes #10392
Fixes #11475
Fixes #13755