Corrige lentidão do frontend com múltiplos clientes simultâneos
---
- Substituída consulta individual por stream (supervisorctl status {p}-{id} × 5) por uma única chamada supervisorctl status que retorna todos os programas de uma vez, com cache de 3s em fetchAllStatuses();
- Adicionado endpoint GET /api/streams/statuses que retorna os statuses de todos os streams em uma resposta batch;
- Frontend alterado para usar o endpoint batch em vez de N requests paralelos por ciclo de polling;
- Token de sessão HMAC-SHA256 em auth.ts agora é computado uma vez e cacheado no módulo, eliminando operações de crypto a cada requisição;
- Rolling session no middleware limitado a rotas de página, removendo overhead de Set-Cookie em respostas de API e HLS;
---
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getAllStreamStatuses } from "@/lib/supervisor"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(getAllStreamStatuses())
|
||||
}
|
||||
+3
-8
@@ -135,14 +135,9 @@ export default function GalleryPage() {
|
||||
|
||||
const fetchStatuses = useCallback(async (list: Stream[]) => {
|
||||
if (list.length === 0) return
|
||||
const results = await Promise.all(
|
||||
list.map(async (s) => {
|
||||
const res = await fetch(`/api/streams/${s.id}/status`)
|
||||
const data = await res.json()
|
||||
return [s.id, data] as const
|
||||
})
|
||||
)
|
||||
setStatuses(Object.fromEntries(results))
|
||||
const res = await fetch("/api/streams/statuses")
|
||||
const data: Record<string, Record<string, string>> = await res.json()
|
||||
setStatuses(data)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+10
-8
@@ -3,17 +3,19 @@
|
||||
export const AUTH_ENABLED = !!(process.env.AUTH_USER && process.env.AUTH_PASS)
|
||||
export const COOKIE_NAME = "ds_session"
|
||||
|
||||
// Cached promise — token is deterministic (env vars never change at runtime)
|
||||
let _tokenCache: Promise<string> | null = null
|
||||
|
||||
// HMAC-SHA256(user, key=pass) — deterministic, no in-memory state, survives restarts
|
||||
// Works in both Edge (SubtleCrypto) and Node.js runtime
|
||||
export async function computeSessionToken(): Promise<string> {
|
||||
export function computeSessionToken(): Promise<string> {
|
||||
if (_tokenCache) return _tokenCache
|
||||
const user = process.env.AUTH_USER ?? ""
|
||||
const pass = process.env.AUTH_PASS ?? ""
|
||||
const enc = new TextEncoder()
|
||||
const key = await globalThis.crypto.subtle.importKey(
|
||||
"raw", enc.encode(pass),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false, ["sign"]
|
||||
)
|
||||
const sig = await globalThis.crypto.subtle.sign("HMAC", key, enc.encode(user))
|
||||
return Array.from(new Uint8Array(sig), b => b.toString(16).padStart(2, "0")).join("")
|
||||
_tokenCache = globalThis.crypto.subtle
|
||||
.importKey("raw", enc.encode(pass), { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
|
||||
.then(key => globalThis.crypto.subtle.sign("HMAC", key, enc.encode(user)))
|
||||
.then(sig => Array.from(new Uint8Array(sig), b => b.toString(16).padStart(2, "0")).join(""))
|
||||
return _tokenCache
|
||||
}
|
||||
|
||||
+38
-12
@@ -132,25 +132,51 @@ export function captureThumb(streamId: string, delay = 60): void {
|
||||
|
||||
export type ProgramStatus = "RUNNING" | "STOPPED" | "FATAL" | "STARTING" | "UNKNOWN"
|
||||
|
||||
export function getStreamStatus(id: string): Record<string, ProgramStatus> {
|
||||
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
|
||||
// Cache for supervisorctl status — refreshed at most once every 3 seconds across all callers
|
||||
let _statusCache: Record<string, Record<string, ProgramStatus>> | null = null
|
||||
let _statusCacheAt = 0
|
||||
const STATUS_CACHE_TTL = 3000
|
||||
|
||||
if (IS_DEV) {
|
||||
return Object.fromEntries(programs.map((p) => [p, "STOPPED" as ProgramStatus]))
|
||||
}
|
||||
function fetchAllStatuses(): Record<string, Record<string, ProgramStatus>> {
|
||||
const now = Date.now()
|
||||
if (_statusCache && now - _statusCacheAt < STATUS_CACHE_TTL) return _statusCache
|
||||
|
||||
const result: Record<string, ProgramStatus> = {}
|
||||
for (const p of programs) {
|
||||
const result: Record<string, Record<string, ProgramStatus>> = {}
|
||||
try {
|
||||
// One call for all programs — avoid N×5 blocking execSync calls per poll cycle
|
||||
const out = execSync(
|
||||
`supervisorctl -c /etc/supervisor/supervisord.conf status ${p}-${id}`,
|
||||
`supervisorctl -c /etc/supervisor/supervisord.conf status`,
|
||||
{ stdio: "pipe" }
|
||||
).toString()
|
||||
const match = out.match(/\b(RUNNING|STOPPED|FATAL|STARTING)\b/)
|
||||
result[p] = (match?.[1] as ProgramStatus) ?? "UNKNOWN"
|
||||
for (const line of out.split("\n")) {
|
||||
// e.g. "ffmpeg-abc123 RUNNING pid 42, uptime 0:01:00"
|
||||
const m = line.match(/^(\S+)-(\S+)\s+(RUNNING|STOPPED|FATAL|STARTING)/)
|
||||
if (!m) continue
|
||||
const [, program, id, status] = m
|
||||
if (!result[id]) result[id] = {}
|
||||
result[id][program] = status as ProgramStatus
|
||||
}
|
||||
} catch {
|
||||
result[p] = "UNKNOWN"
|
||||
}
|
||||
// supervisorctl can exit non-zero; return whatever was parsed
|
||||
}
|
||||
|
||||
_statusCache = result
|
||||
_statusCacheAt = now
|
||||
return result
|
||||
}
|
||||
|
||||
export function getStreamStatus(id: string): Record<string, ProgramStatus> {
|
||||
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
|
||||
if (IS_DEV) return Object.fromEntries(programs.map((p) => [p, "STOPPED" as ProgramStatus]))
|
||||
const all = fetchAllStatuses()
|
||||
return all[id] ?? Object.fromEntries(programs.map((p) => [p, "UNKNOWN" as ProgramStatus]))
|
||||
}
|
||||
|
||||
export function getAllStreamStatuses(): Record<string, Record<string, ProgramStatus>> {
|
||||
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
|
||||
if (IS_DEV) {
|
||||
const { readStreams } = require("./db") as typeof import("./db")
|
||||
return Object.fromEntries(readStreams().map((s) => [s.id, Object.fromEntries(programs.map((p) => [p, "STOPPED" as ProgramStatus]))]))
|
||||
}
|
||||
return fetchAllStatuses()
|
||||
}
|
||||
|
||||
+4
-1
@@ -22,7 +22,10 @@ export async function middleware(request: NextRequest) {
|
||||
return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// Rolling session — refresh cookie on every request, resetting the 30-day timer
|
||||
// Rolling session — refresh cookie only on page navigations, not API/HLS/asset requests
|
||||
if (pathname.startsWith("/api/") || pathname.startsWith("/player")) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
const res = NextResponse.next()
|
||||
res.cookies.set(COOKIE_NAME, cookie, {
|
||||
httpOnly: true,
|
||||
|
||||
Reference in New Issue
Block a user