import { ApiError, apiErrorCode } from '@/api/client'; /** * Цвет роли сервера приходит целым числом (0 — цвет не задан). * Возвращаем CSS-цвет или `null`, чтобы отрисовать нейтральную заглушку. */ export function roleColorHex(color: number): string | null { if (!Number.isFinite(color) || color <= 0) { return null; } const rgb = Math.trunc(color) & 0xffffff; return `#${rgb.toString(16).padStart(6, '0')}`; } export function formatBytes(bytes: number, locale = 'ru'): string { if (!Number.isFinite(bytes) || bytes < 0) { return '—'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } const formatted = new Intl.NumberFormat(locale, { maximumFractionDigits: value < 10 && unit > 0 ? 1 : 0, }).format(value); return `${formatted} ${units[unit]}`; } // instanceof is unreliable across module instances (aliases, lazy chunks), // so the error shape is checked structurally as well. function hasErrorCode(value: unknown): value is { code: string } { return ( typeof value === 'object' && value !== null && typeof (value as { code?: unknown }).code === 'string' ); } export function errorCode(error: unknown): string { // The code from the server error envelope wins: it survives serialization // boundaries where instanceof checks do not. const envelopeCode = apiErrorCode(error); if (envelopeCode !== null) { return envelopeCode; } if (error instanceof ApiError) { return error.code; } if (hasErrorCode(error)) { return error.code; } if (error instanceof Error && error.name === 'AbortError') { return 'network.aborted'; } return 'network.unreachable'; }