perf: sunucu waterfall'ları ve blocking analitik düzeltildi (Faz 1)

Vercel React Best Practices denetimi, sunucu tarafı:
- odeme: analitik olayı after() ile yanıt sonrasına alındı; ucuz ürün
  doğrulaması oturum turundan öne çekildi; sonuç kartında oturum+sipariş
  paralel okunuyor
- /api/soru POST: gövde+oturum ve 3 bağımsız sorgu Promise.all'a bağlandı
- rehber: prod'da globalThis cache + özetler için frontmatter fast-path
  (24 dosya okuma + marked.parse her render'da tekrarlanıyordu); arama
  kutusuna giden payload RehberAramaKaydi'ya kırpıldı (tarih serileşmiyor)
- /sonuc hattı: ProgramListeSatiri (fakulte/puan2025 client'a serileşmiyor)
- katalog: universite/bolum listelerinde O(n²) filtreler tek geçişe indi
- sira-gerekli + katalog-arama: effect'te setState kalktı (lint yeşil)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 13:48:27 +03:00
parent d2732828cc
commit 53edb2b307
15 changed files with 256 additions and 99 deletions

View File

@@ -106,17 +106,19 @@ export async function GET() {
}
export async function POST(request: NextRequest) {
const session = await getSession();
// Oturum ve gövde ayrıştırma bağımsız — paralel başlar, hata önceliği aynı
// kalır (önce 401, sonra 400).
const [session, hamBody] = await Promise.all([
getSession(),
request.json().catch(() => null),
]);
if (!session) {
return Response.json({ error: "Oturum gerekli." }, { status: 401 });
}
let body: { message?: unknown; clientMessageId?: unknown };
try {
body = await request.json();
} catch {
if (hamBody === null || typeof hamBody !== "object") {
return Response.json({ error: "Geçersiz istek." }, { status: 400 });
}
const body = hamBody as { message?: unknown; clientMessageId?: unknown };
const mesaj = typeof body.message === "string" ? body.message.trim() : "";
const clientMessageId =
typeof body.clientMessageId === "string" ? body.clientMessageId : "";
@@ -124,24 +126,30 @@ export async function POST(request: NextRequest) {
return Response.json({ error: "Geçersiz mesaj." }, { status: 400 });
}
// Soru sormak için önce liste oluşturulmuş olmalı (bayat dev mock sayılmaz)
const rapor = await kullanicininRaporu(session.user.id);
// Üç okuma da yalnızca session.user.id'ye bağlı — tek round trip'te paralel.
// Geçmiş krediyi düşmeden ÖNCE okunur ki kullanıcı mesajı dahil olmasın;
// hasPaket'i spendCreditForMessage değiştirmez, erken okumak güvenli.
const [rapor, gecmisHam, kullanici] = await Promise.all([
// Soru sormak için önce liste oluşturulmuş olmalı (bayat dev mock sayılmaz)
kullanicininRaporu(session.user.id),
appDb
.select()
.from(schema.chatMessages)
.where(eq(schema.chatMessages.userId, session.user.id))
.orderBy(desc(schema.chatMessages.createdAt))
.limit(MAX_GECMIS),
appDb.query.user.findFirst({
where: eq(schema.user.id, session.user.id),
columns: { hasPaket: true },
}),
]);
if (!rapor?.result) {
return Response.json(
{ error: "Önce listeni oluşturmalısın.", code: "NO_REPORT" },
{ status: 409 },
);
}
// Geçmişi krediyi düşmeden ÖNCE oku ki kullanıcı mesajı dahil olmasın
const gecmis = (
await appDb
.select()
.from(schema.chatMessages)
.where(eq(schema.chatMessages.userId, session.user.id))
.orderBy(desc(schema.chatMessages.createdAt))
.limit(MAX_GECMIS)
).reverse();
const gecmis = gecmisHam.reverse();
// Kredi düşümü stream'den önce — yetersizse hiç başlama
const harcama = await spendCreditForMessage({
@@ -166,10 +174,6 @@ export async function POST(request: NextRequest) {
// Paketsiz kullanıcının prompt'una yalnızca açık satırlar girer — aksi
// halde danışman kilitli listeyi sözle sızdırabilir.
const kullanici = await appDb.query.user.findFirst({
where: eq(schema.user.id, session.user.id),
columns: { hasPaket: true },
});
const kilitli = !kullanici?.hasPaket;
const tamOzet = listeOzetiCikar(
rapor.result as RaporSonuc,