RUST 2024 // AXUM // SQLITE (WAL) // HTMX // ALPINE.JS // PWA

SPEEDCUBE TIMER PWA _

Cronómetro de alta precisión WCA y Progressive Web App diseñado para la comunidad de Speedcubing. Construido bajo los principios de Clean Architecture en un workspace modular de Rust, persistencia ACID en SQLite y una interfaz reactiva e hipermedia sin la sobrecarga de un SPA tradicional.

>> FICHA TÉCNICA DEL STACK

🦀
CORE BACKEND

Rust 2024 con Axum 0.8 y Tokio. Workspace modular desacoplado (Domain, DB, API).

🗄️
PERSISTENCIA ACID

SQLite optimizado con Sqlx, WAL mode (Write-Ahead Logging) y Window Functions.

⚡
INTERFAZ HIPERMEDIA

HTMX para swaps de fragmentos en el servidor y Alpine.js para estados reactivos en el cliente.

📐
ESTÁNDAR OFICIAL WCA

Generación de scrambles con 'twips' (Uniform Random State) y cálculo Ao5 con corte del 5%.

>> 01. EL PROBLEMA TÉCNICO Y MOTIVACIÓN

En el Speedcubing profesional (resolución a ciegas y a velocidad del Cubo de Rubik), las milésimas de segundo marcan la diferencia. La mayoría de cronómetros web convencionales presentan tres problemas críticos:

  • Desviación del Event Loop (Timer Drift): El uso descuidado de setInterval en JavaScript genera retrasos e irregularidades cuando el hilo principal de renderizado está saturado.
  • Mezclas no oficiales (Scramble non-compliance): Muchos cronómetros aplican secuencias aleatorias simples de movimientos en vez del algoritmo de Uniform Random State exigido por el reglamento oficial de la WCA (World Cube Association).
  • Sobrediseño en Frontend (SPA Bloat): Depender de frameworks de cientos de kilobytes (React, Next.js, etc.) que aumentan el tiempo de inicio (TTI) y dificultan el uso sin conexión en torneos sin cobertura.

✔ La solución: Un backend nativo ultrarrápido en Rust compilado a binario estático, SQLite en modo WAL para escritura inmediata de cada solve, y un frontend ultraligero guiado por hipermedios (HTMX) y PWA instalable con Service Worker para funcionamiento 100% offline.

>> 02. ARQUITECTURA LIMPIA (RUST WORKSPACE)

El proyecto está organizado como un Cargo Workspace desacoplado en tres capas siguiendo los patrones de Clean Architecture y Domain-Driven Design (DDD):

┌─────────────────────────────────────────────────────────────┐ │ NAVEGADOR / PWA │ │ HTMX (DOM Swaps) + Alpine.js (State) + Service Worker │ └──────────────────────────────┬──────────────────────────────┘ │ HTTP / JSON / HTML Fragments ┌──────────────────────────────▼──────────────────────────────┐ │ crates/speedcube_api (Delivery Layer) │ │ - Axum 0.8 Web Framework + Tokio Async Engine │ │ - Offload de scrambles intensivos con spawn_blocking │ │ - Generación sanitizada de fragmentos HTML para HTMX │ └──────────────┬──────────────────────────────┬───────────────┘ │ │ ┌──────────────▼──────────────┐┌──────────────▼───────────────┐ │ crates/speedcube_db ││ crates/speedcube_domain │ │ (Persistence Layer) ││ (Core Domain & Rules) │ │ - Sqlx + SQLite Connection ││ - Modelos: Solve, Penalty │ │ - PRAGMA journal_mode = WAL ││ - Scrambler WCA via 'twips' │ │ - Window Functions (CTE) ││ - Cálculo de Ao5 / AoN (5%) │ │ - Índices (event_code, id) ││ - Cero dependencias externas │ └─────────────────────────────┘└──────────────────────────────┘

1. crates/speedcube_domain

El núcleo puro del sistema, totalmente agnóstico a la base de datos o el transporte web. Implementa los modelos del dominio (Solve, enum Penalty: None, +2, DNF), la lógica de generación de estados aleatorios oficiales de la WCA para 2x2, 3x3 y 4x4 usando la librería twips, y el algoritmo de cálculo de estadísticas (Average of N y Average of 5) con la regla de corte oficial del 5% superior e inferior redondeado hacia arriba.

2. crates/speedcube_db

Encapsula la persistencia mediante sqlx sobre SQLite. Emplea un pool de conexiones configurado con journal_mode = WAL (permite lecturas y escrituras concurrentes sin bloqueos de archivo) y synchronous = NORMAL. Resuelve la numeración exacta e inmutable de los solves mediante una Common Table Expression (CTE) con la Window Function: ROW_NUMBER() OVER (ORDER BY id ASC) as solve_number, asegurando que la numeración no sufra saltos al paginar con LIMIT.

3. crates/speedcube_api

Punto de entrada de la aplicación con axum. Implementa endpoints REST y endpoints de hipermedia que devuelven fragmentos HTML optimizados para HTMX. Para proteger el reactor de hilos asíncronos de Tokio ante la complejidad combinatoria del generador de mezclas de twips, delega el cálculo a hilos bloqueantes dedicados mediante tokio::task::spawn_blocking.

>> 03. FRAGMENTOS CLAVE DE IMPLEMENTACIÓN

crates/speedcube_domain/src/stats.rs RUST

Cálculo de Average of N (AoN) con descarte del 5% WCA y gestión de penalizaciones DNF y +2:

// Regla oficial WCA: descartar el 5% superior y 5% inferior
let trim = ((n as f64) * 0.05).ceil() as usize;

if dnf_count > trim {
    return AverageResult::Dnf;
}

times.sort_unstable();

// Excluimos los peores y mejores según el corte
let valid_times = &times[trim..(n - trim)];
let sum: i64 = valid_times.iter().map(|&t| t as i64).sum();
let divisor = (n - 2 * trim) as i64;

AverageResult::Ok((sum / divisor) as i32)
crates/speedcube_db/src/lib.rs SQL & RUST

Consulta optimizada con Window Functions para numeración secuencial global en SQLite:

WITH numbered AS (
    SELECT 
        id, 
        event_code, 
        time_ms, 
        penalty, 
        scramble,
        ROW_NUMBER() OVER (ORDER BY id ASC) as solve_number
    FROM solves 
    WHERE event_code = ?1
)
SELECT id, event_code, time_ms, penalty, scramble, solve_number
FROM numbered
ORDER BY id DESC
LIMIT ?2
crates/speedcube_api/src/main.rs AXUM + TOKIO

Generación asíncrona de mezclas sin bloquear el event loop de Tokio:

/// GET /scramble/:event
async fn get_scramble_handler(Path(event): Path<String>) -> Result<String, (StatusCode, String)> {
    // Despacho a hilo de cálculo para operaciones CPU-bound intensivas de twips
    tokio::task::spawn_blocking(move || generate_wca_scramble(&event))
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
        .map_err(|e| (StatusCode::BAD_REQUEST, e))
}

>> 04. AUDITORÍA TÉCNICA Y LECCIONES APRENDIDAS

Durante la evolución del proyecto se llevó a cabo una auditoría integral de arquitectura que identificó y resolvió los siguientes puntos clave:

[RESUELTO]
Fallo en la numeración del Solve #50:

Al consultar 50 filas con LIMIT 50, numerar en base a solves.len() - i provocaba que cualquier solve a partir del #51 mantuviera la fila superior en "#50". Se corrigió delegando la numeración al motor SQL mediante ROW_NUMBER() OVER (ORDER BY id ASC).

[RESUELTO]
Bloqueo del Event Loop por CPU-bound:

La exploración combinatoria del estado del cubo en twips se aisló con tokio::task::spawn_blocking, asegurando que el servidor pueda responder peticiones concurrentes a milisegundos de latencia.

[RESUELTO]
Sanitización estricta contra XSS:

En la generación de fragmentos HTML para HTMX se implementó una función escape_html completa que sustituye caracteres como <, >, " y '.

[INFRAESTRUCTURA]
Persistencia desacoplada con Docker:

Uso de DATABASE_URL configurable por variable de entorno para montar volúmenes persistentes en /app/data/speedcube_data.db bajo contenedores Alpine/Debian ultraligeros.