feat(game): serve panorama index from Neon Postgres

- panoramas + pano_provinces tables replace 28MB bundled JSON; pano-index.js
  now draws via cached COUNT + ORDER BY id OFFSET with rejection sampling,
  composite (province,id)/(district,id) indexes support the skip
- scripts/seed-pano-db.mjs validates pipeline artifacts (the old real-data
  vitest invariants, extracted to scripts/lib/pano-artifacts.mjs), stages
  into panoramas_next, verifies, renames into place in one transaction,
  keeps panoramas_old as backup; --province reseeds in place; --check
  validates only
- pipeline writes gitignored data-build/panos/; pano barrel removed
- tests run against PGlite mocked in at the @neondatabase/serverless
  boundary, mirroring the fake-upstash pattern; fixtures replace real data
- infrastructure errors rethrow instead of reading as missing coverage;
  session ids via crypto.randomUUID (uuid package dropped)
This commit is contained in:
tiennm99 committed 2026-08-31 09:44:03 +07:00
1 parent da7a715478
commit 6569f095c9
45 files changed
+1510 -425270

No files matched your search

+21 -9
View File
@@ -1,7 +1,7 @@
# Tech Stack
## Frontend Framework
- **Next.js 15.5**: React-based full-stack framework with App Router
- **Next.js 16**: React-based full-stack framework with App Router
- **React 19.2**: Component architecture
- **Tailwind CSS 4**: Utility-first CSS framework for styling
@@ -23,16 +23,23 @@
## Geographic Processing
- **@turf/turf**: distance, point-in-polygon, union, simplify, and
point-to-line distance -- used both at runtime and by the offline builds
- **Generated region tree**: `src/data/regions/` holds the nodes,
`src/data/boundaries/<province>/` the simplified outlines, and
`src/data/panos/` the per-province panorama indexes
- **Generated region tree**: `src/data/regions/` holds the nodes and
`src/data/boundaries/<province>/` the simplified outlines. The per-province
panorama indexes live in Postgres (see below), seeded from local pipeline
artifacts in `data-build/panos/` (gitignored)
- **Client/server split**: `src/lib/regions.js` is the client-safe view and
imports nothing from `src/data/panos/` or `src/lib/pano-index.js`. That
imports nothing from `src/lib/pano-index.js` or `src/lib/pano-db.js`. That
boundary is enforced by an import-graph walk in `tests/regions.test.js` -- the
indexes are ~29MB of exact answers
panorama rows are exact answers
- **Server-side Calculations**: All geographic processing on backend
## Data Storage & Session Management
- **Neon Postgres (HTTP)**: The panorama index -- 424k rows of id, province,
district, lat, lng -- read via `@neondatabase/serverless` (`DATABASE_URL` or
`POSTGRES_URL`). One random-draw query per round, count queries cached
per process. Seeded by `scripts/seed-pano-db.mjs`, which stages into
`panoramas_next`, verifies, then renames into place, keeping the previous
generation as `panoramas_old`
- **Upstash Redis (REST)**: Session and leaderboard storage via `@upstash/redis` SDK (REST client, no sockets)
- **Credential Flexibility**: Accepts `UPSTASH_REDIS_REST_URL`+`UPSTASH_REDIS_REST_TOKEN` (vanilla Upstash) or `KV_REST_API_URL`+`KV_REST_API_TOKEN` (Vercel Marketplace aliases)
- **Multi-tenant Key Prefix**: All physical keys carry `KEY_PREFIX` (default `vngeoguessr:`) to safely share Upstash DB with other Vercel projects. Prefix applied centrally in `src/lib/upstash.js`; callers use logical keys only.
@@ -41,7 +48,7 @@
`city:{regionCode}` for every province and district -- the `city:` segment is
kept so existing scores stay addressable
- **Sorted Sets**: Leaderboard ranking with automatic trimming (top 200)
- **UUID v4**: Session identifier generation
- **UUID v4**: Session identifier generation via built-in `crypto.randomUUID()`
- **30-minute Session Expiry**: Automatic TTL-based cleanup
## UI Components & Styling
@@ -55,8 +62,14 @@
## Testing
- **Vitest**: Test runner for the logic in `src/lib/`, the API routes, the
generated region data, and the leaderboard migration
- **In-memory Upstash fake**: Default backing store, no service required
- **In-memory Upstash fake**: Default Redis backing store, no service required
- **PGlite**: In-process Postgres (WASM) mocked in at the
`@neondatabase/serverless` boundary, so the panorama queries run against real
Postgres semantics with no service
- **SRH + Redis (Docker)**: Optional lane running the same tests against real Redis
- **Playwright**: Chromium smoke tests for the UI (`tests/e2e/`), with every
API call and the panorama image stubbed at the browser boundary -- no
services, no env vars
## Development & Analytics
- **ESLint**: Code linting with Next.js configuration
@@ -65,7 +78,6 @@
- **@vercel/speed-insights**: Performance monitoring
## Key Dependencies
- **uuid**: unique session identifier generation
- **@upstash/redis**: REST-based Upstash client; no socket pooling, works in edge compute and serverless
- **JavaScript Only**: No TypeScript - pure JavaScript implementation
- **Individual Parameters**: Functions use separate parameters instead of object destructuring