containers update + docker file build
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s

This commit is contained in:
Gal Podlipnik 2025-06-09 02:29:55 +02:00
parent 4a2a9eba30
commit dac64ac41a
6 changed files with 354 additions and 198 deletions

View File

@ -4,7 +4,8 @@
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts", "dev": "tsx watch src/index.ts",
"build": "tsc", "build": "tsc",
"start": "node dist/index.js" "start": "node dist/index.js",
"build": "tsc"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.14.3", "@hono/node-server": "^1.14.3",

View File

@ -1,5 +0,0 @@
import Docker from 'dockerode';
export function createDockerClient() {
return new Docker();
}

View File

@ -1,5 +1,5 @@
import { createDockerClient } from './client.js'; import createDockerClient from "../utils/client.js";
import type { DockerContainer, Networks } from './types.js'; import type { DockerContainer, Networks } from "./types.js";
const docker = createDockerClient(); const docker = createDockerClient();
@ -21,8 +21,8 @@ export async function listContainers(): Promise<DockerContainer[]> {
})), })),
})); }));
} catch (error) { } catch (error) {
console.error('Error listing containers:', error); console.error("Error listing containers:", error);
throw new Error('Failed to list Docker containers'); throw new Error("Failed to list Docker containers");
} }
} }
@ -35,10 +35,13 @@ export async function getContainerStats(containerId: string) {
const currSys = stats.cpu_stats.system_cpu_usage; const currSys = stats.cpu_stats.system_cpu_usage;
const cpuDelta = currCpu - prevCpu; const cpuDelta = currCpu - prevCpu;
const sysDelta = currSys - prevSys; const sysDelta = currSys - prevSys;
const numCpus =
stats.cpu_stats.online_cpus ||
(stats.cpu_stats.cpu_usage.percpu_usage
? stats.cpu_stats.cpu_usage.percpu_usage.length
: 1);
const cpuPercent = const cpuPercent =
sysDelta > 0 sysDelta > 0 ? (cpuDelta / sysDelta) * numCpus * 100.0 : 0.0;
? (cpuDelta / sysDelta) * stats.cpu_stats.cpu_usage.percpu_usage.length * 100.0
: 0.0;
const memUsage = stats.memory_stats.usage; const memUsage = stats.memory_stats.usage;
const memLimit = stats.memory_stats.limit; const memLimit = stats.memory_stats.limit;
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0; const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
@ -58,8 +61,8 @@ export async function getContainerStats(containerId: string) {
Array.isArray(stats.blkio_stats.io_service_bytes_recursive) Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
) { ) {
for (const entry of stats.blkio_stats.io_service_bytes_recursive) { for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
if (entry.op === 'Read') blkRead += entry.value; if (entry.op === "Read") blkRead += entry.value;
if (entry.op === 'Write') blkWrite += entry.value; if (entry.op === "Write") blkWrite += entry.value;
} }
} }
return { return {
@ -82,19 +85,26 @@ export async function streamContainerStats(
): Promise<() => void> { ): Promise<() => void> {
const container = docker.getContainer(containerId); const container = docker.getContainer(containerId);
try { try {
const stream = (await container.stats({ stream: true })) as import('stream').Readable; const stream = (await container.stats({
stream.on('data', (chunk: Buffer) => { stream: true,
})) as import("stream").Readable;
stream.on("data", (chunk: Buffer) => {
const stats = JSON.parse(chunk.toString()); const stats = JSON.parse(chunk.toString());
const prevCpu = stats.precpu_stats.cpu_usage.total_usage; const prevCpu = stats.precpu_stats.cpu_usage.total_usage;
const prevSys = stats.precpu_stats.system_cpu_usage; const prevSys = stats.precpu_stats.system_cpu_usage;
const currCpu = stats.cpu_stats.cpu_usage.total_usage; const currCpu = stats.cpu_stats.cpu_usage.total_usage;
const currSys = stats.cpu_stats.system_cpu_usage; const currSys = stats.cpu_stats.system_cpu_usage;
const cpuDelta = currCpu - prevCpu; const cpuDelta = currCpu - prevCpu;
const sysDelta = currSys - prevSys; const sysDelta = currSys - prevSys;
const numCpus =
stats.cpu_stats.online_cpus ||
(stats.cpu_stats.cpu_usage.percpu_usage
? stats.cpu_stats.cpu_usage.percpu_usage.length
: 1);
const cpuPercent = const cpuPercent =
sysDelta > 0 sysDelta > 0 ? (cpuDelta / sysDelta) * numCpus * 100.0 : 0.0;
? (cpuDelta / sysDelta) * stats.cpu_stats.cpu_usage.percpu_usage.length * 100.0
: 0.0;
const memUsage = stats.memory_stats.usage; const memUsage = stats.memory_stats.usage;
const memLimit = stats.memory_stats.limit; const memLimit = stats.memory_stats.limit;
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0; const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
@ -114,8 +124,8 @@ export async function streamContainerStats(
Array.isArray(stats.blkio_stats.io_service_bytes_recursive) Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
) { ) {
for (const entry of stats.blkio_stats.io_service_bytes_recursive) { for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
if (entry.op === 'Read') blkRead += entry.value; if (entry.op === "Read") blkRead += entry.value;
if (entry.op === 'Write') blkWrite += entry.value; if (entry.op === "Write") blkWrite += entry.value;
} }
} }
callback({ callback({
@ -131,14 +141,17 @@ export async function streamContainerStats(
timestamp: Date.now(), timestamp: Date.now(),
}); });
}); });
stream.on('error', (err: Error) => { stream.on("error", (err: Error) => {
console.error(`Error streaming stats for container ${containerId}:`, err); console.error(`Error streaming stats for container ${containerId}:`, err);
}); });
return () => { return () => {
stream.destroy(); stream.destroy();
}; };
} catch (error) { } catch (error) {
console.error(`Error getting stats stream for container ${containerId}:`, error); console.error(
`Error getting stats stream for container ${containerId}:`,
error
);
throw new Error(`Failed to get stats stream for container ${containerId}`); throw new Error(`Failed to get stats stream for container ${containerId}`);
} }
} }

View File

@ -1,112 +1,133 @@
import { serve } from '@hono/node-server' import { serve } from "@hono/node-server";
import { Hono } from 'hono' import { serveStatic } from "@hono/node-server/serve-static";
import { createNodeWebSocket } from '@hono/node-ws' import { createNodeWebSocket } from "@hono/node-ws";
import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { import {
registerContainerClient,
registerStatsClient,
registerCombinedClient, registerCombinedClient,
registerContainerClient,
registerSingleContainerClient, registerSingleContainerClient,
removeContainerClient, registerStatsClient,
removeStatsClient,
removeCombinedClient, removeCombinedClient,
removeContainerClient,
removeSingleContainerClient, removeSingleContainerClient,
stopAllStreams removeStatsClient,
} from './ws/manager.js'; stopAllStreams,
} from "./ws/manager.js";
const app = new Hono(); const app = new Hono();
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }); const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
app.get('/', (c) => c.text('WS Docker Stats server (ESM) is running.')) app.use("/*", serveStatic({ root: "./public" }));
app.get("/", (c) => c.text("WS Docker Stats server (ESM) is running."));
// Containers-only endpoint app.get(
app.get('/ws/containers', upgradeWebSocket(c => ({ "/ws/containers",
onOpen(_, ws) { upgradeWebSocket((c) => ({
console.log('WebSocket containers connection opened');
registerContainerClient(ws);
},
onClose(_, ws) {
console.log('❌ WS containers client disconnected');
removeContainerClient(ws);
},
onError(err) {
console.error('❌ WS containers error:', err);
}
})))
// Stats-only endpoint
app.get('/ws/stats', upgradeWebSocket(c => ({
onOpen(_, ws) {
console.log('WebSocket stats connection opened');
registerStatsClient(ws);
},
onClose(_, ws) {
console.log('❌ WS stats client disconnected');
removeStatsClient(ws);
},
onError(err) {
console.error('❌ WS stats error:', err);
}
})))
// Combined endpoint (explicit)
app.get('/ws/combined', upgradeWebSocket(c => ({
onOpen(_, ws) {
console.log('WebSocket combined connection opened');
registerCombinedClient(ws);
},
onClose(_, ws) {
console.log('❌ WS combined client disconnected');
removeCombinedClient(ws);
},
onError(err) {
console.error('❌ WS combined error:', err);
}
})))
// Single container endpoint
app.get('/ws/container', upgradeWebSocket(c => {
const containerId = c.req.query('id');
if (!containerId) {
throw new Error('Container ID is required');
}
return {
onOpen(_, ws) { onOpen(_, ws) {
console.log(`WebSocket connection opened for container ${containerId}`); console.log("WebSocket containers connection opened");
registerSingleContainerClient(ws, containerId); registerContainerClient(ws);
}, },
onClose(_, ws) { onClose(_, ws) {
console.log(`❌ WS client for container ${containerId} disconnected`); console.log("❌ WS containers client disconnected");
removeSingleContainerClient(ws, containerId); removeContainerClient(ws);
}, },
onError(err) { onError(err) {
console.error(`❌ WS error for container ${containerId}:`, err); console.error("❌ WS containers error:", err);
} },
}; }))
})) );
const server = serve({ // Stats-only endpoint
fetch: app.fetch, app.get(
port: 3000 "/ws/stats",
}, (info) => { upgradeWebSocket((c) => ({
console.log(`Server is running on http://localhost:${info.port}`) onOpen(_, ws) {
}) console.log("WebSocket stats connection opened");
registerStatsClient(ws);
},
onClose(_, ws) {
console.log("❌ WS stats client disconnected");
removeStatsClient(ws);
},
onError(err) {
console.error("❌ WS stats error:", err);
},
}))
);
// Combined endpoint (explicit)
app.get(
"/ws/combined",
upgradeWebSocket((c) => ({
onOpen(_, ws) {
console.log("WebSocket combined connection opened");
registerCombinedClient(ws);
},
onClose(_, ws) {
console.log("❌ WS combined client disconnected");
removeCombinedClient(ws);
},
onError(err) {
console.error("❌ WS combined error:", err);
},
}))
);
// Single container endpoint
app.get(
"/ws/container",
upgradeWebSocket((c) => {
const containerId = c.req.query("id");
if (!containerId) {
throw new Error("Container ID is required");
}
return {
onOpen(_, ws) {
console.log(`WebSocket connection opened for container ${containerId}`);
registerSingleContainerClient(ws, containerId);
},
onClose(_, ws) {
console.log(`❌ WS client for container ${containerId} disconnected`);
removeSingleContainerClient(ws, containerId);
},
onError(err) {
console.error(`❌ WS error for container ${containerId}:`, err);
},
};
})
);
app.get("*", (c) => {
return c.html(readFileSync("./public/index.html", "utf-8"));
});
const server = serve(
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(`Server is running on http://localhost:${info.port}`);
}
);
injectWebSocket(server); injectWebSocket(server);
process.on('SIGTERM', () => cleanupAndExit()); process.on("SIGTERM", () => cleanupAndExit());
process.on('SIGINT', () => cleanupAndExit()); process.on("SIGINT", () => cleanupAndExit());
function cleanupAndExit() { function cleanupAndExit() {
console.log('Shutting down gracefully...'); console.log("Shutting down gracefully...");
stopAllStreams(); stopAllStreams();
server.close(() => { server.close(() => {
console.log('Server closed'); console.log("Server closed");
process.exit(0); process.exit(0);
}); });
setTimeout(() => { setTimeout(() => {
console.log('Forcing exit after timeout'); console.log("Forcing exit after timeout");
process.exit(1); process.exit(1);
}, 5000); }, 5000);
} }

View File

@ -1,9 +1,13 @@
import type { WSContext } from 'hono/ws'; import type { WSContext } from "hono/ws";
import { listContainers, streamContainerStats } from '../docker/containers.js'; import { listContainers, streamContainerStats } from "../docker/containers.js";
import { sendContainerInfo, sendStats, sendCombined, sendToSingleContainer } from './sender.js'; import type { DockerContainer } from "../docker/types.js";
import type { ContainerInfo, ContainerStats } from './types.js'; import {
import type { DockerContainer } from '../docker/types.js'; sendCombined,
sendContainerInfo,
sendStats,
sendToSingleContainer,
} from "./sender.js";
import type { ContainerInfo, ContainerStats } from "./types.js";
const containerClients = new Set<WSContext<WebSocket>>(); const containerClients = new Set<WSContext<WebSocket>>();
const statsClients = new Set<WSContext<WebSocket>>(); const statsClients = new Set<WSContext<WebSocket>>();
@ -11,18 +15,29 @@ const combinedClients = new Set<WSContext<WebSocket>>();
const singleContainerClients = new Map<string, Set<WSContext<WebSocket>>>(); const singleContainerClients = new Map<string, Set<WSContext<WebSocket>>>();
const activeStreams = new Map<string, () => void>(); const activeStreams = new Map<string, () => void>();
const containerCache = new Map<
const containerCache = new Map<string, { string,
data: DockerContainer | null; {
lastUpdated: number; data: DockerContainer | null;
}>(); lastUpdated: number;
}
>();
const CACHE_TTL = 2000; const CACHE_TTL = 2000;
// Add a map to collect all container info
const allContainerInfo = new Map<string, ContainerInfo>();
let containerUpdateTimer: NodeJS.Timeout | null = null;
export function registerContainerClient(ws: WSContext<WebSocket>) { export function registerContainerClient(ws: WSContext<WebSocket>) {
const wasEmpty = allClientsEmpty(); const wasEmpty = allClientsEmpty();
containerClients.add(ws); containerClients.add(ws);
if (wasEmpty) setupStreams(); if (wasEmpty) setupStreams();
// Send current container data immediately if available
if (allContainerInfo.size > 0) {
const containers = Array.from(allContainerInfo.values());
sendContainerInfo(new Set([ws]), containers);
}
} }
export function registerStatsClient(ws: WSContext<WebSocket>) { export function registerStatsClient(ws: WSContext<WebSocket>) {
@ -37,7 +52,10 @@ export function registerCombinedClient(ws: WSContext<WebSocket>) {
if (wasEmpty) setupStreams(); if (wasEmpty) setupStreams();
} }
export function registerSingleContainerClient(ws: WSContext<WebSocket>, containerId: string) { export function registerSingleContainerClient(
ws: WSContext<WebSocket>,
containerId: string
) {
if (!singleContainerClients.has(containerId)) { if (!singleContainerClients.has(containerId)) {
singleContainerClients.set(containerId, new Set()); singleContainerClients.set(containerId, new Set());
} }
@ -48,7 +66,6 @@ export function registerSingleContainerClient(ws: WSContext<WebSocket>, containe
} }
} }
export function removeContainerClient(ws: WSContext<WebSocket>) { export function removeContainerClient(ws: WSContext<WebSocket>) {
containerClients.delete(ws); containerClients.delete(ws);
checkAndCleanup(); checkAndCleanup();
@ -64,7 +81,10 @@ export function removeCombinedClient(ws: WSContext<WebSocket>) {
checkAndCleanup(); checkAndCleanup();
} }
export function removeSingleContainerClient(ws: WSContext<WebSocket>, containerId: string) { export function removeSingleContainerClient(
ws: WSContext<WebSocket>,
containerId: string
) {
const clients = singleContainerClients.get(containerId); const clients = singleContainerClients.get(containerId);
if (clients) { if (clients) {
clients.delete(ws); clients.delete(ws);
@ -76,10 +96,12 @@ export function removeSingleContainerClient(ws: WSContext<WebSocket>, containerI
} }
function allClientsEmpty() { function allClientsEmpty() {
return containerClients.size === 0 && return (
containerClients.size === 0 &&
statsClients.size === 0 && statsClients.size === 0 &&
combinedClients.size === 0 && combinedClients.size === 0 &&
singleContainerClients.size === 0; singleContainerClients.size === 0
);
} }
function checkAndCleanup() { function checkAndCleanup() {
@ -94,37 +116,105 @@ async function setupStreams() {
const containers = await listContainers(); const containers = await listContainers();
for (const container of containers) { for (const container of containers) {
const containerInfo: ContainerInfo = {
id: container.id,
name:
container.names?.[0]?.replace("/", "") ?? container.id.slice(0, 12),
image: container.image,
state: container.state,
status: container.status,
};
allContainerInfo.set(container.id, containerInfo);
containerCache.set(container.id, { containerCache.set(container.id, {
data: container, data: container,
lastUpdated: Date.now() lastUpdated: Date.now(),
}); });
} }
// Send all containers to clients
if (containerClients.size > 0) {
sendContainerInfo(
containerClients,
Array.from(allContainerInfo.values())
);
}
// Setup periodic container list updates
if (!containerUpdateTimer) {
containerUpdateTimer = setInterval(updateAllContainers, 5000);
}
for (const container of containers) { for (const container of containers) {
if (activeStreams.has(container.id)) continue; if (activeStreams.has(container.id)) continue;
setupStreamForContainer(container.id); setupStreamForContainer(container.id);
} }
} catch (error) { } catch (error) {
console.error('Error setting up streams:', error); console.error("Error setting up streams:", error);
} }
} }
async function getContainerData(containerId: string): Promise<DockerContainer | null> { async function updateAllContainers() {
if (allClientsEmpty()) {
if (containerUpdateTimer) {
clearInterval(containerUpdateTimer);
containerUpdateTimer = null;
}
return;
}
try {
const containers = await listContainers();
// Update container cache and allContainerInfo
const now = Date.now();
for (const container of containers) {
containerCache.set(container.id, {
data: container,
lastUpdated: now,
});
const containerInfo: ContainerInfo = {
id: container.id,
name:
container.names?.[0]?.replace("/", "") ?? container.id.slice(0, 12),
image: container.image,
state: container.state,
status: container.status,
};
allContainerInfo.set(container.id, containerInfo);
}
// Send all containers to clients
if (containerClients.size > 0) {
sendContainerInfo(
containerClients,
Array.from(allContainerInfo.values())
);
}
} catch (error) {
console.error("Error updating containers:", error);
}
}
async function getContainerData(
containerId: string
): Promise<DockerContainer | null> {
const cached = containerCache.get(containerId); const cached = containerCache.get(containerId);
const now = Date.now(); const now = Date.now();
if (cached && (now - cached.lastUpdated < CACHE_TTL)) { if (cached && now - cached.lastUpdated < CACHE_TTL) {
return cached.data; return cached.data;
} }
try { try {
const containers = await listContainers(); const containers = await listContainers();
const containerData = containers.find(c => c.id === containerId) || null; const containerData = containers.find((c) => c.id === containerId) || null;
for (const container of containers) { for (const container of containers) {
containerCache.set(container.id, { containerCache.set(container.id, {
data: container, data: container,
lastUpdated: now lastUpdated: now,
}); });
} }
@ -146,53 +236,70 @@ async function setupStreamForContainer(containerId: string) {
return; return;
} }
const stopStream = await streamContainerStats(containerId, async (stats: ContainerStats) => { const stopStream = await streamContainerStats(
if (allClientsEmpty()) { containerId,
stopAllStreams(); async (stats: ContainerStats) => {
return; if (allClientsEmpty()) {
} stopAllStreams();
return;
}
const freshContainerData = await getContainerData(containerId);
const freshContainerData = await getContainerData(containerId); const containerInfo: ContainerInfo = freshContainerData
? {
id: freshContainerData.id,
name:
freshContainerData.names?.[0]?.replace("/", "") ??
freshContainerData.id.slice(0, 12),
image: freshContainerData.image,
state: freshContainerData.state,
status: freshContainerData.status,
}
: {
id: containerData.id,
name:
containerData.names?.[0]?.replace("/", "") ??
containerData.id.slice(0, 12),
image: containerData.image,
state: "exited",
status: "Container stopped",
};
const containerInfo: ContainerInfo = freshContainerData ? { // Update the containerInfo in our map
id: freshContainerData.id, allContainerInfo.set(containerId, containerInfo);
name: freshContainerData.names?.[0]?.replace('/', '') ?? freshContainerData.id.slice(0, 12),
image: freshContainerData.image,
state: freshContainerData.state,
status: freshContainerData.status,
} : {
id: containerData.id,
name: containerData.names?.[0]?.replace('/', '') ?? containerData.id.slice(0, 12),
image: containerData.image,
state: 'exited',
status: 'Container stopped',
};
if (!freshContainerData && !singleContainerClients.has(containerId)) {
stopStreamForContainer(containerId);
return;
}
if (!freshContainerData && !singleContainerClients.has(containerId)) { // We don't send containerInfo here anymore, it's sent by updateAllContainers
stopStreamForContainer(containerId);
return;
}
if (containerClients.size > 0) { if (statsClients.size > 0) {
sendContainerInfo(containerClients, containerInfo); sendStats(statsClients, stats);
}
if (combinedClients.size > 0) {
sendCombined(combinedClients, containerInfo, stats);
}
const singleClients = singleContainerClients.get(containerId);
if (singleClients && singleClients.size > 0) {
sendToSingleContainer(
singleContainerClients,
containerId,
containerInfo,
stats
);
}
} }
if (statsClients.size > 0) { );
sendStats(statsClients, stats);
}
if (combinedClients.size > 0) {
sendCombined(combinedClients, containerInfo, stats);
}
const singleClients = singleContainerClients.get(containerId);
if (singleClients && singleClients.size > 0) {
sendToSingleContainer(singleContainerClients, containerId, containerInfo, stats);
}
});
activeStreams.set(containerId, stopStream); activeStreams.set(containerId, stopStream);
} catch (error) { } catch (error) {
console.error(`Failed to setup stream for container ${containerId}:`, error); console.error(
`Failed to setup stream for container ${containerId}:`,
error
);
containerCache.delete(containerId); containerCache.delete(containerId);
} }
@ -205,6 +312,7 @@ function stopStreamForContainer(containerId: string) {
activeStreams.delete(containerId); activeStreams.delete(containerId);
containerCache.delete(containerId); containerCache.delete(containerId);
allContainerInfo.delete(containerId);
} }
} }
@ -214,4 +322,10 @@ export function stopAllStreams() {
} }
activeStreams.clear(); activeStreams.clear();
containerCache.clear(); containerCache.clear();
allContainerInfo.clear();
if (containerUpdateTimer) {
clearInterval(containerUpdateTimer);
containerUpdateTimer = null;
}
} }

View File

@ -1,12 +1,21 @@
import type { WSContext } from 'hono/ws'; import type { WSContext } from "hono/ws";
import type { ContainerInfo, ContainerStats } from './types.js'; import type { ContainerInfo, ContainerStats } from "./types.js";
export function sendContainerInfo(containerClients: Set<WSContext<WebSocket>>, containerInfo: ContainerInfo) { export function sendContainerInfo(
const payload = JSON.stringify({ containers: [containerInfo] }); containerClients: Set<WSContext<WebSocket>>,
containerInfo: ContainerInfo | ContainerInfo[]
) {
const containers = Array.isArray(containerInfo)
? containerInfo
: [containerInfo];
const payload = JSON.stringify({ containers });
sendToClientSet(containerClients, payload); sendToClientSet(containerClients, payload);
} }
export function sendStats(statsClients: Set<WSContext<WebSocket>>, stats: ContainerStats) { export function sendStats(
statsClients: Set<WSContext<WebSocket>>,
stats: ContainerStats
) {
const payload = JSON.stringify({ stats: [stats] }); const payload = JSON.stringify({ stats: [stats] });
sendToClientSet(statsClients, payload); sendToClientSet(statsClients, payload);
} }
@ -18,7 +27,7 @@ export function sendCombined(
) { ) {
const payload = JSON.stringify({ const payload = JSON.stringify({
containers: [containerInfo], containers: [containerInfo],
stats: [stats] stats: [stats],
}); });
sendToClientSet(combinedClients, payload); sendToClientSet(combinedClients, payload);
} }
@ -31,7 +40,7 @@ export function sendToSingleContainer(
) { ) {
const combinedData = { const combinedData = {
...containerInfo, ...containerInfo,
...stats ...stats,
}; };
const payload = JSON.stringify({ container: combinedData }); const payload = JSON.stringify({ container: combinedData });
const clients = singleContainerClients.get(containerId); const clients = singleContainerClients.get(containerId);
@ -40,13 +49,16 @@ export function sendToSingleContainer(
} }
} }
export function sendToClientSet(clients: Set<WSContext<WebSocket>>, payload: string) { export function sendToClientSet(
clients: Set<WSContext<WebSocket>>,
payload: string
) {
for (const ws of clients) { for (const ws of clients) {
if (ws.readyState === 1) { if (ws.readyState === 1) {
try { try {
ws.send(payload); ws.send(payload);
} catch (err) { } catch (err) {
console.error('Error sending to client:', err); console.error("Error sending to client:", err);
clients.delete(ws); clients.delete(ws);
} }
} else { } else {