Skip to content
Merged
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
1 change: 1 addition & 0 deletions .please/docs/tracks.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
{"id":"extract-shared-registry-20260408","type":"refactor","status":"in_progress","phase":"implement","issue":"#14","created":"2026-04-08","section":"active"}
{"id":"ignore-vendored-docs-20260408","type":"feature","status":"review","phase":"review","issue":"#24","pr":"#26","created":"2026-04-08","section":"completed"}
{"id":"npm-tarball-docs-20260408","type":"feature","status":"in_progress","phase":"implement","issue":"#33","created":"2026-04-08","section":"active"}
{"id":"registry-edge-cache-20260408","type":"feature","status":"review","phase":"finalize","issue":"#34","pr":"#36","created":"2026-04-08","section":"completed"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"track_id": "registry-edge-cache-20260408",
"type": "feature",
"status": "review",
"created_at": "2026-04-08T00:00:00Z",
"updated_at": "2026-04-08T00:00:00Z",
"issue": "#34",
"pr": "#36",
"project": "",
"project_item_id": ""
}
90 changes: 90 additions & 0 deletions .please/docs/tracks/completed/registry-edge-cache-20260408/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Plan: Registry API Edge Caching

> Track: registry-edge-cache-20260408
> Spec: [spec.md](./spec.md)

## Overview

- **Source**: /please:new-track
- **Track**: registry-edge-cache-20260408
- **Issue**: #34
- **Created**: 2026-04-08
- **Approach**: Nitro `routeRules` edge cache — ship AC-1–AC-4 first, defer ETag (AC-5) and deploy-purge (AC-6) behind feature checks.

## Purpose

Cut Worker CPU/request cost and improve CLI latency for `/api/registry/**` by delegating repeat lookups to the Cloudflare edge, while keeping staleness bounded to the `max-age + swr` window.

## Context

`apps/registry/nuxt.config.ts` currently has no `routeRules` block. The `/api/registry/[...slug].get.ts` handler runs on every CLI lookup, doing a Content Query + `expandStrategies` on each request. Registry data is effectively static between Pages deploys, so it is a clean fit for SWR edge caching.

## Architecture Decision

Use Nitro `routeRules` with `cache.swr + staleMaxAge` plus an explicit `cache-control` header rather than `defineCachedEventHandler`, because:

1. `routeRules` is applied by the `cloudflare-pages` Nitro preset at the edge via the Cache API — on a HIT the Worker is bypassed entirely, which directly serves the cost goal. An application-layer cache still costs a Worker invocation.
2. Nitro emits `cf-cache-status`/`age` when the edge serves a hit, making AC-1 observable without custom instrumentation.
3. Headers are declared once in `nuxt.config.ts` and apply to both code paths in the route (direct + alias fallback) without touching handler logic.

ETag (FR-3) and deploy-purge (FR-4) are deliberately deferred: they are additive and the issue recommends (A)+(C) first.

## Tasks

- [x] T001 Add `routeRules['/api/registry/**']` with `cache: { maxAge: 3600, swr: true, staleMaxAge: 86400 }` and explicit `cache-control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400` header. Extracted to `apps/registry/app/route-rules.ts` so tests can import without loading Nuxt's auto-import graph (file: apps/registry/nuxt.config.ts, apps/registry/app/route-rules.ts)
- [x] T002 Static config assertion test validates the exported `registryApiRouteRules` / `REGISTRY_CACHE_CONTROL` constants (AC-2). Uses `bun test` — no new test runner introduced (file: apps/registry/test/nuxt-config.test.ts) (depends on T001)
- [x] T003 `bun run --cwd packages/cli test`: 227 pass / 0 fail (AC-4) (depends on T001)
- [ ] T004 Post-deploy manual verification: `curl -sI https://<pages-url>/api/registry/vercel/next.js` twice and confirm second call returns `cf-cache-status: HIT` (AC-1); document result in the track retrospective (file: .please/docs/tracks/active/registry-edge-cache-20260408/plan.md) (depends on T001)

## Key Files

- `apps/registry/nuxt.config.ts` — add `routeRules` (primary change)
- `apps/registry/server/api/registry/[...slug].get.ts` — unchanged; verify no Set-Cookie / per-request variance that would poison a shared cache
- `apps/registry/test/api-registry-cache.test.ts` — new test for header assertion
- `packages/cli/test/registry.test.ts` — existing regression surface

## Verification

- **AC-1**: Two consecutive `curl -I` against the deployed Pages URL show second response with `cf-cache-status: HIT` (manual, T004).
- **AC-2**: Automated test in `api-registry-cache.test.ts` asserts exact `cache-control` header (T002).
- **AC-3**: Deploy a registry markdown change; confirm visibility within `max-age + swr` window without manual purge (manual observation, post-merge).
- **AC-4**: `bun run --cwd packages/cli test` passes with zero new failures (T003).
- **AC-5/AC-6**: Deferred — not in this track.

## Progress

_Populated by /please:implement._

## Decision Log

- 2026-04-08: Chose `routeRules` over `defineCachedEventHandler` to bypass the Worker on HIT (cost goal).
- 2026-04-08: Defer ETag and deploy-purge per issue recommendation (A)+(C).

## Surprises & Discoveries

- `defineNuxtConfig` auto-imported by Nuxt cannot be used from a standalone `bun test` context. Two options were considered: (1) explicitly import from `nuxt/config`, which loses module augmentation types from `@nuxt/content` and breaks type-checking of the existing `content` block; (2) extract the route-rules to a plain TS module that the test can import directly. Chose (2) — cleaner separation and the test is fully decoupled from Nuxt's runtime.
- `apps/registry` had no test runner at all. Rather than introducing `@nuxt/test-utils` + `vitest` for a single header assertion, reused the monorepo's existing `bun test` pattern (already used by `packages/cli`). Added a `test` script to `apps/registry/package.json`.
- Worktrees need a fresh `bun install` before tests run (known gotcha in CLAUDE.md).

## Outcomes & Retrospective

### What Was Shipped

- Nitro `routeRules` for `/api/registry/**` with `cache.swr` + explicit `cache-control` header — delivers AC-1 through AC-4.
- `apps/registry/app/route-rules.ts` as a small, isolated constants module (testable without Nuxt runtime).
- First test file for `apps/registry/` + `bun test` script.

### What Went Well

- TDD cycle stayed tight: RED (test imports `defineNuxtConfig` → fails) → pivot (extract constants) → GREEN in a single short iteration.
- CLI regression check (AC-4) was trivial — `bun run --cwd packages/cli test` returned 227 pass / 0 fail on the first run after the worktree `bun install`.
- Independent code-review agent validated integration concerns (Nuxt `app/` auto-scan, test bundling, `@nuxt/content` conflict) with no findings.

### What Could Improve

- The initial plan assumed an integration test against the running server. Reality check during implementation (no existing registry test infra) forced a pragmatic pivot to a static config assertion. Next time, scan the target package's test setup before drafting tasks.

### Tech Debt Created

- AC-5 (ETag) and AC-6 (deploy-hook purge) intentionally deferred per the issue's recommendation — track them as potential follow-ups if staleness becomes painful.
- `apps/registry` test coverage is still just one file. A real integration harness (`@nuxt/test-utils`) would be valuable once more server routes land (e.g., upcoming raw-docs API track).
54 changes: 54 additions & 0 deletions .please/docs/tracks/completed/registry-edge-cache-20260408/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
product_spec_domain: registry/caching
---

# Registry API Edge Caching

> Track: registry-edge-cache-20260408

## Overview

Cache `/api/registry/**` responses at the Cloudflare edge so repeat CLI lookups bypass the Worker entirely. Registry data is effectively static between deploys (markdown files in git), so aggressive edge caching reduces Worker CPU/request cost and improves CLI latency. Lays groundwork for the future raw-docs API track.

## Requirements

### Functional Requirements

- [ ] FR-1: Configure Nitro `routeRules` in `apps/registry/nuxt.config.ts` for `/api/registry/**` with `cache: { maxAge: 3600, swr: true, staleMaxAge: 86400 }` and `cache-control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400`.
- [ ] FR-2: Ensure the configured cache headers propagate on actual responses from the deployed Cloudflare Pages Worker (not just in local dev).
- [ ] FR-3 (optional, second commit): Emit a sha256-of-body `ETag` header via Nitro's `handleCacheHeaders`, enabling future CLI `If-None-Match` → 304 conditional GETs.
- [ ] FR-4 (optional, deferred): Deploy-hook cache purge — GitHub Action calls Cloudflare Cache Purge API on main-branch deploys. Ship only if maintainers report staleness pain.

### Non-functional Requirements

- [ ] NFR-1: Zero regressions in `packages/cli/test/registry.test.ts` and live-registry integration.
- [ ] NFR-2: Cache strategy must tolerate up to ~1 hour of staleness after a registry markdown change without manual intervention.

## Acceptance Criteria

- [ ] AC-1: Hitting `/api/registry/vercel/next.js` twice in a row from two different machines shows the second response served with `cf-cache-status: HIT`.
- [ ] AC-2: `cache-control` response header exactly matches `public, max-age=300, s-maxage=3600, stale-while-revalidate=86400`.
- [ ] AC-3: A registry markdown change deployed via Cloudflare Pages becomes visible within the `max-age + swr` window without any manual purge.
- [ ] AC-4: No regression for existing tests in `packages/cli/test/registry.test.ts` and the integration with the live registry.
- [ ] AC-5 (if ETag included): A second request with `If-None-Match: <etag>` returns 304 with no body.
- [ ] AC-6 (if deploy purge included): GH Action workflow calls the CF cache purge API on main-branch deploys and the run is observable in workflow logs.

## Out of Scope

- Caching the docs files themselves (separate raw-docs API track).
- Origin-side application cache (`defineCachedEventHandler`).
- Per-region cache differentiation.
- CLI-side `If-None-Match` sending (follow-up track).

## Assumptions

- Cloudflare Pages + Nitro's `cloudflare-pages` preset honors `routeRules.cache` at the edge via the Cache API.
- Registry markdown changes ship via normal Pages deploys; up to ~1 hour staleness is acceptable by default.
- The route currently has no per-user or per-request variance that would make a shared edge cache unsafe.

## Related

- #33 — `npm-tarball-docs-20260408` (server-side disambiguation made caching valuable)
- Future: registry raw-docs API track
- Future: telemetry implementation track
- Issue: pleaseai/ask#34
29 changes: 29 additions & 0 deletions apps/registry/app/route-rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Edge caching configuration for the registry lookup API.
*
* Applied via Nitro `routeRules` in `nuxt.config.ts`. On a cache HIT the
* Cloudflare Pages Worker is bypassed entirely via the Cache API.
*
* - Browsers / CLI: 5 min fresh (`max-age=300`)
* - Cloudflare edge: 1 h fresh (`s-maxage=3600`), 24 h stale-while-revalidate
*
* Exported separately so it can be asserted from tests without evaluating
* the full `nuxt.config.ts` (which relies on Nuxt's auto-imports).
*
* Track: registry-edge-cache-20260408
*/
export const REGISTRY_CACHE_CONTROL
= 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400'

export const registryApiRouteRules = {
'/api/registry/**': {
cache: {
maxAge: 60 * 60,
swr: true,
staleMaxAge: 60 * 60 * 24,
},
headers: {
'cache-control': REGISTRY_CACHE_CONTROL,
},
},
} as const
6 changes: 6 additions & 0 deletions apps/registry/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { registryApiRouteRules } from './app/route-rules'

export default defineNuxtConfig({
modules: [
'@nuxt/content',
Expand All @@ -20,5 +22,9 @@ export default defineNuxtConfig({
},
},

// Edge caching for the registry lookup API (track registry-edge-cache-20260408).
// On a cache HIT the Cloudflare Pages Worker is bypassed entirely via the Cache API.
routeRules: registryApiRouteRules,

compatibilityDate: '2026-04-03',
})
3 changes: 2 additions & 1 deletion apps/registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"dev": "nuxt dev",
"preview": "nuxt preview",
"lint": "eslint",
"lint:fix": "eslint --fix"
"lint:fix": "eslint --fix",
"test": "bun test"
},
"dependencies": {
"@nuxt/content": "^3",
Expand Down
15 changes: 15 additions & 0 deletions apps/registry/test/nuxt-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from 'bun:test'

import { REGISTRY_CACHE_CONTROL, registryApiRouteRules } from '../app/route-rules'

test('registry API routeRules configure edge caching', () => {
const rule = registryApiRouteRules['/api/registry/**']
expect(rule).toBeDefined()
expect(rule.headers['cache-control']).toBe(REGISTRY_CACHE_CONTROL)
expect(REGISTRY_CACHE_CONTROL).toBe('public, max-age=300, s-maxage=3600, stale-while-revalidate=86400')
expect(rule.cache).toEqual({
maxAge: 60 * 60,
swr: true,
staleMaxAge: 60 * 60 * 24,
})
})