import "server-only"; import { count } from "drizzle-orm"; import { connection } from "next/server"; import { appDb, schema } from "@/lib/appdb"; import type { SosyalKanitVerisi } from "@/lib/sosyal-kanit-sabitler"; /** * Fiyat bölümündeki sosyal kanıt şeridinin sayıları. * * İki sayı, iki ayrı kaynak — bilerek: * * 1. "X öğrenci listesini oluşturdu" KENDİ veritabanımızdan gelir (`reports` * tablosu; satır yalnızca liste gerçekten üretildikten sonra yazılır, * bkz. app/sonuc/actions.ts). Bu sayı ziyaretçiye verilen bir söz olduğu * için gerçek olmak zorunda; Rybbit'ten okunsaydı reklam engelleyiciler * yüzünden olduğundan düşük çıkardı. * 2. "Şu anda X kişi sitede" yalnızca Rybbit'ten gelebilir: giriş yapmamış * ziyaretçinin bizim veritabanımızda hiçbir izi yok. * * Rybbit env'i eksikse (dev, anahtar tanımsız) canlı sayı `null` döner ve * şeritte o satır hiç çizilmez — asla uydurulmuş sayı gösterilmez. */ /** Rybbit'in "şu an sitede" penceresi (GA'nın 30 dakikalık tanımıyla aynı). */ const CANLI_DAKIKA = 30; const LISTE_TTL_MS = 10 * 60_000; const CANLI_TTL_MS = 30_000; const HOST = process.env.RYBBIT_HOST; const API_KEY = process.env.RYBBIT_API_KEY; /** * DİKKAT: Rybbit'in istatistik API'si SAYISAL site kimliği ister * (`/api/sites/:siteId/...` sorgusu ClickHouse'ta `site_id` Int32'ye * bağlanıyor). `RYBBIT_SITE_ID` ise script'in kullandığı herkese açık * kimlik (f2cb...) — ikisi farklı, birbirinin yerine geçmez. */ const STATS_SITE_ID = process.env.RYBBIT_STATS_SITE_ID; type Kutu = { deger: T; zaman: number }; /** * Süreç içi TTL önbelleği. `use cache` yerine bunu kullanıyoruz: derleme * anında `data/` boş (bkz. Dockerfile), dolayısıyla prerender edilen bir * sayı 0 olarak taşa yazılırdı. `connection()` + bu bellek, sayıyı istek * anına taşır ama her ziyaretçide DB/Rybbit'e gitmez. Uygulama tek süreçli * standalone olarak çalıştığı için modül belleği yeterli. */ const bellek: { liste?: Kutu; canli?: Kutu } = {}; /** Üretilmiş tercih listesi sayısı. DB okunamazsa son bilinen değer korunur. */ export async function olusturulanListeSayisi(): Promise { await connection(); const onceki = bellek.liste; if (onceki && Date.now() - onceki.zaman < LISTE_TTL_MS) return onceki.deger; let deger = onceki?.deger ?? 0; try { const [satir] = await appDb.select({ n: count() }).from(schema.reports); deger = satir?.n ?? 0; } catch { // Sayı gösterilemezse şerit gizlenir; sayfa asla hata vermez. } bellek.liste = { deger, zaman: Date.now() }; return deger; } /** Son 30 dakikadaki tekil ziyaretçi. Rybbit yoksa/erişilemezse null. */ export async function anlikZiyaretci(): Promise { if (!HOST || !API_KEY || !STATS_SITE_ID) return null; // `Date.now()`den önce şart: prerender sırasında saat okumak yasak, istek // anına bağlanmamız gerekiyor. Kardeş fonksiyondaki `connection()` bunu // karşılamıyor — ikisi `Promise.all` ile paralel koşuyor. await connection(); const onceki = bellek.canli; if (onceki && Date.now() - onceki.zaman < CANLI_TTL_MS) return onceki.deger; let deger: number | null = null; try { const cevap = await fetch( `${HOST}/api/sites/${STATS_SITE_ID}/live-user-count?minutes=${CANLI_DAKIKA}`, { headers: { Authorization: `Bearer ${API_KEY}` }, cache: "no-store", // Analitik hiçbir koşulda sayfayı bekletmesin signal: AbortSignal.timeout(3000), }, ); if (cevap.ok) { const veri = (await cevap.json()) as { count?: number }; if (typeof veri.count === "number" && Number.isFinite(veri.count)) { deger = veri.count; } } } catch { // Yut: canlı satır çizilmez, şeridin geri kalanı ayakta kalır. } bellek.canli = { deger, zaman: Date.now() }; return deger; } export async function sosyalKanitVerisi(): Promise { const [liste, canli] = await Promise.all([ olusturulanListeSayisi(), anlikZiyaretci(), ]); return { liste, canli }; }