Skip to content

IPC y manejo de errores

Pablo Martínez López edited this page Oct 27, 2026 · 14 revisions

IPC y manejo de errores

English · Español

Todo lo que la pantalla le pide a Rust pasa por una sola función tipada, call() en src/lib/ipc.ts; todo lo que Rust le devuelve pasa por eventos tipados que se escuchan con on(). Los fallos llegan siempre como un AppError { code, params } que la pantalla convierte en una frase en el idioma actual. Esta página lista los 177 comandos, el modelo de errores, los bindings de TypeScript generados, el protocolo de trabajos largos y los diálogos de ficheros.

El puente: call() y on()

src/lib/ipc.ts exporta:

Export Función
interface Commands Una entrada por cada comando de Rust: name: { args: {...}; result: T }. El compilador comprueba cada call() contra ella.
call<C extends keyof Commands>(cmd, args?) Envoltorio fino sobre invoke de @tauri-apps/api/core. Es el único sitio donde se importa invoke (comprobado con grep: ningún otro fichero de src/ llama a invoke).
interface Events Nombre de evento a tipo de payload para los eventos que la pantalla escucha con on() (19 entradas). No están los avisos del juego en red (red://aviso, salas://…) ni mesa://originales: se escuchan con listen directamente (véase Eventos).
on(evento, f) Importa listen de @tauri-apps/api/event de forma dinámica; devuelve la función para dejar de escuchar (como promesa).

Convenciones que dan problemas:

  • Los nombres de los argumentos van en camelCase en el lado de JS. Los parámetros de Rust van en snake_case; Tauri los traduce: job_id es jobId, scryfall_id es scryfallId, type_filter es typeFilter, highres_only es highresOnly. Los campos de struct dentro de los payloads siguen el #[serde(rename_all = "camelCase")] de cada struct.
  • Los resultados unitarios pasan a ser null (result: null en Commands), y Option<T> pasa a ser T | null.
  • Commands se mantiene a mano. Los tipos de argumentos y resultado a los que hace referencia están generados (véase Bindings de ts-rs), pero la lista en sí no. Añadir un comando de Rust sin entrada en Commands compila sin problema y simplemente no se puede llamar desde código tipado. Hoy los 177 nombres de generate_handler! y las 177 claves de Commands coinciden exactamente (comprobado comparando ambas listas).
  • Las importaciones directas de Tauri fuera de ipc.ts existen solo para APIs que no son comandos: @tauri-apps/plugin-dialog (src/lib/dialogos.ts), getCurrentWindow().show() (src/main.tsx), getCurrentWebview() para soltar ficheros (src/features/preview/arrastrar-ficheros.ts), getVersion() (src/features/settings/settings-dialog.tsx), convertFileSrc (src/lib/image-url.ts), getCurrentWindow() para la guarda de cierre (src/features/shell/confirmar-cierre.tsx) y la pantalla completa (src/features/mesa/barra-mesa.tsx), la clase Channel de @tauri-apps/api/core (src/features/mesa/conexion.ts, véase Canales) y listen de @tauri-apps/api/event para los eventos que no están en Events (src/features/mesa/red/conexion-red.ts, src/features/salas/conexion-salas.ts, src/features/mesa/originales.ts).

Registro en el lado de Rust

// src-tauri/src/lib.rs
.invoke_handler(tauri::generate_handler![
    commands::sistema::ping,
    commands::carpetas::carpeta_recordar,
    // ... 177 entries
])

Cada área es un módulo en src-tauri/src/commands/ (mod.rs los lista): actualizar, cards, carpetas, decklist, diseno, export, fuentes, identify, imagen, layout, library, mesa, red, salas, sistema, stats, temas. Los dos comandos de enlaces (enlace_pendiente, enlace_invitacion) viven fuera de esa carpeta, en src-tauri/src/enlace.rs. Los comandos reciben State<'_, AppState> (véase Capa de aplicación Rust), a veces un AppHandle (para emitir eventos o usar plugins), y devuelven AppResult<T> (Result<T, AppError>). Algunos devuelven T directamente porque no pueden fallar: consultas baratas (ping, parse_decklist, deck_list_text, arrange_cards, cache_stats, themes_installed, ocr_ready, names_index_info, design_structure, design_remove, update_status, red_estado, salas_estado, enlace_pendiente, enlace_invitacion), los de cancelar (job_cancel, export_cancel), los datagramas del movimiento en vivo (mesa_fantasma, mesa_senal, resultado unitario) y los trabajos que solo devuelven un id y cuentan los fallos en su evento done (resolve_decklist, export_pdf, identify_batch, mesa_empezar, mesa_retomar). Los trabajos red_unirse, red_empezar y salas_deck devuelven AppResult<u32> porque pueden negarse antes de empezar (Busy, un código que no se entiende, sin sala); design_complete_lang también devuelve AppResult<u32>, pero nunca falla antes del id.

Referencia de comandos

Los ficheros están bajo src-tauri/src/commands/. «Trabajo» significa que el comando devuelve al instante un id de trabajo y comunica el progreso y la finalización mediante eventos (véase Trabajos largos).

Sistema (sistema.rs)

Comando Función
ping Devuelve "pong"; demuestra que el puente funciona.
ping_error Falla siempre con InvalidInput; demuestra que los errores cruzan el puente y se traducen.

Carpetas para los diálogos (carpetas.rs)

Comando Función
carpeta_recordar Recuerda la carpeta de una ruta elegida bajo la clave de app_settings carpeta_importar o carpeta_exportar (TipoCarpeta).
carpeta_inicial Devuelve la ruta en la que abrir un diálogo: la última carpeta de ese tipo (si todavía existe), unida a un nombre de fichero sugerido.

Actualizador (actualizar.rs; la lógica está en src-tauri/src/actualizar.rs, véase Versiones y actualizaciones automáticas)

Comando Función
update_status Versión actual y cómo se instaló la app.
update_check Consulta latest.json; devuelve ActualizacionInfo o null.
update_download Descarga la versión encontrada; emite update://progress.
update_install Instala la versión descargada y reinicia. Se niega con Busy mientras AppState::ocupado().

Texto de la lista (decklist.rs; véanse Parseo de la lista del mazo, Exportación)

Comando Función
parse_decklist Parsea una lista pegada a ParsedDecklist (proxy_core::decklist::parse). Síncrono y barato; se llama mientras se escribe.
deck_list_text Escribe el mazo como lista en un FormatoLista (Moxfield, Arena, MTGO, texto plano) para copiarla.
deck_list_save Guarda esa lista en una ruta .txt absoluta (rechaza cualquier otra con InvalidInput).

Layout (layout.rs; véase Motor de maquetación)

Comando Función
compute_page_plan layout::plan(&deck) a PagePlan. Errores: InvalidInput con motivo card_does_not_fit o invalid_settings.
arrange_cards «Auto ordenar»: devuelve el nuevo orden de los ids de las cartas.

Biblioteca: mazos, etiquetas, blobs, ajustes, presets, paquetes (library.rs; véanse Biblioteca de mazos, Almacenamiento y blobs, Formato del paquete de mazo)

Comando Función
deck_list Resúmenes de todos los mazos.
deck_create Mazo nuevo vacío con el reverso genérico de la app.
deck_open Carga un mazo (y lo registra como abierto).
deck_peek Carga un mazo sin registrarlo como abierto (selector de portada).
deck_details Colores, clase, precio, idiomas y formatos de los mazos (todos si ids está vacío), a partir de la caché de Scryfall, sin red.
deck_save Persiste el mazo; devuelve SaveResult { updatedAt }. Lo llama el autoguardado.
deck_rename / deck_duplicate / deck_delete Renombrar un mazo, copiarlo con un nombre nuevo, borrarlo.
tag_list / tag_create / tag_update / tag_delete Etiquetas del usuario (create devuelve la etiqueta existente si el nombre ya existe).
deck_tags_set Añade y quita etiquetas en varios mazos a la vez.
deck_pin Marca de favorito para varios mazos.
deck_notes_set Texto de las notas de un mazo.
deck_cover_set Carta de portada elegida (o null).
deck_lang_set Idioma del mazo fijado a mano (o null para calcularlo).
blob_import_files Copia al almacén de blobs los ficheros de imagen que elige el usuario; devuelve FaceImage[].
blob_generic_back El reverso genérico de carta de la app como FaceImage.
blob_gc Borra los blobs sin referencias con más de 24 h; Busy mientras corre un trabajo.
app_setting_get / app_setting_set Ajustes clave/valor en SQLite (app_settings).
cache_stats / cache_clear Tamaño de app_cache_dir/renders / lo borra. Nunca toca los blobs.
deck_export_package Escribe un mazo con todas sus imágenes en un .pbdeck; devuelve bytes.
deck_import_package Importa un .pbdeck (ids nuevos, imágenes verificadas); devuelve el Deck.
preset_list / preset_save / preset_delete Presets con nombre por PresetKind (ajustes de impresión, etc.).

Cartas: Scryfall y listas (cards.rs; véanse Integración con Scryfall, Añadir carta y variantes, Explorador de sets)

Comando Función
resolve_decklist Trabajo. Resuelve un ParsedDecklist a cartas con sus imágenes guardadas. Eventos resolve://progress, resolve://done.
job_cancel Cancela cualquier trabajo iniciado desde AppState::jobs (misma implementación que export_cancel). Devuelve si el trabajo todavía existía.
card_autocomplete Nombres del autocompletado de Scryfall.
card_search Búsqueda con la sintaxis de Scryfall, como máximo 50 resultados (CardSummary[]); idioma y solo alta resolución opcionales.
card_prints Todas las impresiones del nombre de una carta (selector de variantes).
card_fetch Descarga una impresión y la devuelve como DeckCard.
card_fetch_externa Lo mismo para una carta de una fuente que no es Scryfall (FuenteCarta, hoy las fichas de Cockatrice).
deck_tokens_detect Propone las fichas que faltan en un mazo (FichaPropuesta[]); no añade nada.
sets_list / set_cards Catálogo de sets; cartas de un set, opcionalmente por tipo.
import_archidekt Importa un mazo público de Archidekt por URL.

Exportar (export.rs; véanse Exportación, Imagen y PDF)

Comando Función
export_pdf Trabajo. Exporta la instantánea de mazo dada a una ruta de PDF (varios ficheros si el mazo se divide). Eventos export://progress, export://done.
export_estimate Tamaño aproximado del PDF (bytes, images, pages).
export_calibration Escribe la hoja de calibración a doble cara; los textos vienen de la pantalla.
export_cancel Cancela el trabajo de exportación.
export_open / export_reveal Abren el fichero con el visor del sistema / lo muestran en su carpeta. Solo se aceptan ficheros .pdf o .pbdeck que existan (exportado()); si no, InvalidInput.

Imagen de una carta suelta (imagen.rs)

Comando Función
card_export_image Guarda una cara de una carta (frente o dorso; deckBack es el dorso del mazo) como imagen, la misma que imprimiría el PDF (retoques, oscurecido, carta diseñada), a los PPP de la calidad del PDF y sin sangrado. El formato sale de la extensión del fichero (PNG si no tiene; una desconocida es InvalidInput con motivo: formato). Devuelve la ruta escrita. Corre en spawn_blocking.

Estadísticas (stats.rs; véase Estadísticas)

Comando Función
deck_stats DeckStats para unas cartas y un DeckKind opcional; pide a /cards/collection de Scryfall los datos que faltan o están caducados (más de 24 h).

Identificar y vincular cartas propias (identify.rs; véase Identificación de cartas y OCR)

Comando Función
ocr_ready ¿Están descargados los modelos de OCR?
card_identify Propone cartas de Scryfall para una imagen guardada (texto de pista y OCR opcional).
identify_batch Trabajo. Identifica muchas imágenes; el resultado de cada una va en identify://progress y el final en identify://done. Se cancela con job_cancel.
card_link Información de carta para un id de Scryfall elegido (primero la caché, luego la red, luego lo guardado para las estadísticas sin conexión).
card_link_externa Lo mismo para una carta de una fuente externa.

Índice de nombres y temas (fuentes.rs, temas.rs; véanse Fuentes externas, Temas y skins)

Comando Función
names_index_info / names_index_download / names_index_remove Estado, descarga (emite names://progress) y eliminación del índice de nombres en varios idiomas de MTGJSON.
themes_catalog El registro de tweakcn.
theme_fetch Un tema por id o enlace, para previsualizarlo.
theme_install / themes_installed / theme_remove Instalar (CSS + letras), listar y quitar temas de tweakcn.
themes_open_community Abre la galería de la comunidad de tweakcn en el navegador.

Diseñador de cartas (diseno.rs; véanse Capa de aplicación del diseñador, Motor del diseñador, Packs de marcos)

Comando Función
design_draft Borrador de diseño para una carta del mazo (instantánea mecánica EN/ES, nombres originales, siguiente número libre del pie).
collection_list / collection_save / collection_delete Colecciones de diseño.
fonts_list / font_import / font_remove Letras propias.
packs_list / pack_import / pack_remove Packs de marcos.
fonts_free_list / font_free_download Catálogo de letras libres (OFL) y su descarga.
original_info / original_download Estado y descarga de los marcos «Original» (emite original://progress).
packs_downloadable_list / pack_downloadable_download Packs descargables y su descarga (emite packs://progress).
pack_extra_use Copia un extra de un pack (reverso, ficha, carta terminada) al almacén de blobs.
symbols_status Estado de los símbolos de maná; los descarga si faltan o están caducados (refresh).
design_register Registra el diseño que se está editando para pbimg://…/d/<hash>/<width>; devuelve el hash.
design_overlay La carta sin ilustración más la ventana de la ilustración (pbimg://…/c/…), para mover la ilustración en vivo; null para cartas compuestas o grandes.
design_fit_report Avisos: texto que no cabe, símbolos o letras que faltan.
design_structure Cómo está maquetada una cara (layout y texto dividido) para el editor.
design_suggest_translation Sugerencia experimental de traducción para una cara.
design_drafts_for_deck Trabajo. Borradores para todas las cartas sin diseñar. Eventos design://progress, design://done.
design_complete_lang Trabajo. Completa un idioma de diseño en las cartas diseñadas del mazo: baja las letras CJK si es japonés, coreano o chino y la impresión que le falte a cada carta. Cada carta hecha llega por designlang://progress y el final por designlang://done; se cancela con job_cancel.
design_export_png Trabajo. Exporta las cartas diseñadas como PNG a una carpeta. Eventos designpng://progress, designpng://done.
design_save / design_remove Aplicar un diseño a una carta (miniatura al almacén de blobs) / revertir la carta.
design_rerender / design_renumber Refrescar miniaturas caducadas / renumerar los diseños según el orden del mazo; devuelven las cartas que han cambiado.
art_from_clipboard / art_from_blob / art_from_scryfall / art_from_card Obtener la ilustración: portapapeles del sistema, fichero guardado, art_crop de Scryfall, recorte de una carta existente (fracciones 0–1).

Mesa de juego (mesa.rs; la lógica en src-tauri/src/mesa/, véase Mesa de juego)

Comando Para qué
mesa_empezar Trabajo. Prepara los decks de una ConfigMesa y abre la partida. Eventos mesa://progress, mesa://done.
mesa_retomar Trabajo. Sigue la partida guardada y vuelve a preparar las imágenes que falten en la caché. Mismos eventos.
mesa_estado EstadoMesa { abierta, deckIds, guardada }: si hay una partida abierta y, si no, el resumen de la guardada.
mesa_suscribir Recibe un Channel<MensajeMesa>; manda la vista entera y, desde ahí, un parche por evento.
mesa_vista La VistaMesa entera (cuando un parche llega fuera de orden), sin el registro.
mesa_accion Manda una Accion (opcionalmente como otro jugador local); el cambio llega por el canal y un rechazo es Rejected (espectador para un espectador).
mesa_mirar_como En una partida local de varios, pasa a mirar (y a jugar como) otro jugador; Rejected red en red.
mesa_encuentro, mesa_banquillo, mesa_banquillo_cambiar, mesa_siguiente_partida El marcador del «al mejor de», el mazo y el banquillo de un jugador, el cambio de cartas entre partidas y abrir la siguiente. mesa_banquillo y mesa_banquillo_cambiar son async: en una partida en red cada jugador cambia solo su mazo, y un invitado se lo pide al anfitrión y espera hasta 10 s (si no, Network sin_respuesta). Rechazos: ajeno, invitado, sin_terminar, sin_mazos, cambio.
mesa_resumen El ResumenPartida de la partida abierta (puestos, rondas, duración), contado por mesa::resumir a partir del registro.
partidas_de_deck HistorialDeck de un deck: sus partidas terminadas (FilaPartida), las victorias y los ids de las partidas.
partidas_todas HistorialGeneral: una página de todas las partidas terminadas de todos los decks (limite, desde), con el total y las victorias.
partida_borrar Borra una partida del historial junto con su repetición.
repeticion_abrir Abre una partida del historial como repetición en su propia sesión (VistaRepeticion); Rejected partida_abierta con una partida abierta, sin_registro sin registro, Unsupported con version si viene de un catálogo más nuevo.
repeticion_actual, repeticion_ir, repeticion_salir Repetir la partida que acaba de terminar, en su sitio (Rejected sin_terminar si no ha terminado); ir a un punto (pos, como); salir de la repetición (vuelve a la partida terminada o, si se abrió desde el historial, cierra la mesa). Mientras se repite, mesa_accion es Rejected repeticion.
mesa_guardar Guarda la partida ya (al salir de la pestaña, al cerrar la ventana).
mesa_salir Sale de la mesa: con guardar la partida queda guardada; sin él, se borra el fichero.
mesa_info_carta El texto de reglas de una carta que quien mira puede ver («Inspeccionar»).
mesa_origen_scryfall Un OrigenCarta para una carta o ficha de Scryfall que no está en el deck (baja sus imágenes).
mesa_sparring_carta El OrigenCarta de una de las cartas del rival fantasma del sparring, por su nombre (solo nombres de su lista; si no, InvalidInput).
mesa_originales Pone en marcha «Originales en mi idioma»: para las cartas con vínculo a Scryfall que se le dan (≤ 1.000) busca la impresión en el idioma de la interfaz y contesta cada una por mesa://originales. Solo pantalla.
mesa_listas ListasDeDeck { listas, cuentan } de un deck: sus listas para jugar y cuántas cartas cuentan sin lista.
mesa_lista_cartas Las copias elegibles del deck con sus datos (CartaDeLista, de Scryfall o del diseño) para el editor.
mesa_lista_comprobar Los AvisoLista[] de una lista para un formato (avisos o que impiden, véase Mesa de juego).
mesa_lista_guardar / mesa_lista_borrar Guarda (crea o sustituye; la devuelve con su id) o borra una lista para jugar. mesa_empezar rechaza un deck que necesita lista y no la tiene (InvalidInput, motivo: falta_lista) o una lista que no vale (motivo: lista).
mesa_tapete_get / mesa_tapete_set / mesa_tapete_ajustes El tapete de un deck (deck: null = el general): leerlo, ponerlo o quitarlo, cambiar solo el velo y el desenfoque.
mesa_dados_get / mesa_dados_set / mesa_dados_imagen El aspecto de los dados y la moneda del jugador (VistaAspectoDados): leerlo, guardarlo (con los colores comprobados), poner o quitar la imagen propia.

Juego en red con un código directo (red.rs; la lógica en src-tauri/src/red/, véase Juego en línea)

Comando Para qué
red_identidad / red_identidad_set La Identidad con la que se juega en red (clave pública, apodo, avatar); cambiar el apodo y el avatar (una huella de blob o ninguno).
red_identidad_exportar / red_identidad_importar Guardar la identidad de la instalación en un fichero / sustituirla por una exportada antes. Importar es Busy (motivo: partida) con una partida, una sala por código o una sala del servidor abiertas; un fichero que no es una identidad es InvalidInput identidad.
red_estado EstadoRed { papel, codigo, sala, empezada, puedeCeder }: si esta app es anfitrión, jugador o espectador, el código para invitar (solo el anfitrión), quién hay en la sala (solo el anfitrión; el invitado lo recibe por red://aviso; cada fila lleva un id para elegir asientos y equipos) y, para el anfitrión, si al salir ahora la partida pasa a otro jugador (puedeCeder) en vez de cerrarse.
red_crear Abre una partida en red como anfitrión: levanta el nodo, atiende a quien llegue con el código y lo devuelve (PB1-…). Busy partida si ya hay una partida o una sala abiertas.
red_unirse Trabajo. Se une con una PeticionUnion (código, apodo, rol, deck, lista, formato): prepara el deck (si juega) y conecta. Eventos mesa://progress, mesa://done; un rechazo del anfitrión es Rejected con su motivo.
red_empezar Trabajo. El anfitrión empieza la partida con los de la sala que trajeron deck; config lleva primero al anfitrión (y su deck) y, tras él, lo que el anfitrión eligió de cada invitado (color y equipo) en el orden de invitados, los ids de la sala (EnSala.id) por asiento; si no son exactamente los jugadores con deck de la sala, Rejected sala_cambio. En red no hay sparring. Mismos eventos. NotFound sin_sala si no hay una sala sin empezar.
red_salir Sale de la partida en red (en la sala o empezada): el anfitrión la cede a otro jugador si puede (Anfitrion::irse) y, si no, la cierra para todos; el invitado se despide. La mesa la cierra mesa_salir.
mesa_fantasma, mesa_senal Movimiento en vivo: mandan un fantasma (carta o cursor, coordenadas 0–10.000) o un aro de 2 s como datagramas; el anfitrión pone el remitente y limita el ritmo. Sin partida en red, o como espectador, no hacen nada.

Servidor de salas y playgroups (salas.rs; el cliente en src-tauri/src/red/servidor/, véase Juego en línea)

Todo comando que necesita el servidor falla con Network sin_servidor si no hay cliente; las negativas del propio servidor llegan como Server (véase Modelo de errores).

Comando Para qué
salas_conectar Arranca el cliente del servidor si no lo estaba (con el apodo) y devuelve EstadoSalas.
salas_estado EstadoSalas: si hay servidor configurado, si está conectado, la clave de esta instalación (yo), la sala y el papel en ella, y el deck preparado para la sala.
salas_playgroups / salas_pg_crear / salas_pg_salir / salas_pg_expulsar / salas_pg_rol Listar, crear y dejar un playgroup, expulsar a un miembro (por clave), cambiar el RolPlaygroup de un miembro; cada uno devuelve los Playgroup[] nuevos.
salas_invitar / salas_revocar Una Invitacion { codigo, caduca } a un playgroup (o, con null, a la sala en la que se está) / revocar una.
salas_usar Usa un código de invitación; devuelve Usada (playgroups o la sala en la que se entró).
salas_sala_crear / salas_sala_unirse Crear una sala (SalaNueva, de 2 a 10 jugadores) / unirse a una como jugador o espectador; devuelven EstadoSala. Los dos, y salas_usar, son Busy partida con una partida o una sala por código abiertas.
salas_sala_salir Sale de la sala (y de su partida, si la hay).
salas_sala_expulsar, salas_sala_expulsar_jugador Expulsan a un miembro de la sala (por clave) o a un jugador de la mesa (por asiento): quien creó la sala, por el servidor de salas, o el anfitrión con un código directo (Rejected no_anfitrion para cualquier otro).
salas_deck Trabajo. Prepara el deck con el que se juega en la sala y se lo dice al servidor. Eventos mesa://progress, mesa://done; NotFound sin_sala fuera de una sala.
salas_empezar / salas_terminar Dicen al servidor que la partida de la sala empieza / ha terminado.

Enlaces de invitación (src-tauri/src/enlace.rs)

Comando Para qué
enlace_pendiente El código del enlace proxybuilder://unirse/<código> con el que arrancó la app, una vez (después, null). Los enlaces que llegan más tarde vienen como el evento salas://enlace.
enlace_invitacion El enlace de invitación de un código (el que se copia en el panel), o null.

Síncrono frente a asíncrono

Los comandos fn normales son síncronos y deben ser baratos (consultas a SQLite, layout::plan). Los comandos async fn esperan la red o delegan el trabajo. El trabajo pesado de CPU se delega de forma explícita: export_pdf usa tauri::async_runtime::spawn_blocking; el diseñador envuelve los renders en un ayudante que se ejecuta en un hilo aparte (commands/diseno.rs, «Corre f en un hilo aparte»). Deducido: como Tauri ejecuta los comandos síncronos normales en el hilo principal, mantenerlos baratos importa para la capacidad de respuesta de la interfaz; consulta la documentación de Tauri al añadir un comando síncrono pesado.

Modelo de errores

AppError

Definido en src-tauri/src/error.rs:

pub struct AppError { pub code: ErrorCode, pub params: BTreeMap<String, String> }
impl AppError { pub fn new(code) -> Self; pub fn with(self, clave, valor: impl ToString) -> Self }
pub type AppResult<T> = Result<T, AppError>;

Deriva Serialize y TS, así que un invoke fallido se rechaza con el objeto JSON { code: "InvalidInput", params: { motivo: "…" } }. Rust nunca escribe prosa para el usuario: params lleva datos para la frase, no un mensaje.

ErrorCode tiene 14 variantes:

Código Significado Orígenes típicos
Network Sin conexión o el servidor no responde scryfall/client.rs (red(), 5xx tras los reintentos, parámetro estado), diseno/letras_libres.rs, diseno/original.rs, actualizador (Reqwest, Network)
RateLimited HTTP 429 tras los reintentos scryfall/client.rs
NotFound Falta una carta, mazo, blob, tema o versión de actualización render.rs leer_blob (parámetro blob), card_link, actualizar.rs
Io Lectura o escritura de disco From<std::io::Error>, fallos de export_open
Db Error de SQLite From<rusqlite::Error>, un lock envenenado es Internal
InvalidInput Disparates enviados desde la pantalla From<LayoutError>, deck_list_save, exportado(), card_export_image (motivo: formato), listas para jugar (falta_lista, lista, lista_sin_nombre), en red (codigo, deck, identidad)
Cancelled El usuario canceló comprobaciones del flag de jobs en resolver, exportar, identificar, PNG, lote de diseño; PdfError::Cancelled; AuthenticationFailed del actualizador
Pdf No se pudo construir el PDF From<PdfError>, errores de imagen de krilla en exportar
Image Imagen ilegible o demasiado grande From<ImagingError>, From<ProbeError>
Unsupported Formato de fichero no aceptado ImagingError::Unsupported, ProbeError::Unsupported; mesa/guardada.rs con motivo: version para una partida guardada o una repetición de un catálogo más nuevo
Busy Un trabajo, o una partida abierta en la mesa, impide esto ahora (motivo: partida cuando es la partida o una sala en red) blob_gc, update_install, red_crear, red_unirse, red_identidad_importar, salas_usar, salas_sala_crear, salas_sala_unirse y las guardas del lado de JS
Internal Inesperado (error de programación, pool de hilos fallido, lock envenenado) varios map_err
Rejected La mesa de juego no acepta esa acción. params.motivo es el Rechazo del motor (NoExiste, SinPermiso, ZonaVacia, FueraDeTurno, Limite, NoSePuede, escrito con {:?}) o un motivo de la capa de la app en snake_case: la sesión (repeticion, como, red, no_existe, no_anfitrion), el encuentro al mejor de (invitado, sin_encuentro, sin_terminar, encuentro_terminado, cambio), la repetición (partida_abierta, sin_registro, sin_terminar, no_es_repeticion), un espectador que actúa (espectador), un anfitrión de código directo sin la mayoría (sin_mayoria) y el rechazo del anfitrión a un invitado (codigo, version, llena, empezada, sin_deck, ritmo, sin_partida, expulsado, no_activo…) mesa/sesion.rs (impl From<Rechazo> for AppError), mesa/encuentro.rs, mesa/repeticion.rs, commands/mesa.rs (mesa_accion, repeticion_abrir, repeticion_actual, mesa_info_carta), red/invitado.rs, salas_sala_expulsar_jugador
Server El servidor de salas ha dicho que no; params.code es el código del protocolo (version, firma, limite, no_existe, caducada, llena, sin_permiso, ya_empezada, expulsado, mensaje), con los params del servidor red/servidor/cliente.rs (pedir, y el saludo inicial, que solo lleva code)

Claves de parámetros usadas en toda la app (contadas con grep): detalle (detalle técnico libre, la más común), motivo (motivo para máquinas como card_does_not_fit, invalid_settings, too_large, corrupt), estado (estado HTTP), blob, url, fichero, deck, carta, ficha, además de las claves de la carga de packs (estilo, hueco, falta, color, version). Trampa conocida: las frases en español de src/lib/errores.ts no contienen {placeholders}, así que estos parámetros hoy no se muestran al usuario; son para registros, tests y textos futuros.

Errores de proxy-core y su conversión

proxy-core no conoce AppError. Cada módulo tiene un enum de thiserror y la app convierte con From:

Error del core Definido en Convertido en Se convierte en
LayoutError (CardDoesNotFit, InvalidSettings) proxy-core/src/layout/mod.rs src-tauri/src/error.rs InvalidInput + motivo
ImagingError (Unsupported, Corrupt, TooLarge, Encode) proxy-core/src/imaging/mod.rs src-tauri/src/render.rs Unsupported; Image + motivo=too_large; Image + detalle
ProbeError (Unsupported, Corrupt) proxy-core/src/imaging/probe.rs src-tauri/src/store/blobs.rs Unsupported; Image + motivo=corrupt
PdfError (Geometry, Image, Write, Cancelled) proxy-core/src/pdf/mod.rs src-tauri/src/commands/export.rs Cancelled; en otro caso Pdf + detalle
PackError proxy-core/src/diseno/pack.rs src-tauri/src/diseno/recursos.rs → de_pack claves del pack (falta, estilo, hueco, color, version)
DisenoError proxy-core/src/diseno/mod.rs capa de aplicación del diseñador Deducido: se convierte en los puntos de llamada con map_err (no leído entero)
LineError proxy-core/src/decklist/mod.rs no es un tipo de error en el puente Se serializa dentro de ParsedLine; la pantalla muestra un texto por variante

También en el crate de la app: From<std::io::Error> (Io) y From<rusqlite::Error> (Db) en error.rs; el actualizador convierte tauri_plugin_updater::Error en actualizar.rs → error().

Ambos crates prohíben unwrap()/expect() fuera de los tests (#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]), así que los fallos deben fluir hacia estos tipos.

Traducción en la interfaz

src/lib/errores.ts:

  • TEXTOS: Record<ErrorCode, string> tiene una frase fuente en español por código, envuelta en paraTraducir(...) para que las herramientas de i18n (pnpm idiomas) la vean; el inglés sale de src/i18n/en/. El tipo Record<ErrorCode, …> hace que el compilador falle si Rust añade un código que la pantalla no cubre (tras pnpm bindings).
  • esAppError(e): objeto cuyo code es una clave de TEXTOS. Los rechazos de invoke son unknown, y un objeto JS plano como { code: "Busy", params: {} } cumple, que es como el frontend lanza Busy por su cuenta.
  • MOTIVOS: Partial<Record<ErrorCode, Record<string, string>>>: una frase propia para los motivos conocidos de un código, buscada por params.motivo o, si no hay, por params.code. Hoy Rejected tiene una por variante de Rechazo, además de los rechazos del anfitrión (véase más abajo) y de partida_abierta, sin_registro, sin_terminar y repeticion (historial y repetición de partidas); InvalidInput tiene los motivos de las listas para jugar (falta_lista, lista, lista_sin_nombre) y codigo, deck, identidad; Unsupported tiene version; Busy tiene partida («Hay una partida abierta: termínala o sal de la mesa para actualizar.», escrita para la actualización pero que sale también cuando se rechaza un comando de red por lo mismo); y Server tiene una por código del protocolo. Los motivos sin entrada (como, red, no_existe, no_es_repeticion, invitado, sin_encuentro, encuentro_terminado, cambio) enseñan la frase general del código. Un motivo desconocido cae en la frase general del código, así un motor más nuevo nunca enseña un motivo en crudo.
  • mensajeDeError(e, t): AppError → t(MOTIVOS[code][motivo] ?? TEXTOS[code], params); Error → su mensaje; cualquier otra cosa → String(e).
  • useLang().mensaje(e) (src/components/language-provider.tsx) es mensajeDeError ligada al t activo. Las pantallas llaman a mensaje(e) en los bloques catch y lo muestran en un toast o en línea.

Lo vigilan src/lib/__tests__/errores.test.ts y el test de Rust el_error_viaja_como_codigo_y_datos en error.rs.

Errores dentro de los trabajos

Un comando de trabajo nunca se rechaza por fallos del trabajo; su evento done lleva error: AppError | null junto a result. El frontend comprueba d.error y trata Cancelled como un resultado silencioso (src/features/decklist/acciones.ts → resolver devuelve null; src/features/export/acciones.ts → exportarA anuncia «Exportación cancelada»).

Motivos del juego en línea

Los textos de src/lib/errores.ts::MOTIVOS cubren también el juego en red: Network con motivo sin_servidor, sin_respuesta o desconectado; NotFound sin_sala; InvalidInput codigo (un código directo que no se entiende), deck e identidad (un fichero importado que no es una identidad); Rejected del host (codigo, version, llena, empezada, sin_deck, ritmo, espectador, sin_partida, expulsado, no_anfitrion, además de los motivos del motor; el rechazo de una acción mandada después le llega a un invitado como MensajeMesa::Rechazo, no como el error de mesa_accion); y todos los códigos de Server, expulsado incluido. red y como (mirar o actuar como otro jugador en red) no tienen frase propia. Los avisos de la red van como eventos (red://aviso, salas://…), no como errores. Ver Juego en línea.

Bindings de ts-rs

Los tipos compartidos derivan TS y llevan #[ts(export)]; algunos también #[serde(rename_all = "camelCase")]. Hay 295 ficheros generados en src/bindings/ (no se editan; la cabecera lo dice).

  • pnpm bindings ejecuta cargo test --manifest-path src-tauri/Cargo.toml --workspace export_bindings. ts-rs genera un test export_bindings_<Type> por cada tipo exportado, y al ejecutar los tests se escriben los ficheros.
  • El directorio de salida lo fija .cargo/config.toml en la raíz del repo: TS_RS_EXPORT_DIR = { value = "src/bindings", relative = true }. Tiene que estar en la raíz, no en src-tauri/.cargo/, porque Cargo lee la configuración del directorio desde el que se lanza, y los scripts de pnpm se lanzan desde la raíz.
  • Los campos u64/i64 usan #[ts(type = "number")] (p. ej. ExportResult.bytes, SaveResult.updatedAt) para evitar bigint.
  • Los comentarios de doc de los tipos de Rust se copian a los ficheros .ts (fíjate en el texto en español de AppError.ts).
  • ErrorCode es una unión de cadenas; AppError.params es { [key in string]: string }.
  • El CI ejecuta pnpm check (que incluye tsc --noEmit), así que un binding desactualizado que ya no coincide con Commands rompe la compilación. Véase Tests y CI.

Trabajos largos y eventos de progreso

Lado de Rust

src-tauri/src/jobs.rs → Jobs (campo AppState.jobs):

Método Comportamiento
alta() Reserva el siguiente id u32 (contador atómico) y un flag de cancelación Arc<AtomicBool>; devuelve ambos.
cancelar(id) Activa el flag; false si el trabajo ya no existe.
baja(id) Quita el trabajo; el trabajo la llama al terminar.

Por separado, AppState::trabajo() devuelve una guarda TrabajoEnMarcha (RAII, contador trabajos); AppState::ocupado() es verdadero mientras viva alguna guarda. Sirve para que la GC de blobs y las instalaciones del actualizador nunca se ejecuten bajo un trabajo largo. Las guardas las mantienen resolve_decklist, export_pdf, identify_batch, design_drafts_for_deck, design_complete_lang, design_export_png, names_index_download, card_fetch, card_fetch_externa, deck_tokens_detect, mesa_empezar, mesa_retomar, red_unirse, red_empezar, salas_deck (busca .trabajo() con grep). ocupado() también es verdadero mientras hay una partida abierta en la mesa (AppState::hay_partida()), porque una actualización reiniciaría la app en mitad de ella.

Forma canónica de un comando de trabajo (de resolve_decklist y export_pdf):

  1. let (job_id, cancelado) = state.jobs.alta();
  2. Lanzar (tauri::async_runtime::spawn o spawn_blocking) y mantener let _marca = estado.trabajo();
  3. Ejecutar, llamando a app.emit("<name>://progress", XProgressEvent { job_id, … }), comprobando cancelado.load(...) entre pasos y devolviendo Cancelled.
  4. estado.jobs.baja(job_id) y después app.emit("<name>://done", XDoneEvent { job_id, result, error }).
  5. Devolver job_id al instante.

La cancelación es cooperativa: no se mata nada; el trabajador comprueba el flag. Tanto job_cancel como export_cancel llaman a jobs.cancelar.

Eventos

Evento Payload (src/bindings/) Emitido por Escuchado por
resolve://progress / resolve://done ResolveProgressEvent { jobId, progress } / ResolveDoneEvent { jobId, result, error } resolve_decklist features/decklist/acciones.ts mediante lanzarTrabajo
export://progress / export://done ExportProgressEvent { jobId, phase (prepare/compose/write), done, total } / ExportDoneEvent export_pdf features/export/acciones.ts
identify://progress / identify://done IdentifyProgressEvent { jobId, done, total, blob, … } / IdentifyDoneEvent { jobId, error } identify_batch features/variants/vincular-lote.tsx
design://progress / design://done DesignProgressEvent { jobId, done, total, … } / DesignDoneEvent { jobId, error } design_drafts_for_deck (diseno/lote.rs) features/disenar/lote-dialog.tsx
designlang://progress / designlang://done El mismo DesignProgressEvent (con la card hecha) / DesignDoneEvent design_complete_lang (diseno/idioma.rs) features/disenar/renumerar.ts (useTrabajo de tipo idioma)
designpng://progress / designpng://done DesignPngProgressEvent, DesignPngDoneEvent design_export_png (diseno/png.rs) features/disenar/png-dialog.tsx
mesa://progress / mesa://done MesaProgressEvent { jobId, mazo, mazos, hechas, total } / MesaDoneEvent { jobId, error } mesa_empezar, mesa_retomar, red_unirse, red_empezar, salas_deck features/mesa/conexion.ts (conProgreso, al que llaman también features/mesa/red/conexion-red.ts y features/salas/conexion-salas.ts)
original://progress { done, total } original_download features/disenar/marcos-originales.ts
packs://progress { id, done, total } pack_downloadable_download features/disenar/packs-descargables.ts
names://progress { fase, hecho, total } names_index_download features/settings/nombres-idiomas.tsx
update://progress { hecho, total } update_download stores/actualizacion-store.ts
salas://enlace string (el código) enlace.rs, cuando un enlace proxybuilder://unirse/… llega a la app ya abierta features/salas/enlaces-entrantes.ts

original://, packs://, names:// y update://progress no tienen id de trabajo: pertenecen a un único comando que se espera (el propio invoke se resuelve cuando termina la descarga), no se pueden cancelar y se escuchan directamente con on().

Algunos eventos no están en Events y se escuchan con listen de @tauri-apps/api/event, con el tipo puesto en la llamada:

Evento Payload Emitido por Escuchado por
red://aviso AvisoRed red/mesa.rs (avisos de la partida en red: sala, inicio, conexión, enlaces) features/mesa/red/conexion-red.ts
salas://conexion, salas://playgroups, salas://sala, salas://enlaces, salas://host, salas://fuera boolean, Playgroup[], EstadoSala, EnlaceSala[], HostSala, MotivoFuera red/servidor/sala.rs features/salas/conexion-salas.ts
mesa://originales OriginalMesa (uno por carta) mesa_originales (commands/mesa.rs) features/mesa/originales.ts

Canales: la mesa de juego

La mesa de juego no usa eventos para su estado: mesa_suscribir recibe un tauri::ipc::Channel<MensajeMesa> creado en la pantalla (new Channel() en src/features/mesa/conexion.ts, con el tipo Channel<MensajeMesa> en Commands). Un canal es de un solo suscriptor y entrega en orden, que es lo que necesita una ristra de parches (un parche perdido o desordenado desincronizaría la vista). La sesión lo guarda y manda Vista { vista, registro } (con los últimos 500 eventos que siguen contando, Registro::vigentes(), REGISTRO_EN_VISTA en mesa/sesion.rs), Parche { parche, evento }, Cerrada o, en red, Rechazo { motivo } cuando el anfitrión no aceptó una intención de esta pantalla (llega después, no como respuesta de mesa_accion); un parche cuya versión no sigue a la anterior hace que la pantalla pida mesa_vista. Volver a suscribirse (tras cambiar de pestaña) sustituye el canal y empieza con la vista entera. Un espectador, que no tiene sesión propia, se suscribe a la vista que mira (AppState.mirada, red/mirada.rs). Véase Mesa de juego.

Frontend: lanzarTrabajo

src/lib/trabajos.ts:

lanzarTrabajo(progreso, final, lanzar: () => Promise<number>, alProgreso)
  : Promise<{ jobId: number; fin: Promise<DoneEvent> }>

Invariantes:

  1. Escuchar antes de lanzar. Un trabajo corto puede terminar antes de que invoke devuelva el id; su evento final se perdería. Por eso se registran primero on(progreso) y on(final).
  2. Los eventos que llegan antes de conocer el id se almacenan en un búfer (pendientesP, pendientesD) y se reproducen cuando llega el id; los posteriores se filtran por jobId.
  3. fin se resuelve con el payload de done (incluso cuando lleva un error); ambos listeners se quitan cuando eso ocurre. Si lanzar() lanza una excepción, se quitan los listeners y se relanza el error.

Probado en src/lib/__tests__/trabajos.test.ts.

trabajo-store y la regla de uno a la vez

src/stores/trabajo-store.ts → useTrabajo guarda el resolver, el exportar, el trabajo de idioma de diseño o la preparación de la mesa en marcha (TipoTrabajo = "resolver" | "exportar" | "idioma" | "mesa"): { tipo, jobId, fase, hechas, total } con empezar, setId, avanzar, terminar. El pie del panel izquierdo y el botón de exportar lo leen. resolver() y exportarA() lanzan { code: "Busy", params: {} } si actual está definido: la regla de un solo trabajo a la vez se hace cumplir en el frontend (Rust solo cuenta los trabajos en marcha para ocupado(); resolve_decklist por sí mismo no rechaza una segunda llamada). cancelarTrabajo() en acciones.ts llama a export_cancel para las exportaciones y a job_cancel en los demás casos. El lote del diseñador, la exportación a PNG y la vinculación en bloque usan lanzarTrabajo desde sus propios diálogos sin useTrabajo.

Diálogos de ficheros

Los diálogos vienen de @tauri-apps/plugin-dialog (capability dialog:default), envueltos solo por src/lib/dialogos.ts:

Función Comportamiento
abrirParaImportar(opciones) open() empezando en la última carpeta de importar; recuerda la carpeta de la primera ruta elegida. Devuelve string[] | null.
guardarParaExportar(opciones, nombre) save() empezando en la última carpeta de exportar con el nombre sugerido; recuerda la elección.
elegirCarpetaParaExportar() Selector de carpeta para exportar imágenes sueltas.
recordar(tipo, ruta) carpeta_recordar sin esperar respuesta; también se usa cuando se arrastran ficheros sobre la app.

Rust recuerda por separado las carpetas de importar y de exportar (commands/carpetas.rs, claves de app_settings carpeta_importar y carpeta_exportar) para que, por ejemplo, exportar un PDF no cambie dónde se abre «importar imágenes». carpeta_inicial devuelve solo el nombre sugerido si no hay nada guardado o la carpeta guardada ha desaparecido; si la consulta falla, inicial() recurre al nombre a secas. CLAUDE.md exige que todos los diálogos de ficheros pasen por este módulo; lo cubre src/lib/__tests__/dialogos.test.ts.

Las rutas vuelven a la pantalla como cadenas simples y van a los comandos de Rust (export_pdf, deck_export_package, font_import…). Rust vuelve a validar lo que abre (exportado() en export.rs, comprobación de .txt en deck_list_save), porque no se confía en que el WebView nombre ficheros arbitrarios. El fichero de capabilities no concede ningún permiso de fs.

Añadir un comando

  1. Escribe #[tauri::command] pub fn|async fn name(state: State<'_, AppState>, …) -> AppResult<T> en src-tauri/src/commands/<área>.rs; pon la lógica real en una función que no sea comando para que los tests puedan llamarla con un AppState construido sobre directorios de tempfile.
  2. Regístralo en generate_handler! en src-tauri/src/lib.rs.
  3. Añade su entrada a Commands en src/lib/ipc.ts (nombres de argumentos en camelCase).
  4. Deriva Serialize, TS con #[ts(export)] en cualquier tipo de payload nuevo; ejecuta pnpm bindings.
  5. Devuelve los errores como AppError con un ErrorCode existente; añade un código solo si la pantalla necesita una frase distinta (entonces añade su texto a TEXTOS y a src/i18n/en/).
  6. Para un trabajo largo: sigue la forma de cinco pasos de arriba, añade ambos eventos a Events y llama a lanzarTrabajo desde la pantalla.
  7. Ejecuta pnpm check, pnpm check:clippy, pnpm test:rust.

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