Skip to content

Prefetch Hermes+Lazer feeds at build time for the docs search index - #3917

Open
aditya520 wants to merge 4 commits into
mainfrom
hydra/i-uhuwizmn/head
Open

Prefetch Hermes+Lazer feeds at build time for the docs search index#3917
aditya520 wants to merge 4 commits into
mainfrom
hydra/i-uhuwizmn/head

Conversation

@aditya520

@aditya520 aditya520 commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

The developer-hub docs search route (apps/developer-hub/src/app/api/search/route.ts) fetched ~6.2 MB across three external APIs — hermes.pyth.network, hermes-beta.pyth.network, and the Lazer symbols API — on every cold-start lambda before building the Fumadocs/Orama index. This moves those fetches to build time so the route serves the feed list from a locally-generated static JSON snapshot imported synchronously.

No behavior change from the user's perspective: feed titles, IDs, links, and price-feed-core / price-feed-pro tags stay identical, and global docs search still surfaces feed entries.

Changes

  • New apps/developer-hub/scripts/generate-price-feeds-snapshot.ts — fetches the three sources in parallel, validates them with the shared zod schemas, and writes src/generated/price-feeds.json. Best-effort per source: a failed fetch/validation logs a warning and falls back to the previously-generated snapshot (or an empty set on a cold build). A top-level try/catch guarantees the import target exists so the build never fails.
  • New apps/developer-hub/src/app/api/search/feed-schemas.ts — extracts the Hermes/Lazer zod schemas, their inferred types, and the PriceFeedsSnapshot shape into a module shared by both the route and the generator (avoids schema drift).
  • route.ts — replaces the getHermesFeeds() / getLazerFeeds() runtime fetches with a static import of the snapshot. The indexes callback is now fully synchronous and performs zero external HTTP calls. The AdvancedIndex transforms (title / description / url / id / tag / structuredData) are byte-identical to before.
  • package.json — adds generate:price-feeds alongside generate:docs / generate:changelog.
  • turbo.json — adds the generate:price-feeds task and wires it into build.dependsOn. The task is cache: false and its src/generated/** output is intentionally kept out of the build task outputs, mirroring the existing generate:changelog precedent for gitignored, externally-fetched build artifacts. Because the feed lists come from external APIs (not source-controlled inputs), a cacheable task would replay a stale — or transient-outage incomplete — snapshot until an unrelated tracked file changed; cache: false makes it regenerate on every build, and keeping it out of the build outputs means a warm build cache never restores a stale snapshot over the freshly generated one. The task still runs before build via build.dependsOn, so the route's static-import target is always present.
  • .gitignore — ignores src/generated/ (regenerated at each build, never committed).

Verification

  • pnpm turbo run test:types --filter=@pythnetwork/developer-hub — pass (full build ran generate:price-feeds fresh, then tsc).
  • pnpm turbo run test:lint --filter=@pythnetwork/developer-hub — pass.
  • pnpm generate:price-feeds on a deleted src/generated/ — recreates the dir and writes the snapshot (hermes 3056, hermes-beta 2890, lazer 3435).
  • turbo run generate:price-feeds --dry=json confirms cache.local: false / cache.remote: false for the task.
  • git merge-tree origin/main HEAD — no conflicts.

@aditya520
aditya520 requested a review from a team as a code owner July 21, 2026 13:37
@vercel
vercel Bot temporarily deployed to Preview – proposals July 21, 2026 13:37 Inactive
@vercel
vercel Bot temporarily deployed to Preview – staking July 21, 2026 13:37 Inactive
@vercel
vercel Bot temporarily deployed to Preview – insights July 21, 2026 13:37 Inactive
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
component-library Ready Ready Preview, Comment Jul 27, 2026 2:08pm
developer-hub Building Building Preview, Comment Jul 27, 2026 2:08pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
api-reference Skipped Skipped Jul 27, 2026 2:08pm
entropy-explorer Skipped Skipped Jul 27, 2026 2:08pm
insights Skipped Skipped Jul 27, 2026 2:08pm
proposals Skipped Skipped Jul 27, 2026 2:08pm
staking Skipped Skipped Jul 27, 2026 2:08pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 637287c22e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +55 to +58
"generate:price-feeds": {
"dependsOn": ["//#install:modules"],
"outputs": ["src/generated/**"]
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disable caching for feed snapshots

When only the upstream Hermes/Lazer feed lists change (or a transient API outage produced an incomplete snapshot), this task can be served from Turbo cache instead of re-fetching: Turborepo's config docs state that cache defaults to true and task inputs default to source-controlled package files, while src/generated/ is gitignored and the API responses are external (https://turborepo.dev/docs/reference/configuration#cache). In the inspected Vercel path (apps/developer-hub/vercel.json runs turbo run build --filter @pythnetwork/developer-hub), a cache hit replays the old snapshot/.next output, so search can stay stale until a tracked file changes or the cache is forced; please make the snapshot contents part of the build cache key or disable caching for the relevant generation/build path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both halves of this are now addressed:

  • Generation caching: generate:price-feeds is cache: false since 3b18e659d, so the snapshot is re-fetched on every build.
  • Build cache key: fixed in ab3b14b4f. src/generated/price-feeds.json is gitignored, so it wasn't in the build task's default inputs ($TURBO_DEFAULT$ hashes tracked files only) — a warm cache could replay a stale .next over a fresh snapshot. The snapshot is now listed in build.inputs.

Verified: the file appears in the hashed inputs via --dry=json; removing a single feed from the snapshot flips the build hash (147893e6…8feb5d3b…); and two consecutive builds with unchanged feed data still cache-hit, since Turbo hashes inputs after dependency tasks run — so the key always reflects what the generator just wrote, and caching stays useful when feed data hasn't changed.

aditya520 and others added 3 commits July 26, 2026 13:19
…cs search

The docs search route fetched ~6.2 MB across three external APIs
(hermes.pyth.network, hermes-beta.pyth.network, and the Lazer symbols
API) on every cold-start lambda before building the Orama index. Move
those fetches to a build-time generator that writes a static
`src/generated/price-feeds.json` snapshot, which the route now imports
synchronously.

- New `scripts/generate-price-feeds-snapshot.ts`: fetches the three
  sources in parallel, validates with the shared zod schemas, and writes
  the snapshot. Best-effort per source — a failed fetch warns and falls
  back to the previously-generated file (or an empty set on a cold
  build) and never fails the build.
- Extract the Hermes/Lazer zod schemas and types into a shared
  `feed-schemas.ts` consumed by both the route and the generator.
- The `/api/search` route performs zero external HTTP calls at runtime;
  its `indexes` callback is now fully synchronous. Feed titles, IDs,
  URLs, and `price-feed-core` / `price-feed-pro` tags are unchanged.
- Wire `generate:price-feeds` into package.json and turbo.json (build
  dependency + cached `src/generated/**` output); gitignore the output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generate:price-feeds task fetches feed lists from external Hermes/Lazer
APIs, whose contents are not reflected in any source-controlled input. With
Turbo's default cache:true, a cache hit replays a stale (or transient-outage
incomplete) snapshot until an unrelated tracked file changes.

Mirror the generate:changelog precedent for gitignored, externally-fetched
build artifacts: set cache:false so the snapshot is always regenerated. The
task still runs before build via build.dependsOn, so the route's
static-import target is always present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The search route returned every match with no cap: "e" produced 16,802
results / 8.1 MB and pasting a feed symbol took 4.66s against production.
fumadocs groups Orama hits by page_id with no limit on group count, and
every one of the ~9.4k feeds is its own group, so any common token
("usd", "price", "e") matched thousands of them. Note that Orama's
`limit` does not bound group count, so the cap has to be applied in the
route handler.

- Cap responses at 24 results with per-family quotas (12 docs / 6 Core /
  6 Pro, unused slots backfilled). Without quotas "USD" pushed the first
  documentation hit to rank 624 and "solana" surfaced no feeds at all.
- Drop `structuredData.contents` from feed entries. fumadocs already
  indexes `title` and `description` as their own documents, so those
  entries duplicated text at 2-3x the document count. Symbol, name,
  description and feed ID all remain searchable.
- Deduplicate Core feeds by symbol. hermes-beta republishes 99.4% of the
  mainnet symbol list (2,873 of 2,890) into rows with identical titles
  and identical URLs, i.e. literal duplicate entries in the dialog.
  Mainnet wins ties; the 17 beta-only symbols stay searchable.
- Add a deterministic exact-match pass over feed symbols and names.
  BM25 over the remaining documents does not favour the feed a user
  typed: searching "Crypto.BTC/USD" returned 24 results without that
  feed among them.
- Apply the biome fixes CI misses (`biome ci --changed` reports
  "Checked 0 files" on this repo's PR runs).

Measured over 6 interleaved A/B rounds of local production builds
(15-query set, 9 iterations each): 542.9ms -> 117.5ms median total
latency (4.62x) and 38.57 MB -> 0.12 MB total payload. Worst pairing of
fastest-baseline against slowest-optimized round is 3.23x. Loopback
makes payload nearly free, so this understates the production gain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aditya520
aditya520 force-pushed the hydra/i-uhuwizmn/head branch from 3cddf64 to 3b18e65 Compare July 26, 2026 17:41
@aditya520

Copy link
Copy Markdown
Member Author

Added scope: search performance and relevance (ec0063c9c)

Folded in from #3931 so this lands as one change — that work rewrote the route.ts this PR introduces, so reviewing them separately meant reading code that was replaced two commits later.

Problem. The route returned every match with no cap. Against production: USD → 5.2 MB / 15,769 results, e → 8.1 MB / 16,802, and pasting a feed symbol (Equity.US.RIVN/USD.ON) → 4.66s / 6.2 MB / 23,494 results. The dialog debounces at 100ms, so typing fires several of those in a row.

Cause: searchAdvanced calls Orama with groupBy: { properties: ["page_id"], maxResult: 8 } and no limit on group count, and every one of the ~9.4k feeds is its own group. Worth knowing for anyone touching this: search: { limit: N } does nothing here — Orama's limit bounds hits, not groups. I verified the output is byte-identical with and without it, so the cap has to be applied in the route handler.

Changes

  • Cap at 24 results with per-family quotas (12 docs / 6 Core / 6 Pro, unused slots backfilled). Load-bearing, not cosmetic: for USD the first documentation hit sat at rank 624, and a naive top-24 slice made solana return no feeds at all.
  • Drop structuredData.contents from feed entries. fumadocs already indexes title and description as their own documents, so those entries duplicated text at 2-3x the document count. Index build 660ms → 260ms.
  • Deduplicate Core feeds by symbol. hermes-beta republishes 99.4% of the mainnet symbol list (2,873 of 2,890) into rows with identical titles and identical URLs — literal duplicate entries in the dialog. Mainnet wins ties; the 17 beta-only symbols stay searchable.
  • Add a deterministic exact-match pass. Capping exposed a latent relevance bug: searching the exact symbol Crypto.BTC/USD returned 24 results without that feed among them, because BM25 favours common tokens across thousands of near-identical symbols.
  • Apply the biome fixes CI misses (//:test:lint logs Checked 0 files on PR runs, so biome ci --changed never inspected these files).

Results. 6 interleaved A/B rounds of local production builds, 15-query set, 9 iterations per query, interleaved so machine drift cannot bias either side:

before after
median total latency 542.9ms 117.5ms
total payload 38.57 MB 0.12 MB

4.62x median. Per-round: 4.75x, 4.27x, 3.23x, 4.44x, 4.86x, 5.32x. Pairing the fastest baseline round against the slowest optimized round still gives 3.23x. Loopback makes an 8 MB response nearly free, so this understates the production gain.

Verification. 20/20 correctness checks — Core by symbol / bare ticker / 64-char feed ID, Pro by name / symbol / numeric Lazer ID / description word, BTC+ETH+USD each returning both families, docs still reachable on prose queries, no duplicate feed rows. Build passes 45/45, tsc and biome check clean.

Still open on this PR

Two items from review that are not addressed here:

  1. An upstream outage at build time silently ships an empty feed index. The fallback to "the previously-generated snapshot" cannot work on Vercel: src/generated/ is gitignored and the task is cache: false, so there is never a previous snapshot to read. The build goes green and search quietly loses thousands of entries.
  2. No refresh path. No cron, no ISR, so new feeds only appear on the next deploy. Lazer added 64 feeds in the 5 days after this PR was opened.

generate:price-feeds is cache:false, but src/generated/price-feeds.json
is gitignored and therefore absent from the build task's default inputs
($TURBO_DEFAULT$ hashes tracked files only). A warm build cache could
replay a stale .next over a freshly generated snapshot, so search served
old feeds until an unrelated tracked file changed.

List the snapshot in the build task's inputs so its content is part of
the cache key. Package-level inputs replace the root list, so the root
excludes are restated. Verified: the file now appears in the hashed
inputs (--dry=json), removing a single feed flips the build hash, and
two consecutive builds with unchanged feed data still produce a cache
hit -- Turbo hashes inputs after dependency tasks run, so the key always
reflects what the generator just wrote.

Addresses the remaining half of the Codex review comment on turbo.json
(the cache:false half landed earlier).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant