14 KiB
Modelos de datos compartidos — Go, Svelte y Flutter
Estado: contrato base definido el 6 de octubre de 2026 por indicación del usuario. Hay una fuente JSON Schema, DTOs generados TypeScript/Go/Dart y fixtures comprobables. Esto no implementa sesiones, salas, WSS ni una app Flutter. Los adaptadores de error del runtime y los contratos completos de esos flujos siguen pendientes.
1. Una fuente para los tres ámbitos
Los datos que cruzan una frontera se definen en contracts/shared/schema.json, con JSON Schema 2020-12. De esa fuente se generan TypeScript, Go y Dart. No se mantienen tres definiciones manuales del mismo DTO. Los nombres JSON, tipos, campos obligatorios, ausencia/null, códigos, unidades y significado deben ser iguales.
Un DTO compartido no es una entidad de base de datos, un componente Svelte ni un widget Flutter. Go mantiene entidades y causas internas separadas y proyecta el DTO autorizado; web y app lo decodifican y lo representan. Ningún campo del DTO acredita actor, edad o permiso. La lectura con otro lenguaje no cambia la información que recibe un mismo destinatario autorizado.
| Familia definida | Modelos y reglas |
|---|---|
| Identidad y versión | UUID: representación canónica minúscula; Revision: entero entre 0 y 2^53−1, sin pérdida de precisión JSON. Un UUID no concede autorización. |
| Tiempo e idioma | Instant: RFC 3339 UTC con Z; Locale: etiqueta estructural BCP 47. El patrón de idioma no sustituye canonicalización, soporte real ni revisión de traducciones. |
| Presentación de mensajes | LocalizedMessage { key, args }; argumentos escalares acotados, sin HTML, objetos anidados ni valores secretos. Misma clave/argumentos para cada SDK; traducción según preferencia. |
| Errores | ErrorData, ErrorCategory, ErrorOrigin, OperationOutcome, Recovery, Violation; núcleo único en todos los transportes. |
| Correlación y transporte | Correlation, HttpProblem, ErrorMessagePayload, CommandRejection; contexto separado del error duradero. |
| Catálogo | GameCard, CatalogPage, CatalogResponse; nombres y valores compatibles con la base actual. Antes de adoptar el límite de 100 fichas se materializa paginación en servidor. |
Los valores enteros deben seguir el rango seguro aunque Go/Dart dispongan de enteros más amplios. NaN/infinito no son datos JSON admitidos. Listas y mapas vacíos se emiten como [] y {}, no null; en Go deben inicializarse. Un campo opcional se omite; null solo se acepta donde el esquema lo declara, como nextCursor. Los instantes UTC son distintos de los intervalos monotónicos *Ms ligados a clockEpoch del perfil temporal.
Los modelos de cuentas, salas, admisión, operaciones, recibos completos y proyecciones se incorporarán por cortes, desde los documentos que definen su dominio. Una clase con nombre parecido no convierte esos flujos pendientes en implementados. Cada campo nuevo debe declarar destinatarios y tratamiento de privacidad además de su tipo.
2. Núcleo común del error
Todos los errores públicos usan ErrorData, sin un formato diferente para Svelte, Go o Flutter:
{
"modelVersion": 1,
"origin": "server",
"code": "ROOM_FULL",
"category": "conflict",
"outcome": "rejected",
"message": { "key": "errors.roomFull", "args": {} },
"recovery": { "action": "change-selection" },
"violations": []
}
| Campo | Información que comparte |
|---|---|
modelVersion |
Versión del modelo, independiente de /api/v2, protocolo de juego y versión de renderer. |
origin |
server, client o transport. Una pérdida de red normalizada por el SDK no se presenta como un rechazo del servidor. |
code |
Identificador estable de causa pública. Nunca una frase, tipo de excepción Go/Dart/JS o código HTTP usado como sustituto de causa. |
category |
Clasificación común para políticas de presentación/diagnóstico: validación, autenticación, autorización, conflicto, disponibilidad, etc. No concede permisos. |
outcome |
Lo que se sabe sobre la operación, según la sección siguiente. Impide equiparar timeout con rechazo confirmado. |
message |
Clave traducible y argumentos públicos tipados. No se programa comparando frases traducidas. |
recovery |
Acción semántica de recuperación y retryAfterMs opcional. El SDK combina esa indicación con el estado local y la idempotencia. |
violations |
Lista de campos inválidos: pointer JSON Pointer, código y mensaje estructurado. Vacía si no corresponde. No devuelve el valor recibido. |
El esquema permite códigos futuros con el mismo formato; un código desconocido recibe mensaje genérico y conserva categoría/resultado válidos para diagnóstico. Una clave de traducción desconocida no rompe la pantalla. Una categoría, acción o versión desconocida requiere fallback seguro de protocolo, sin ejecutar automáticamente una recuperación nueva. Los escritores solo emiten campos publicados; los lectores pueden ignorar extensiones opcionales desconocidas. No interpretan esos campos como permisos ni almacenan/loguean todo el objeto recibido.
3. Resultado incierto y recuperación
outcome |
Significado |
|---|---|
not-applicable |
Lectura, negociación o fallo sin una mutación de dominio asociada. |
not-applied |
Existe evidencia de que este intento no aplicó la operación. No garantiza que otro envío del mismo comando esté sin resolver. |
rejected |
Rechazo de negocio definitivo registrado en un recibo del servidor. Reenviar el mismo UUID devuelve ese recibo; no vuelve a ejecutar. |
unknown |
No se puede demostrar aplicación o rechazo. Puede haber ocurrido un commit. |
unknown exige recovery.action: resolve-pending y, en un sobre, referencia propia de comando u operación. El cliente consulta el recibo/estado pertinente y, cuando corresponda, reenvía el mismo sobre. No crea otro UUID, cambia precondición ni anuncia fracaso definitivo para desbloquear una acción duplicada. Un fallo local de decodificación después de enviar una mutación también conserva incertidumbre. Autenticación usa su registro de intento específico; no se inventa un recibo de juego para recuperar credenciales.
Las acciones publicadas son none, correct-input, authenticate, complete-requirements, resync, resolve-pending, retry-same-request, change-selection, update-client y contact-support. retry-same-request solo se utiliza con evidencia y una operación reintentable/idempotente. rejected no autoriza ese reintento automático. retryAfterMs es un entero no negativo, máximo 24 h en este perfil, y solo acompaña reintento/consulta pendiente; cuotas superiores necesitan otra política/versionado.
Una nueva intención después de rechazo/revisión obsoleta exige oferta actual y decisión del usuario. Los errores no contienen comandos ejecutables, destinos arbitrarios ni nuevas credenciales. Una recomendación de recuperación no elude condiciones, protección, membresía o generación de mando.
4. HTTP, mensajes y recibos
HTTP utiliza Problem Details, RFC 9457, con error y correlation como extensiones propias:
{
"type": "urn:juegoland:problem:room-full",
"title": "La sala está completa",
"status": 409,
"correlation": {
"requestId": "f8aa2fa1-aec8-41c8-a27b-644e999df03a",
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9"
},
"error": {
"modelVersion": 1,
"origin": "server",
"code": "ROOM_FULL",
"category": "conflict",
"outcome": "rejected",
"message": { "key": "errors.roomFull", "args": {} },
"recovery": { "action": "change-selection" },
"violations": []
}
}
Se emite application/problem+json, con status igual al HTTP real, caché no-store y título público localizado cuando proceda. type se deriva de code como urn:juegoland:problem: más código en minúsculas y guiones; cada código necesita documentación antes de publicarse. La URN identifica el tipo y no se descarga desde el cliente. title es fallback humano, sin ser la autoridad de causa; los SDK usan el mismo error.
Los mensajes v2 platform.error/game.error incorporan ErrorMessagePayload { correlation, error } dentro de su sobre negociado. No llevan un estado HTTP ficticio. Los rechazos de negocio incorporan el mismo error en su recibo, conservando recurso/revisiones/UUID según el contrato completo. CommandRejection valida solo ese fragmento; no constituye un recibo completo de sala o juego. Un rechazo necesita origen servidor y resultado rejected.
El error duradero de un recibo no cambia al reintentarlo ni al cambiar idioma/dispositivo. Correlation pertenece a la entrega: requestId de intento/traza, commandId estable, operationId o subscriptionId cuando corresponda y esté autorizado. No se guarda una nueva traza dentro del recibo para modificarlo en cada consulta. Si la recepción ya identifica el comando, un adaptador no elimina esa correlación al normalizar el fallo. Un ID de soporte no expone stack, hosts, SQL o estado privado.
Los fixtures comunes contienen el mismo núcleo ROOM_FULL en HTTP, mensaje y rechazo; el verificador compara igualdad. La nueva estructura reemplaza los errores planos del borrador de plataforma al adoptar este contrato. El protocolo de juego v1 conserva sus sobres/códigos; cualquier puente normaliza al modelo común en el SDK sin modificar silenciosamente v1.
5. Códigos, localización y privacidad
Cada código publicado registra significado, categoría, clave/argumentos, estados HTTP admitidos y recuperaciones válidas en su contrato de dominio. No se renombra o reutiliza para otra causa. Las categorías de API siguen siendo referencia de plataforma; los juegos registran sus causas al publicar el paquete. Los códigos son extensibles, pero los argumentos/recovery deben estar documentados y probados.
| Ejemplo | Categoría / recuperación |
|---|---|
INVALID_REQUEST |
validation; corregir entrada; violations solo de campos públicos del formulario. |
SESSION_EXPIRED |
authentication; reautenticar y eliminar vistas privadas. |
FORBIDDEN |
authorization; mensaje genérico sin identificar a terceros. |
ROOM_FULL |
conflict; cambiar selección; rechazo confirmado si ya tiene recibo. |
STALE_CONTROL_GENERATION |
conflict; recuperar recibo/operación y sincronizar antes de una nueva intención. |
RATE_LIMITED |
rate-limit; demora acotada; distinguir intento no aplicado de una intención anterior pendiente. |
CONNECTION_LOST |
transport; resolver pendiente si se envió una mutación; lectura reintentable si no la hubo. |
INVALID_RESPONSE |
protocol; fallback local; conservar incertidumbre de una mutación enviada. |
Web y Flutter traducen las mismas claves/argumentos revisados de es/en; idioma de interfaz no cambia código, resultado ni recuperación. No se presentan mensajes crudos de excepciones. Las causas internas permanecen en diagnóstico restringido, fuera del DTO. No se incluyen teléfono/OTP, prueba de edad, tokens, datos de otra plaza ni payload rechazado. También violations evita enumerar recursos ajenos; un fallo de secreto/credencial conserva el mensaje genérico de su política.
Los fallos de proxy, HTML inesperado, bytes inválidos, cortes y timeout se normalizan localmente con esta estructura, manteniendo origin y el resultado que realmente se conoce. El SDK no inventa un requestId servidor ni una causa de negocio a partir de un 502 genérico.
6. Generación, conformidad y adopción
npm run contracts:generate
npm run contracts:check
go test ./contracts/shared/go
dart contracts/shared/dart/verify.dart
dart analyze contracts/shared/dart
El generador y el verificador funcionan desde la raíz. contracts:check detecta si un DTO generado se separó del esquema y verifica ejemplos positivos/negativos. El esquema es la fuente de estructura; las relaciones semánticas de las secciones 3/4 se comprueban también en validate.ts y en los fixtures compartidos. Esas relaciones no se deducen solo de los tipos generados. El perfil del generador cubre únicamente las construcciones usadas en esta fuente; ampliarlo exige fixtures y revisión de las tres salidas. No es un generador general de OpenAPI ni del SDK de reglas.
Los bindings Go/Dart son DTOs estructurales: sus tipos/decodificación no sustituyen rangos, enums, relaciones, límites ni autorización. La validación de esquema/semántica se verifica en las herramientas TypeScript; Go/Dart comprueban lectura/serialización de los mismos ejemplos válidos. Antes de usar los adaptadores en red deben superar también los casos negativos con validadores propios conformes. La recepción textual necesita además límites de bytes/profundidad, rechazo de claves duplicadas y Unicode inválido antes de parsear; JSON Schema sobre un objeto no detecta claves duplicadas.
Adopción por cortes: usar los tipos compartidos para el catálogo; integrar ErrorData en adaptadores HTTP y errores locales web; después binding/recibos v2 y futura app con su validador. Una capacidad sigue desactivada hasta tener su camino completo verificado. Las entidades privadas nunca se serializan directamente por haber generado una clase pública.
Comprobación inicial: 22 modelos generados sin deriva, 14 fixtures válidos y 25 rechazos de esquema/semántica; núcleo de error idéntico entre HTTP, mensaje y recibo. Go y Dart conservan los 14 fixtures al decodificar/serializar; análisis Dart y pruebas de API Go pasan. TypeScript del frontend y Svelte pasan; para Svelte se usó temporalmente configuración por defecto equivalente a las opciones de compilador actuales, porque cargar Vite intentaba escribir una caché bloqueada por el entorno. No se hizo un ensayo de binding WSS ni de una app Flutter.