realtime-stats.ts73 lines · main
1import { env } from '../env.js';
2import { log } from '../lib/logger.js';
3
4/**
5 * Live snapshot of the realtime service: per-project subscription counts,
6 * per-channel refcounts, the caps the service is enforcing right now.
7 * Authenticates via the shared secret — same gate as /v1/subscribe so
8 * an unauth scrape can't enumerate project ids through this surface.
9 *
10 * Reads are cheap (an O(subs.size) walk on the realtime side) so we
11 * fetch live every time instead of caching. At the year-one 10k
12 * concurrent-sub target a snapshot is single-digit-millisecond work.
13 */
14
15export interface RealtimeStats {
16 readonly totalSubscriptions: number;
17 readonly totalChannels: number;
18 readonly limits: {
19 readonly perWs: number;
20 readonly perProject: number;
21 };
22 readonly byProject: readonly { projectId: string; subscriptions: number }[];
23 readonly byChannel: readonly { channel: string; subscriptions: number }[];
24}
25
26export async function fetchRealtimeStats(): Promise<RealtimeStats | null> {
27 if (!env.BRIVEN_REALTIME_URL || !env.BRIVEN_RUNTIME_SHARED_SECRET) return null;
28 try {
29 const res = await fetch(`${env.BRIVEN_REALTIME_URL}/v1/realtime/stats`, {
30 headers: { authorization: `Bearer ${env.BRIVEN_RUNTIME_SHARED_SECRET}` },
31 });
32 if (!res.ok) {
33 log.warn('realtime_stats_http', { status: res.status });
34 return null;
35 }
36 return (await res.json()) as RealtimeStats;
37 } catch (err) {
38 log.warn('realtime_stats_failed', {
39 message: err instanceof Error ? err.message : String(err),
40 });
41 return null;
42 }
43}
44
45export interface ProjectRealtimeStats {
46 /** Live concurrent subscriptions for the project. Zero when no entry. */
47 readonly subscriptions: number;
48 /** The hard cap the realtime service is enforcing right now. */
49 readonly limit: number;
50 /** subscriptions / limit, 0-1. Convenience for severity coloring. */
51 readonly fillRatio: number;
52}
53
54/**
55 * Per-project realtime usage. Reuses fetchRealtimeStats() under the hood
56 * but scopes the result to a single project so the dashboard surface
57 * doesn't accidentally enumerate other projects. Returns null when the
58 * realtime service is unconfigured/unreachable; callers render a "—".
59 */
60export async function fetchProjectRealtimeStats(
61 projectId: string,
62): Promise<ProjectRealtimeStats | null> {
63 const stats = await fetchRealtimeStats();
64 if (!stats) return null;
65 const hit = stats.byProject.find((p) => p.projectId === projectId);
66 const subscriptions = hit?.subscriptions ?? 0;
67 const limit = stats.limits.perProject;
68 return {
69 subscriptions,
70 limit,
71 fillRatio: limit > 0 ? subscriptions / limit : 0,
72 };
73}