Skip to content

feat: lru_cache and nextjs update#362

Merged
Producdevity merged 22 commits into
stagingfrom
feat/improve-caching-and-seo
May 23, 2026
Merged

feat: lru_cache and nextjs update#362
Producdevity merged 22 commits into
stagingfrom
feat/improve-caching-and-seo

Conversation

@Producdevity

@Producdevity Producdevity commented May 22, 2026

Copy link
Copy Markdown
Owner

Description

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor
  • Other (please describe):

How Has This Been Tested?

  • Local build
  • Lint
  • Typecheck
  • Unit tests
  • Manual testing

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have made corresponding changes to the documentation
  • I have checked that all checks (lint, typecheck, test) pass

Notes for reviewers

Summary by CodeRabbit

  • New Features

    • Add env flag to disable reCAPTCHA in dev/test and new client-side admin pages/layout.
  • Improvements

    • Upgraded Next/React and toolchain; enabled component-level caching and SEO cache lifetimes.
    • Migrated server caches to LRU-backed implementations and improved cache invalidation/segmentation.
    • Pages adopt Suspense with loading skeletons for smoother UX.
  • Changes

    • Monitoring dashboard UI and related warming endpoints simplified/removed.
    • Footer year made dynamic; Terms page shows fixed "Last updated" date.
  • Tests

    • Added/updated tests for captcha, cache invalidation, and SEO/cache queries.

Review Change Stack

@vercel

vercel Bot commented May 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
emuready Ready Ready Preview, Comment May 23, 2026 3:09pm

Request Review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Walkthrough

Adds NEXT_PUBLIC_DISABLE_RECAPTCHA and captcha gating; migrates in-repo caches to lru-cache LRUCache; adopts next/cache tags/lifetimes for SEO queries and tests; replaces custom SEO cache/warm/metrics with revalidatePath/revalidateTag helpers and tests; refactors proxy, many routes, RootLayout/Clerk, admin UI, tooling, and related tests.

Changes

Comprehensive PR changes

All changes (single cohort)

Layer / File(s) Summary
Env and captcha
.env.docker.example, .env.example, .env.test.example, src/lib/captcha/config.ts, src/lib/captcha/verify.test.ts
Adds NEXT_PUBLIC_DISABLE_RECAPTCHA across env examples, exports isCaptchaDisabled(), and adds a test asserting the explicit disable flag disables client/server captcha checks.
Tooling and deps
eslint.config.mjs, package.json, tsconfig.json
Refactors Flat ESLint composition, updates multiple react/next/eslint packages, and changes TS JSX transform to react-jsx.
Next config & SEO cache profiles
next.config.ts, src/app/sitemap.ts
Enable component caching and add SEO cacheLife profiles; remove sitemap revalidate export and omit lastModified from static pages.
SEO DB queries & tests
src/server/db/seo-queries.ts, src/server/db/seo-queries.test.ts
Migrate SEO DB query helpers to Next.js next/cache with cacheTag/cacheLife; add tests validating tags and lifetimes.
Remove seo-cache / warming / monitoring
src/lib/cache/seo-cache.ts, src/server/cache/warming.ts, src/lib/monitoring/seo-metrics.ts, monitoring components, and related API routes
Remove custom seo-cache, cache warming scheduler, monitoring collectors/components, and the /api/cache/* endpoints.
Cache instances migration
src/server/utils/cache/instances.ts, src/server/utils/cache/index.ts
Replace MemoryCache instances with lru-cache LRUCache, change option names, and add per-device catalog compatibility invalidation helpers.
Invalidation rework & tests
src/server/cache/invalidation.ts, src/server/cache/invalidation.test.ts
Switch to revalidatePath/revalidateTag(tag,'max'), add listing/PC-listing batch invalidators and unit tests ensuring deduplication and expected calls.
Server routers and TRPC
src/server/api/routers/cache.ts (removed), src/server/api/root.ts, src/server/api/routers/listings/admin.ts, src/server/api/routers/pcListings.ts, tests
Remove cacheRouter; update listings and pcListings to use new SEO invalidation and catalog-compat invalidators; update tests to mock new helpers.
Proxy & route connection changes
src/proxy.ts, many src/app/api/*/route.ts and [trpc]/route.ts
Refactor proxy to exported proxy function, centralize rate-limit bypass, add same-origin fast path; add await connection() in many API routes and replace/ remove dynamic = 'force-dynamic' exports as noted.
Layout & admin client pages
src/app/layout.tsx, src/app/admin/AdminLayoutClient.tsx, many admin client/page components
Move ClerkBoundary into body, add service-worker flag script, add DevelopmentClerkProvider awaiting DB connection, add AdminLayoutClient (client) and convert many admin pages to client components or Suspense wrappers.
Pages Suspense refactors
various src/app/*/page.tsx (games, listings, pc-listings, users, admin edits, emulator custom fields)
Convert many async page exports into synchronous Suspense wrappers delegating to async helpers or client pages to improve rendering and gating.
Media/search caching
src/server/tgdb.ts, src/server/utils/steamGameBatcher.ts, steamGameSearch.ts, switchGameSearch.ts, threeDsGameSearch.ts
Replace MemoryCache with LRUCache-backed CachedData wrappers, adjust cache shapes, and update stats/read/write flows while keeping public signatures.
Misc UI, logging, tests
src/components/footer/Footer.tsx, src/components/SessionTracker.tsx, src/lib/trpc-client-errors.ts, tests, src/rest/http.ts, upload routes
Footer year -> state; SessionTracker defers refs and tightens guards; add TRPC client error helpers and tests; add logger.error in HTTP interceptor; upload routes preserved with formatting changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/improve-caching-and-seo
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/improve-caching-and-seo

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🧹 Nitpick comments (4)
src/server/db/seo-queries.test.ts (1)

29-31: ⚡ Quick win

Add coverage for remaining SEO cache entry points.

Please include tests for getUserForSEO, getApprovedListingsForSitemap, and getApprovedPcListingsForSitemap to lock tag/profile contracts across all exported query functions.

🤖 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 `@src/server/db/seo-queries.test.ts` around lines 29 - 31, Add tests that
mirror the existing patterns for getApprovedGamesForSitemap/getGameForSEO by
importing the remaining exports from './seo-queries' (getUserForSEO,
getApprovedListingsForSitemap, getApprovedPcListingsForSitemap) and asserting
their output shapes and cache tag/profile contracts; specifically, create test
cases that call getUserForSEO with a sample user id and verify returned fields
and cache tags, call getApprovedListingsForSitemap and
getApprovedPcListingsForSitemap to return lists and verify each item shape plus
sitemap-friendly fields and that cache tags/profiles match the same contract
used by getApprovedGamesForSitemap (e.g., tag names, id formats, and any
ttl/variant expectations). Ensure you reuse existing fixtures/mocks and test
helpers from the file so the new tests follow the same setup/teardown and
assertion style as the current getApprovedGamesForSitemap tests.
src/app/api/catalog/compatibility/route.ts (1)

61-63: ⚡ Quick win

Consider the performance impact of removing HTTP-level caching.

The change from public, s-maxage=600, stale-while-revalidate=300 to private, no-store eliminates all HTTP-level caching, meaning every request—even identical ones—will hit the server. While the comments correctly note that API-key quota tracking and role-scoped visibility justify avoiding shared caches, the complete removal of caching (including client-side private cache with a short max-age) could significantly increase server load for frequently accessed device/system combinations.

The service-level LRU cache helps, but clients making repeated identical requests will still consume API quota and server resources unnecessarily.

⚡ Consider allowing short client-side caching

If API quota is tracked per-request regardless of cache hits, consider:

-        'Cache-Control': 'private, no-store',
+        'Cache-Control': 'private, max-age=60',

This allows individual clients to cache their own requests for 60 seconds without exposing data to shared caches or affecting quota tracking accuracy significantly.

🤖 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 `@src/app/api/catalog/compatibility/route.ts` around lines 61 - 63, The
response header change in route.ts replaced shared caching with 'private,
no-store', which removes client-side caching and increases server/API-quota
load; update the 'Cache-Control' value used where headers are set (the
'Cache-Control' entry in src/app/api/catalog/compatibility/route.ts) to a short
client-only policy such as a private max-age plus optional
stale-while-revalidate (e.g., private, max-age=60, stale-while-revalidate=30) so
individual clients can cache identical requests briefly while still preventing
shared CDN caching and preserving role-scoped visibility and quota semantics.
eslint.config.mjs (2)

52-60: ⚡ Quick win

Avoid version-skewed react-hooks rule IDs.

These compiler-aware rule IDs (purity, refs, set-state-in-effect, static-components, etc.) come from the newer eslint-plugin-react-hooks ruleset. The repo still pins eslint-plugin-react-hooks to 5.2.0 in package.json, so this config is brittle unless ESLint always resolves a newer transitive copy instead. Please bump or remove the direct pin before relying on these keys. (ja.react.dev)

🤖 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 `@eslint.config.mjs` around lines 52 - 60, The config enables new
compiler-aware react-hooks rule IDs (e.g., 'react-hooks/purity',
'react-hooks/refs', 'react-hooks/set-state-in-effect',
'react-hooks/static-components', etc.) but package.json currently pins
eslint-plugin-react-hooks to 5.2.0; either remove these newer rule keys from
eslint.config.mjs or update the package.json entry for eslint-plugin-react-hooks
to a version that exposes those rules (bump the version instead of a strict
5.2.0 pin), then run install and verify ESLint no longer errors on unknown rule
IDs; reference the rule names in the config and the eslint-plugin-react-hooks
dependency in package.json when making the change.

31-32: ⚡ Quick win

Restore Next's TypeScript flat preset.

Next 16's flat-config docs still pair eslint-config-next/typescript with core-web-vitals for TypeScript projects. Dropping it here means this file now hand-maintains the TS lint wiring instead of using the supported preset. (nextjs.org)

♻️ Proposed fix
 import eslintConfigPrettier from 'eslint-config-prettier'
 import nextCoreWebVitals from 'eslint-config-next/core-web-vitals'
+import nextTypeScript from 'eslint-config-next/typescript'
@@
   ...nextCoreWebVitals,
+  ...nextTypeScript,
   eslintConfigPrettier,
🤖 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 `@eslint.config.mjs` around lines 31 - 32, Restore Next's TypeScript flat
preset by adding "eslint-config-next/typescript" back into the exported config
array alongside nextCoreWebVitals and eslintConfigPrettier; update the array
that currently contains nextCoreWebVitals and eslintConfigPrettier to include
"eslint-config-next/typescript" (preserving the existing entries) so Next 16's
flat-config TypeScript preset is used instead of hand-maintaining TS rules.
🤖 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 `@src/proxy.ts`:
- Around line 7-8: The current module-scoped Map rateLimitMap is process-local
and must be replaced with a shared store (e.g., Redis or a Next.js KV) so limits
are consistent across replicas; update any code that reads/writes rateLimitMap
to instead use a shared client (create/initialize a single Redis/KV client used
by the proxy handler), store per-identifier counters and expiry in the shared
store, and perform atomic increments and TTL handling (e.g., INCR + EXPIRE or an
equivalent atomic operation) to implement count and resetTime logic; ensure keys
include the same identifier used before (IP or user ID) and update functions
that referenced rateLimitMap to read the counter and reset timestamp from
Redis/KV and to enforce limits based on those values.

In `@src/server/api/routers/listings/admin.ts`:
- Around line 225-228: bulkApprove and bulkReject change listing visibility but
don't invalidate per-device compatibility cache; add the same invalidation used
for single-item approval. In the bulkApprove and bulkReject handlers call the
existing invalidateCatalogCompatibilityCacheForDevice for each affected
listing's deviceId (import it via the same dynamic import pattern used around
listingToApprove), deduplicate deviceIds and run invalidations (e.g.,
Promise.all) after the DB update so every touched device cache is cleared;
reference functions/handlers bulkApprove, bulkReject and the helper
invalidateCatalogCompatibilityCacheForDevice.

In `@src/server/api/routers/pcListings.ts`:
- Around line 482-497: The admin update path (updateAdmin) must perform the same
SEO invalidation as the user update: when the existing pcListing has status
ApprovalStatus.APPROVED and fields that affect SEO change, call
invalidatePcListingSeoForUpdate with the old keys (pcListing.id,
pcListing.gameId, pcListing.cpuId, pcListing.gpuId) and the new keys from
updatedPcListing (updatedPcListing.gameId, updatedPcListing.cpuId,
updatedPcListing.gpuId); add this logic into the updateAdmin flow (mirror the
block that currently exists for user edits) so admin edits to approved listings
also revalidate listing path, list pages, sitemap, and derived tags.

In `@src/server/db/seo-queries.ts`:
- Around line 1-3: Reorder the import statements to satisfy the import/order
rule by placing the external package import for ApprovalStatus from
'`@orm/client`' before the next/cache import; specifically update the imports so
ApprovalStatus from '`@orm/client`' appears above the imports for cacheLife and
cacheTag from 'next/cache' (keep the prisma import from '`@/server/db`' where
appropriate among internal/alias imports).

In `@src/server/services/catalog.service.test.ts`:
- Around line 1-2: ESLint import/order requires module imports (Role and type
PrismaClient from '`@orm/client`') to appear before test framework imports
(beforeEach, describe, expect, it, vi from 'vitest'); fix by moving the import {
Role, type PrismaClient } from '`@orm/client`' above the import { beforeEach,
describe, expect, it, vi } from 'vitest' so the '`@orm/client`' symbols appear
first.

---

Nitpick comments:
In `@eslint.config.mjs`:
- Around line 52-60: The config enables new compiler-aware react-hooks rule IDs
(e.g., 'react-hooks/purity', 'react-hooks/refs',
'react-hooks/set-state-in-effect', 'react-hooks/static-components', etc.) but
package.json currently pins eslint-plugin-react-hooks to 5.2.0; either remove
these newer rule keys from eslint.config.mjs or update the package.json entry
for eslint-plugin-react-hooks to a version that exposes those rules (bump the
version instead of a strict 5.2.0 pin), then run install and verify ESLint no
longer errors on unknown rule IDs; reference the rule names in the config and
the eslint-plugin-react-hooks dependency in package.json when making the change.
- Around line 31-32: Restore Next's TypeScript flat preset by adding
"eslint-config-next/typescript" back into the exported config array alongside
nextCoreWebVitals and eslintConfigPrettier; update the array that currently
contains nextCoreWebVitals and eslintConfigPrettier to include
"eslint-config-next/typescript" (preserving the existing entries) so Next 16's
flat-config TypeScript preset is used instead of hand-maintaining TS rules.

In `@src/app/api/catalog/compatibility/route.ts`:
- Around line 61-63: The response header change in route.ts replaced shared
caching with 'private, no-store', which removes client-side caching and
increases server/API-quota load; update the 'Cache-Control' value used where
headers are set (the 'Cache-Control' entry in
src/app/api/catalog/compatibility/route.ts) to a short client-only policy such
as a private max-age plus optional stale-while-revalidate (e.g., private,
max-age=60, stale-while-revalidate=30) so individual clients can cache identical
requests briefly while still preventing shared CDN caching and preserving
role-scoped visibility and quota semantics.

In `@src/server/db/seo-queries.test.ts`:
- Around line 29-31: Add tests that mirror the existing patterns for
getApprovedGamesForSitemap/getGameForSEO by importing the remaining exports from
'./seo-queries' (getUserForSEO, getApprovedListingsForSitemap,
getApprovedPcListingsForSitemap) and asserting their output shapes and cache
tag/profile contracts; specifically, create test cases that call getUserForSEO
with a sample user id and verify returned fields and cache tags, call
getApprovedListingsForSitemap and getApprovedPcListingsForSitemap to return
lists and verify each item shape plus sitemap-friendly fields and that cache
tags/profiles match the same contract used by getApprovedGamesForSitemap (e.g.,
tag names, id formats, and any ttl/variant expectations). Ensure you reuse
existing fixtures/mocks and test helpers from the file so the new tests follow
the same setup/teardown and assertion style as the current
getApprovedGamesForSitemap tests.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 844118cf-8559-4d01-80d4-bdcd53c25ebd

📥 Commits

Reviewing files that changed from the base of the PR and between e242d14 and 701f679.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (53)
  • .env.docker.example
  • .env.example
  • .env.test.example
  • eslint.config.mjs
  • next.config.ts
  • package.json
  • src/app/admin/monitoring/components/MonitoringOverview.tsx
  • src/app/admin/monitoring/components/SEOMetricsDashboard.tsx
  • src/app/admin/monitoring/page.tsx
  • src/app/api/cache/metrics/route.ts
  • src/app/api/cache/warm/route.ts
  • src/app/api/catalog/compatibility/route.ts
  • src/app/api/health/route.ts
  • src/app/api/proxy-image/route.ts
  • src/app/api/trpc/[trpc]/route.ts
  • src/app/api/upload/games/route.ts
  • src/app/api/upload/profile/route.ts
  • src/app/games/new/NewGamePage.tsx
  • src/app/layout.tsx
  • src/app/sitemap.ts
  • src/components/admin/CacheMetrics.tsx
  • src/lib/cache/seo-cache.ts
  • src/lib/captcha/config.ts
  • src/lib/captcha/verify.test.ts
  • src/lib/monitoring/seo-metrics.ts
  • src/proxy.ts
  • src/schemas/cache.ts
  • src/server/api/root.ts
  • src/server/api/routers/cache.ts
  • src/server/api/routers/listings/admin.test.ts
  • src/server/api/routers/listings/admin.ts
  • src/server/api/routers/mobile/pcListings.ts
  • src/server/api/routers/pcListings.test.ts
  • src/server/api/routers/pcListings.ts
  • src/server/cache/invalidation.test.ts
  • src/server/cache/invalidation.ts
  • src/server/cache/warming.ts
  • src/server/db/seo-queries.test.ts
  • src/server/db/seo-queries.ts
  • src/server/notifications/analyticsService.ts
  • src/server/services/catalog.service.test.ts
  • src/server/services/catalog.service.ts
  • src/server/tgdb.ts
  • src/server/utils/cache/MemoryCache.test.ts
  • src/server/utils/cache/MemoryCache.ts
  • src/server/utils/cache/index.ts
  • src/server/utils/cache/instances.ts
  • src/server/utils/driver-versions.ts
  • src/server/utils/steamGameBatcher.ts
  • src/server/utils/steamGameSearch.ts
  • src/server/utils/switchGameSearch.ts
  • src/server/utils/threeDsGameSearch.ts
  • tsconfig.json
💤 Files with no reviewable changes (21)
  • src/schemas/cache.ts
  • src/server/utils/cache/MemoryCache.test.ts
  • src/app/api/cache/metrics/route.ts
  • src/server/utils/cache/index.ts
  • src/components/admin/CacheMetrics.tsx
  • src/app/api/trpc/[trpc]/route.ts
  • src/server/utils/cache/MemoryCache.ts
  • src/lib/monitoring/seo-metrics.ts
  • src/server/notifications/analyticsService.ts
  • src/app/api/cache/warm/route.ts
  • src/server/api/routers/cache.ts
  • src/app/api/upload/profile/route.ts
  • src/app/admin/monitoring/components/SEOMetricsDashboard.tsx
  • src/app/admin/monitoring/components/MonitoringOverview.tsx
  • src/lib/cache/seo-cache.ts
  • src/server/api/root.ts
  • src/server/cache/warming.ts
  • src/app/games/new/NewGamePage.tsx
  • src/app/api/proxy-image/route.ts
  • src/app/api/upload/games/route.ts
  • src/app/sitemap.ts

Comment thread src/proxy.ts
Comment on lines +7 to 8
// Process-local rate limiting only; counts are not shared across server instances.
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()

@coderabbitai coderabbitai Bot May 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect src/proxy.ts around the referenced lines
FILE="src/proxy.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "---- $FILE (lines 1-120) ----"
nl -ba "$FILE" | sed -n '1,120p'

echo "---- $FILE (lines 120-240) ----"
nl -ba "$FILE" | sed -n '120,240p' || true

echo "---- Search for rate limit usage ----"
rg -n "rateLimitMap|shouldBypassRateLimit|isSameOriginSource|proxy\s*=|export\s+(const|function)\s+proxy|export\s+.*proxy" "$FILE" || true

Repository: Producdevity/EmuReady

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/proxy.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "---- $FILE (lines 1-200) ----"
awk 'NR>=1 && NR<=200 {printf "%5d: %s\n", NR, $0}' "$FILE"

echo "---- Search for rate limit usage ----"
rg -n "rateLimitMap|shouldBypassRateLimit|isSameOriginSource|export\s+(const|function)\s+proxy|proxy\s*=|export\s+default\s+proxy" "$FILE" || true

Repository: Producdevity/EmuReady

Length of output: 8376


🌐 Web query:

Next.js 16 proxy documentation in-memory state global variables not shared across instances

💡 Result:

In Next.js 16, the reason “in-memory/global variables aren’t shared across instances” is that each running server instance (replica) has isolated Node.js RAM. So anything you store in module scope or other process globals will only be visible to requests handled by that same instance, not other instances. 1) Proxy (proxy.ts) explicitly discourages relying on shared globals Next.js’s Proxy documentation says you should not attempt relying on shared modules or globals from proxy.ts, and instead pass information to the application using headers/cookies/rewrites/redirects/URL. [1][2] 2) Multi-instance deployments cause “split-brain” for in-memory caches A concrete example from the Next.js 16 caching ecosystem: the default in-memory cache “fragments per-instance,” and “Two tasks behind one ALB will independently evaluate… write into their own local LRU, and never see each other's writes.” [3] That is the same fundamental behavior you’ll see with any in-memory/global state: it won’t be shared across replicas. 3) Even “singleton” patterns can be duplicated depending on bundling/runtime context Next.js users have reported that “bundle for API routes would be separate to bundle for pages, so we have 2 instances of the same code and classic JS singleton approach doesn’t work.” [4] So relying on plain process-global/module-global state can also fail within the same deployment due to how code gets instantiated. What to do instead - For proxy.ts: pass data to your app via headers/cookies/rewrites/redirects/URL (not in-memory globals). [1][2] - For cross-request/cross-instance shared state: use shared infrastructure (Redis/database/shared cache) or configure Next.js to use a cache handler that writes to a shared store (so invalidation/reads are not trapped in per-instance memory). [3] Bottom line Next.js 16 proxy.ts and your server code will not reliably share in-memory/global state across instances. Proxy docs discourage globals, and in-memory state/cache behavior is per-instance by design (unless you externalize it). [1][2][3]

Citations:


Back the rate limiter with shared storage (module-scoped Map is process-local).

src/proxy.ts keeps request counts in the module-global rateLimitMap (lines 7-8), so rate limiting is isolated to a single Node.js instance and won’t hold up across replicas/restarts—making it easy to bypass. Next.js proxy guidance discourages relying on module/global state in proxy handlers; move the counter to Redis/KV or another shared store. https://nextjs.org/docs/app/api-reference/file-conventions/proxy

🤖 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 `@src/proxy.ts` around lines 7 - 8, The current module-scoped Map rateLimitMap
is process-local and must be replaced with a shared store (e.g., Redis or a
Next.js KV) so limits are consistent across replicas; update any code that
reads/writes rateLimitMap to instead use a shared client (create/initialize a
single Redis/KV client used by the proxy handler), store per-identifier counters
and expiry in the shared store, and perform atomic increments and TTL handling
(e.g., INCR + EXPIRE or an equivalent atomic operation) to implement count and
resetTime logic; ensure keys include the same identifier used before (IP or user
ID) and update functions that referenced rateLimitMap to read the counter and
reset timestamp from Redis/KV and to enforce limits based on those values.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

fine for now

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Producdevity, understood. Would you like me to open a follow-up GitHub issue to track migrating rateLimitMap to a shared store (e.g., Redis/KV) for multi-instance deployments?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai make a new github issue for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

The GitHub issue has been created and assigned to you, @Producdevity. It covers the problem with the process-local rateLimitMap, the proposed fix (shared Redis/KV with atomic increment + TTL), and includes backlinks to this PR and review comment.


🏁 Script executed:

gh issue create \
  --repo Producdevity/EmuReady \
  --title "feat: migrate proxy rate limiter from in-memory Map to shared store (Redis/KV)" \
  --assignee Producdevity \
  --body "## Summary

The current rate limiting implementation in \`src/proxy.ts\` uses a module-scoped \`Map\` (\`rateLimitMap\`) to track request counts per client identifier. Because this is process-local, rate limit counters are not shared across server instances or replicas, making it easy to bypass limits in multi-instance/horizontal deployments.

## Problem

- \`rateLimitMap\` is isolated to a single Node.js process
- Restarts reset all counters
- Requests routed to different replicas are each rate-limited independently, so a client can exceed the intended threshold by the number of replicas

## Proposed Solution

Migrate \`rateLimitMap\` to a shared store such as Redis or a Next.js KV provider:

- Initialize a single Redis/KV client used by the proxy handler
- Store per-identifier counters and expiry in the shared store
- Use atomic increment + TTL operations (e.g., \`INCR\` + \`EXPIRE\`) to implement count/resetTime logic
- Update \`checkRateLimit\` and \`shouldBypassRateLimit\` to read/write from the shared store

## References

- PR: https://github.com/Producdevity/EmuReady/pull/362
- Review comment: https://github.com/Producdevity/EmuReady/pull/362#discussion_r3288975902
- Next.js proxy guidance: https://nextjs.org/docs/app/api-reference/file-conventions/proxy

Raised by `@Producdevity`."

Length of output: 52


The issue has been created: #363 — assigned to you, @Producdevity.

Comment thread src/server/api/routers/listings/admin.ts Outdated
Comment thread src/server/api/routers/pcListings.ts
Comment thread src/server/db/seo-queries.ts
Comment thread src/server/services/catalog.service.test.ts
…s, add dynamic copyright year handling in Footer component

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
src/app/admin/AdminLayoutClient.tsx (1)

14-15: ⚡ Quick win

Redundant alias and import order.

Line 14 has a no-op alias (hasRolePermission as hasRolePermission). Line 15's @orm import should precede external packages per ESLint.

-import { hasRolePermission as hasRolePermission } from '`@/utils/permissions`'
-import { Role } from '`@orm`'
+import { Role } from '`@orm`'
+import { hasRolePermission } from '`@/utils/permissions`'
🤖 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 `@src/app/admin/AdminLayoutClient.tsx` around lines 14 - 15, Remove the
redundant alias by importing hasRolePermission directly (use import {
hasRolePermission } from '`@/utils/permissions`') and reorder the imports so the
Role import from `@orm` appears before the external/internal utils import; update
the import lines referencing hasRolePermission and Role in AdminLayoutClient.tsx
accordingly.
src/app/layout.tsx (1)

63-65: 💤 Low value

Consider a placeholder for Navbar to prevent layout shift.

Using fallback={null} for the Navbar could cause visible layout shift when it loads. A skeleton or height-preserving placeholder would provide a smoother experience.

🤖 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 `@src/app/layout.tsx` around lines 63 - 65, The Suspense wrapper around the
Navbar uses fallback={null}, which can cause layout shift; replace the null
fallback with a height-preserving placeholder component (e.g., NavbarSkeleton or
a simple div with the same height/spacing as Navbar) and use that as the
Suspense fallback so the page reserves the Navbar space while the real Navbar
(component name: Navbar) is loading; update the Suspense JSX to
fallback={<NavbarSkeleton />} (or similar) and ensure the skeleton matches
Navbar's expected height and padding.
🤖 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
`@src/app/admin/emulators/`[emulatorId]/custom-fields/EmulatorCustomFieldsClientPage.tsx:
- Around line 117-143: The component EmulatorCustomFieldsClientPage currently
checks customFieldDefinitionsQuery.error but not categoriesQuery.error; add an
explicit error branch for categoriesQuery (similar to the existing custom fields
check) that returns a FeedbackCard titled e.g. "Failed to load categories" and
uses categoriesQuery.error.message ?? 'Please try again later.' and the same
backToEmulatorsAction so failed category fetches aren't silently treated as
empty arrays; ensure this check sits after the loading block and before
rendering the main UI.
- Around line 3-11: The import ordering violates ESLint import/order because the
`Role` import from '`@orm`' is placed after application/internal imports; reorder
the imports so external packages (lucide-react, next/link, react), then
absolute/internal libs (e.g., '`@/components/`...', '`@/lib/api`', '`@/utils/`...'),
and finally project alias imports like `import { Role } from '`@orm`'` to satisfy
import/order and restore CI linting for EmulatorCustomFieldsClientPage.tsx.

In `@src/app/admin/emulators/`[emulatorId]/EditEmulatorClientPage.tsx:
- Around line 3-9: The imports are out of order per ESLint import/order: move
the Role import from '`@orm/client`' so it sits with external/third-party imports
(e.g., alongside 'lucide-react' and 'next/navigation') before your app aliased
imports (like '`@/components/ui`' and '`@/lib/api`'), ensuring import groups are:
external packages (ArrowLeft, notFound/useRouter, Role from '`@orm/client`'), then
internal aliases (Button/PageSkeletonLoading, api, hasPermission/PERMISSIONS,
EmulatorEditForm); update the import statement that references Role to use
'`@orm/client`' if needed and place it into that external group to satisfy the
linter.

In `@src/app/admin/games/`[id]/AdminGameEditClientPage.tsx:
- Line 20: The current check in AdminGameEditClientPage conflates fetch errors
and missing records by returning notFound() when gameQuery.error is truthy;
instead, handle them separately: keep the notFound() call only for when
gameQuery.data is null/undefined, and add explicit error handling for
gameQuery.error (e.g., throw the error, return an error UI/component, or call an
error handler) so that operational failures are not misclassified as 404. Locate
the conditional using gameQuery and notFound() and split it into two branches:
one that returns notFound() for missing data and one that surfaces or logs
gameQuery.error appropriately.

In `@src/app/admin/listings/`[id]/edit/EditListingClientPage.tsx:
- Around line 29-31: Replace the direct rendering of the raw backend error in
the EditListingClientPage component (the usage of listingQuery.error.message)
with a generic, user-safe message such as "Unable to load listing. Please try
again later." and ensure the real error is recorded for developers (e.g.,
console.error(listingQuery.error) or send to telemetry) so debugging information
is not exposed in the UI but still available for diagnostics.

In `@src/app/api/admin/listings/`[id]/config/route.ts:
- Around line 2-6: Move the Role import so it precedes the Next.js server
imports to satisfy import/order: import Role from '`@orm`' (the Role symbol)
before importing connection, NextRequest, and NextResponse from 'next/server';
ensure external library imports remain grouped and internal/project imports
(e.g., '`@/server/db`', '`@/server/utils/emulator-config/emulator-detector`',
'`@/utils/permission-system`') keep their relative order after external imports.

In `@src/app/api/admin/notifications/analytics/route.ts`:
- Around line 2-6: Reorder the top-level imports to satisfy import/order: move
the Role import (from '`@orm`') so it appears before the external/module imports
like NextRequest and connection (from 'next/server'); keep the other imports
(prisma, notificationAnalyticsService, hasRolePermission) in the same relative
order and ensure imports for Role, connection, and NextRequest are grouped
correctly to resolve the lint failure.

---

Nitpick comments:
In `@src/app/admin/AdminLayoutClient.tsx`:
- Around line 14-15: Remove the redundant alias by importing hasRolePermission
directly (use import { hasRolePermission } from '`@/utils/permissions`') and
reorder the imports so the Role import from `@orm` appears before the
external/internal utils import; update the import lines referencing
hasRolePermission and Role in AdminLayoutClient.tsx accordingly.

In `@src/app/layout.tsx`:
- Around line 63-65: The Suspense wrapper around the Navbar uses
fallback={null}, which can cause layout shift; replace the null fallback with a
height-preserving placeholder component (e.g., NavbarSkeleton or a simple div
with the same height/spacing as Navbar) and use that as the Suspense fallback so
the page reserves the Navbar space while the real Navbar (component name:
Navbar) is loading; update the Suspense JSX to fallback={<NavbarSkeleton />} (or
similar) and ensure the skeleton matches Navbar's expected height and padding.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7d2fe54c-a619-4ff8-b441-1f71378850d8

📥 Commits

Reviewing files that changed from the base of the PR and between e2fc784 and 67a2b91.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • eslint.config.mjs
  • package.json
  • src/app/admin/AdminLayoutClient.tsx
  • src/app/admin/emulators/[emulatorId]/EditEmulatorClientPage.tsx
  • src/app/admin/emulators/[emulatorId]/custom-fields/EmulatorCustomFieldsClientPage.tsx
  • src/app/admin/emulators/[emulatorId]/custom-fields/page.tsx
  • src/app/admin/emulators/[emulatorId]/page.tsx
  • src/app/admin/games/[id]/AdminGameEditClientPage.tsx
  • src/app/admin/games/[id]/page.tsx
  • src/app/admin/layout.tsx
  • src/app/admin/listings/[id]/edit/EditListingClientPage.tsx
  • src/app/admin/listings/[id]/edit/page.tsx
  • src/app/api/account/export/route.ts
  • src/app/api/admin/listings/[id]/config/route.ts
  • src/app/api/admin/notifications/analytics/route.ts
  • src/app/api/catalog/compatibility/route.ts
  • src/app/api/health/route.ts
  • src/app/api/mobile/auth/route.ts
  • src/app/api/mobile/trpc/[trpc]/route.ts
  • src/app/api/notifications/stream/route.ts
  • src/app/api/proxy-image/route.ts
  • src/app/api/trpc/[trpc]/route.ts
  • src/app/games/[id]/page.tsx
  • src/app/layout.tsx
  • src/app/listings/[id]/page.tsx
  • src/app/pc-listings/[id]/page.tsx
  • src/app/terms/page.tsx
  • src/app/users/[id]/page.tsx
  • src/proxy.ts
  • src/server/api/routers/pcListings.test.ts
  • src/server/api/routers/pcListings.ts
  • src/server/db/seo-queries.test.ts

Comment thread src/app/admin/emulators/[emulatorId]/EditEmulatorClientPage.tsx
Comment thread src/app/admin/games/[id]/AdminGameEditClientPage.tsx Outdated
Comment thread src/app/admin/listings/[id]/edit/EditListingClientPage.tsx Outdated
Comment thread src/app/api/admin/listings/[id]/config/route.ts
Comment thread src/app/api/admin/notifications/analytics/route.ts

@coderabbitai coderabbitai 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.

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
`@src/app/admin/emulators/`[emulatorId]/custom-fields/EmulatorCustomFieldsClientPage.tsx:
- Around line 127-135: The new "Emulator not found" branch is unreachable
because the pre-existing access gate (the !canAccess check that depends on
emulatorQuery.data) runs first; update the logic so
isTRPCNotFoundError(emulatorQuery.error) is checked before or independently of
canAccess: either move the isTRPCNotFoundError(emulatorQuery.error) branch above
the !canAccess check, or compute canAccess in a way that does not rely on
emulatorQuery.data when emulatorQuery.error indicates a not-found error,
ensuring the isTRPCNotFoundError(...) condition will render the FeedbackCard for
missing emulators rather than being short-circuited by !canAccess.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4cf17b3a-2b7b-4c17-995e-bfc718759193

📥 Commits

Reviewing files that changed from the base of the PR and between 67a2b91 and 31e9f62.

📒 Files selected for processing (10)
  • eslint.config.mjs
  • public/sw-register.js
  • src/app/admin/emulators/[emulatorId]/custom-fields/EmulatorCustomFieldsClientPage.tsx
  • src/app/admin/games/[id]/AdminGameEditClientPage.tsx
  • src/app/admin/listings/[id]/edit/EditListingClientPage.tsx
  • src/app/layout.tsx
  • src/lib/api.tsx
  • src/lib/trpc-client-errors.test.ts
  • src/lib/trpc-client-errors.ts
  • src/proxy.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/trpc-client-errors.ts

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
src/app/admin/emulators/[emulatorId]/EditEmulatorClientPage.tsx (1)

67-110: 💤 Low value

Inconsistent retry mechanisms.

Line 87 uses systemsQuery.refetch() while Line 107 uses window.location.reload(). Similarly, Line 77 uses emulatorsQuery.refetch() while Line 97 uses the same. Consider using the query-specific refetch consistently to avoid full page reloads.

Suggested fix
   if (!systemsQuery.data) {
     return (
       <AdminErrorState
         title="Unable to load systems"
         message="The system list could not be loaded. Please try again."
-        onRetry={() => window.location.reload()}
+        onRetry={() => void systemsQuery.refetch()}
       />
     )
   }
🤖 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 `@src/app/admin/emulators/`[emulatorId]/EditEmulatorClientPage.tsx around lines
67 - 110, The onRetry handlers use inconsistent retry mechanisms: swap the
full-page reload to use the query-specific refetch so both error states use the
same strategy; update the AdminErrorState onRetry for the systems missing-data
branch to call systemsQuery.refetch() (matching the other systemsQuery and
emulatorsQuery onRetry usages) and ensure all onRetry handlers consistently
invoke emulatorsQuery.refetch() or systemsQuery.refetch() as appropriate rather
than window.location.reload().
src/app/admin/games/[id]/AdminGameEditClientPage.tsx (1)

36-44: 💤 Low value

Inconsistent retry mechanism.

The error case (Line 31) uses gameQuery.refetch() while this no-data fallback uses window.location.reload(). Consider using gameQuery.refetch() here as well for consistency and to avoid a full page reload.

Suggested fix
-        onRetry={() => window.location.reload()}
+        onRetry={() => void gameQuery.refetch()}
🤖 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 `@src/app/admin/games/`[id]/AdminGameEditClientPage.tsx around lines 36 - 44,
The no-data fallback in AdminGameEditClientPage uses window.location.reload()
for AdminErrorState.onRetry which is inconsistent with the other error path that
calls gameQuery.refetch(); update the onRetry handler to call
gameQuery.refetch() instead (i.e., pass a function that invokes the existing
gameQuery.refetch method) so the component uses the same retry mechanism and
avoids a full page reload.
🤖 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 `@src/app/admin/emulators/`[emulatorId]/EditEmulatorClientPage.tsx:
- Around line 67-110: The onRetry handlers use inconsistent retry mechanisms:
swap the full-page reload to use the query-specific refetch so both error states
use the same strategy; update the AdminErrorState onRetry for the systems
missing-data branch to call systemsQuery.refetch() (matching the other
systemsQuery and emulatorsQuery onRetry usages) and ensure all onRetry handlers
consistently invoke emulatorsQuery.refetch() or systemsQuery.refetch() as
appropriate rather than window.location.reload().

In `@src/app/admin/games/`[id]/AdminGameEditClientPage.tsx:
- Around line 36-44: The no-data fallback in AdminGameEditClientPage uses
window.location.reload() for AdminErrorState.onRetry which is inconsistent with
the other error path that calls gameQuery.refetch(); update the onRetry handler
to call gameQuery.refetch() instead (i.e., pass a function that invokes the
existing gameQuery.refetch method) so the component uses the same retry
mechanism and avoids a full page reload.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 07764d98-8c89-4691-98a9-d68c37a1f565

📥 Commits

Reviewing files that changed from the base of the PR and between 31e9f62 and 98a76d4.

📒 Files selected for processing (5)
  • src/app/admin/AdminLayoutClient.tsx
  • src/app/admin/emulators/[emulatorId]/EditEmulatorClientPage.tsx
  • src/app/admin/emulators/[emulatorId]/custom-fields/EmulatorCustomFieldsClientPage.tsx
  • src/app/admin/games/[id]/AdminGameEditClientPage.tsx
  • src/app/admin/listings/[id]/edit/EditListingClientPage.tsx

Comment thread eslint.config.mjs

@coderabbitai coderabbitai 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.

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 `@src/app/admin/users/page.tsx`:
- Line 36: Move the external module import for Role from '`@orm`' so it follows
the project's import ordering rule (external modules before internal/project
imports); update the import statement for Role (import { Role } from '`@orm`') to
appear with other external/third-party imports above the internal imports in
page.tsx to satisfy the import/order lint rule and restore CI.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 261bcdd9-8474-4c17-a5ec-ba5f9078076e

📥 Commits

Reviewing files that changed from the base of the PR and between 98a76d4 and 182b5be.

📒 Files selected for processing (7)
  • src/app/admin/users/page.tsx
  • src/app/api/catalog/compatibility/route.ts
  • src/app/api/mobile/auth/route.ts
  • src/app/api/notifications/stream/route.ts
  • src/app/api/proxy-image/route.ts
  • src/lib/errors.ts
  • src/rest/http.ts
💤 Files with no reviewable changes (2)
  • src/lib/errors.ts
  • src/app/api/mobile/auth/route.ts

Comment thread src/app/admin/users/page.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant