Files
fristy 864b9226fc
Build & Push / Lint & Build (pull_request) Failing after 52s
Build & Push / Build & Push Image (pull_request) Skipped
Build & Push / Trigger Deploy (pull_request) Skipped
desloppify: code health overhaul plus abuse hardening (140 green, strict 87.2)
Honest baseline after removing 45 gaming suppressions. Structural: lib/game plus root hooks extraction, CORS origin echo, DEV_ORIGINS layering. Fixes: error-cause binding, discord 5xx split, null-cache guard, co-located tests, rejecting-fetch coverage, naming and typing cleanup, socket controller, cache bound, cascade helpers, latch resets. Hardening: per-IP rate limit on API routes, deterministic-miss UX, retry cooldown plus cap. Suite 140 green, build plus lint clean.
2026-09-11 23:54:59 +03:00

208 lines
6.0 KiB
TypeScript

import { useEffect, useState } from "react";
import type { LanyardData } from "@/lib/game/lanyard";
import { DISCORD_ID, LANYARD_API, LANYARD_WS } from "@/lib/game/lanyard";
import { fetchJson } from "@/lib/client-fetch";
const WS_OPEN_TIMEOUT_MS = 10_000;
const RECONNECT_BASE_MS = 2_000;
const RECONNECT_MAX_MS = 30_000;
const SAFETY_POLL_MS = 60_000;
const HEARTBEAT_FALLBACK_MS = 30_000;
interface LanyardMessage {
op: number;
t?: string;
d?: unknown;
}
function parseLanyardMessage(data: unknown): LanyardMessage | null {
if (typeof data !== "string") {
return null;
}
try {
return JSON.parse(data) as LanyardMessage;
} catch {
return null;
}
}
function isLanyardData(value: unknown): value is LanyardData {
if (typeof value !== "object" || value === null) {
return false;
}
const candidate = value as { activities?: unknown; discord_status?: unknown };
if (!Array.isArray(candidate.activities)) {
return false;
}
const status = candidate.discord_status;
return status === "online" || status === "idle" || status === "dnd" || status === "offline";
}
function nextBackoffDelay(current: number): number {
return Math.min(current * 2, RECONNECT_MAX_MS);
}
async function fetchLanyardSnapshot(): Promise<LanyardData | null> {
const json = await fetchJson<{ success: boolean; data: LanyardData }>(LANYARD_API, {
cache: "no-store",
});
return json && json.success ? json.data : null;
}
interface LanyardSocketEvents {
onData: (data: LanyardData) => void;
}
interface LanyardSocket {
start: () => void;
stop: () => void;
}
function createLanyardSocket(events: LanyardSocketEvents): LanyardSocket {
let disposed = false;
let socket: WebSocket | null = null;
let heartbeatTimer: number | undefined;
let reconnectTimer: number | undefined;
let safetyTimer: number | undefined;
let reconnectDelay = RECONNECT_BASE_MS;
async function pollOnce(): Promise<void> {
const snapshot = await fetchLanyardSnapshot();
if (snapshot && !disposed) {
events.onData(snapshot);
}
}
function scheduleReconnect(): void {
if (disposed) {
return;
}
window.clearTimeout(reconnectTimer);
reconnectTimer = window.setTimeout(() => {
reconnectDelay = nextBackoffDelay(reconnectDelay);
connect();
}, reconnectDelay);
}
function dropSocket(current: WebSocket): void {
current.onclose = null;
current.onerror = null;
current.onmessage = null;
if (socket === current) {
socket = null;
}
try {
current.close();
} catch {
if (socket === current) {
socket = null;
}
}
}
function connect(): void {
if (disposed) {
return;
}
let current: WebSocket;
try {
current = new WebSocket(LANYARD_WS);
} catch {
scheduleReconnect();
return;
}
socket = current;
let settled = false;
const openTimeout = window.setTimeout(() => {
if (!settled) {
settled = true;
dropSocket(current);
if (!disposed) {
void pollOnce();
scheduleReconnect();
}
}
}, WS_OPEN_TIMEOUT_MS);
const handleDrop = (): void => {
if (settled && socket !== current) {
return;
}
settled = true;
window.clearTimeout(openTimeout);
window.clearInterval(heartbeatTimer);
dropSocket(current);
if (!disposed) {
void pollOnce();
scheduleReconnect();
}
};
current.onmessage = (event: MessageEvent) => {
const message = parseLanyardMessage(event.data);
if (!message) {
return;
}
if (message.op === 1) {
settled = true;
window.clearTimeout(openTimeout);
reconnectDelay = RECONNECT_BASE_MS;
const interval =
(message.d as { heartbeat_interval?: number } | undefined)
?.heartbeat_interval ?? HEARTBEAT_FALLBACK_MS;
window.clearInterval(heartbeatTimer);
heartbeatTimer = window.setInterval(() => {
try {
current.send(JSON.stringify({ op: 3 }));
} catch {
handleDrop();
}
}, interval);
current.send(JSON.stringify({ op: 2, d: { subscribe_to_id: DISCORD_ID } }));
} else if (
message.op === 0 &&
(message.t === "INIT_STATE" || message.t === "PRESENCE_UPDATE")
) {
if (!disposed && isLanyardData(message.d)) {
events.onData(message.d);
}
}
};
current.onerror = handleDrop;
current.onclose = handleDrop;
}
function start(): void {
connect();
safetyTimer = window.setInterval(() => {
if (!socket || socket.readyState !== WebSocket.OPEN) {
void pollOnce();
}
}, SAFETY_POLL_MS);
}
function stop(): void {
disposed = true;
window.clearTimeout(reconnectTimer);
window.clearInterval(safetyTimer);
window.clearInterval(heartbeatTimer);
if (socket) {
dropSocket(socket);
}
}
return { start, stop };
}
export function useLanyard(initial: LanyardData | null): LanyardData | null {
const [data, setData] = useState<LanyardData | null>(initial);
useEffect(() => {
const controller = createLanyardSocket({ onData: (snapshot) => setData(snapshot) });
controller.start();
return () => controller.stop();
}, []);
return data;
}