change around pings, fix main.js so the actual real fucking number shows instead of it freaking out.
This commit is contained in:
@@ -76,6 +76,78 @@ func TestHandlePing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlePingPreservesZeroTimestamp(t *testing.T) {
|
||||||
|
writer := &testWriter{}
|
||||||
|
env := &rt.Env{
|
||||||
|
Conn: writer,
|
||||||
|
Cfg: config.Config{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
envelope := map[string]interface{}{
|
||||||
|
"ts": int64(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := HandlePing(ctx, env, envelope); err != nil {
|
||||||
|
t.Fatalf("HandlePing failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.After(2 * time.Second)
|
||||||
|
for len(writer.msgs) < 1 {
|
||||||
|
select {
|
||||||
|
case <-deadline:
|
||||||
|
t.Fatal("Timed out waiting for pong message")
|
||||||
|
default:
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pong wire.Pong
|
||||||
|
if err := msgpack.Unmarshal(writer.msgs[0], &pong); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal pong: %v", err)
|
||||||
|
}
|
||||||
|
if pong.TS != 0 {
|
||||||
|
t.Fatalf("expected timestamp 0, got %d", pong.TS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlePingFallsBackForInvalidTimestamp(t *testing.T) {
|
||||||
|
writer := &testWriter{}
|
||||||
|
env := &rt.Env{
|
||||||
|
Conn: writer,
|
||||||
|
Cfg: config.Config{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
before := time.Now().UnixMilli()
|
||||||
|
envelope := map[string]interface{}{
|
||||||
|
"ts": "not-a-timestamp",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := HandlePing(ctx, env, envelope); err != nil {
|
||||||
|
t.Fatalf("HandlePing failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.After(2 * time.Second)
|
||||||
|
for len(writer.msgs) < 1 {
|
||||||
|
select {
|
||||||
|
case <-deadline:
|
||||||
|
t.Fatal("Timed out waiting for pong message")
|
||||||
|
default:
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pong wire.Pong
|
||||||
|
if err := msgpack.Unmarshal(writer.msgs[0], &pong); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal pong: %v", err)
|
||||||
|
}
|
||||||
|
after := time.Now().UnixMilli()
|
||||||
|
if pong.TS < before || pong.TS > after {
|
||||||
|
t.Fatalf("expected fallback timestamp between %d and %d, got %d", before, after, pong.TS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleCommand_Ping(t *testing.T) {
|
func TestHandleCommand_Ping(t *testing.T) {
|
||||||
writer := &testWriter{}
|
writer := &testWriter{}
|
||||||
env := &rt.Env{
|
env := &rt.Env{
|
||||||
|
|||||||
@@ -3,36 +3,44 @@ package handlers
|
|||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
|
|
||||||
func extractTimestamp(v interface{}) int64 {
|
func extractTimestamp(v interface{}) int64 {
|
||||||
|
ts, ok := extractTimestampIfPresent(v)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTimestampIfPresent(v interface{}) (int64, bool) {
|
||||||
switch t := v.(type) {
|
switch t := v.(type) {
|
||||||
case uint64:
|
case uint64:
|
||||||
if t <= uint64(^uint64(0)>>1) {
|
if t <= uint64(^uint64(0)>>1) {
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
}
|
}
|
||||||
return 0
|
return 0, false
|
||||||
case uint32:
|
case uint32:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case uint16:
|
case uint16:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case uint8:
|
case uint8:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case int64:
|
case int64:
|
||||||
return t
|
return t, true
|
||||||
case int32:
|
case int32:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case int16:
|
case int16:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case int8:
|
case int8:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case int:
|
case int:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case float64:
|
case float64:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case float32:
|
case float32:
|
||||||
return int64(t)
|
return int64(t), true
|
||||||
case json.Number:
|
case json.Number:
|
||||||
if n, err := t.Int64(); err == nil {
|
if n, err := t.Int64(); err == nil {
|
||||||
return n
|
return n, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,22 @@ func TestExtractTimestamp_Zero(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestampIfPresent_ZeroIsValid(t *testing.T) {
|
||||||
|
got, ok := extractTimestampIfPresent(int64(0))
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected zero timestamp to be valid")
|
||||||
|
}
|
||||||
|
if got != 0 {
|
||||||
|
t.Fatalf("expected 0, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestampIfPresent_InvalidIsNotValid(t *testing.T) {
|
||||||
|
if got, ok := extractTimestampIfPresent("hello"); ok || got != 0 {
|
||||||
|
t.Fatalf("expected invalid timestamp to return ok=false and 0, got ok=%v value=%d", ok, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractTimestamp_Negative(t *testing.T) {
|
func TestExtractTimestamp_Negative(t *testing.T) {
|
||||||
var ts int64 = -100
|
var ts int64 = -100
|
||||||
if got := extractTimestamp(ts); got != ts {
|
if got := extractTimestamp(ts); got != ts {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
func HandlePing(ctx context.Context, env *runtime.Env, envelope map[string]interface{}) error {
|
func HandlePing(ctx context.Context, env *runtime.Env, envelope map[string]interface{}) error {
|
||||||
|
|
||||||
ts := extractTimestamp(envelope["ts"])
|
ts, ok := extractTimestampIfPresent(envelope["ts"])
|
||||||
if ts == 0 {
|
if !ok {
|
||||||
ts = time.Now().UnixMilli()
|
ts = time.Now().UnixMilli()
|
||||||
}
|
}
|
||||||
env.SetLastPong(time.Now().UnixMilli())
|
env.SetLastPong(time.Now().UnixMilli())
|
||||||
|
|||||||
@@ -1119,7 +1119,9 @@ function initializeRenderer() {
|
|||||||
rendererSetLayout = rSetLayout;
|
rendererSetLayout = rSetLayout;
|
||||||
registerRenderer((data, options) => {
|
registerRenderer((data, options) => {
|
||||||
renderMerge(data, options);
|
renderMerge(data, options);
|
||||||
updateDashboardStatsFromClients(data);
|
if (isUnfilteredClientView()) {
|
||||||
|
updateDashboardStatsFromClients(data);
|
||||||
|
}
|
||||||
if (!options?.fromPluginDashboard) {
|
if (!options?.fromPluginDashboard) {
|
||||||
refreshDashboardPluginContributions(data?.items || []);
|
refreshDashboardPluginContributions(data?.items || []);
|
||||||
}
|
}
|
||||||
@@ -1151,6 +1153,15 @@ function initializeRenderer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUnfilteredClientView() {
|
||||||
|
return !state.searchTerm &&
|
||||||
|
(state.filterStatus || "all") === "all" &&
|
||||||
|
(state.filterOs || "all") === "all" &&
|
||||||
|
(state.filterCountry || "all") === "all" &&
|
||||||
|
(state.filterGroup || "all") === "all" &&
|
||||||
|
(state.filterWebcam || "all") === "all";
|
||||||
|
}
|
||||||
|
|
||||||
if (logoutBtn && !logoutBtn.dataset.boundLogout) {
|
if (logoutBtn && !logoutBtn.dataset.boundLogout) {
|
||||||
logoutBtn.dataset.boundLogout = "true";
|
logoutBtn.dataset.boundLogout = "true";
|
||||||
logoutBtn.addEventListener("click", async () => {
|
logoutBtn.addEventListener("click", async () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { encodeMessage } from "./protocol";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { metrics } from "./metrics";
|
import { metrics } from "./metrics";
|
||||||
import { logAudit, AuditAction } from "./auditLog";
|
import { logAudit, AuditAction } from "./auditLog";
|
||||||
|
import { sendPingRequest } from "./wsHandlers";
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 12;
|
const DEFAULT_PAGE_SIZE = 12;
|
||||||
const CORS_HEADERS = {
|
const CORS_HEADERS = {
|
||||||
@@ -61,10 +62,7 @@ export function handleCommand(target: ClientInfo, action: string, req: Request)
|
|||||||
metrics.recordCommand(action);
|
metrics.recordCommand(action);
|
||||||
|
|
||||||
if (action === "ping") {
|
if (action === "ping") {
|
||||||
const nonce = Date.now() + Math.floor(Math.random() * 1000);
|
sendPingRequest(target, target.ws, "manual", 0);
|
||||||
target.lastPingSent = Date.now();
|
|
||||||
target.lastPingNonce = nonce;
|
|
||||||
target.ws.send(encodeMessage({ type: "ping", ts: nonce }));
|
|
||||||
return OK();
|
return OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { getHeartbeatBatchSize } from "./maintenance-loops";
|
import { getHeartbeatBatchSize, shouldSendHeartbeatPing } from "./maintenance-loops";
|
||||||
|
|
||||||
describe("maintenance heartbeat batching", () => {
|
describe("maintenance heartbeat batching", () => {
|
||||||
test("spreads large fleets across the heartbeat interval", () => {
|
test("spreads large fleets across the heartbeat interval", () => {
|
||||||
@@ -13,4 +13,11 @@ describe("maintenance heartbeat batching", () => {
|
|||||||
test("handles empty fleets", () => {
|
test("handles empty fleets", () => {
|
||||||
expect(getHeartbeatBatchSize(0, 30_000)).toBe(0);
|
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));
|
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 {
|
export function startMaintenanceLoops(params: StartMaintenanceParams): void {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
@@ -82,8 +92,11 @@ export function startMaintenanceLoops(params: StartMaintenanceParams): void {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!shouldSendHeartbeatPing(info, now, params.heartbeatIntervalMs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
sendPingRequest(info, info.ws, "heartbeat");
|
sendPingRequest(info, info.ws, "heartbeat", params.heartbeatIntervalMs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(`[ping] heartbeat failed for ${id}`, 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 { encodeMessage } from "../../protocol";
|
||||||
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
|
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
|
||||||
import { clearThumbnail } from "../../thumbnails";
|
import { clearThumbnail } from "../../thumbnails";
|
||||||
|
import type { ClientInfo } from "../../types";
|
||||||
|
import { sendPingRequest } from "../../wsHandlers";
|
||||||
|
|
||||||
type RequestIpProvider = {
|
type RequestIpProvider = {
|
||||||
requestIP: (req: Request) => { address?: string } | null | undefined;
|
requestIP: (req: Request) => { address?: string } | null | undefined;
|
||||||
@@ -32,6 +34,20 @@ type ClientCommandDeps = {
|
|||||||
pendingCommandReplies: Map<string, PendingCommandReply>;
|
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(
|
export async function handleClientCommandRoute(
|
||||||
req: Request,
|
req: Request,
|
||||||
url: URL,
|
url: URL,
|
||||||
@@ -71,12 +87,13 @@ export async function handleClientCommandRoute(
|
|||||||
let success = true;
|
let success = true;
|
||||||
|
|
||||||
if (action === "ping") {
|
if (action === "ping") {
|
||||||
const nonce = Date.now() + Math.floor(Math.random() * 1000);
|
sendPingRequest(target, target.ws, "manual", 0);
|
||||||
target.lastPingSent = Date.now();
|
metrics.recordCommand("ping");
|
||||||
target.lastPingNonce = nonce;
|
|
||||||
target.ws.send(encodeMessage({ type: "ping", ts: nonce }));
|
|
||||||
} else if (action === "ping_bulk") {
|
} 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") {
|
} else if (action === "disconnect") {
|
||||||
try {
|
try {
|
||||||
requirePermission(user, "clients:disconnect");
|
requirePermission(user, "clients:disconnect");
|
||||||
|
|||||||
@@ -30,6 +30,25 @@ describe("wsHandlers ping/pong", () => {
|
|||||||
expect(ws.sent.length).toBe(1);
|
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", () => {
|
test("handlePing responds with pong without starting another server ping", () => {
|
||||||
const ws: MockWs = {
|
const ws: MockWs = {
|
||||||
sent: [],
|
sent: [],
|
||||||
@@ -53,6 +72,50 @@ describe("wsHandlers ping/pong", () => {
|
|||||||
expect(info.lastPingNonce).toBeUndefined();
|
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", () => {
|
test("handlePong clears nonce and records ping", () => {
|
||||||
metrics.reset();
|
metrics.reset();
|
||||||
const ws: MockWs = {
|
const ws: MockWs = {
|
||||||
|
|||||||
@@ -171,19 +171,28 @@ export function handlePing(info: ClientInfo, payload: Ping, ws: any) {
|
|||||||
isAdmin: info.isAdmin,
|
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();
|
const now = Date.now();
|
||||||
if (info.lastPingSent && now - info.lastPingSent < 1_000) {
|
if (minIntervalMs > 0 && info.lastPingSent && now - info.lastPingSent < minIntervalMs) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
const nonce = now + Math.floor(Math.random() * 1000);
|
const nonce = now + Math.floor(Math.random() * 1000);
|
||||||
info.lastPingSent = now;
|
info.lastPingSent = now;
|
||||||
info.lastPingNonce = nonce;
|
info.lastPingNonce = nonce;
|
||||||
//console.log(`[ping] send ping to client=${info.id} reason=${reason} nonce=${nonce}`);
|
//console.log(`[ping] send ping to client=${info.id} reason=${reason} nonce=${nonce}`);
|
||||||
ws.send(encodeMessage({ type: "ping", ts: nonce }));
|
ws.send(encodeMessage({ type: "ping", ts: nonce }));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handlePong(info: ClientInfo, payload: WireMessage) {
|
export function handlePong(info: ClientInfo, payload: WireMessage) {
|
||||||
|
|||||||
Reference in New Issue
Block a user