change around pings, fix main.js so the actual real fucking number shows instead of it freaking out.

This commit is contained in:
KingKDot
2026-07-09 13:55:36 -05:00
parent f96b3d0721
commit 43ee5a7d4b
12 changed files with 293 additions and 32 deletions
+2 -4
View File
@@ -5,6 +5,7 @@ import { encodeMessage } from "./protocol";
import { v4 as uuidv4 } from "uuid";
import { metrics } from "./metrics";
import { logAudit, AuditAction } from "./auditLog";
import { sendPingRequest } from "./wsHandlers";
const DEFAULT_PAGE_SIZE = 12;
const CORS_HEADERS = {
@@ -61,10 +62,7 @@ export function handleCommand(target: ClientInfo, action: string, req: Request)
metrics.recordCommand(action);
if (action === "ping") {
const nonce = Date.now() + Math.floor(Math.random() * 1000);
target.lastPingSent = Date.now();
target.lastPingNonce = nonce;
target.ws.send(encodeMessage({ type: "ping", ts: nonce }));
sendPingRequest(target, target.ws, "manual", 0);
return OK();
}
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { getHeartbeatBatchSize } from "./maintenance-loops";
import { getHeartbeatBatchSize, shouldSendHeartbeatPing } from "./maintenance-loops";
describe("maintenance heartbeat batching", () => {
test("spreads large fleets across the heartbeat interval", () => {
@@ -13,4 +13,11 @@ describe("maintenance heartbeat batching", () => {
test("handles empty fleets", () => {
expect(getHeartbeatBatchSize(0, 30_000)).toBe(0);
});
test("only sends heartbeat pings after the per-client interval", () => {
const now = Date.now();
expect(shouldSendHeartbeatPing({}, now, 30_000)).toBe(true);
expect(shouldSendHeartbeatPing({ lastPingSent: now - 29_999 }, now, 30_000)).toBe(false);
expect(shouldSendHeartbeatPing({ lastPingSent: now - 30_000 }, now, 30_000)).toBe(true);
});
});
@@ -35,6 +35,16 @@ export function getHeartbeatBatchSize(totalClients: number, heartbeatIntervalMs:
return Math.max(1, Math.ceil(totalClients / ticksPerSweep));
}
export function shouldSendHeartbeatPing(
info: Pick<ClientInfo, "lastPingSent">,
now: number,
heartbeatIntervalMs: number,
): boolean {
if (heartbeatIntervalMs <= 0) return false;
if (!info.lastPingSent) return true;
return now - info.lastPingSent >= heartbeatIntervalMs;
}
export function startMaintenanceLoops(params: StartMaintenanceParams): void {
setInterval(() => {
const startedAt = Date.now();
@@ -82,8 +92,11 @@ export function startMaintenanceLoops(params: StartMaintenanceParams): void {
}
continue;
}
if (!shouldSendHeartbeatPing(info, now, params.heartbeatIntervalMs)) {
continue;
}
try {
sendPingRequest(info, info.ws, "heartbeat");
sendPingRequest(info, info.ws, "heartbeat", params.heartbeatIntervalMs);
} catch (err) {
logger.debug(`[ping] heartbeat failed for ${id}`, err);
}
@@ -0,0 +1,47 @@
import { describe, expect, test } from "bun:test";
import { decodeMessage } from "../../protocol";
import type { ClientInfo } from "../../types";
import { dispatchPingBulk } from "./client-command-routes";
function makeClient(): ClientInfo {
const sent: Uint8Array[] = [];
return {
id: "bulk-ping-client",
role: "client",
lastSeen: Date.now(),
ws: {
send(data: Uint8Array) {
sent.push(data);
},
_sent: sent,
},
} as any;
}
describe("client command ping diagnostics", () => {
test("dispatchPingBulk sends the requested number of pings", () => {
const client = makeClient();
const sent = (client.ws as any)._sent as Uint8Array[];
const count = dispatchPingBulk(client, 3);
expect(count).toBe(3);
expect(sent.length).toBe(3);
for (const raw of sent) {
const payload = decodeMessage(raw) as any;
expect(payload.type).toBe("ping");
expect(typeof payload.ts).toBe("number");
}
expect(typeof client.lastPingNonce).toBe("number");
});
test("dispatchPingBulk clamps invalid and large counts", () => {
const invalid = makeClient();
expect(dispatchPingBulk(invalid, "bad")).toBe(1);
expect((invalid.ws as any)._sent.length).toBe(1);
const large = makeClient();
expect(dispatchPingBulk(large, 5000)).toBe(1000);
expect((large.ws as any)._sent.length).toBe(1000);
});
});
@@ -7,6 +7,8 @@ import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
import { clearThumbnail } from "../../thumbnails";
import type { ClientInfo } from "../../types";
import { sendPingRequest } from "../../wsHandlers";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -32,6 +34,20 @@ type ClientCommandDeps = {
pendingCommandReplies: Map<string, PendingCommandReply>;
};
function clampPositiveInt(value: unknown, fallback: number, max: number): number {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
return Math.min(max, Math.max(1, Math.floor(parsed)));
}
export function dispatchPingBulk(target: ClientInfo, countValue: unknown): number {
const count = clampPositiveInt(countValue, 1, 1000);
for (let i = 0; i < count; i++) {
sendPingRequest(target, target.ws, "manual-bulk", 0);
}
return count;
}
export async function handleClientCommandRoute(
req: Request,
url: URL,
@@ -71,12 +87,13 @@ export async function handleClientCommandRoute(
let success = true;
if (action === "ping") {
const nonce = Date.now() + Math.floor(Math.random() * 1000);
target.lastPingSent = Date.now();
target.lastPingNonce = nonce;
target.ws.send(encodeMessage({ type: "ping", ts: nonce }));
sendPingRequest(target, target.ws, "manual", 0);
metrics.recordCommand("ping");
} else if (action === "ping_bulk") {
Math.max(1, Math.min(1000, Number(body?.count || 1)));
const count = dispatchPingBulk(target, body?.count);
metrics.recordCommand("ping_bulk");
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, details: `ping_bulk:${count}`, success: true });
return Response.json({ ok: true, sent: count }, { headers: deps.CORS_HEADERS });
} else if (action === "disconnect") {
try {
requirePermission(user, "clients:disconnect");
+63
View File
@@ -30,6 +30,25 @@ describe("wsHandlers ping/pong", () => {
expect(ws.sent.length).toBe(1);
});
test("sendPingRequest can bypass the interval for manual pings", () => {
const ws: MockWs = {
sent: [],
send(msg) {
this.sent.push(msg);
},
};
const info = {
id: "client-manual-ping",
role: "client",
ws,
lastSeen: Date.now(),
} as any;
expect(sendPingRequest(info, ws, "test")).toBe(true);
expect(sendPingRequest(info, ws, "manual", 0)).toBe(true);
expect(ws.sent.length).toBe(2);
});
test("handlePing responds with pong without starting another server ping", () => {
const ws: MockWs = {
sent: [],
@@ -53,6 +72,50 @@ describe("wsHandlers ping/pong", () => {
expect(info.lastPingNonce).toBeUndefined();
});
test("handlePing preserves zero timestamps", () => {
const ws: MockWs = {
sent: [],
send(msg) {
this.sent.push(msg);
},
};
const info = {
id: "client-zero-ping",
role: "client",
ws,
lastSeen: Date.now(),
} as any;
handlePing(info, { type: "ping", ts: 0 } as any, ws);
const payload = decodeMessage(ws.sent[0]) as any;
expect(payload.type).toBe("pong");
expect(payload.ts).toBe(0);
});
test("handlePing falls back for invalid timestamps", () => {
const ws: MockWs = {
sent: [],
send(msg) {
this.sent.push(msg);
},
};
const info = {
id: "client-invalid-ping",
role: "client",
ws,
lastSeen: Date.now(),
} as any;
const before = Date.now();
handlePing(info, { type: "ping", ts: "bad" } as any, ws);
const payload = decodeMessage(ws.sent[0]) as any;
expect(payload.type).toBe("pong");
expect(payload.ts).toBeGreaterThanOrEqual(before);
expect(payload.ts).toBeLessThanOrEqual(Date.now());
});
test("handlePong clears nonce and records ping", () => {
metrics.reset();
const ws: MockWs = {
+13 -4
View File
@@ -171,19 +171,28 @@ export function handlePing(info: ClientInfo, payload: Ping, ws: any) {
isAdmin: info.isAdmin,
});
}
ws.send(encodeMessage({ type: "pong", ts: payload.ts || Date.now() }));
const ts = typeof payload.ts === "number" && Number.isFinite(payload.ts)
? payload.ts
: Date.now();
ws.send(encodeMessage({ type: "pong", ts }));
}
export function sendPingRequest(info: ClientInfo, ws: any, reason: string) {
export function sendPingRequest(
info: ClientInfo,
ws: any,
reason: string,
minIntervalMs = 1_000,
): boolean {
const now = Date.now();
if (info.lastPingSent && now - info.lastPingSent < 1_000) {
return;
if (minIntervalMs > 0 && info.lastPingSent && now - info.lastPingSent < minIntervalMs) {
return false;
}
const nonce = now + Math.floor(Math.random() * 1000);
info.lastPingSent = now;
info.lastPingNonce = nonce;
//console.log(`[ping] send ping to client=${info.id} reason=${reason} nonce=${nonce}`);
ws.send(encodeMessage({ type: "ping", ts: nonce }));
return true;
}
export function handlePong(info: ClientInfo, payload: WireMessage) {