cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

main
goq / portal / network-diagnostics.mjs
network-diagnostics.mjs 13.5K
1import {2 normalizeSessionDiagnostics,3 sessionDiagnosticsPresentation,4} from './session-diagnostics.mjs';56const NETWORK_DIAGNOSTICS_VERSION = 2;7const PATH_MODES = new Set(['direct', 'relay', 'custom', 'unknown']);89function record(value, label) {10 if (!value || typeof value !== 'object' || Array.isArray(value)) {11 throw new TypeError(`${label} must be an object`);12 }13 return value;14}1516function exactUnsigned(value, label) {17 if (!Number.isSafeInteger(value) || value < 0) {18 throw new TypeError(`${label} must be an exact unsigned integer`);19 }20 return value;21}2223function optionalExactUnsigned(value, label) {24 return value === null ? null : exactUnsigned(value, label);25}2627function boolean(value, label) {28 if (typeof value !== 'boolean') throw new TypeError(`${label} must be a boolean`);29 return value;30}3132function durationMs(value, label) {33 if (!Number.isFinite(value) || value < 0) {34 throw new TypeError(`${label} must be a finite non-negative duration in milliseconds`);35 }36 return value;37}3839function percentileSummary(value, prefix, sampleCount, overflowTotal) {40 const p50 = value[`${prefix}_p50_ms`];41 const p95 = value[`${prefix}_p95_ms`];42 const max = value[`${prefix}_max_ms`];43 if (sampleCount === 0) {44 if (overflowTotal !== 0 || p50 !== null || p95 !== null || max !== null) {45 throw new TypeError(`${prefix} percentiles require at least one retained sample`);46 }47 return { sampleCount, p50Ms: null, p95Ms: null, maxMs: null, overflowTotal };48 }4950 if (overflowTotal > sampleCount) {51 throw new RangeError(`${prefix}_overflow_total cannot exceed its sample count`);52 }53 const retainedSamples = sampleCount - overflowTotal;54 const p50Available = retainedSamples >= Math.ceil(sampleCount * 0.5);55 const p95Available = retainedSamples >= Math.ceil(sampleCount * 0.95);56 if ((p50 !== null) !== p50Available || (p95 !== null) !== p95Available) {57 throw new TypeError(`${prefix} percentile availability does not match bounded samples`);58 }59 const p50Ms = p50 === null ? null : durationMs(p50, `${prefix}_p50_ms`);60 const p95Ms = p95 === null ? null : durationMs(p95, `${prefix}_p95_ms`);61 const maxMs = durationMs(max, `${prefix}_max_ms`);62 if ((p50Ms !== null && p50Ms > maxMs)63 || (p95Ms !== null && p95Ms > maxMs)64 || (p50Ms !== null && p95Ms !== null && p50Ms > p95Ms)) {65 throw new RangeError(`${prefix} percentiles must be monotonic`);66 }67 return { sampleCount, p50Ms, p95Ms, maxMs, overflowTotal };68}6970function normalizeLeg(value, label) {71 const leg = record(value, label);72 if (!PATH_MODES.has(leg.mode)) throw new TypeError(`${label}.mode is unsupported`);7374 const sampleCount = exactUnsigned(leg.sample_count, `${label}.sample_count`);75 const directSamples = exactUnsigned(leg.direct_samples, `${label}.direct_samples`);76 const relaySamples = exactUnsigned(leg.relay_samples, `${label}.relay_samples`);77 const customSamples = exactUnsigned(leg.custom_samples, `${label}.custom_samples`);78 const unknownSamples = exactUnsigned(leg.unknown_samples, `${label}.unknown_samples`);79 const classifiedSamples = directSamples + relaySamples + customSamples + unknownSamples;80 if (!Number.isSafeInteger(classifiedSamples) || classifiedSamples !== sampleCount) {81 throw new RangeError(`${label} path sample buckets must exactly match sample_count`);82 }8384 const rttSampleCount = exactUnsigned(leg.rtt_sample_count, `${label}.rtt_sample_count`);85 const rttOverflowTotal = exactUnsigned(leg.rtt_overflow_total, `${label}.rtt_overflow_total`);86 const rtt = percentileSummary(leg, 'rtt', rttSampleCount, rttOverflowTotal);87 const rttCurrentMs = leg.rtt_current_ms === null88 ? null89 : durationMs(leg.rtt_current_ms, `${label}.rtt_current_ms`);90 if ((rttSampleCount === 0 || leg.mode === 'unknown') && rttCurrentMs !== null) {91 throw new RangeError(`${label}.rtt_current_ms requires a selected measured path`);92 }9394 const counterRegressionTotal = exactUnsigned(95 leg.counter_regression_total,96 `${label}.counter_regression_total`,97 );98 const complete = boolean(leg.complete, `${label}.complete`);99 if (complete !== (counterRegressionTotal === 0 && rttOverflowTotal === 0)) {100 throw new RangeError(`${label}.complete is inconsistent with bounded telemetry state`);101 }102 if (rttSampleCount > sampleCount) {103 throw new RangeError(`${label}.rtt_sample_count cannot exceed sample_count`);104 }105106 return {107 mode: leg.mode,108 sampleCount,109 pathSamples: {110 direct: directSamples,111 relay: relaySamples,112 custom: customSamples,113 unknown: unknownSamples,114 },115 pathEpoch: exactUnsigned(leg.path_epoch, `${label}.path_epoch`),116 pathSwitchTotal: exactUnsigned(leg.path_switch_total, `${label}.path_switch_total`),117 modeTransitionTotal: exactUnsigned(118 leg.mode_transition_total,119 `${label}.mode_transition_total`,120 ),121 rttCurrentMs,122 rtt,123 txDatagrams: exactUnsigned(leg.tx_datagrams, `${label}.tx_datagrams`),124 txBytes: exactUnsigned(leg.tx_bytes, `${label}.tx_bytes`),125 rxDatagrams: exactUnsigned(leg.rx_datagrams, `${label}.rx_datagrams`),126 rxBytes: exactUnsigned(leg.rx_bytes, `${label}.rx_bytes`),127 lostPackets: exactUnsigned(leg.lost_packets, `${label}.lost_packets`),128 lostBytes: exactUnsigned(leg.lost_bytes, `${label}.lost_bytes`),129 cwndBytes: optionalExactUnsigned(leg.cwnd_bytes, `${label}.cwnd_bytes`),130 mtu: optionalExactUnsigned(leg.mtu, `${label}.mtu`),131 congestionEvents: optionalExactUnsigned(leg.congestion_events, `${label}.congestion_events`),132 blackHolesDetected: optionalExactUnsigned(133 leg.black_holes_detected,134 `${label}.black_holes_detected`,135 ),136 counterRegressionTotal,137 complete,138 };139}140141function normalizeInputAck(value) {142 const ack = record(value, 'network_diagnostics.input_ack');143 const sentTotal = exactUnsigned(ack.sent_total, 'network_diagnostics.input_ack.sent_total');144 const acknowledgedTotal = exactUnsigned(145 ack.acknowledged_total,146 'network_diagnostics.input_ack.acknowledged_total',147 );148 const pendingCount = exactUnsigned(149 ack.pending_count,150 'network_diagnostics.input_ack.pending_count',151 );152 const pendingCapacity = exactUnsigned(153 ack.pending_capacity,154 'network_diagnostics.input_ack.pending_capacity',155 );156 if (acknowledgedTotal > sentTotal) {157 throw new RangeError('acknowledged_total cannot exceed sent_total');158 }159 if (pendingCount > pendingCapacity) {160 throw new RangeError('pending_count cannot exceed pending_capacity');161 }162163 const latencySampleCount = exactUnsigned(164 ack.latency_sample_count,165 'network_diagnostics.input_ack.latency_sample_count',166 );167 const latencyOverflowTotal = exactUnsigned(168 ack.latency_overflow_total,169 'network_diagnostics.input_ack.latency_overflow_total',170 );171172 const malformed = boolean(ack.malformed, 'network_diagnostics.input_ack.malformed');173 const closed = boolean(ack.closed, 'network_diagnostics.input_ack.closed');174 const untrackedTotal = exactUnsigned(175 ack.untracked_total,176 'network_diagnostics.input_ack.untracked_total',177 );178 const complete = boolean(ack.complete, 'network_diagnostics.input_ack.complete');179 if (complete !== (!malformed && !closed && untrackedTotal === 0 && latencyOverflowTotal === 0)) {180 throw new RangeError(181 'network_diagnostics.input_ack.complete is inconsistent with bounded telemetry state',182 );183 }184185 return {186 negotiated: boolean(ack.negotiated, 'network_diagnostics.input_ack.negotiated'),187 sentTotal,188 acknowledgedTotal,189 pendingCount,190 pendingCapacity,191 duplicateTotal: exactUnsigned(192 ack.duplicate_total,193 'network_diagnostics.input_ack.duplicate_total',194 ),195 untrackedTotal,196 malformed,197 closed,198 latency: percentileSummary(ack, 'latency', latencySampleCount, latencyOverflowTotal),199 complete,200 };201}202203function normalizeAdaptive(value) {204 const adaptive = record(value, 'network_diagnostics.adaptive');205 const localPressure = new Set(['awaiting_report', 'clean', 'pressured', 'recovering']);206 const aggregateState = new Set(['unavailable', 'hold', 'decrease', 'increase']);207 const recoveryState = new Set(['active', 'floor_limited', 'detached']);208 if (adaptive.scope !== 'local_viewer') {209 throw new TypeError('network_diagnostics.adaptive.scope must be local_viewer');210 }211 if (!localPressure.has(adaptive.local_pressure)) {212 throw new TypeError('network_diagnostics.adaptive.local_pressure is unsupported');213 }214 if (!aggregateState.has(adaptive.aggregate_state)) {215 throw new TypeError('network_diagnostics.adaptive.aggregate_state is unsupported');216 }217 if (!recoveryState.has(adaptive.recovery_state)) {218 throw new TypeError('network_diagnostics.adaptive.recovery_state is unsupported');219 }220 const aggregateTargetKbps = adaptive.aggregate_target_kbps === null221 ? null222 : exactUnsigned(223 adaptive.aggregate_target_kbps,224 'network_diagnostics.adaptive.aggregate_target_kbps',225 );226 return {227 scope: 'local-viewer',228 localPressure: adaptive.local_pressure.replace('_', '-'),229 aggregateTargetKbps,230 aggregateState: adaptive.aggregate_state,231 recoveryState: adaptive.recovery_state.replace('_', '-'),232 applied: boolean(adaptive.applied, 'network_diagnostics.adaptive.applied'),233 };234}235236/**237 * Strictly validates the versioned network snapshot and returns a presentation-safe238 * allowlist. Peer identities, addresses, and unknown future fields never cross this boundary.239 */240export function normalizeNetworkDiagnostics(value) {241 const diagnostics = record(value, 'network_diagnostics');242 if (diagnostics.version !== NETWORK_DIAGNOSTICS_VERSION) {243 throw new TypeError(`unsupported network_diagnostics version: ${diagnostics.version}`);244 }245246 return {247 version: NETWORK_DIAGNOSTICS_VERSION,248 sessionElapsedMs: exactUnsigned(249 diagnostics.session_elapsed_ms,250 'network_diagnostics.session_elapsed_ms',251 ),252 media: normalizeLeg(diagnostics.media, 'network_diagnostics.media'),253 input: normalizeLeg(diagnostics.input, 'network_diagnostics.input'),254 audio: diagnostics.audio === null255 ? null256 : normalizeLeg(diagnostics.audio, 'network_diagnostics.audio'),257 inputAck: normalizeInputAck(diagnostics.input_ack),258 adaptive: normalizeAdaptive(diagnostics.adaptive),259 session: normalizeSessionDiagnostics(diagnostics.session),260 };261}262263function formatBytes(value) {264 if (value < 1024) return `${value} B`;265 if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB`;266 if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MiB`;267 return `${(value / (1024 * 1024 * 1024)).toFixed(1)} GiB`;268}269270function formatPercentiles(summary) {271 if (summary.sampleCount === 0) return `— · 0 samples · ${summary.overflowTotal} overflow`;272 const p50 = summary.p50Ms === null ? '—' : summary.p50Ms.toFixed(1);273 const p95 = summary.p95Ms === null ? '—' : summary.p95Ms.toFixed(1);274 return `${p50} / ${p95} / ${summary.maxMs.toFixed(1)} ms · ${summary.sampleCount} samples · ${summary.overflowTotal} overflow`;275}276277function formatOptionalBytes(value) {278 return value === null ? '—' : formatBytes(value);279}280281function formatOptionalInteger(value, suffix = '') {282 return value === null ? '—' : `${value}${suffix}`;283}284285export function formatNetworkLeg(leg) {286 if (leg === null) return 'unavailable';287 const paths = leg.pathSamples;288 const currentRtt = leg.rttCurrentMs === null ? '—' : `${leg.rttCurrentMs.toFixed(1)} ms`;289 return `${leg.mode} · RTT current ${currentRtt} · history ${formatPercentiles(leg.rtt)} · paths d/r/c/u ${paths.direct}/${paths.relay}/${paths.custom}/${paths.unknown} · epoch ${leg.pathEpoch} · switches ${leg.pathSwitchTotal} · transitions ${leg.modeTransitionTotal} · tx ${leg.txDatagrams} datagrams / ${formatBytes(leg.txBytes)} · rx ${leg.rxDatagrams} datagrams / ${formatBytes(leg.rxBytes)} · lost ${leg.lostPackets} packets / ${formatBytes(leg.lostBytes)} · cwnd ${formatOptionalBytes(leg.cwndBytes)} · MTU ${formatOptionalInteger(leg.mtu, ' B')} · congestion ${formatOptionalInteger(leg.congestionEvents)} · black holes ${formatOptionalInteger(leg.blackHolesDetected)} · regressions ${leg.counterRegressionTotal} · ${leg.complete ? 'complete' : 'partial'}`;290}291292export function formatInputAck(ack) {293 const negotiation = ack.negotiated ? 'negotiated' : 'not negotiated';294 const health = ack.malformed ? 'malformed' : ack.closed ? 'closed' : 'open';295 return `${negotiation} · ${ack.acknowledgedTotal}/${ack.sentTotal} acknowledged · ${ack.pendingCount}/${ack.pendingCapacity} pending · duplicates ${ack.duplicateTotal} · untracked ${ack.untrackedTotal} · latency ${formatPercentiles(ack.latency)} · ${health} · ${ack.complete ? 'complete' : 'partial'}`;296}297298export function networkDiagnosticsPresentation(diagnostics) {299 if (diagnostics === null) {300 return {301 session: 'unavailable',302 media: 'unavailable',303 input: 'unavailable',304 audio: 'unavailable',305 inputAck: 'unavailable',306 adaptive: 'unavailable',307 };308 }309 return {310 session: `v${diagnostics.version} · ${(diagnostics.sessionElapsedMs / 1000).toFixed(1)} s · ${sessionDiagnosticsPresentation(diagnostics.session)}`,311 media: formatNetworkLeg(diagnostics.media),312 input: formatNetworkLeg(diagnostics.input),313 audio: formatNetworkLeg(diagnostics.audio),314 inputAck: formatInputAck(diagnostics.inputAck),315 adaptive: `${diagnostics.adaptive.scope} · local ${diagnostics.adaptive.localPressure} · aggregate ${diagnostics.adaptive.aggregateTargetKbps === null ? 'awaiting decision' : `${diagnostics.adaptive.aggregateTargetKbps} kbps ${diagnostics.adaptive.aggregateState}`} · recovery ${diagnostics.adaptive.recoveryState} · ${diagnostics.adaptive.applied ? 'applied' : 'advisory'}`,316 };317}