Skip to content

IPC y manejo de errores

Pablo Martínez López edited this page Nov 9, 2026 · 23 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,
    // ... 216 entries
])

Cada área es un módulo en src-tauri/src/commands/ (mod.rs los lista): actualizar, cards, carpetas, decklist, diseno, export, fichero, fuentes, iconos, identify, imagen, layout, library, mesa, pasar, 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, igual que pbdeck_pendiente (src-tauri/src/abrir.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.
app_plataforma Plataforma (escritorio, android o ios, de cfg!(target_os)): la pantalla lo pregunta una vez (src/lib/plataforma.ts) para decidir el comportamiento táctil y qué esconder en un móvil («Mostrar en carpeta», por ejemplo). Es el mismo enum que lleva el Hola del servidor de salas para que no se elija un móvil de anfitrión.
app_salir app.exit(0): el botón «atrás» de Android en la biblioteca no tiene «cerrar», así que la pantalla pregunta dos veces («Pulsa otra vez para salir») y luego llama a esto, tras guardar lo suyo.
app_pantalla_completa(si) Esconde o enseña las barras del sistema de Android. La pantalla ya no la llama: la actividad tiene la app a pantalla completa todo el rato (Móvil). En escritorio no hace nada (sin_barras_es_ok toma el Unsupported de android.rs como «nada que hacer»; cualquier otro error se devuelve).
app_compartir_texto(texto) Comparte un texto (un código de partida o un enlace de invitación) con otra app del móvil. El plugin solo comparte ficheros, así que el texto va en invitacion.txt de la caché y se comparte como text/plain. Unsupported fuera de Android: la pantalla solo lo ofrece allí.

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.

Ficheros en el móvil (fichero.rs, y pbdeck_pendiente en src-tauri/src/abrir.rs; véase Rutas y URI content://)

Comando Función
fichero_temporal(nombre) Una ruta nueva dentro de la caché (app_cache_dir/fichero/<uuid>/<nombre>, nombre saneado) donde se escribe una exportación antes de compartirla. La copia se conserva: Copia::conservar.
compartir_fichero(ruta) Comparte un fichero con otra app (ACTION_SEND). Solo un content:// o un fichero dentro de app_cache_dir (canonicalizado, así que ni .. ni un enlace sacan de ella); cualquier otra ruta es InvalidInput, motivo: compartir_fuera, para que la vista web no pueda dar a otra app la base de datos ni la clave del nodo (fichero::para_compartir). Corre en spawn_blocking. Unsupported fuera de Android.
pbdeck_pendiente El content:// de un .pbdeck que otra app abrió con esta (o con el que arrancó la app), una sola vez (luego null). Los siguientes llegan como el evento pbdeck://abierto. Rust no importa nada en ese momento: la pantalla lo ofrece, con las opciones del importador, y la persona confirma.

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, si esta instalación puede actualizarse (soportada) e instalaElSistema (Android: el instalador del sistema pide confirmar y relanza la app; no hay «reiniciar»).
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).
tag_icon_set Pone o quita (null) el icono de una etiqueta, un id de Iconify (InvalidInput, motivo: icono, si no lo es).
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[]. Los nombres pueden ser URI content://.
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.
optimizar_decks, optimizar_estimar(ids), optimizar_lanzar(ids) Los decks con imágenes que optimizar, lo que se liberaría y el trabajo (optimizar://progress, optimizar://done; se cancela con job_cancel); Busy con un trabajo o una partida en marcha.
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).
cartas_buscar Una página de la pantalla Buscar (FiltrosCartas, página desde 1) → PaginaCartas (cartas, total, hayMas). Sin consulta o sin resultados es una página vacía, no un error. Véase Búsqueda de cartas.
carta_ficha / carta_aclaraciones Los detalles de una impresión (FichaCarta); sus aclaraciones (AclaracionCarta[]).
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 o a un content:// (varios ficheros si el mazo se divide; en Android, un solo ZIP). Eventos export://progress, export://done.
export_estimate Tamaño aproximado del PDF (bytes, images, pages), cuántos PDF saldrán (files) y el aviso de memoria (memory, lowMemory); véase Exportación.
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. En Android export_open comparte el fichero en su lugar y export_reveal es Unsupported.

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.

Iconos (iconos.rs; véase Iconos)

Comando Función
iconos_colecciones Todas las colecciones visibles de Iconify (ColeccionIconos { prefijo, nombre, total, categoria }), en caché una semana.
iconos_nombres(prefijo) Los nombres de una colección, en caché una semana.
iconos_buscar(texto) Busca por nombre en todas las colecciones (hasta 999 ids).
iconos_datos(ids) { id: DatosIcono { body, vb } } de los iconos pedidos (memoria, elegidos en disco o la API); los cuerpos SVG peligrosos se descartan.
iconos_elegir(id) Guarda un icono elegido en app_data_dir/iconos/elegidos.json para verlo sin red; NotFound si no existe.

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 salvo el chat y los avisos rápidos; en red, lo que hacen los demás jugadores se sigue aplicando a la partida de verdad.
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_apodo Cambia el nombre de esta app en la sala de espera: el anfitrión se lo dice a todos al momento, un invitado se lo pide al anfitrión (AlHost::Apodo). NotFound sin_sala sin sala.
red_anfitrion_deck El anfitrión cambia el nombre del deck con el que se le ve en la sala de espera (None: ninguno). NotFound sin_sala si no hay sala o ya empezó.
red_deck Trabajo. Un invitado cambia el deck (y la lista) con el que juega en la sala de espera: lo prepara y se lo manda al anfitrión. Eventos mesa://progress, mesa://done.
red_vuelta La partida en red de la que este invitado se cayó y a la que puede volver (Option<VueltaRed>: apodo y hora), de red/vuelta.rs.
red_vuelta_olvidar Olvida esa partida.
red_volver Trabajo. Se vuelve a unir a esa partida con su código, como jugador sin deck (el anfitrión reconoce el nodo y le devuelve el asiento); también con la mesa abierta tras perder la conexión. NotFound sin_vuelta si no la hay; un Rejected del anfitrión la olvida.
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.

Pasar decks entre equipos (pasar.rs; la lógica en src-tauri/src/pasar/, véase Pasar decks)

Comando Función
pasar_recibir_abrir / pasar_recibir_estado / pasar_recibir_cerrar / pasar_recibir_cancelar «Recibir»: abrirlo (levanta el nodo de iroh compartido si hace falta; devuelve el EstadoReceptor con el código PBD1-, su QR y el código de seis cifras), leer su estado, cerrarlo o cancelar lo que se esté recibiendo. Los cambios llegan como el evento pasar://receptor.
pasar_peticion(aceptar) / pasar_responder(decisiones) Aceptar o rechazar a un equipo de la red local que quiere enviar; contestar, deck por deck, qué hacer con la oferta (Decision: cuánto coger y, si el mismo deck ya existe, Resolucion).
pasar_cerca_buscar / pasar_cerca_parar Buscar equipos cerca con «Recibir» abierto (mDNS; en Android toma el bloqueo de multicast de la Wi-Fi); la lista llega como pasar://cerca.
pasar_compartir_codigo(texto) Ofrece el código de «Recibir» a otra app con el selector del sistema. El texto tiene que ser un código PBD1- válido. Solo en Android; fuera, Unsupported.
pasar_codigo_comprobar(texto) Comprueba un código PBD1- pegado o leído antes de enviar. Uno de partida (PB1-) pegado aquí da InvalidInput con motivo: codigo_de_partida; y uno de deck pegado donde va el de partida (red_unirse) da codigo_de_deck, que tiene frase propia. Véase Modelo de errores.
pasar_nombre / pasar_nombre_set(nombre) El nombre de este equipo al pasar decks; vacío vuelve al del sistema. Asíncronos: en Android preguntan al plugin de Kotlin.
pasar_enviar(destino, decks, modo) / pasar_enviar_cancelar / pasar_emisor_estado / pasar_emisor_olvidar Enviar decks a un Destino (equipo cercano o código); vuelve al momento y el avance llega como pasar://emisor.
pasar_completar(deckId) Trabajo (un id, con pasar://imagenes-progress y pasar://imagenes-done): baja las imágenes que le faltan a un deck recibido como «solo originales».
deck_paquete_info, deck_paquete_guardar, deck_paquete_compartir Tamaño del .pbdeck de un deck en cada ModoPaquete (original, optimizado, sinProxies); guardarlo (una ruta o un content://) o compartirlo con otra app en uno de ellos.
paquete_leer, paquete_importar Leer un .pbdeck sin importarlo (qué trae y si ya hay el mismo deck aquí) / importarlo con lo que eligió la persona; InvalidInput eleccion si no eligió nada y falta_resolucion si el deck ya existe y no se dio resolución.

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

Comando Para qué
enlace_pendiente El código del enlace proxybuilder://unirse/<código> (o tapete://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; con el motivo de error::red / error::estado_http (HTTP) o error::red_sin_http (iroh, mDNS), una frase por causa error.rs (red, estado_http, red_sin_http; esta última la usa pasar/), que usan scryfall/client.rs, fuentes/mod.rs, commands/cards.rs (Archidekt), ocr.rs, diseno/{letras_libres,letras_cjk,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, un content:// que no se puede abrir (io_uri de fichero.rs)
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), un código de la clase equivocada (codigo_de_deck, codigo_de_partida, red/codigo.rs), pasar decks (eleccion, falta_resolucion, oferta_grande, sin_espacio, sin_oferta, huella, importar), leer_bytes de fichero.rs por encima de su tope (fichero_grande) y compartir_fichero con una ruta fuera de la caché (compartir_fuera)
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, o algo que solo existe en otra plataforma 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; android.rs fuera de Android (detalle: android), export_reveal sobre un content:// (detalle: content), elegirCarpetaParaExportar en un móvil (detalle: carpeta, lanzado en la pantalla) y, en Android, motivo: instalar_permiso cuando el sistema aún no deja a esta app instalar paquetes (véase Versiones y actualizaciones automáticas)
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, optimizar_lanzar, update_install, red_crear, red_unirse, red_volver, 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: casi ninguna frase de src/lib/errores.ts contiene {placeholders}, así que estos parámetros no suelen mostrarse al usuario; son para registros, tests y textos futuros. La excepción es estado en las frases de Network rechazo y estado.

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 e instalar_permiso (Android: «Android necesita tu permiso para instalar la actualización: se ha abierto su ajuste…»); InvalidInput tiene además codigo_de_deck (el código de «Recibir un deck», PBD1-, pegado donde va el de una partida); 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.

Los fallos de HTTP corriente (Scryfall, Archidekt, fuentes externas, descargas) montan su error Network con crate::error::red(reqwest::Error) o crate::error::estado_http(StatusCode), nunca a mano, para que la pantalla los distinga: conexion (no se pudo conectar: revisar la conexión, el cortafuegos o el antivirus), tiempo (se pasó el plazo), respuesta (una respuesta que la app no entiende), rechazo (401/403, con {estado}: suele ser una VPN, un proxy o la red bloqueada) y estado (cualquier otro estado de error, con {estado}). Las dos funciones escriben además una línea warn en proxy-builder.log con el host y la cadena entera de causas, que es lo que hay que pedir cuando alguien cuenta un problema de red. Los fallos de red que no son de HTTP (las conexiones de iroh y el mDNS de Pasar decks) usan en cambio crate::error::red_sin_http(motivo, Option<&dyn Error>): el mismo Network con motivo y, con un error de fondo, la misma cadena de causas en el registro y en detalle; sin él (un plazo, un rechazo del otro lado) solo se apunta el motivo. El código sigue siendo Network, así que los reintentos y los matches!(e.code, ErrorCode::Network | …) siguen funcionando. Ver Integración con Scryfall.

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_volver, 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_volver, 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
ciclo://pausa / ciclo://vuelta ninguno ciclo.rs (solo Android): la app pasó a segundo plano / volvió lib/ciclo-de-vida.ts (guarda el deck y la carta que se diseña) / features/mesa/red/conexion-red.ts (la partida en red reconecta)
pbdeck://abierto string (un content://) abrir.rs, cuando otra app abre un .pbdeck con esta features/library/abrir-pbdeck.ts
pasar://receptor, pasar://emisor, pasar://cerca EstadoReceptor, EstadoEmisor, EquipoCerca[] pasar/ features/pasar/estado.ts
pasar://imagenes-progress / pasar://imagenes-done ImagenesProgreso / ImagenesFin pasar_completar features/pasar/imagenes.ts
salas://enlace string (el código) enlace.rs, cuando un enlace proxybuilder://unirse/… o tapete://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. No en un móvil (Unsupported, lanzado en la pantalla): Android no tiene selector de carpetas.
elegirDestinoDeImagenes({ titulo, nombre }) Dónde poner imágenes que van en carpetas por idioma: una carpeta en escritorio, un ZIP en un móvil.
recordar(tipo, ruta) carpeta_recordar sin esperar respuesta; también se usa cuando se arrastran ficheros sobre la app.

En Android los selectores del sistema devuelven una URI content:// en vez de una ruta, así que nada de esto se aplica allí: no se recuerda nada, defaultPath es solo el nombre propuesto, los filtros pasan a tipos MIME (filtrosParaAbrirEnMovil, filtrosParaGuardarEnMovil: Android no conoce .pbdeck ni .ttf, así que un filtro de abrir con una extensión desconocida se descarta y se puede elegir cualquier fichero, y un filtro de guardar sin tipo conocido cae en application/octet-stream, con el que Android respeta el nombre tal cual) y la pregunta es esMovil() de src/lib/plataforma.ts. Los caminos del móvil están en Rutas y URI content://.

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 (en Android, URI) 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.

Rutas y URI content://

En escritorio la pantalla le da a Rust una ruta y Rust la usa. En Android un diálogo devuelve en cambio una URI content://: un permiso que el sistema da a ese único fichero, sin carpeta, a menudo sin extensión en la URI y a veces con un proveedor que solo se puede leer como tubería. La regla es que todo comando que lee o escribe un fichero que elige la persona pasa por src-tauri/src/fichero.rs, nunca std::fs sobre una ruta de diálogo, y que en escritorio el resultado es exactamente el de antes (una ruta es una ruta). El formato y el nombre llegan como parámetro o del nombre que da Android (DISPLAY_NAME), nunca de la URI.

Función Qué hace
es_uri(ruta) true para una cadena que empieza por content://.
leer(app, ruta, nombre?) Pone el fichero al alcance de std::fs: una ruta tal cual; una URI copiada a la caché (app_cache_dir/fichero/<uuid>/), porque el .pbdeck es un ZIP y necesita Seek, que la tubería de un proveedor en la nube no da. La Copia devuelta borra la carpeta al soltarse.
leer_bytes(app, ruta, tope) El contenido entero; una URI por encima de tope bytes es InvalidInput con motivo: fichero_grande.
escribir(app, ruta, nombre?, f) Da a f una ruta local donde escribir: la real, o un fichero temporal de la caché que luego se copia a la URI. Una URI no se puede renombrar, así que escribir_atomico (el .part y renombrar) no existe allí.
escribir_bytes, copiar_a Escribir bytes / copiar un fichero local a una ruta (a trozos, a un nombre provisional, y luego renombrado) o a una URI.
temporal(app, nombre) Una ruta vacía nueva en la caché, que se borra al soltarse (Copia::conservar la mantiene). Sirve para exportar y para compartir.
para_compartir(app, ruta) Deja el fichero donde el FileProvider del proyecto de Android lo ve (la caché) y devuelve la ruta y el tipo MIME (mime_de); la copia se queda hasta el siguiente barrido. Solo acepta una URI o un fichero que ya esté dentro de la caché (si no, compartir_fuera).
zip_de_carpeta, zip_de_lista Empaquetan ficheros en un ZIP (imágenes, PDF y ZIP se guardan tal cual; el resto, con deflate).
nombre_seguro, nombre_destino Un nombre que se puede crear en cualquier carpeta (sin separadores ni caracteres de control, no vacío, 120 caracteres) / el nombre que tendrá lo que se escriba, el de Android para una URI.

Por qué una copia a la caché y no leer en flujo: los importadores (.pbdeck, packs ZIP, letras) quieren un fichero con Seek, y una copia reduce los tres tipos de origen (ruta, URI local, URI en la nube) a uno. Por qué Acceso: la lógica corre contra el trait Acceso { abrir, nombre }, implementado sobre tauri-plugin-fs en la app (DeLaApp, que abre una URI con app.fs().open(..) y obtiene un std::fs::File de verdad del descriptor que da el sistema) y sobre ficheros locales en los tests. tauri-plugin-fs se usa solo desde Rust: al WebView no se le concede ningún permiso fs:. La carpeta de la caché se barre al usarla (lo que lleve más de 24 horas en fichero/ se borra).

Abrir un fichero desde fuera (otra app, ACTION_VIEW) es src-tauri/src/abrir.rs: el plugin de Android entrega una URI, que se guarda (PENDIENTE) y se anuncia con pbdeck://abierto; la pantalla pide pbdeck_pendiente una vez si aún no escuchaba. Solo valen cadenas content:// de 2048 caracteres como máximo (valida). Rust no importa nada en ese momento.

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

Mobile · Móvil

Frontend · Frontend

Reference · Referencia

Clone this wiki locally