leaderBoard realtime per player and global
This commit is contained in:
@@ -5,7 +5,9 @@ import { NameManager } from "./utils/nameManager";
|
||||
import { getAllowedUuidCount, listAllowedUuids } from "./utils/uuidRegistry";
|
||||
|
||||
// SSE connections storage
|
||||
const sseClients = new Set<Response>();
|
||||
const sseClients = new Set<Response>(); // dashboard/rooms stream
|
||||
const sseUuidClients = new Set<Response>(); // uuids stream
|
||||
const ssePlayerActionsClients = new Set<Response>(); // per-player actions stream
|
||||
|
||||
const adminRouter = Router();
|
||||
|
||||
@@ -686,9 +688,164 @@ async function sendDashboardUpdate(client?: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// SSE endpoint for real-time UUIDs (allowlist + known names)
|
||||
adminRouter.get("/uuids-stream", (req: Request, res: Response) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
try { (res as any).flushHeaders?.(); } catch {}
|
||||
|
||||
sseUuidClients.add(res);
|
||||
console.log(`[AdminAPI] UUID SSE client connected. Total: ${sseUuidClients.size}`);
|
||||
|
||||
// Initial push
|
||||
sendUuidsUpdate(res);
|
||||
|
||||
req.on('close', () => {
|
||||
sseUuidClients.delete(res);
|
||||
console.log(`[AdminAPI] UUID SSE client disconnected. Total: ${sseUuidClients.size}`);
|
||||
});
|
||||
|
||||
const heartbeat = setInterval(() => {
|
||||
if ((res as any).destroyed) {
|
||||
clearInterval(heartbeat);
|
||||
sseUuidClients.delete(res);
|
||||
return;
|
||||
}
|
||||
res.write(':heartbeat\n\n');
|
||||
}, 30000);
|
||||
|
||||
req.on('close', () => clearInterval(heartbeat));
|
||||
});
|
||||
|
||||
async function sendUuidsUpdate(client?: Response) {
|
||||
try {
|
||||
const uuids = listAllowedUuids();
|
||||
const nameManager = NameManager.getInstance();
|
||||
const payload = {
|
||||
count: (uuids || []).length,
|
||||
uuids: (uuids || []).map(uuid => ({
|
||||
uuid,
|
||||
name: nameManager.getPlayerName(uuid) || null,
|
||||
hasName: !!nameManager.getPlayerName(uuid)
|
||||
}))
|
||||
};
|
||||
const message = `data: ${JSON.stringify(payload)}\n\n`;
|
||||
if (client) {
|
||||
if (!(client as any).destroyed) client.write(message);
|
||||
} else {
|
||||
const dead: Response[] = [];
|
||||
sseUuidClients.forEach(c => {
|
||||
if ((c as any).destroyed) dead.push(c);
|
||||
else {
|
||||
try { c.write(message); } catch { dead.push(c); }
|
||||
}
|
||||
});
|
||||
dead.forEach(c => sseUuidClients.delete(c));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AdminAPI] Error sending UUIDs SSE update:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to broadcast dashboard updates (called from room events)
|
||||
function broadcastDashboardUpdate() {
|
||||
sendDashboardUpdate();
|
||||
// Also push UUIDs updates for any name/assignment changes indirectly affected
|
||||
sendUuidsUpdate();
|
||||
sendPlayersActionsUpdate();
|
||||
}
|
||||
|
||||
// SSE endpoint for per-player actions made (real-time)
|
||||
adminRouter.get("/players-actions-stream", (req: Request, res: Response) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
try { (res as any).flushHeaders?.(); } catch {}
|
||||
|
||||
ssePlayerActionsClients.add(res);
|
||||
console.log(`[AdminAPI] Player actions SSE client connected. Total: ${ssePlayerActionsClients.size}`);
|
||||
|
||||
sendPlayersActionsUpdate(res);
|
||||
|
||||
req.on('close', () => {
|
||||
ssePlayerActionsClients.delete(res);
|
||||
console.log(`[AdminAPI] Player actions SSE client disconnected. Total: ${ssePlayerActionsClients.size}`);
|
||||
});
|
||||
|
||||
const heartbeat = setInterval(() => {
|
||||
if ((res as any).destroyed) {
|
||||
clearInterval(heartbeat);
|
||||
ssePlayerActionsClients.delete(res);
|
||||
return;
|
||||
}
|
||||
res.write(':heartbeat\n\n');
|
||||
}, 30000);
|
||||
|
||||
req.on('close', () => clearInterval(heartbeat));
|
||||
});
|
||||
|
||||
function isActionMade(kind: string, role?: string) {
|
||||
const k = (kind || '').toString();
|
||||
if (!k) return false;
|
||||
const prefix = k.slice(0,3).toLowerCase();
|
||||
if (prefix === 'p1_' || prefix === 'p2_') {
|
||||
const evRole = prefix === 'p1_' ? 'P1' : 'P2';
|
||||
return ((role || '').toUpperCase() === evRole);
|
||||
}
|
||||
// System/agnostic events are ignored for this stream; only actions list below are counted
|
||||
return false;
|
||||
}
|
||||
|
||||
const ACTION_EVENTS = [
|
||||
'p1_propose','p1_no_offer','p2_snatch','p2_accept','p2_force','p2_no_force','p2_reject','p1_shame','p1_no_shame','p1_report','p1_no_report'
|
||||
];
|
||||
|
||||
async function sendPlayersActionsUpdate(client?: Response) {
|
||||
try {
|
||||
const nameManager = NameManager.getInstance();
|
||||
const uuids = nameManager.getAllKnownUuids?.() || [];
|
||||
const players = uuids.map((uuid: string) => {
|
||||
const history = nameManager.getSystemHistory(uuid) || [];
|
||||
const counts: any = Object.fromEntries(ACTION_EVENTS.map(k => [k, 0]));
|
||||
for (const entry of history) {
|
||||
const kind = (entry as any)?.kind || '';
|
||||
if (!ACTION_EVENTS.includes(kind)) continue;
|
||||
if (!isActionMade(kind, (entry as any)?.role)) continue;
|
||||
counts[kind] = (counts[kind] || 0) + 1;
|
||||
}
|
||||
const total = ACTION_EVENTS.reduce((acc, k) => acc + (counts[k] || 0), 0);
|
||||
return {
|
||||
uuid,
|
||||
name: nameManager.getPlayerName(uuid) || null,
|
||||
counts,
|
||||
total
|
||||
};
|
||||
}).filter((p: any) => p.total > 0 || p.name);
|
||||
|
||||
const payload = { players };
|
||||
const message = `data: ${JSON.stringify(payload)}\n\n`;
|
||||
if (client) {
|
||||
if (!(client as any).destroyed) client.write(message);
|
||||
} else {
|
||||
const dead: Response[] = [];
|
||||
ssePlayerActionsClients.forEach(c => {
|
||||
if ((c as any).destroyed) dead.push(c);
|
||||
else { try { c.write(message); } catch { dead.push(c); } }
|
||||
});
|
||||
dead.forEach(c => ssePlayerActionsClients.delete(c));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AdminAPI] Error sending players actions SSE update:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export { adminRouter, broadcastDashboardUpdate };
|
||||
|
||||
Reference in New Issue
Block a user