diff --git a/src/app/api/eterra/uats/route.ts b/src/app/api/eterra/uats/route.ts index f9e4f4f..3664663 100644 --- a/src/app/api/eterra/uats/route.ts +++ b/src/app/api/eterra/uats/route.ts @@ -8,6 +8,49 @@ import { getSessionCredentials } from "@/modules/parcel-sync/services/session-st export const runtime = "nodejs"; export const dynamic = "force-dynamic"; +/* ------------------------------------------------------------------ */ +/* Feature count cache (expensive query, cached 5 min) */ +/* ------------------------------------------------------------------ */ + +const gCache = globalThis as { + __featureCountCache?: { map: Map; ts: number }; +}; + +async function getCachedFeatureCounts(): Promise> { + const TTL = 5 * 60 * 1000; // 5 minutes + const now = Date.now(); + + if (gCache.__featureCountCache && now - gCache.__featureCountCache.ts < TTL) { + return gCache.__featureCountCache.map; + } + + // Run in background if cache exists but expired (return stale, refresh async) + if (gCache.__featureCountCache) { + void refreshFeatureCounts(); + return gCache.__featureCountCache.map; + } + + // First call: must wait + return refreshFeatureCounts(); +} + +async function refreshFeatureCounts(): Promise> { + try { + const groups = await prisma.gisFeature.groupBy({ + by: ["siruta"], + _count: { id: true }, + }); + const map = new Map(); + for (const g of groups) { + map.set(g.siruta, g._count.id); + } + gCache.__featureCountCache = { map, ts: Date.now() }; + return map; + } catch { + return gCache.__featureCountCache?.map ?? new Map(); + } +} + /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ @@ -119,37 +162,25 @@ function unwrapArray(data: any): any[] { /* No eTerra credentials needed — instant response. */ /* ------------------------------------------------------------------ */ -export async function GET(req: Request) { +export async function GET() { try { - const url = new URL(req.url); - const withFeatures = url.searchParams.get("features") === "true"; - // CRITICAL: select only needed fields — geometry column has huge polygon data const rows = await prisma.gisUat.findMany({ orderBy: { name: "asc" }, select: { siruta: true, name: true, county: true, workspacePk: true }, }); - // Feature counts are expensive (scans entire GisFeature table) - // Only include when explicitly requested - let featureCounts: Map | null = null; - if (withFeatures) { - const groups = await prisma.gisFeature.groupBy({ - by: ["siruta"], - _count: { id: true }, - }); - featureCounts = new Map(); - for (const g of groups) { - featureCounts.set(g.siruta, g._count.id); - } - } + // Feature counts: use in-memory cache (refreshed every 5 min) + // The groupBy query is expensive (~25s without cache) but the data + // changes rarely (only when sync jobs run) + const featureCounts = await getCachedFeatureCounts(); const uats: UatResponse[] = rows.map((r) => ({ siruta: r.siruta, name: r.name, county: r.county ?? "", workspacePk: r.workspacePk ?? 0, - localFeatures: featureCounts?.get(r.siruta) ?? 0, + localFeatures: featureCounts.get(r.siruta) ?? 0, })); // Populate in-memory workspace cache for search route