feat: add article for when to use Bloom Filter#7928
Conversation
Adds a new post explaining the Postgres bloom index for wide cache-style tables, with a CodeHike-powered animated walkthrough of the bloom algorithm (BloomFilterDemo) and two AgentPrompt animations for the SQL schema morph and a recorded run of the bun demo. Extends AgentPrompt's mark extractor to recognize SQL "-- !mark" comments so the schema morph can highlight added lines. Sample numbers were captured by running the live demo in demos-for-content/bun-bloom-filters against a temporary Prisma Postgres DB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two follow-up animations in the bloom blog were prompt-style chat bubbles, but neither one is actually a prompt - both are commands / schema changes. Replaces them with structures that match what they are: - "How Postgres turns this into an index" now shows a static SQL block instead of a morph animation. - "Run it" now uses a new BloomDemoRunner: code panel on the left with the currently-executing slice of index.ts highlighted, terminal panel on the right that fills cumulatively, six clickable labeled steps, plus prev / play-pause / next. Each step has a one-line caption explaining what that section of code does. Also slows the bloom filter visualization (5.2s per step) and adds the same prev / play-pause / next controls plus clickable step pills, so readers can move through the algorithm at their own pace. Drops the obsolete snippets.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runner looked broken because the terminal showed only the current step's two output lines, leaving most of the panel empty. And the side-by-side 1.4fr:1fr split was too cramped at the blog's article width (~768px), so the code panel was narrow enough to force horizontal scrolling on every long line. Fixes: - Terminal now shows the full output across all six steps from the start. Lines for past steps are dimmed (60%), the current step's lines have a green border and full opacity, future lines are faintly visible. Auto-scrolls to the active line on advance. - Code panel auto-scrolls so the highlighted slice is in view. - Side-by-side now uses 1:1 split. - Container query: when the runner element itself drops below 720px the body stacks vertically (code above terminal) so the blog body width never forces side-by-side at unreadable sizes. - Each pane gets a small uppercase label (INDEX.TS, TERMINAL OUTPUT) so the relationship is obvious. - Step counter now includes the active step's title; nav buttons are bordered 28px squares instead of unlabeled 24px gray icons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post jumped from a bloom-filter visualization straight to Postgres
bloom indexes without explaining how the two relate, and it kept using
btrees as the comparison baseline without ever describing what a btree
is or what it gives you.
- New "A quick refresher on btrees" section before the problem
statement: walks through what a btree is, what it does well (exact
lookup, range, sort), and where its costs land as indexes accumulate.
- New "From a bloom filter to a bloom index" section after the
visualization: explicitly connects the single-bit-array filter the
reader just played with to the per-row signature an index stores.
- "When to use it" expanded with concrete SQL examples for each bullet
(wide tables, mixed filter subsets, write amplification, recheck
overhead).
- "When not to use it" recast with bolded leads and the actual reason
next to each.
- Trimmed filler ("That's the whole idea.", redundant recap line about
being smaller and faster) and minor grammar fixes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace "btree" / "btrees" with "B-tree" / "B-trees" in the bloom filter blog's prose, meta description, runner step titles, captions, and the displayed terminal output. JS identifiers (btreeMB, btreeMs) and the literal Postgres index name prefix (btree_<col>) stay as is. - Add btree, btrees, Btree, Btrees to the repo's cspell dictionary so the variant spellings inside code identifiers and index names do not trip the docs spellcheck. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…button Two fixes for the runner terminal pane: - Active-step highlight was being clipped at the original parent width when the line content overflowed horizontally. The line div's box was staying at parent-content-box width while the text overflowed past it, so the green background stopped at the visible viewport edge. Switching the line to "width: max-content; min-width: 100%" makes the box grow to the content's width when wide and to the parent's width when short, so the background covers the whole scrollable line on both axes. - New "Copy" button in the terminal pane label that puts the full cumulative output on the clipboard. Uses navigator.clipboard and flips the label to "Copied" with a Check icon for 1.6s after a successful copy. The setTimeout is tracked in a ref and cleaned up on unmount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
WalkthroughAdds a new MDX article on Postgres bloom indexes plus interactive demos (bloom filter, bloom-index benchmark runner, B-tree walkthrough, hash walkthrough), server-side highlighting wrappers, client visualizers, demo CSS, small AgentPrompt mark parsing tweak, cspell entries, and author/profile pages. ChangesBloom Index Blog Post and Demos
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomDemoRunner.tsx`:
- Around line 91-100: The caption claims "Drop the B-trees" but the SOURCE block
never drops them, so update SOURCE (the block that creates indexes and measures
bloomMB via totalIndexMB()) to make the measurement honest: either (A) insert
DROP INDEX IF EXISTS statements for the six per-column B-tree indexes (the same
index names created earlier) immediately before the CREATE INDEX … USING bloom
so bloomMB reflects only the bloom index, or (B) change the caption string in
the object with title "B: One bloom index" to remove the "Drop the B-trees"
wording so it accurately matches the code; modify the SOURCE block or the
caption accordingly and ensure bloomMB = await totalIndexMB() then measures the
intended state.
- Around line 108-112: The hardcoded "70% smaller" in the output array conflicts
with the computed percentage (1 - bloomMB / btreeMB) * 100 and the byte figures;
update the output to compute and inject the actual percentage using the existing
bloomMB and btreeMB values (or else change the byte figures to match 70%), i.e.
replace the literal "70% smaller" string with a formatted value derived from the
computed percentage expression used elsewhere (referencing bloomMB and btreeMB)
so the output array and BloomDemoRunner.tsx's displayed numbers stay consistent.
In
`@apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomDemoRunnerClient.tsx`:
- Around line 107-115: In copyOutput(), handle rejection from
navigator.clipboard.writeText by chaining a .catch handler on the returned
promise (keeping the existing .then path); inside the .catch log or process the
error (e.g., processLogger/error or console.error) and ensure UI state is
consistent (don’t setCopied(true) on failure and clear any pending
copyTimeoutRef as needed), referencing the copyOutput function,
navigator.clipboard.writeText call, setCopied, copyTimeoutRef, and allOutput to
locate the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a089a0fc-0eda-4eb1-b5a5-bfe2193d9c08
📒 Files selected for processing (8)
apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomDemoRunner.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomDemoRunnerClient.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomFilterDemo.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomFilterDemoClient.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/index.mdxapps/blog/src/app/global.cssapps/blog/src/components/AgentPrompt/index.tsxapps/docs/cspell.json
| { | ||
| title: "B: One bloom index", | ||
| caption: | ||
| "Drop the B-trees and create a single bloom index spanning all six columns. Same three lookups.", | ||
| lines: { from: 37, to: 43 }, | ||
| output: [ | ||
| "", | ||
| "B. One bloom index (all six columns)...", | ||
| " index size: 0.2 MB", | ||
| " 3 lookups: 302.7 ms", |
There was a problem hiding this comment.
Caption says "drop the B-trees" but the script never does.
The step caption promises "Drop the B-trees and create a single bloom index", yet section B of SOURCE (lines 37-43) only runs CREATE INDEX … USING bloom — there's no DROP INDEX. Since bloomMB = await totalIndexMB() measures total index size, leaving the six B-trees in place means the bloom-vs-btree comparison would be measuring btrees+bloom against btrees if a reader actually ran it. Either add the drop to the source so the measurement is honest, or soften the caption to match what the code does.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomDemoRunner.tsx`
around lines 91 - 100, The caption claims "Drop the B-trees" but the SOURCE
block never drops them, so update SOURCE (the block that creates indexes and
measures bloomMB via totalIndexMB()) to make the measurement honest: either (A)
insert DROP INDEX IF EXISTS statements for the six per-column B-tree indexes
(the same index names created earlier) immediately before the CREATE INDEX …
USING bloom so bloomMB reflects only the bloom index, or (B) change the caption
string in the object with title "B: One bloom index" to remove the "Drop the
B-trees" wording so it accurately matches the code; modify the SOURCE block or
the caption accordingly and ensure bloomMB = await totalIndexMB() then measures
the intended state.
| output: [ | ||
| "", | ||
| " Bloom index is 70% smaller (0.2 MB vs 0.5 MB),", | ||
| " and one index covers any subset of those six columns.", | ||
| ], |
There was a problem hiding this comment.
The "70% smaller" figure doesn't match the numbers next to it.
Here's the arithmetic the script itself uses on line 48: (1 - bloomMB / btreeMB) * 100. With 0.2 MB vs 0.5 MB that's (1 - 0.2/0.5) * 100 = 60%, not 70%. Note the MDX prose (line 119) describes it as "roughly 2.5x smaller", and 0.5 / 0.2 = 2.5 — so the prose is right and this hardcoded output is the outlier. Either correct the percentage or adjust the byte figures so the demo and the article tell the same story.
📐 Proposed fix
- " Bloom index is 70% smaller (0.2 MB vs 0.5 MB),",
+ " Bloom index is 60% smaller (0.2 MB vs 0.5 MB),",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| output: [ | |
| "", | |
| " Bloom index is 70% smaller (0.2 MB vs 0.5 MB),", | |
| " and one index covers any subset of those six columns.", | |
| ], | |
| output: [ | |
| "", | |
| " Bloom index is 60% smaller (0.2 MB vs 0.5 MB),", | |
| " and one index covers any subset of those six columns.", | |
| ], |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomDemoRunner.tsx`
around lines 108 - 112, The hardcoded "70% smaller" in the output array
conflicts with the computed percentage (1 - bloomMB / btreeMB) * 100 and the
byte figures; update the output to compute and inject the actual percentage
using the existing bloomMB and btreeMB values (or else change the byte figures
to match 70%), i.e. replace the literal "70% smaller" string with a formatted
value derived from the computed percentage expression used elsewhere
(referencing bloomMB and btreeMB) so the output array and BloomDemoRunner.tsx's
displayed numbers stay consistent.
| function copyOutput() { | ||
| const text = allOutput.map((e) => e.line).join("\n"); | ||
| if (!navigator.clipboard) return; | ||
| navigator.clipboard.writeText(text).then(() => { | ||
| setCopied(true); | ||
| if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); | ||
| copyTimeoutRef.current = setTimeout(() => setCopied(false), 1600); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Add a .catch to the clipboard write.
navigator.clipboard.writeText returns a promise that can reject — denied permission, a non-secure context, or the user dismissing a prompt. Right now a rejection becomes an unhandled promise rejection and the "Copied" state silently never appears. A small catch keeps things tidy and lets you optionally surface a failure.
🛡️ Proposed fix
- navigator.clipboard.writeText(text).then(() => {
- setCopied(true);
- if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
- copyTimeoutRef.current = setTimeout(() => setCopied(false), 1600);
- });
+ navigator.clipboard
+ .writeText(text)
+ .then(() => {
+ setCopied(true);
+ if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
+ copyTimeoutRef.current = setTimeout(() => setCopied(false), 1600);
+ })
+ .catch(() => {
+ /* clipboard unavailable or permission denied */
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function copyOutput() { | |
| const text = allOutput.map((e) => e.line).join("\n"); | |
| if (!navigator.clipboard) return; | |
| navigator.clipboard.writeText(text).then(() => { | |
| setCopied(true); | |
| if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); | |
| copyTimeoutRef.current = setTimeout(() => setCopied(false), 1600); | |
| }); | |
| } | |
| function copyOutput() { | |
| const text = allOutput.map((e) => e.line).join("\n"); | |
| if (!navigator.clipboard) return; | |
| navigator.clipboard | |
| .writeText(text) | |
| .then(() => { | |
| setCopied(true); | |
| if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); | |
| copyTimeoutRef.current = setTimeout(() => setCopied(false), 1600); | |
| }) | |
| .catch(() => { | |
| /* clipboard unavailable or permission denied */ | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BloomDemoRunnerClient.tsx`
around lines 107 - 115, In copyOutput(), handle rejection from
navigator.clipboard.writeText by chaining a .catch handler on the returned
promise (keeping the existing .then path); inside the .catch log or process the
error (e.g., processLogger/error or console.error) and ensure UI state is
consistent (don’t setCopied(true) on failure and clear any pending
copyTimeoutRef as needed), referencing the copyOutput function,
navigator.clipboard.writeText call, setCopied, copyTimeoutRef, and allOutput to
locate the change.
The post opened with a B-tree primer, which felt wrong for a piece titled "Bloom Filters in Postgres" - the reader landed on a paragraph about a different index type before anything else. The "The problem it solves" heading also used a bare "it" with no clear antecedent. - Reordered sections: bloom filter concept and visualization come first, followed by the bloom-filter-to-bloom-index bridge, then the wide-table use case, then the B-tree primer (now framed as comparison context right before the demo). - Renamed "The problem it solves" to "The wide-table problem". - Added a one-line lead to the B-tree primer explaining why it shows up at that point in the article. - Reworded the intro's "what it is, when it wins" pointer to match the new section order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… and hash demos - Hero image: copied the new "Bloom Filters in Postgres" PNG into the blog's public folder; set heroImagePath, metaImagePath and heroImageAlt on the post frontmatter. - Series: marked this post as part of a new "postgres-features" series at index 2. Updated the previous post (you-dont-need-redis-postgres-already-has-pub-sub) to be index 1 of the same series. Added a short intro paragraph linking back to the Pub/Sub post. - BTreeDemo: new CodeHike-driven walkthrough of a B-tree lookup. Single shared pseudocode block with the active line marked per step; a small 2-level tree (root with 2 keys, 3 leaves of 3 entries each) highlights the active node and the matched key as the lookup descends to find "t42" -> row 142. Six labeled steps with prev/play/next and clickable pills. - HashDemo: new CodeHike-driven walkthrough of the hashes() function used by the bloom filter. Shows three independent hashes (fnv1a, djb2, murmur3) running on "alice", their raw outputs, the mod 16 step, and the three bit positions [2, 7, 11] lighting up on a 16-cell array. Five labeled steps. - Famous-users line: added a one-liner naming where bloom filters show up in production (Chrome safe browsing, Cassandra/HBase, Akamai, Medium, Bitcoin SPV). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BTreeDemo.tsx`:
- Around line 26-63: Phases are off-by-one against SOURCE because line numbering
is mismatched; update the phase data and/or the mapping logic in codeForPhase so
indices align: either change the literal phase entries—set lookup("t42") to
{from:10,to:10} and set "Search the leaf" and "Return the row pointer" to
{from:7,to:7}—or (preferable) fix codeForPhase to treat SOURCE as having 1-based
line numbers (clamp requested from/to to [1, lineCount] and convert to
zero-based when slicing) so all phase labels (e.g., "lookup(\"t42\")", "Pick the
child", "Search the leaf", "Return the row pointer") produce the correct
highlighted lines.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 019dee17-7391-4873-a12a-9914840ca984
⛔ Files ignored due to path filters (1)
apps/blog/public/postgres-bloom-index-the-overlooked-postgres-feature/imgs/bloom-filters-in-postgres.pngis excluded by!**/*.png
📒 Files selected for processing (7)
apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BTreeDemo.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/BTreeDemoClient.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/HashDemo.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/HashDemoClient.tsxapps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/index.mdxapps/blog/content/blog/you-dont-need-redis-postgres-already-has-pub-sub/index.mdxapps/blog/src/app/global.css
✅ Files skipped from review due to trivial changes (1)
- apps/blog/content/blog/postgres-bloom-index-the-overlooked-postgres-feature/index.mdx
- Add the postgres-features series to the series registry so
/blog/series/postgres-features stops 404ing and renders the two
posts in it (the Pub/Sub post and the Bloom Filters post). Marked
featured so it surfaces on the home shelf.
- New /blog/author/[slug] route. Slug uses the existing toAuthorSlug
helper, so "Ankur Datta" -> ankur-datta and so on. Page lists every
post that includes the author, newest first. generateStaticParams
enumerates the union of author slugs across the corpus.
- New lib/authors-pages.ts collecting unique author profiles
(slug, display name, avatar src, post count) and the per-slug post
filter.
- AuthorAvatarGroup now links each author name to the author landing
page. The link can be turned off with linkAuthors={false} where the
parent doesn't want it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/blog/src/app/(blog)/author/[slug]/page.tsx (1)
7-7: ⚡ Quick winDrop the unused
blogimport and thevoid blog;placeholder.Here's the teaching moment:
void blog;is doing nothing functional — it just evaluates the reference and discards it. Its only purpose is to silence an "unused import" lint warning. If a symbol isn't used, the cleaner answer is to not import it at all. Importing@/lib/sourcepurely as a side-effect anchor is a trap for the next maintainer, who will reasonably wonder what invariant that line protects. (If the import is meant to trigger module side-effects, that intent should be documented explicitly rather than expressed asvoid.)♻️ Proposed cleanup
-import { blog } from "`@/lib/source`"; import { findAuthorProfile, getAllAuthorProfiles, getPostsByAuthorSlug, } from "`@/lib/authors-pages`";- -void blog;Also applies to: 129-129
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/blog/src/app/`(blog)/author/[slug]/page.tsx at line 7, Remove the unused imported symbol blog from the import of "`@/lib/source`" and delete the corresponding void blog; placeholder; if the import was intended to trigger module side-effects instead of providing blog, replace with an explicit side-effect import (e.g., import "`@/lib/source`" with a comment) or add a clear comment explaining the intent — otherwise simply drop the import and placeholder to clean lint warnings and avoid confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/blog/src/app/`(blog)/author/[slug]/page.tsx:
- Line 7: Remove the unused imported symbol blog from the import of
"`@/lib/source`" and delete the corresponding void blog; placeholder; if the
import was intended to trigger module side-effects instead of providing blog,
replace with an explicit side-effect import (e.g., import "`@/lib/source`" with a
comment) or add a clear comment explaining the intent — otherwise simply drop
the import and placeholder to clean lint warnings and avoid confusion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: be9250e8-52f5-4e8e-951f-712368e7fb3c
📒 Files selected for processing (4)
apps/blog/src/app/(blog)/author/[slug]/page.tsxapps/blog/src/components/AuthorAvatarGroup.tsxapps/blog/src/lib/authors-pages.tsapps/blog/src/lib/series-registry.ts
✅ Files skipped from review due to trivial changes (1)
- apps/blog/src/lib/series-registry.ts
Two phases of the BTreeDemo referenced source lines that didn't exist:
- Phase 2 ("lookup('t42')") pointed at line 11, but the source has
only 10 lines, so its actual call site is line 10.
- Phases 4 and 5 ("Search the leaf" / "Return the row pointer")
pointed at line 6, which is the closing brace of the while loop;
the line we actually want to highlight is line 7
(return node.findEntry(key)?.rowPointer).
When the annotation referenced a line beyond the end of the source,
CodeHike's applyBlockAnnotation read undefined.range and threw,
which froze the next button and the rest of the demo. Fixed by
pointing at the correct lines, and clamping from/to to
[1, totalLines] in codeForPhase/codeForStep so a stray index in any
demo can't take the page down again. Applied the same clamp in
BloomDemoRunnerClient and HashDemoClient for defence-in-depth.
Verified by clicking through all 5 BTreeDemo step pills under
playwright with zero page errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Documentation
Style
Bug Fixes
Chores