aura-gui 0.3.5

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! El transporte pide respuestas comprimidas y las descomprime al vuelo, también en streaming (como el SDK de
//! Claude): menos bytes por la red. Si alguien quita la función `gzip` de ureq, esto falla.

use super::stream_sse;
use crate::ai::provider::Decoder;
use crate::ai::sse::SseEvent;
use crate::ai::types::{AiError, Event};
use crate::core::CancelToken;
use std::io::{Read, Write};
use std::net::TcpListener;

/// gzip con bloques «stored» de deflate (sin comprimir): basta para probar la descompresión sin otra dependencia.
fn gzip(data: &[u8]) -> Vec<u8> {
    let mut out = vec![0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 0xff];
    let chunks: Vec<&[u8]> = data.chunks(0xFFFF).collect();
    for (index, chunk) in chunks.iter().enumerate() {
        let length = chunk.len() as u16;
        out.push(u8::from(index + 1 == chunks.len()));
        out.extend(length.to_le_bytes());
        out.extend((!length).to_le_bytes());
        out.extend_from_slice(chunk);
    }
    out.extend(crc32(data).to_le_bytes());
    out.extend((data.len() as u32).to_le_bytes());
    out
}

fn crc32(data: &[u8]) -> u32 {
    !data.iter().fold(!0u32, |crc, byte| {
        (0..8).fold(crc ^ u32::from(*byte), |crc, _| if crc & 1 == 1 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 })
    })
}

/// Servidor de una petición: entrega las cabeceras recibidas y responde `body` como SSE comprimido con gzip.
fn serve_gzip(body: &str) -> (String, std::sync::mpsc::Receiver<String>) {
    let listener = TcpListener::bind("127.0.0.1:0").expect("puerto libre");
    let url = format!("http://{}/v1/stream", listener.local_addr().expect("dirección"));
    let compressed = gzip(body.as_bytes());
    let (sender, receiver) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let (mut socket, _) = listener.accept().expect("conexión");
        let (mut received, mut buffer) = (Vec::new(), [0u8; 8192]);
        loop {
            let read = socket.read(&mut buffer).expect("lectura");
            received.extend_from_slice(&buffer[..read]);
            let text = String::from_utf8_lossy(&received).to_string();
            let Some(head) = text.find("\r\n\r\n") else { continue };
            let length = text
                .lines()
                .find_map(|line| {
                    line.to_lowercase().strip_prefix("content-length:").map(|value| value.trim().to_owned())
                })
                .and_then(|value| value.parse::<usize>().ok())
                .unwrap_or(0);
            if read == 0 || received.len() >= head + 4 + length {
                let _ = sender.send(text[..head].to_owned());
                break;
            }
        }
        let head = format!(
            "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncontent-encoding: gzip\r\ncontent-length: {}\r\n\
             connection: close\r\n\r\n",
            compressed.len()
        );
        let _ = socket.write_all(head.as_bytes());
        let _ = socket.write_all(&compressed);
    });
    (url, receiver)
}

/// Cada `data:` es un trozo de texto; `[DONE]` marca el final.
#[derive(Default)]
struct Lines {
    done: bool,
}

impl Decoder for Lines {
    fn feed(&mut self, event: &SseEvent, out: &mut Vec<Event>) -> Result<(), AiError> {
        if event.data == "[DONE]" {
            self.done = true;
        } else {
            out.push(Event::Text(event.data.clone()));
        }
        Ok(())
    }

    fn finish(&mut self, _out: &mut Vec<Event>) {}

    fn is_complete(&self) -> bool {
        self.done
    }
}

#[test]
fn pide_gzip_y_descomprime_el_stream() {
    let (url, headers) = serve_gzip("data: hola\n\ndata: mundo\n\ndata: [DONE]\n\n");
    let mut texts = Vec::new();
    let body = serde_json::json!({ "prompt": "hola" });
    stream_sse(&url, &[], &body, &CancelToken::default(), &mut Lines::default(), &mut |event| {
        if let Event::Text(text) = event {
            texts.push(text);
        }
    })
    .expect("el stream comprimido se lee entero");
    assert_eq!(texts, ["hola", "mundo"]);
    let headers = headers.recv().expect("cabeceras").to_lowercase();
    assert!(headers.lines().any(|line| line.starts_with("accept-encoding:") && line.contains("gzip")), "{headers}");
}