Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion packages/elements/__tests__/code-block.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";

import { CodeBlock, CodeBlockCopyButton } from "../src/code-block";
import {
CodeBlock,
CodeBlockCopyButton,
highlightCode,
} from "../src/code-block";

describe("codeBlock", () => {
it("renders code content", async () => {
Expand Down Expand Up @@ -148,3 +152,38 @@ describe("codeBlockCopyButton", () => {
});
});
});

// Must match MAX_TOKENS_CACHE_SIZE in src/code-block.tsx
const TOKENS_CACHE_BOUND = 100;

const awaitHighlight = (code: string) =>
new Promise<void>((resolve) => {
const result = highlightCode(code, "javascript", () => resolve());
if (result) {
resolve();
}
});

describe("token cache eviction", () => {
it("bounds the cache and keeps recently used entries", async () => {
const batchA = Array.from(
{ length: TOKENS_CACHE_BOUND },
(_, i) => `const a${i} = ${i};`
);
await Promise.all(batchA.map((code) => awaitHighlight(code)));

// Every entry of the fill batch is retained; refresh recency of the first
expect(highlightCode(batchA[0], "javascript")).not.toBeNull();

// Overflow the cache with new unique entries
const batchB = Array.from(
{ length: TOKENS_CACHE_BOUND - 1 },
(_, i) => `const b${i} = ${i};`
);
await Promise.all(batchB.map((code) => awaitHighlight(code)));

// The refreshed entry survived eviction; unrefreshed entries did not
expect(highlightCode(batchA[0], "javascript")).not.toBeNull();
expect(highlightCode(batchA[1], "javascript")).toBeNull();
});
});
34 changes: 24 additions & 10 deletions packages/elements/src/code-block.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
"use client";

import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
import type {
BundledLanguage,
BundledTheme,
HighlighterGeneric,
ThemedToken,
} from "shiki";

import { Button } from "@repo/shadcn-ui/components/ui/button";
import {
Select,
Expand All @@ -10,7 +18,6 @@ import {
} from "@repo/shadcn-ui/components/ui/select";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
import {
createContext,
memo,
Expand All @@ -21,12 +28,6 @@ import {
useRef,
useState,
} from "react";
import type {
BundledLanguage,
BundledTheme,
HighlighterGeneric,
ThemedToken,
} from "shiki";
import { createHighlighter } from "shiki";

// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
Expand Down Expand Up @@ -135,7 +136,9 @@ const highlighterCache = new Map<
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
>();

// Token cache
// Token cache — bounded LRU so streamed intermediate versions of a code
// string (each producing a unique key) don't accumulate for the page lifetime
const MAX_TOKENS_CACHE_SIZE = 100;
const tokensCache = new Map<string, TokenizedCode>();

// Subscribers for async token updates
Expand Down Expand Up @@ -189,9 +192,11 @@ export const highlightCode = (
): TokenizedCode | null => {
const tokensCacheKey = getTokensCacheKey(code, language);

// Return cached result if available
// Return cached result if available, refreshing its LRU recency
const cached = tokensCache.get(tokensCacheKey);
if (cached) {
tokensCache.delete(tokensCacheKey);
tokensCache.set(tokensCacheKey, cached);
return cached;
}

Expand Down Expand Up @@ -224,7 +229,16 @@ export const highlightCode = (
tokens: result.tokens,
};

// Cache the result
// Cache the result, evicting the least recently used entry when full
if (
!tokensCache.has(tokensCacheKey) &&
tokensCache.size >= MAX_TOKENS_CACHE_SIZE
) {
const oldestKey = tokensCache.keys().next().value;
if (oldestKey !== undefined) {
tokensCache.delete(oldestKey);
}
}
tokensCache.set(tokensCacheKey, tokenized);

// Notify all subscribers
Expand Down