Files
kolaytercih/src/lib/sosyal-kanit.ts
bilalgursen 820eea7eca refactor: ortak tipler @/types/yokatlas + features/rapor/types'a çıkarıldı, server-only korumaları eklendi (Faz 1)
- Program/PUAN_TURLERI/DilimKey/RankResults/UniturGrubu/ProgramNetSatiri/
  SihirbazFacetleri artık src/types/yokatlas.ts'te; lib/db.ts yalnızca sorgu
  fonksiyonları barındırıyor
- RaporSonuc/RaporKapsam/RaporParams/MaskeliRapor src/features/rapor/types/
  rapor.ts'e taşındı; ölü kalan TercihSchema/RaporSchema silindi
- 11 lib dosyasına import "server-only" eklendi; scripts'in düz node ile
  import ettiği dosyalara (db, appdb, rapor-havuzu, tadimlik-havuzu, ai/client,
  ai/cagri, credits) bilinçli eklenmedi
- Davranış değişikliği yok; pnpm build yeşil

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:06:06 +03:00

111 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<T> = { 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<number>; canli?: Kutu<number | null> } = {};
/** Üretilmiş tercih listesi sayısı. DB okunamazsa son bilinen değer korunur. */
export async function olusturulanListeSayisi(): Promise<number> {
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<number | null> {
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<SosyalKanitVerisi> {
const [liste, canli] = await Promise.all([
olusturulanListeSayisi(),
anlikZiyaretci(),
]);
return { liste, canli };
}