Skip to content

Arquitectura

Pablo Martínez López edited this page Oct 29, 2026 · 8 revisions

Arquitectura

English · Español

Proxy Builder es una aplicación de escritorio Tauri 2: un frontend React/TypeScript que solo pinta pantallas y guarda el estado de pantalla, un crate de la app (proxy-builder) que se encarga de la E/S (SQLite, disco, red, Tauri) y un crate puro (proxy-core) que se encarga de todo cálculo determinista (parseo, geometría de página, procesado de imagen, PDF, estadísticas, motor de diseño de cartas). Esta página es la vista de todo el sistema; cada caja enlaza a la página que profundiza en ella.

Las tres capas

Capa Dónde Se encarga de No debe
Frontend React src/ Pintado, estado de pantalla (stores de Zustand), i18n, temas Calcular geometría, hablar con la red, llamar a invoke directamente
Crate de la app proxy-builder (lib proxy_builder_lib) src-tauri/src/ Cableado de Tauri, comandos, biblioteca SQLite, almacén de blobs, Scryfall y otras fuentes, cachés, trabajos, protocolo pbimg://, actualizador Contener lógica pura que podría probarse sin Tauri
Crate core proxy-core src-tauri/crates/proxy-core/src/ model, decklist, layout, imaging, pdf, stats, diseno (motor del diseñador de cartas), paper, units Conocer Tauri, la red, la base de datos ni AppError
Crate del motor de la mesa mesa src-tauri/crates/mesa/src/ La mesa de juego: modelo, catálogo de acciones, azar determinista, visibilidad, deshacer y repetición (véase Motor de la mesa) Lo mismo que proxy-core, y además reloj o azar de fuera
Servidor de salas (repositorio aparte) server/ (ignorado por la app) Identidad sin cuentas, playgroups, invitaciones, salas, señalización, elección y migración del host, el callout del relé (ver Juego en línea) Llevar la partida: las partidas son P2P entre las apps

Los crates de Rust son miembros de un mismo workspace de Cargo declarado en src-tauri/Cargo.toml (members = [".", "crates/proxy-core", "crates/mesa"]). proxy-core tiene sus propios enums de error (LayoutError, ImagingError, PdfError, ProbeError, DisenoError, PackError, LineError) y la app los convierte a AppError con implementaciones de From (véase IPC y manejo de errores).

flowchart TB
    subgraph WV["WebView (React 19, sin red)"]
        UI["features/* (decklist, preview, export, variants, library, disenar, ...)"]
        ST["stores/* (deck-store, plan-store, ui-store, trabajo-store, actualizacion-store, edicion-store)"]
        IPC["lib/ipc.ts: call() y on()"]
        IMG["lib/image-url.ts: URLs pbimg://"]
        UI --> ST
        UI --> IPC
        UI --> IMG
    end

    subgraph APP["Crate de la app proxy-builder (src-tauri/src)"]
        LIB["lib.rs: run(), plugins, setup, generate_handler!"]
        CMD["commands/*.rs (108 comandos)"]
        PROTO["protocol.rs + render.rs (pbimg://)"]
        STATE["state.rs: AppState"]
        STORE["store/*: Db (SQLite), BlobStore, settings, presets"]
        SCRY["scryfall/*: client, cache, resolve, nombres"]
        FUE["fuentes/*: lista permitida Externo, Cockatrice, MTGJSON, MTG.io"]
        DIS["diseno/*: capa de app del diseñador"]
        MISC["package.rs, ocr.rs, originales.rs, temas.rs, actualizar.rs, biblioteca.rs"]
        LIB --> CMD
        LIB --> PROTO
        CMD --> STATE
        PROTO --> STATE
        STATE --> STORE
        STATE --> SCRY
        STATE --> FUE
        STATE --> DIS
    end

    subgraph CORE["Crate core proxy-core (puro)"]
        MODEL["model, units, paper"]
        DL["decklist"]
        LAY["layout: PagePlan"]
        IM["imaging"]
        PDF["pdf"]
        STA["stats"]
        DSN["diseno: motor"]
    end

    IPC -- "invoke / eventos" --> CMD
    IMG -- "GET pbimg://localhost/..." --> PROTO
    CMD --> CORE
    PROTO --> CORE
    SCRY -- HTTPS --> NET1["api.scryfall.com, cards.scryfall.io"]
    FUE -- "HTTPS, solo lista permitida" --> NET2["Cockatrice, MTGJSON, MTG.io, tweakcn, GitHub"]
    STORE --> DISK1[("app_data_dir")]
    PROTO --> DISK2[("app_cache_dir")]
Loading

Reglas de arquitectura y su razón

Vienen de CLAUDE.md; cada una es un invariante que la revisión de código y el CI hacen cumplir o del que depende la organización del código.

Decisión Motivo Dónde se ve
Todo lo pesado vive en Rust: parseo de la lista, Scryfall, descargas, cachés, geometría de página, procesado de imagen, PDF, estadísticas. React solo pinta y guarda el estado de pantalla. Una sola implementación, probable con cargo test, y el WebView nunca se bloquea con trabajo de CPU ni toca la red. Módulos de proxy-core; src/lib/ipc.ts como único puente
Una sola geometría para pantalla y papel. PagePlan (milímetros) lo calcula una vez proxy_core::layout::plan y lo consumen tanto la vista previa como el compositor del PDF. La pantalla nunca recalcula posiciones. Lo que ves es lo que se imprime. Una segunda implementación de la maquetación acabaría divergiendo. commands/layout.rs → compute_page_plan; stores/plan-store.ts → usePlan; commands/export.rs → exportar llama a plan(deck); doc de proxy-core/src/pdf/mod.rs: «Este módulo no calcula posiciones: dibuja el PagePlan»
Una sola canalización de imagen para pantalla y papel. Las miniaturas y las imágenes del PDF pasan por el mismo render() con un RenderSpec. El mismo motivo; fidelidad de la vista previa. src-tauri/src/render.rs → spec_de_miniatura, spec_de_casilla; proxy-core/src/imaging/pipeline.rs
Las imágenes de los mazos viven en el almacén de blobs (app_data_dir/blobs/, direccionado por SHA-256), nunca en la caché. La caché (app_cache_dir/renders/) solo guarda derivados y se puede borrar sin perder nada. Los mazos dependen de sus imágenes; el sistema operativo puede vaciar las cachés. cache_clear nunca debe perder datos del usuario. store/blobs.rs → BlobStore; render.rs → AppState::carpeta_renders; commands/library.rs → cache_clear (un test comprueba que renders está bajo paths.cache y no bajo paths.data)
La vista previa desmonta las imágenes de las páginas que no se ven (IntersectionObserver cuyo root es el viewport del ScrollArea). Solo las cartas visibles las renderiza Rust y las retiene WebKit. El documento en sí nunca hace scroll, así que un observer con raíz en el documento consideraría visible todo. src/features/preview/visibilidad.tsx → useVisible, RaizScroll
proxy-core es puro: sin Tauri, sin red, sin base de datos. Tests rápidos (cargo test -p proxy-core), límite claro. Comentario de doc de proxy-core/src/lib.rs; Cargo.toml del crate (sin tauri, reqwest ni rusqlite)
lib.rs solo arranca y registra; cada área tiene su commands/<área>.rs. El lib.rs de un proyecto anterior creció hasta unas 12k líneas; el fichero se mantiene pequeño a propósito. src-tauri/src/lib.rs (unas 200 líneas, su comentario de doc lo dice)
Los comandos devuelven Result<T, AppError> ({ code, params }); la pantalla traduce con useLang().mensaje(e). Rust nunca escribe prosa para el usuario. El idioma puede cambiar sin tocar Rust; un solo sitio para los textos. src-tauri/src/error.rs; src/lib/errores.ts
Los tipos compartidos vienen de Rust: #[derive(TS)] #[ts(export)] y pnpm bindings escriben src/bindings/. No se editan a mano. Es imposible que el desfase de tipos entre frontend y backend llegue sin que se note. .cargo/config.toml define TS_RS_EXPORT_DIR
La pantalla habla con Rust solo por call() de src/lib/ipc.ts. La interfaz Commands hace que el compilador compruebe los nombres de comando y los tipos de argumentos y resultado. src/lib/ipc.ts
El WebView nunca sale a internet; toda la red la hace Rust. Un único sitio para limitar la frecuencia, las listas permitidas, la redacción y el registro; CSP estricta. CSP de tauri.conf.json (connect-src es solo ipc:); scryfall/client.rs; fuentes::Externo
Todo lo que no es Scryfall se descarga por fuentes::Externo: lista permitida cerrada de sitio y ruta, cada redirección se vuelve a comprobar, topes de tamaño, imágenes siempre decodificadas y recodificadas. Limita el radio de daño de un sitio de terceros comprometido o cambiado. Fuentes externas
Los cambios al árbol del mantenedor llegan como ediciones sin commit; el flujo en la nube nunca hace push. Regla de proceso, no de código. Véanse Convenciones y Tests y CI

Límites de proceso

Hay un único proceso del sistema operativo (la app de Tauri) con tres contextos de ejecución:

  1. Proceso principal de Rust. Bucle de eventos de Tauri, manejadores de plugins, AppState. Las funciones #[tauri::command] síncronas se ejecutan en el lado de Tauri; los comandos async se ejecutan en el runtime asíncrono de Tauri (tokio). El trabajo largo se pasa a tauri::async_runtime::spawn (trabajos limitados por red: resolve_decklist, identify_batch) o a spawn_blocking (limitados por CPU: export_pdf). Dentro de un trabajo, los pools de hilos de rayon hacen el trabajo en paralelo por imagen (la exportación a PDF construye su propio pool de como mucho 8 hilos en commands/export.rs → exportar).
  2. El WebView (WebKitGTK en Linux, WebView2 en Windows, WKWebView en macOS). Carga dist/ (o el servidor de desarrollo de Vite en el puerto 1420). Su CSP prohíbe cualquier red salvo IPC (connect-src: ipc: http://ipc.localhost). Las imágenes vienen del esquema propio pbimg:.
  3. Hilos y pools de trabajo dentro de Rust (rayon, spawn_blocking, el semáforo renders que limita los renders de miniaturas simultáneos al número de núcleos, AppState.renders).

El WebView y Rust se comunican exactamente por tres canales:

Canal Dirección Mecanismo Página
Comandos WebView a Rust, petición/respuesta invoke envuelto por call() IPC y manejo de errores
Eventos Rust al WebView, sin esperar respuesta app.emit("<job>://progress" | "<job>://done", payload) escuchado por on() IPC y manejo de errores
Protocolo de imagen WebView a Rust, GET pbimg:// (http://pbimg.localhost/ en Windows), servido por protocol.rs Capa de aplicación Rust

Flujo de punta a punta: pegar la lista, resolver, planificar, vista previa, exportar

sequenceDiagram
    actor U as Usuario
    participant P as Panel de la lista (acciones.ts)
    participant IPC as ipc.ts call/on
    participant R as Comandos de Rust
    participant SC as Cliente de Scryfall + BlobStore
    participant DS as deck-store
    participant PS as plan-store
    participant PC as proxy-core
    participant PV as Vista previa (slot-view.tsx)
    participant PR as protocolo pbimg

    U->>P: pega la lista, pulsa Cargar
    P->>IPC: call parse_decklist {text}
    IPC->>R: invoke
    R->>PC: decklist::parse
    PC-->>P: ParsedDecklist
    P->>IPC: lanzarTrabajo(resolve://progress, resolve://done)
    IPC->>R: call resolve_decklist {list, options}
    R-->>P: jobId (de inmediato)
    R->>SC: lotes de /cards/collection, idioma, fichas
    SC->>SC: imágenes a blobs/ (SHA-256)
    R-->>P: evento resolve://progress {jobId, progress}
    R-->>P: evento resolve://done {jobId, result, error}
    P->>DS: aplicar(cards)  (entrada de historial, version++)
    DS->>PS: se dispara la suscripción (100 ms de espera)
    PS->>IPC: call compute_page_plan {deck}
    IPC->>R: invoke
    R->>PC: layout::plan(deck)
    PC-->>PS: PagePlan (mm)
    PS-->>PV: páginas y casillas
    PV->>PV: useVisible(page) true
    PV->>PR: GET pbimg://localhost/r/{blob}/{params}/{width}
    PR->>PC: imaging::pipeline::render (en caché en app_cache_dir/renders)
    PR-->>PV: JPEG
    DS->>IPC: call deck_save (800 ms tras el último cambio)
    U->>P: Exportar PDF, elige una ruta (plugin dialog)
    P->>IPC: lanzarTrabajo(export://progress, export://done)
    IPC->>R: call export_pdf {deck snapshot, path}
    R-->>P: jobId
    R->>PC: layout::plan, renderiza cada imagen distinta (rayon), pdf::compose
    R-->>P: export://progress {phase: prepare / compose / write}
    R-->>P: export://done {jobId, result, error}
Loading

Puntos clave que se ven en el diagrama:

  • resolve_decklist y export_pdf devuelven un jobId al instante y terminan por evento; el frontend escucha antes de lanzar (src/lib/trabajos.ts → lanzarTrabajo) para que un trabajo rápido no pueda terminar sin que nadie lo vea.
  • La exportación envía la instantánea del mazo que hay en el store, no la que hay en SQLite, porque el autoguardado puede ir hasta 800 ms por detrás (src/features/export/acciones.ts → exportarA).
  • La vista previa y el PDF ejecutan el mismo render(); la vista previa pide un ancho de ANCHOS = [146, 244, 488, 672, 745, 1000], el PDF pide los PPP de la calidad elegida.

Páginas más detalladas: Parseo de la lista del mazo, Integración con Scryfall, Motor de maquetación, Vista previa en vivo, Imagen y PDF, Exportación.

Ubicación de los datos

La app pide a Tauri los directorios en el setup de lib.rs y los guarda en state::Paths { data, cache }; el directorio de registros viene de app_log_dir().

Ubicación Contenido Origen ¿Se puede borrar sin perder datos?
app_data_dir/library.db SQLite (WAL): decks, deck_cards, blobs, blob_refs, presets, app_settings, scryfall_cards, tags, deck_tags, deck_listas, partidas, partida_decks, partida_imagenes; migraciones 0001–0008 en src-tauri/src/store/migrations/ state.rs → AppState::open, store/db.rs → Db No
app_data_dir/blobs/<2 hex>/<sha256>.<ext> Todas las imágenes que usa un mazo (PNG de Scryfall, subidas, generadas, diseñadas) store/blobs.rs → BlobStore No
app_data_dir/fuentes/, marcos/, simbolos/ Recursos del diseñador: letras importadas, packs de marcos (incluidos los packs descargados y los marcos «Original»), símbolos de maná diseno/recursos.rs → Recursos Se pueden volver a descargar, pero se pierden las importaciones del usuario
app_data_dir/letras-cjk/ Noto Sans JP/KR/SC/TC (<código>.ttf) y su licencia OFL, bajadas la primera vez que un deck usa el japonés, el coreano o el chino como idioma de diseño diseno/letras_cjk.rs, las lee diseno/recursos.rs Sí (se vuelven a bajar y se comprueba su SHA-256)
app_data_dir/modelos/ Modelos de OCR, descargados la primera vez que se usa el OCR ocr.rs Sí (se vuelven a descargar)
app_data_dir/temas/ Temas de tweakcn instalados (<id>.json) y sus letras (fuentes/<hash>.woff2) temas.rs Se desinstalan los temas
app_data_dir/partidas/actual.json.gz, partidas/imagenes/ La partida en curso de la mesa de juego; copias de las imágenes propias que pintan las repeticiones del historial mesa/guardada.rs, mesa/imagenes.rs (Mesa de juego) Se pierde la partida en curso; las repeticiones del historial pierden esas imágenes
app_data_dir/iconos/elegidos.json; app_cache_dir/iconos/ Iconos de Iconify elegidos por la persona (guardados para verlos sin red); el catálogo del selector, renovado cada semana iconos.rs (Iconos) La caché sí; los elegidos vuelven a necesitar la red
app_cache_dir/renders/ JPEG de miniaturas con clave RenderSpec::key(); t/ miniaturas de resultados de búsqueda; b/ ilustración reducida render.rs, protocol.rs, diseno/vista.rs Sí (cache_clear)
app_cache_dir/scryfall-card-names.json, scryfall-set-codes.json Catálogos de nombres y de códigos de set de Scryfall, renovados cada semana scryfall/nombres.rs Sí
app_cache_dir/cockatrice-tokens.xml; app_cache_dir/mtgjson/nombres.sqlite Catálogo de fichas y el índice de nombres en varios idiomas fuentes/cockatrice.rs, fuentes/mtgjson.rs Sí
app_log_dir/proxy-builder.log Una línea por petición a Scryfall (método, host, ruta, estado) registro.rs, scryfall/client.rs Sí
Almacenamiento del navegador (localStorage) Clave del idioma de la interfaz proxybuilder.lang, otras comodidades por espectador src/components/language-provider.tsx Sí

Obsérvese que la caché de datos de cartas de Scryfall no está en app_cache_dir: es la tabla scryfall_cards dentro de library.db (scryfall/cache.rs, TTL de una semana), de modo que las estadísticas y la biblioteca funcionan sin conexión. Como cache_clear solo vacía renders/, esa tabla sobrevive.

Véase Almacenamiento y blobs para las tablas y la recolección de basura.

Pila tecnológica

Las versiones son los rangos declarados en los manifiestos; donde importa se indican las versiones resueltas de src-tauri/Cargo.lock.

Área Tecnología Declarada Resuelta / nota
Shell Tauri tauri = "2" 2.12.3
Plugins tauri-plugin-dialog, -opener, -updater, -window-state "2" 2.8.2, 2.7.1, 2.14.0, 2.5.0 (window-state solo en escritorio)
Tipos ts-rs "12" 12.0.1
Base de datos rusqlite con SQLite bundled "0.40" 0.40.2
HTTP reqwest (rustls, sin OpenSSL; el CI comprueba que cargo tree -i openssl esté vacío) "0.13" 0.13.5
Asíncrono tokio (rt, sync, time, macros) "1" 1.53.2
Paralelismo rayon "1.12"
PDF krilla (raster-images, simple-text) "0.8.2" 0.8.2
Imágenes image, fast_image_resize (Lanczos3, SIMD) 0.25.10, 6.1.0
Renderizado de cartas parley (texto), tiny-skia, resvg, skrifa 0.12, 0.12, 0.48, 0.44 0.12.0, 0.12.0, 0.48.1
OCR ocrs + rten 0.13, 0.26 0.13.1, 0.26.0
Paquetes zip (solo deflate) para .pbdeck "9"
Registro tracing, tracing-subscriber (fmt) 0.1, 0.3
Edición de Rust / MSRV 2021, rust-version = "1.92"
Frontend React, React DOM ^19.1.0
Estado Zustand ^5.0.15
Kit de UI Radix UI (radix-ui ^1.5.0), lucide-react ^1.17.0 (solo en components/ui/), @iconify/utils + @iconify/json (véase Iconos), sonner ^2.0.7, react-resizable-panels ^4.11.2, @dnd-kit/core ^6.3.1
Estilos Tailwind CSS 4 (@tailwindcss/vite), tw-animate-css ^4.3.0
Compilación Vite ^7.3.6, TypeScript ~5.8.3
Tests / lint Vitest ^5.0.1 (happy-dom), Biome 2.4.13, knip ^6.34.0
Gestor de paquetes pnpm 12.14.0 (packageManager), minimumReleaseAge: 1440 en pnpm-workspace.yaml
Node >=22

Cómo se reparten las piezas en las páginas del wiki

Asunto Página
Puente, comandos, errores, bindings, trabajos IPC y manejo de errores
lib.rs, AppState, pbimg://, render.rs, capabilities, ventana, CSP Capa de aplicación Rust
Deck, DeckCard, PrintSettings y compañía Modelo del mazo
SQLite, almacén de blobs, GC Almacenamiento y blobs
Ficheros .pbdeck Formato del paquete de mazo
Parseo de la lista pegada Parseo de la lista del mazo
Cliente de Scryfall, caché, resolución Integración con Scryfall
Cockatrice, MTGJSON, MTG.io, Externo Fuentes externas
OCR y vinculación de cartas propias Identificación de cartas y OCR
PagePlan Motor de maquetación
Canalización de imagen y PDF Imagen y PDF
Botón de exportar, presets, calibración Exportación
Paneles de la vista previa y construcción de URLs pbimg:// Vista previa en vivo
Diálogo de estadísticas Estadísticas
Diseñador de cartas Visión general del diseñador, Motor del diseñador, Composición del diseñador, Packs de marcos, Capa de aplicación del diseñador, Interfaz del diseñador
Estructura del frontend, stores, i18n, temas, ajustes Arquitectura del frontend, Stores de estado, Internacionalización, Temas y skins, Ajustes y shell
Pantallas de funciones Panel de la lista del mazo, Añadir carta y variantes, Explorador de sets, Biblioteca de mazos, Diálogo Editar carta
Entorno de desarrollo, CI, releases, convenciones Preparación del entorno de desarrollo, Tests y CI, Versiones y actualizaciones automáticas, Convenciones

Por dónde empezar para ampliarlo

  • Una nueva capacidad del backend: pon la parte pura en proxy-core (con su propio enum de error), la parte de E/S en el crate de la app, añade commands/<área>.rs, regístralo en el generate_handler! de lib.rs, añade su firma a Commands en src/lib/ipc.ts y ejecuta pnpm bindings. Lista de comprobación en IPC y manejo de errores.
  • Un nuevo trabajo largo: reserva con state.jobs.alta(), mantén state.trabajo() durante todo el trabajo, emite <name>://progress y <name>://done, añade ambos a Events en ipc.ts y lánzalo desde la pantalla con lanzarTrabajo.
  • Una nueva forma de URL de imagen: amplía Ruta y leer_ruta en protocol.rs (con un test de rechazo) y añade un constructor en src/lib/image-url.ts.

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally