fix(ui): correct markdown cache identity, streaming churn, and redundant tiers

Review follow-up on the #2769 highlight caches.

Fingerprint strength. The block/highlight caches are now global and
content-addressed, so a hash collision no longer mis-colors a block — it returns
a *different* block's rendered HTML and shows the user source they never wrote.
Length + one 32-bit FNV-1a is not enough key space for that failure mode at
session scale. `contentFingerprint` now combines two independent 32-bit
multiplicative hashes with a final avalanche (~64 bits); two multiplies per
character are free next to Shiki tokenization.

Streaming churn. Content addressing made every streaming step of the trailing
`live` block insert a new cache entry, so one long message evicted the settled
`full` blocks the fix exists to keep warm. `full` and `live` blocks now use
separate caches; the live cache is small (32 entries / 2MB) because it only has
to absorb repeat renders of the same step.

Redundant worker-side caches. `markdown-worker.ts` is the only sender to the
Shiki worker, and its client cache is larger than the worker-side ones, so the
worker caches could not serve a hit the client had not already served — they
only duplicated up to 48MB of payloads in a second heap. Removed; the reason
memoization belongs on the client is now documented there, along with why only
`highlightTokens` carries a theme in its key.

Dead `cacheKey` plumbing. `renderMarkdownBlocks` kept a `cacheKey` parameter it
only `void`-ed. Removed it and the now-unused `useMorphdomMarkdown` prop; the
remaining call-site local is renamed `fadeKey` for what it actually keys.

Tests: image-mode cache identity, streaming-does-not-evict-settled-blocks,
live-cache reuse, and a 20k same-length-source fingerprint collision check.
Each new guard was verified to fail without its fix.
This commit is contained in:
Serhii Dziupin
2026-08-17 16:45:53 +03:00
parent fc0ae0f445
commit 69150366fd
8 changed files with 181 additions and 123 deletions
@@ -835,7 +835,6 @@ const useMorphdomMarkdown = ({
containerRef,
text,
streaming,
cacheKey,
imageMode = 'inline',
syntaxVars,
ctx,
@@ -843,7 +842,6 @@ const useMorphdomMarkdown = ({
containerRef: React.RefObject<HTMLDivElement | null>;
text: string;
streaming: boolean;
cacheKey: string;
imageMode?: MarkdownImageMode;
syntaxVars: Record<string, string>;
ctx: DecorateContext;
@@ -908,7 +906,7 @@ const useMorphdomMarkdown = ({
const target = container.querySelector<HTMLElement>('[data-markdown-content]') ?? container;
let active = true;
void renderMarkdownBlocks(text, streaming, cacheKey, imageMode).then((blocks) => {
void renderMarkdownBlocks(text, streaming, imageMode).then((blocks) => {
if (!active) return;
const existing = Array.from(target.children) as HTMLElement[];
@@ -959,7 +957,7 @@ const useMorphdomMarkdown = ({
return () => {
active = false;
};
}, [containerRef, text, streaming, cacheKey, imageMode, ctx, refreshMermaidViewers]);
}, [containerRef, text, streaming, imageMode, ctx, refreshMermaidViewers]);
React.useEffect(() => {
const container = containerRef.current;
@@ -1040,13 +1038,13 @@ const MarkdownRendererImpl: React.FC<MarkdownRendererProps> = ({
const syntaxVars = React.useMemo(() => getMarkdownSyntaxVars(currentTheme), [currentTheme]);
const ctx = useDecorateContext(currentTheme, live, effectiveDirectory ? handlePreviewLoopback : undefined, DEFAULT_MERMAID_CONTROLS);
const cacheKey = `markdown-${part?.id ? `part-${part.id}` : `message-${messageId}`}`;
// Identity for the fade-in wrapper: a new part/message restarts the animation.
const fadeKey = `markdown-${part?.id ? `part-${part.id}` : `message-${messageId}`}`;
useMorphdomMarkdown({
containerRef,
text: content,
streaming: live,
cacheKey,
imageMode: variant === 'assistant' ? 'label' : 'inline',
syntaxVars,
ctx,
@@ -1060,7 +1058,7 @@ const MarkdownRendererImpl: React.FC<MarkdownRendererProps> = ({
if (isAnimated) {
return (
<FadeInOnReveal key={cacheKey} skipAnimation={skipFadeIn}>
<FadeInOnReveal key={fadeKey} skipAnimation={skipFadeIn}>
{markdownContent}
</FadeInOnReveal>
);
@@ -1137,9 +1135,6 @@ const SimpleMarkdownRendererImpl: React.FC<{
containerRef,
text: renderedContent,
streaming: false,
// Identity is unused for cache lookup (content-addressed in markdownCore);
// keep a stable per-variant key for effect deps alongside `text`.
cacheKey: `simple:${variant}`,
syntaxVars,
ctx,
});