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
13 changes: 13 additions & 0 deletions .changeset/build-output-seeding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@leejpsd/nextjs-cache-handler": minor
---

Build-output cache seeding: `seedBuildOutput()` (new `/seed` entry point) and
`npx nextjs-cache-handler seed` walk `.next/` after a build and insert
prerendered App Router routes (including PPR segment data), Pages Router
routes, and fetch-cache entries into Redis in the handler's own record
format — with NX semantics so entries already written by live instances are
never overwritten. A fresh deployment's first requests are cache HITs
instead of a regeneration stampede (verified on a real Next 16 app: cold
server + seeded Redis → first request `x-nextjs-cache: HIT`). The
`RedisClientLike.set` contract gains an optional `NX` flag.
11 changes: 11 additions & 0 deletions .changeset/init-doctor-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@leejpsd/nextjs-cache-handler": minor
---

New `nextjs-cache-handler` CLI (zero-dependency): `init` detects the Next.js
version and Redis client, generates the handler wrapper shims, shows the
next.config keys to add (never edits it), appends env templates, and injects
the agent rules block into CLAUDE.md/AGENTS.md idempotently (`--yes` to
apply, `--skills` to install the agent skill locally). `doctor` verifies
Redis connectivity, inspects cache key namespaces, and runs a write/read
round-trip — the first command an agent should reach for when debugging.
13 changes: 13 additions & 0 deletions .changeset/tag-pubsub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@leejpsd/nextjs-cache-handler": minor
---

Opt-in push-based tag propagation (`tagPubSub: true`, plural handler):
`updateTags()` publishes invalidations on a namespaced channel and every
instance maintains a subscription on a dedicated duplicate connection,
updating its local tag mirror in ~3 ms (measured cross-instance over real
Redis with both redis@5 and ioredis) instead of waiting for the next
`refreshTags()` scan (~seconds). The scan keeps running as the consistency
safety net, so a dropped subscription degrades to the previous behavior —
never to staleness. Cluster clients fall back to polling with a one-time
warning. `RedisClientLike` gains optional `publish`/`subscribe`.
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,23 @@ jobs:
- name: Print snapshot fingerprint
run: |
sha256sum docs/next16-spec.md

cluster:
name: Redis Cluster e2e
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: npm
- run: npm ci --no-audit --no-fund
- name: Install redis-server
run: sudo apt-get update -qq && sudo apt-get install -y -qq redis-server redis-tools
- name: Start 3-master cluster
run: scripts/cluster-test-env.sh up
- name: Cluster e2e tests
run: npm run test:cluster
- if: always()
run: scripts/cluster-test-env.sh down
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ name: Release
# below to enable)
on:
workflow_dispatch:
inputs:
publish_mcp:
description: "Publish mcp/ (@leejpsd/nextjs-cache-handler-mcp) instead of the main package"
type: boolean
default: false
# push:
# branches: [main]

Expand Down Expand Up @@ -60,7 +65,16 @@ jobs:
INTEGRATION_REDIS_URL: redis://127.0.0.1:6390
run: npm run test:integration

- name: Publish MCP package (trusted publishing)
if: ${{ inputs.publish_mcp }}
working-directory: mcp
run: |
npm ci --no-audit --no-fund
npm test
npm publish --access public --provenance

- name: Create Release Pull Request or Publish to npm
if: ${{ !inputs.publish_mcp }}
uses: changesets/action@v1
with:
# publish: gets called when there are no changesets left to apply.
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ coverage/
*.tsbuildinfo
.cache/
.turbo/
mcp/node_modules
mcp/dist
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,10 @@ Full reference: [`docs/api.md`](./docs/api.md).
on the same hash slot. Without `hashTag`, cluster deployments will hit
`CROSSSLOT Keys in request don't hash to the same slot`. The flag wraps
the namespace in `{}` so every key for a given deploy hashes together.
Cluster support is implemented but **not yet load-tested at production
scale**; PRs welcome.
Cluster support is validated by a dedicated e2e suite against a real
3-master cluster (`npm run test:cluster`, also in CI) — covering the
multi-key Lua scripts, per-master SCAN propagation, and both handlers.
Not yet load-tested at production scale.
- [ ] **Redis `maxmemory-policy: allkeys-lru` or `noeviction`** — if you need
bounded memory, choose `allkeys-lru`. Otherwise `noeviction` keeps
tag indices intact.
Expand All @@ -261,7 +263,7 @@ Full reference: [`docs/api.md`](./docs/api.md).
| Service | How to use | Tested? |
|---|---|---|
| **Self-hosted Redis 7+** | `{ type: "redis", url }` or `{ type: "ioredis", url }` | ✅ AWS ElastiCache 24h soak |
| **Redis Cluster** | `{ type: "cluster", nodes }` + `hashTag: true` | unit-tested, not yet load-tested at scale |
| **Redis Cluster** | `{ type: "cluster", nodes }` + `hashTag: true` | ✅ e2e-tested against a real 3-master cluster (CI); not yet load-tested at scale |
| **Upstash Redis** | `{ type: "redis", url: "rediss://..." }` (TLS auto-detected) | not yet validated, expected to work via the standard Redis protocol |
| **AWS ElastiCache (replication group)** | `{ type: "redis", url: "rediss://..." }` | ✅ reference deployment (re-verified 2026-08-01, Seoul) |
| **Redis Sentinel** | `{ type: "sentinel", sentinels, name }` | ✅ local master/replica failover drill |
Expand Down
10 changes: 10 additions & 0 deletions bin/cli.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env node
// Thin launcher for the built CLI (keeps the tsup build shebang-free).
const { main } = require("../dist/cli/index.cjs");
main().then(
(code) => process.exit(code),
(err) => {
console.error("[error]", err && err.message ? err.message : err);
process.exit(1);
}
);
2 changes: 2 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Package: `@leejpsd/nextjs-cache-handler` — Redis cache handler for Next.js
| `.../client/ioredis` | `adaptIoredis`, `adaptCluster`, `createIoredisClient`, `createIoredisSentinel`, `createIoredisCluster` | wrap your own ioredis / Cluster / Sentinel client |
| `.../ops` | `getMetricSnapshot` | process-local metric counters for health endpoints |
| `.../otel` | `createOtelMetricEmitter` | built-in OpenTelemetry adapter (needs `@opentelemetry/api` in your app) |
| `.../seed` | `seedBuildOutput` | seed `.next` build output into Redis (NX-protected) — also `npx nextjs-cache-handler seed` |

## Factories

Expand Down Expand Up @@ -48,6 +49,7 @@ ctx contract are accepted (`ctx.cacheControl.revalidate` / `kind` and
| `singleFlightLockTtlSec` | `number` | `10` | lock TTL for `singleFlight` |
| `isBuildPhase` | `() => boolean` | `NEXT_PHASE === "phase-production-build"` | build-phase gate override |
| `hashTag` | `boolean` | `false` | wrap namespace in `{}` — **required on Redis Cluster** (multi-key Lua) |
| `tagPubSub` | `boolean` | `false` | push-based cross-instance tag propagation (plural handler; ~3ms); scan polling remains the safety net; unavailable on Cluster |
| `onMetric` | `(event: MetricEvent) => void` | — | telemetry hook; emitter errors are swallowed |
| `logger` | `Logger` | console (warn+) | injectable 4-level logger |

Expand Down
51 changes: 51 additions & 0 deletions docs/release-checklist-0.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 0.4.0 + MCP Release Checklist (승인 대기)

> 상태: **모든 작업 완료, 사용자 최종 승인 대기.** 승인 시 아래 순서대로
> 실행하면 배포까지 자동으로 이어진다. (0.3.4 = Phase 1 에이전트 자산
> 패치는 별도 예약 배포 — 이 문서와 무관하게 진행됨)

## 승인 시 실행 순서 (0.4.0 — 자동화됨)

1. PR `next/0.4 → main` 머지 (CI green 확인 후)
2. `gh workflow run release.yml --ref main` → "Version Packages" PR 생성
- **버전이 0.4.0 (minor)인지, changeset 4개(CLI/seed/tagPubSub + 잔여 패치)가
소비되는지 diff 확인**
3. Version PR 머지 → `gh workflow run release.yml --ref main` → npm 0.4.0 publish
(Trusted Publishing, 토큰 불필요)
4. 데모 리포 의존성 `^0.4.0` 갱신 + README/문서의 0.4 기능 반영

## MCP 첫 배포 (@leejpsd/nextjs-cache-handler-mcp 0.1.0 — 사용자 액션 1회 필요)

신규 패키지는 Trusted Publishing을 미리 설정할 수 없어 **첫 publish만** 수동:

1. `cd mcp && npm run test` (빌드+스모크 재확인)
2. `npm publish --access public` (npm 로그인/OTP 필요 — 사용자)
3. 이후 npmjs.com에서 이 패키지에도 Trusted Publisher 설정
(leejpsd / nextjs-cache-handler / release.yml) → 다음부터 자동화 가능

## Phase 4 — publish 후 발견성 배포 (승인 후 제가 실행 가능)

- [ ] 공식 MCP 레지스트리 + Smithery/mcp.so 등록 (mcp/README 기반)
- [ ] skills 레지스트리 노출 확인 (`npx skills add leejpsd/nextjs-cache-handler`)
- [ ] 데모 리포 `.mcp.json` 예제 추가
- [ ] 홍보 재개 (docs/ 초안: fortedigital #152 코멘트 1순위 — 0.4.0 실측
수치로 업데이트: 시딩 첫요청 HIT, 전파 3ms, Cluster e2e)

## 이번 브랜치에 담긴 것 (검증 증거)

| 항목 | 검증 |
|---|---|
| init/doctor CLI | 유닛 10 + 실Redis doctor 2 + 실제 bin 스모크 |
| 빌드 캐시 시딩 (`/seed`, CLI seed) | 유닛 6 + **실앱 e2e: 콜드 서버 첫 요청 `x-nextjs-cache: HIT`** (15 라우트 + fetch 1건 시딩) |
| tagPubSub 전파 | 유닛 4 + 실Redis 통합 2 — **크로스 인스턴스 3ms** (redis@5/ioredis 모두; 기존 스캔 ~2.1s) |
| Redis Cluster e2e | 실 3-마스터 클러스터 5 테스트 (Lua/CROSSSLOT, per-master SCAN, ISR, pubsub 폴백) + CI 잡 신설 |
| MCP 서버 (7 도구) | stdio JSON-RPC 스모크 + 실Redis 라이브 콜 (health/tag_state/dry-run) |

전체 스위트: 유닛 153 + 통합 25 + 클러스터 5. 코어 zero-dep 유지
(CLI는 node 내장만, MCP는 별도 패키지).

## 남은 리스크 / 알려진 한계

- tagPubSub는 Cluster 클라이언트에서 폴링 폴백 (경고 1회) — 문서화됨
- 시딩: PPR 세그먼트가 불완전한 라우트는 안전하게 스킵 (카운트 보고)
- CI cluster 잡은 이번 PR에서 첫 실행 — 실패 시 머지 전 수정
50 changes: 50 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# @leejpsd/nextjs-cache-handler-mcp

MCP (Model Context Protocol) server for
[`@leejpsd/nextjs-cache-handler`](https://www.npmjs.com/package/@leejpsd/nextjs-cache-handler)
Redis caches. Lets AI agents (Claude Code, Cursor, …) inspect and operate a
running deployment's Next.js cache: *"why isn't this page updating?"*
becomes a `tag_state` call instead of guesswork.

## Setup

Project `.mcp.json` (Claude Code picks this up automatically):

```json
{
"mcpServers": {
"nextjs-cache": {
"command": "npx",
"args": ["-y", "@leejpsd/nextjs-cache-handler-mcp"],
"env": {
"REDIS_URL": "redis://127.0.0.1:6379",
"DEPLOYMENT_VERSION": "your-deploy-id"
}
}
}
}
```

Runs locally over stdio and connects to YOUR Redis — nothing is hosted.

> **AWS/ElastiCache note**: ElastiCache is VPC-internal, so a locally
> running MCP server needs an SSH tunnel/bastion (point REDIS_URL at the
> tunnel). Agents running inside the VPC (CI, in-cluster) connect directly.
> Pair with the AWS agent skills/MCP: they handle the infrastructure, this
> server handles cache semantics.

## Tools

| Tool | What it answers | Writes? |
|---|---|---|
| `cache_health` | Is Redis up? What's cached, per layer/kind? | no |
| `cache_search` | Which keys match this pattern? | no |
| `cache_inspect` | Decode one entry: kind, age, tags, TTL, compression, sizes | no |
| `tag_state` | Is this tag invalidated right now, on BOTH cache layers? | no |
| `explain_key` | Parse a raw Redis key into layer/kind/namespace/key | no |
| `simulate_swr` | Would this entry be fresh / stale / expired, and what happens on read? | no |
| `invalidate_tag` | Soft (SWR) or hard invalidation — **dry-run unless `confirm: true`** | gated |

Env: `REDIS_URL` (required), `DEPLOYMENT_VERSION` (recommended — scopes
namespace-aware tools), `CACHE_KEY_PREFIX` / `ISR_KEY_PREFIX` (only when the
handlers use custom prefixes).
Loading
Loading