Back to flin
flin

Soporte WebSocket integrado en el lenguaje

Cómo FLIN proporciona soporte nativo de WebSocket a través de bloques de ruta ws: comunicación en tiempo real sin Socket.IO, sin ws, sin un servidor separado.

Thales & Claude | March 30, 2026 7 min flin
EN/ FR/ ES
flinrust

La comunicación en tiempo real en la web siempre ha sido una ocurrencia tardía. Construyes tu aplicación HTTP, luego te das cuenta de que necesitas actualizaciones en vivo, así que acoplas Socket.IO (2,4 MB de dependencias), configuras un servidor WebSocket separado, gestionas el estado de conexión en Redis y escribes manejadores de eventos que duplican la lógica de tus rutas HTTP. La complejidad se duplica, la superficie de despliegue se duplica y la superficie de errores se duplica.

FLIN trata los WebSockets como una característica de primera clase del runtime del lenguaje. El mismo servidor HTTP embebido que maneja solicitudes regulares también maneja las actualizaciones WebSocket. Defines endpoints WebSocket con bloques de ruta ws, y coexisten con bloques route regulares en el mismo archivo. Sin dependencias adicionales. Sin servidor separado. Sin configuración.

El bloque de ruta ws

Los endpoints WebSocket se definen con la palabra clave ws en lugar de route:

flin// app/api/chat.flin

guard auth

ws {
    on connect {
        log_info("User connected: {session.user}")
        broadcast("system", "{session.userName} joined the chat")
    }

    on message(data) {
        save ChatMessage {
            user: session.user,
            content: data.text,
            room: data.room || "general"
        }

        broadcast(data.room || "general", {
            user: session.userName,
            text: data.text,
            time: now()
        })
    }

    on close {
        log_info("User disconnected: {session.user}")
        broadcast("system", "{session.userName} left the chat")
    }
}

Tres manejadores de eventos: connect, message y close. El parámetro data en el manejador message se analiza automáticamente desde JSON. La función broadcast envía un mensaje a todos los clientes conectados en una sala. Los guards se aplican a las conexiones WebSocket igual que a las rutas HTTP -- el guard auth asegura que solo usuarios autenticados puedan abrir una conexión WebSocket.

WebSocket + HTTP en un solo archivo

El verdadero poder emerge cuando las rutas WebSocket y HTTP coexisten:

flin// app/api/notifications.flin

guard auth

// Endpoint REST para crear una notificación
route POST {
    validate {
        user_id: int @required
        message: text @required
    }

    notification = Notification {
        user_id: body.user_id,
        message: body.message,
        read: false
    }
    save notification

    // Enviar a clientes WebSocket conectados
    ws_send(to_text(body.user_id), {
        type: "notification",
        data: notification
    })

    response { status: 201, body: notification }
}

// Endpoint REST para obtener notificaciones
route GET {
    Notification.where(user_id == to_int(session.userId) && read == false)
        .order(created_at, "desc")
        .limit(50)
}

// WebSocket para notificaciones en tiempo real
ws {
    on connect {
        ws_join(session.userId)  // Unirse a sala por ID de usuario
    }

    on message(data) {
        if data.type == "mark_read" {
            notification = Notification.find(data.id)
            if notification != none && notification.user_id == to_int(session.userId) {
                notification.read = true
                save notification
            }
        }
    }
}

Un solo archivo maneja la creación de notificaciones (POST), la obtención de notificaciones no leídas (GET), la transmisión de nuevas notificaciones en tiempo real (WebSocket) y el marcado de notificaciones como leídas (mensaje WebSocket). El enfoque tradicional distribuiría esto en al menos tres archivos y dos servidores.

Gestión de conexiones

El runtime de FLIN gestiona las conexiones WebSocket internamente. Cada conexión se identifica por un ID único y puede asociarse con salas (grupos con nombre):

flin// Gestión de salas
ws_join("room_name")          // La conexión actual se une a una sala
ws_leave("room_name")         // La conexión actual abandona una sala

// Envío de mensajes
ws_send(connection_id, data)  // Enviar a una conexión específica
broadcast(room, data)         // Enviar a todas las conexiones en una sala
broadcast_all(data)           // Enviar a TODOS los clientes conectados

El estado de conexión se almacena en la memoria del runtime:

rustpub struct WebSocketManager {
    connections: HashMap<String, WebSocketConnection>,
    rooms: HashMap<String, HashSet<String>>,
}

pub struct WebSocketConnection {
    id: String,
    sender: mpsc::Sender<Message>,
    session: Session,
    rooms: HashSet<String>,
    connected_at: Instant,
}

Cuando un cliente se desconecta (de forma controlada o por fallo de red), el runtime elimina automáticamente la conexión de todas las salas y activa el manejador on close. No hay código de limpieza que el desarrollador deba escribir.

El proceso de actualización WebSocket

Las conexiones WebSocket comienzan como solicitudes HTTP con una cabecera Upgrade: websocket. El servidor FLIN maneja esta actualización de forma transparente:

rustasync fn handle_request(
    stream: TcpStream,
    request: &Request,
    router: &Router,
) -> Result<(), ServerError> {
    // Verificar actualización WebSocket
    if request.is_websocket_upgrade() {
        let handler = router.find_ws_handler(&request.path)?;

        // Evaluar guards antes de la actualización
        evaluate_guards(&handler.guards, &request.context())?;

        // Realizar handshake WebSocket
        let ws_stream = accept_websocket(stream, request).await?;

        // Lanzar manejador de conexión
        tokio::spawn(handle_websocket(ws_stream, handler, request.session.clone()));

        return Ok(());
    }

    // Manejo HTTP regular
    handle_http(stream, request, router).await
}

Los guards se evalúan ANTES de que se complete el handshake WebSocket. Un cliente no autenticado recibe una respuesta HTTP 401 Unauthorized y la conexión nunca se actualiza. Este es un detalle de seguridad crítico -- muchas bibliotecas WebSocket realizan la actualización primero y verifican la autenticación después, creando una ventana donde clientes no autenticados tienen una conexión abierta.

Datos binarios y streaming

Los WebSockets de FLIN manejan tanto mensajes de texto como binarios:

flin// app/api/stream.flin

ws {
    on message(data) {
        if data.type == "binary" {
            // data.bytes contiene los datos binarios crudos
            file_path = save_binary(data.bytes, ".flindb/streams/")
            ws_send(ws.connection_id, {
                type: "ack",
                path: file_path,
                size: data.bytes.len
            })
        } else {
            // Mensaje de texto (por defecto)
            process_text(data)
        }
    }
}

Heartbeat y salud de la conexión

El runtime envía frames ping periódicos para detectar conexiones muertas. Si un cliente no responde con un pong en 30 segundos, la conexión se cierra y se dispara el manejador on close:

rustconst HEARTBEAT_INTERVAL: Duration = Duration::from_secs(15);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(30);

async fn heartbeat_loop(ws: &mut WebSocketStream, last_pong: &mut Instant) {
    let mut interval = tokio::time::interval(HEARTBEAT_INTERVAL);

    loop {
        interval.tick().await;

        if last_pong.elapsed() > CLIENT_TIMEOUT {
            // El cliente está muerto
            ws.close().await;
            return;
        }

        ws.send(Message::Ping(vec![])).await;
    }
}

Este mecanismo de heartbeat es invisible para los desarrolladores FLIN. Las conexiones muertas se limpian automáticamente, las salas se actualizan y el manejador on close se ejecuta con la misma garantía que si el cliente se hubiera desconectado de forma controlada.

Ejemplo del mundo real: edición colaborativa

Aquí hay un ejemplo más completo que muestra edición colaborativa de documentos con WebSockets:

flin// app/api/documents/[id]/live.flin

guard auth

ws {
    on connect {
        doc = Document.find(params.id)
        if doc == none {
            ws_close("Document not found")
            return
        }

        ws_join("doc:" + params.id)
        broadcast("doc:" + params.id, {
            type: "user_joined",
            user: session.userName
        })

        // Enviar estado actual del documento a la nueva conexión
        ws_send(ws.connection_id, {
            type: "init",
            content: doc.content,
            version: doc.version
        })
    }

    on message(data) {
        if data.type == "edit" {
            doc = Document.find(params.id)
            doc.content = apply_patch(doc.content, data.patch)
            save doc

            broadcast("doc:" + params.id, {
                type: "edit",
                patch: data.patch,
                user: session.userName,
                version: doc.version
            })
        }

        if data.type == "cursor" {
            broadcast("doc:" + params.id, {
                type: "cursor",
                user: session.userName,
                position: data.position
            })
        }
    }

    on close {
        broadcast("doc:" + params.id, {
            type: "user_left",
            user: session.userName
        })
    }
}

Estado del documento, presencia de usuarios, posiciones del cursor y parches de edición -- todo en un solo archivo, todo usando el mismo sistema de entidades, guards y gestión de sesiones que el resto de la aplicación.

Por qué importan los WebSockets integrados

El enfoque tradicional para agregar WebSockets a una aplicación web implica elegir una biblioteca (ws, Socket.IO, uWebSockets), configurarla junto a tu servidor HTTP, gestionar un almacén de estado de conexión separado, manejar la lógica de reconexión y mantener dos canales de comunicación paralelos que deben permanecer sincronizados.

FLIN elimina todo esto. Los WebSockets son parte del mismo servidor, el mismo sistema de enrutamiento, el mismo sistema de guards y el mismo sistema de sesiones que las rutas HTTP. Un manejador WebSocket puede llamar a User.find() igual que un manejador HTTP. Puede leer session.user igual que una plantilla de vista. Puede usar broadcast() para enviar mensajes sin importar una biblioteca.

Esta integración es lo que "integrado en el lenguaje" realmente significa. No es un envoltorio alrededor de una biblioteca. No es un sistema de plugins. Es una capacidad fundamental que es tan natural de usar como una ruta HTTP.

En el próximo artículo, cubrimos el soporte de carga de archivos -- cómo FLIN maneja cargas multipart, almacenamiento de archivos y validación de tamaño sin una sola línea de configuración.


Esta es la Parte 103 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA diseñaron y construyeron un lenguaje de programación desde cero.

Navegación de la serie: - [101] El sistema de middleware - [102] Guards: seguridad declarativa para rutas - [103] Soporte WebSocket integrado en el lenguaje (estás aquí) - [104] Soporte de carga de archivos

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles