Skip to content

Commit ecb1aee

Browse files
committed
fix(terminal): medir la latencia con un ping
El indicador emparejaba cada tecla con el siguiente mensaje, que suele ser el eco de la anterior: con el servidor a 193 ms marcaba 60. Ahora se manda un ping numerado cada dos segundos y se muestra el mínimo de los últimos cinco, que el ruido del navegador solo suma tiempo.
1 parent b613e6f commit ecb1aee

3 files changed

Lines changed: 56 additions & 24 deletions

File tree

‎docs/arquitectura.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ xterm una sola vez y `useTerminalSocket` lleva el WebSocket.
7373
servidores y personas por día.
7474
- **Buscar en lo que ya pasó.** `Ctrl+Shift+F` abre la búsqueda sobre el
7575
historial de esa shell, con las coincidencias resaltadas.
76-
- **La cabecera dice cuánto tarda la red.** Junto a «Conectado», la ida y
77-
vuelta de la última tecla, en tramos de 10 ms.
76+
- **La cabecera dice cuánto tarda la red.** Un ping cada dos segundos, y se
77+
muestra el mínimo de los últimos cinco: el ruido del navegador solo suma.
78+
Medir el eco de las teclas daba números falsos, porque el que llega suele
79+
ser el de la tecla anterior.
7880
- **Arrastrar un archivo lo sube donde está parado.** El destino sale del
7981
`cd` de la shell, leído de lo que anuncia (OSC 7) o de su prompt; si no se
8082
sabe, la carpeta de trabajo de la credencial. Viaja en tramos por el mismo
@@ -99,6 +101,8 @@ xterm una sola vez y `useTerminalSocket` lleva el WebSocket.
99101
disimular; Enter tampoco la borra, que hacía parpadear la línea entera al
100102
enviarla. En vim o htop no adivina, y si la shell deja de repetir lo
101103
tecleado —pide una contraseña— borra lo adelantado y deja de adivinar.
104+
Al borrar con retroceso tampoco se adelanta nada: el servidor hará eco de
105+
la letra y mandará su propio borrado.
102106

103107
## Cabeceras de seguridad
104108

‎src/features/terminal/socket.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ export type MensajeDeCarpetas = {
3737
error?: string
3838
}
3939

40+
/** Respuesta al ping: con su número se mide la ida y vuelta de verdad. */
41+
export type MensajeDePong = { type: 'pong'; ping: number }
42+
4043
/** La shell vive en el servidor: su id permite volver a ella tras un corte. */
4144
export type MensajeEntrante =
4245
| { type: 'output' | 'error'; message: string }
4346
| { type: 'sesion'; id: string }
4447
| MensajeDeSubida
4548
| MensajeDeCarpetas
49+
| MensajeDePong
4650

4751
/** Token en la query: el handshake del navegador no admite cabeceras.
4852
* Dura 30 minutos y produccion exige wss. Con `sesion`, retoma la shell
@@ -158,6 +162,7 @@ export function parseIncoming(raw: string): MensajeEntrante | null {
158162
id?: string
159163
estado?: string
160164
error?: string
165+
ping?: number
161166
ruta?: string
162167
inicio?: string
163168
padre?: string
@@ -167,6 +172,9 @@ export function parseIncoming(raw: string): MensajeEntrante | null {
167172
if (dato.type === 'sesion') {
168173
return typeof dato.id === 'string' ? { type: 'sesion', id: dato.id } : null
169174
}
175+
if (dato.type === 'pong') {
176+
return typeof dato.ping === 'number' ? { type: 'pong', ping: dato.ping } : null
177+
}
170178
if (dato.type === 'carpetas') {
171179
return {
172180
type: 'carpetas',

‎src/features/terminal/use-terminal-socket.ts‎

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ import type { Server, ServerUser } from '@/types/api'
3131
/** Por encima, la letra tarda en aparecer y conviene adelantarla. */
3232
const LATENCIA_PARA_ADELANTAR_MS = 90
3333

34-
/** Media que sigue a la red sin saltar con una medida suelta. */
35-
const PESO_DE_LA_ULTIMA = 0.25
34+
/** Se mira el mínimo de estas: el ruido del navegador solo suma tiempo. */
35+
const MEDIDAS_A_RECORDAR = 5
3636

3737
/** Se avisa por tramos: al ojo no le dice nada un milisegundo arriba. */
3838
const TRAMO_DE_LATENCIA_MS = 10
3939

40-
/** Más tarde no es el eco de la tecla: falsearía la media. */
41-
const LATENCIA_CREIBLE_MS = 1500
40+
/** Cada cuánto se mide la ida y vuelta con el servidor. */
41+
const PING_CADA_MS = 2000
4242

4343
/** Sin eco, la shell no repite lo tecleado: pide una contraseña. */
4444
const SIN_ECO_MS = 600
@@ -66,8 +66,9 @@ export function useTerminalSocket({
6666
const reconexiones = useRef(0)
6767
const [intento, setIntento] = useState(0)
6868
const eco = useRef(crearEcoPredictivo())
69-
const tecleadoEn = useRef<number | null>(null)
7069
const esperaDeEco = useRef<number | undefined>(undefined)
70+
const pings = useRef(new Map<number, number>())
71+
const medidas = useRef<number[]>([])
7172
const latencia = useRef(0)
7273
const avisada = useRef(0)
7374
const [subida, setSubida] = useState<AvanceDeSubida | null>(null)
@@ -88,6 +89,26 @@ export function useTerminalSocket({
8889
}
8990
}, [])
9091

92+
/** Lo mejor de las últimas medidas; decide también si conviene adelantar. */
93+
const anotarLatencia = (numero: number) => {
94+
const salida = pings.current.get(numero)
95+
if (salida === undefined) return
96+
pings.current.delete(numero)
97+
98+
medidas.current = [...medidas.current, performance.now() - salida].slice(
99+
-MEDIDAS_A_RECORDAR,
100+
)
101+
latencia.current = Math.min(...medidas.current)
102+
eco.current.activar(latencia.current > LATENCIA_PARA_ADELANTAR_MS)
103+
104+
const tramo =
105+
Math.round(latencia.current / TRAMO_DE_LATENCIA_MS) * TRAMO_DE_LATENCIA_MS
106+
if (tramo !== avisada.current) {
107+
avisada.current = tramo
108+
avisar.current(tramo)
109+
}
110+
}
111+
91112
const enviarTamano = useCallback(() => {
92113
const vista = terminal.current
93114
if (vista) enviar({ resize: { cols: vista.cols, rows: vista.rows } })
@@ -112,7 +133,6 @@ export function useTerminalSocket({
112133
}, SIN_ECO_MS)
113134
}
114135
}
115-
if (tecleadoEn.current === null) tecleadoEn.current = performance.now()
116136
enviar({ command: datos })
117137
},
118138
[enviar, terminal],
@@ -234,6 +254,7 @@ export function useTerminalSocket({
234254
useEffect(() => {
235255
let vigente = true
236256
let reintento: number | undefined
257+
let reloj: number | undefined
237258
let inicialPendiente = buildInitialCommand(rutaInicial)
238259

239260
const escribir = (texto: string) => terminal.current?.write(texto)
@@ -293,12 +314,22 @@ export function useTerminalSocket({
293314
)
294315
socket.current = abierto
295316

317+
let numeroDePing = 0
318+
const medirLatencia = () => {
319+
if (abierto.readyState !== WebSocket.OPEN) return
320+
numeroDePing += 1
321+
pings.current.set(numeroDePing, performance.now())
322+
abierto.send(JSON.stringify({ ping: numeroDePing }))
323+
}
324+
reloj = window.setInterval(medirLatencia, PING_CADA_MS)
325+
296326
abierto.onopen = () => {
297327
if (reconexiones.current > 0) escribir('\r\n\x1b[2m— Reconectado —\x1b[0m\r\n')
298328
reconexiones.current = 0
299329
onEstado({ fase: 'conectada' })
300330
terminal.current?.focus()
301331
enviarTamano()
332+
medirLatencia()
302333
}
303334

304335
abierto.onmessage = (evento) => {
@@ -327,22 +358,11 @@ export function useTerminalSocket({
327358
window.clearTimeout(esperaDeEco.current)
328359
esperaDeEco.current = undefined
329360

330-
if (tecleadoEn.current !== null) {
331-
const ida = performance.now() - tecleadoEn.current
332-
tecleadoEn.current = null
333-
if (ida <= LATENCIA_CREIBLE_MS) {
334-
latencia.current =
335-
latencia.current * (1 - PESO_DE_LA_ULTIMA) + ida * PESO_DE_LA_ULTIMA
336-
eco.current.activar(latencia.current > LATENCIA_PARA_ADELANTAR_MS)
337-
338-
const tramo =
339-
Math.round(latencia.current / TRAMO_DE_LATENCIA_MS) * TRAMO_DE_LATENCIA_MS
340-
if (tramo !== avisada.current) {
341-
avisada.current = tramo
342-
avisar.current(tramo)
343-
}
344-
}
361+
if (mensaje.type === 'pong') {
362+
anotarLatencia(mensaje.ping)
363+
return
345364
}
365+
346366
escribir(eco.current.reconciliar(mensaje.message))
347367

348368
// El cd espera a la primera salida (el prompt): en onopen la shell
@@ -355,7 +375,6 @@ export function useTerminalSocket({
355375

356376
abierto.onclose = (evento) => {
357377
escribir(eco.current.limpiar())
358-
tecleadoEn.current = null
359378
soltarTurnos()
360379
const contestar = respuesta.current
361380
respuesta.current = null
@@ -384,6 +403,7 @@ export function useTerminalSocket({
384403
document.removeEventListener('visibilitychange', reintentarYa)
385404
window.clearTimeout(apertura)
386405
window.clearTimeout(reintento)
406+
window.clearInterval(reloj)
387407
window.clearTimeout(esperaDeEco.current)
388408

389409
const abierto = socket.current

0 commit comments

Comments
 (0)