You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
19 KiB
153 lines
19 KiB
# Arquitectura de tres capas y evaluación de Go — Juegoland v2
|
|
|
|
**Estado:** propuesta técnica del 5 de octubre de 2026. El usuario fija tres capas —API, servidor y frontend— y sitúa la API en Go; solicita valorar también Go para servidor. La recomendación es Go para ambas capas de backend en un monolito modular, con frontend Svelte/TypeScript. La elección de Go para el servidor y el nuevo transporte se documentan como recomendación, no como migración ya aprobada o ejecutada.
|
|
|
|
El [modelo de plataforma](../platform-spec-v2.md), sus [políticas](product-policies.md) y la [API](api-and-events.md) definen comportamiento independiente del lenguaje. El [SDK actual de diseño](../game-engine/contracts.ts) sí tiene firmas TypeScript y el formato v1 describe entradas de módulos: necesita una adaptación explícita para reglas nativas Go. La producción sigue siendo Node/Socket.IO/PostgreSQL.
|
|
|
|
## 1. Las tres capas
|
|
|
|
| Capa | Responsabilidad | Tecnología propuesta | Límite |
|
|
| -------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
| Frontend | Pantallas, interacción, accesibilidad, vistas autorizadas, animación/audio y recuperación de conexión. | Svelte/TypeScript; HTML/CSS y recursos propios. | No conoce secretos, no decide edad/permiso, resultado o azar; no accede a DB. |
|
|
| API | HTTP/tiempo real, negociación, decodificación y límites, sesión/CSRF/origen, correlación y traducción de errores. | Go. | No implementa reglas de juego ni duplica políticas en cada controlador. |
|
|
| Servidor | Casos de uso, autorización de dominio, cuentas, salas, protección, motor, transacciones, plazos, moderación y publicación. | Go recomendado. | Es la única autoridad que confirma cambios. No depende de componentes UI ni llama a su propia API por HTTP. |
|
|
|
|
API y servidor son separaciones de código/responsabilidad. En la primera topología se compilan y despliegan juntos; se llaman mediante funciones e interfaces Go. No hacen falta dos servicios de red ni bases de datos distintas para conservar tres capas. PostgreSQL, correo/SMS, almacenamiento de recursos y métricas son infraestructura de soporte.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
USER[Navegador] --> PROXY[Proxy HTTPS]
|
|
PROXY --> FE[Frontend Svelte]
|
|
PROXY --> API[API Go: HTTP y tiempo real]
|
|
subgraph BACKEND[Un backend modular Go]
|
|
API --> APP[Casos de uso y autorización]
|
|
APP --> DOMAIN[Identidad, salas y protección]
|
|
APP --> ENGINE[Núcleo y reglas propias]
|
|
JOBS[Plazos y tareas] --> APP
|
|
end
|
|
APP --> DB[(PostgreSQL)]
|
|
DB --> OUT[Outbox y entregas autorizadas]
|
|
OUT --> API
|
|
APP --> PROVIDERS[Adaptadores de email, edad y SMS]
|
|
FE --> ASSETS[Recursos públicos por digest]
|
|
```
|
|
|
|
El frontend puede servirse estático si se adopta una compilación compatible, o conservar SSR de SvelteKit con su proceso Node. Usar Go en el backend no elimina por sí solo Node del build o del SSR. Esta decisión de entrega se toma después de inventariar rutas/SSR, SEO y autenticación; no se exige rehacer Svelte.
|
|
|
|
## 2. Por qué Go encaja y qué no resuelve
|
|
|
|
Go proporciona compilación nativa y mecanismos de concurrencia adecuados para servicios de red; su documentación describe estos objetivos en la [FAQ oficial](https://go.dev/doc/faq). Para este producto, las ventajas esperadas son un backend tipado, distribución del ejecutable, herramientas de análisis/prueba y control explícito de cancelaciones, recursos y errores. Son razones de ingeniería, no una medición de que vaya a superar al servicio actual.
|
|
|
|
Los juegos por turnos no justifican por sí solos una reescritura por rendimiento. Persistencia, proyecciones por usuario, fanout, bloqueos y recuperación pueden dominar la latencia. Hay que medir el perfil de carga de las políticas en hardware documentado. Goroutines no sustituyen límites de concurrencia, colas acotadas ni bloqueos de base de datos.
|
|
|
|
Go tampoco resuelve errores del proxy, rutas/NAT, políticas incompletas, autorización o pérdida de mensajes. Mantener las invariantes importa más que el lenguaje. La API en Go será útil si centraliza entrega y llama a una autoridad de dominio clara; añadirla solo como segundo CRUD que escribe sobre los mismos datos que Node introduciría carreras.
|
|
|
|
## 3. Alternativas comparadas
|
|
|
|
| Alternativa | Ventaja | Coste/riesgo | Uso recomendado |
|
|
| -------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
| API y servidor Node | Menor trabajo inicial, reutiliza reglas y SDK TS. | No satisface la dirección de API Go elegida. | Línea base para comparar resultados y mantener producción durante el cambio. |
|
|
| API Go → servidor Node autoritativo | Conserva motor y una sola transacción en Node. | Dos procesos, salto de red, identidad interna, despliegue y trazas duplicados. | Puente temporal si necesitamos API Go antes de portar el motor. |
|
|
| API y servidor Go, reglas Go incluidas | Una autoridad, misma transacción, un runtime de backend y separación de capas por paquetes. | Portar SDK, reglas, validadores y comprobar equivalencia. | Objetivo recomendado al publicar solo juegos propios. |
|
|
| Go + evaluador JS/WASM | Podría conservar módulos de reglas o abrir varios runtimes. | ABI, límites, compatibilidad JS, aislamiento, determinismo y coordinación añadidos. | Fuera del primer alcance; exige una necesidad y un prototipo específico. |
|
|
|
|
No se incluye un intérprete JS o un sistema de plugins dinámicos solo para simular que el SDK TS funciona en Go. Tampoco se cargan `.go` de un paquete en ejecución. Los juegos propios permiten registrar implementaciones compiladas y versionadas junto al servidor. Publicar nuevos gráficos/metadatos y publicar nuevas reglas tienen ciclos distintos.
|
|
|
|
## 4. Autoridad y transacciones
|
|
|
|
Una operación autorizada entra por un caso de uso común, tanto desde HTTP/tiempo real como desde tareas internas:
|
|
|
|
1. Validar estructura, identidad, presupuesto técnico y contexto de autorización.
|
|
2. Abrir la unidad de trabajo. Serializar autorización/revocaciones aplicables y bloquear agregados en orden global: contexto de permisos, sala, partida, registros dependientes. Toda ruta y tarea usa el mismo orden; relaciones de varias personas se ordenan por ID y se revalidan bajo bloqueo.
|
|
3. Buscar recibo propio y comprobar idempotencia; si el comando es nuevo, comprobar permisos, revisiones, admisión y versión instalada.
|
|
4. Ejecutar reglas locales, puras y acotadas con servicios deterministas. Validar transición y proyecciones.
|
|
5. Confirmar sala, partida, resultado, tareas, recibo y outbox en **una transacción PostgreSQL**.
|
|
6. Fuera de la transacción, entregar acuse/publicaciones con permisos actuales y ejecutar efectos externos idempotentes.
|
|
|
|
La documentación de [transacciones Go](https://go.dev/doc/database/execute-transactions) distingue operaciones sobre `sql.Tx` de operaciones sobre el pool: mezclarlas puede ejecutar escrituras fuera de la transacción. Los repositorios participantes reciben la misma unidad de trabajo; no hacen commit interno ni abren otra conexión para «guardar el resultado».
|
|
|
|
Sin llamadas a SMS, verificador, correo, HTTP interno ni espera de UI dentro de un bloqueo. Los efectos externos usan tarea/outbox y conciliación. Un timeout no demuestra que un commit falló: se consulta/reintenta el mismo comando y se recupera su recibo. No se promete exactamente una entrega de red; se evita repetir el efecto de dominio.
|
|
|
|
La revocación debe cerrar entregas también si no llega otro comando: invalidación directa, reautorización de outbox e historial y purga de cachés controladas. Un snapshot con permisos antiguos no se publica por haber sido calculado antes de la revocación.
|
|
|
|
## 5. Qué se conserva del protocolo y qué requiere adaptación
|
|
|
|
| Contrato actual | Tratamiento con Go |
|
|
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| IDs, sobres JSON, revisiones, recibos, estados, vistas por destinatario | Conservar semántica y fixtures. No cambiar la versión de red solo por cambiar de lenguaje. |
|
|
| Manifiesto, recursos, barajas/dados/tableros, presentación y animaciones | Conservar separación entre reglas privadas y recursos/UI públicos. El frontend sigue TS. |
|
|
| `setup`, acciones, validación, `availableActions`, `project`, `projectEvent`, `onLifecycle` | Diseñar interfaces Go equivalentes; las firmas TS son referencia semántica, no código invocable desde Go. |
|
|
| `rules.entry` y compilación TS descrita por formato v1 | Añadir un perfil de ejecución y distribución versionado antes de publicar paquetes Go. El esquema v1 no se modifica de forma incompatible manteniendo la misma versión. |
|
|
| Zod y sus refinamientos | Zod puede seguir como herramienta de autoría/build. Go necesita validación estructural y semántica equivalente; generar tipos no reproduce refinamientos entre campos. |
|
|
| Lock/digests y reglas retenidas | Relacionar paquete y versión con implementación compilada y build; conservar todas las versiones referenciadas por partidas activas. |
|
|
|
|
El futuro perfil nativo tendrá un identificador de implementación registrado por el build, no una ruta suministrada por un jugador. Debe especificar `runtime`, versión de ABI/SDK, compatibilidad de estado y prueba de que el build contiene exactamente la implementación asociada al digest. Sus nombres/campos definitivos pertenecen a la siguiente versión del esquema de paquete, no se introducen como campos libres en v1.
|
|
|
|
No recompilar un módulo con cambios de reglas bajo la misma versión. Una build nueva puede contener varias implementaciones antiguas; readiness impide servir partidas cuyo módulo no esté disponible. El rollback conserva el conjunto completo necesario o detiene nuevas admisiones, nunca sustituye reglas sobre una partida existente.
|
|
|
|
La equivalencia entre runtimes exige vectores compartidos de estado, acciones, errores, proyecciones, resultados y eventos. En azar: mismo algoritmo/versionado y estado, consumo exacto de tiradas, rechazo de sesgo y reglas de barajado. No sustituir un RNG persistido por el generador estándar de Go. Datos dentro del rango seguro JSON, reglas claras para `null`/ausencia, Unicode, orden canónico, timestamps y mapas; no depender del orden de iteración Go ni de aritmética diferente.
|
|
|
|
Los juegos actuales que dependen de una librería JS, como ajedrez, necesitan equivalencia probada de reglas/casos extremos y revisión de licencia al portar o sustituir la dependencia. No basta con que las primeras jugadas parezcan iguales. Las vistas de Hundido deben probar además ausencia de filtraciones por jugador, eventos y errores.
|
|
|
|
## 6. HTTP, tiempo real y compatibilidad de transporte
|
|
|
|
Mantener HTTP JSON bajo `/api/v2`, contratos explícitos y errores de dominio de la API. La recomendación para el backend Go final es WebSocket estándar con un binding propio pequeño de los sobres ya definidos, evitando depender de una implementación no evaluada de Socket.IO en Go. **La API documentada actualmente usa Socket.IO; la recomendación no cambia ese contrato automáticamente.**
|
|
|
|
Socket.IO añade su propio protocolo; su documentación confirma que un cliente Socket.IO no se conecta a un servidor WebSocket simple ([fuente oficial](https://socket.io/docs/v4/#what-socketio-is-not)). Antes de adoptar el binding nuevo se debe publicar y probar:
|
|
|
|
- Ruta y subprotocolo versionados, un sobre por mensaje de texto UTF-8, separación `platform.*`/`game.*` por contrato y límites propios; rechazo de binario y compresión hasta evaluar su necesidad/riesgos.
|
|
- Cookie de sesión, `Origin` permitido y autenticación/CSRF antes de suscribir. El navegador no añade cabeceras arbitrarias a WebSocket: usar primer mensaje de autenticación con nonce/CSRF obtenido por HTTP, timeout corto y conexión todavía sin acceso; ningún secreto en query string.
|
|
- Negociación, ping/pong, timeouts del proxy, cierre/drenaje, backoff con jitter, reconexión, cursores, límites de buffers y recuperación tras segundo plano móvil.
|
|
- Recibos duraderos y barreras iguales a las del protocolo de aplicación. No confundir confirmación de transporte con commit.
|
|
- Política explícita cuando no se puede abrir WebSocket: mensaje recuperable y reintento acotado; cualquier fallback HTTP requiere rutas y pruebas de equivalencia. No prometer long-polling si no se implementa.
|
|
|
|
Durante la transición `/socket.io` sigue con Node para clientes/partidas que lo requieran. El endpoint nuevo negocia de forma distinta; no se redirige el protocolo antiguo al nuevo como si fueran intercambiables. Un adaptador de compatibilidad, si se necesita, traduce transporte hacia la misma autoridad, nunca aplica dos comandos.
|
|
|
|
## 7. Organización de código propuesta
|
|
|
|
Estructura orientativa, no directorios ya creados:
|
|
|
|
```text
|
|
cmd/juegoland/ composición y arranque
|
|
internal/api/http/ handlers, decodificación, CSRF, errores
|
|
internal/api/realtime/ conexión, negociación, suscripciones
|
|
internal/application/ casos de uso y unidad de trabajo
|
|
internal/domain/identity/ cuentas y sesiones
|
|
internal/domain/safety/ edad, teléfono, tutela y permisos
|
|
internal/domain/rooms/ salas, miembros e invitaciones
|
|
internal/domain/chat/ comunicación y redacción
|
|
internal/domain/moderation/ casos, restricciones y recursos
|
|
internal/engine/ núcleo determinista y versiones
|
|
internal/games/ reglas propias registradas por build
|
|
internal/infra/postgres/ repositorios, migraciones y outbox
|
|
internal/infra/providers/ email, SMS y verificador
|
|
internal/jobs/ tareas que invocan casos de uso
|
|
contracts/ esquemas y fixtures compartidos
|
|
```
|
|
|
|
El dominio no importa HTTP, Socket.IO, drivers SQL o SDK de proveedor. API invoca aplicación; infraestructura implementa interfaces requeridas por esta. El punto de composición conecta implementaciones. No crear una jerarquía vacía por cada entidad ni llamar «microservicio» a un paquete.
|
|
|
|
Se adopta una versión soportada de Go al implementar, fijada en toolchain/CI; driver, router y librería WebSocket se seleccionan con mantenimiento, licencias y compatibilidad comprobados. No se instala ni contrata nada en esta fase de especificación.
|
|
|
|
## 8. Migración sin doble autoridad
|
|
|
|
1. Completar contratos de plataforma y fixtures compartidos; inventariar dependencias/estado de los juegos actuales. Mantener producción estable.
|
|
2. Prototipo vertical Go con Conecta 4: cuenta sintética, sala, comando, commit, outbox, reconexión y reinicio con PostgreSQL. Probar también revocación concurrente. Comparar con Node; aún sin tráfico real.
|
|
3. Probar Hundido para información oculta/acciones independientes y Brisca para azar/mazos. Fijar SDK Go, perfil de paquete y binding antes de migrar el resto.
|
|
4. Si se necesita API Go mientras el dominio sigue Node, Go delega la operación completa al dueño Node, sin escritura parcial. Canal interno autenticado, origen no público, identidad verificable y deadlines; Node sigue validando permisos y controla toda la transacción. Una identidad en cabecera sin autenticar no vale.
|
|
5. Preferir drenar salas activas antiguas en su propietario y enviar **nuevas** salas por cohortes al backend nuevo. No repartir la escritura de una misma sala entre Go y Node. Identidad y protección mantienen una autoridad común, y una revocación afecta a ambos antes de nuevas entregas.
|
|
6. Si un traslado de partida activa fuera imprescindible: detener nuevas escrituras, bloquear y cambiar propietario con generación/fencing duradero, migrar snapshot validado sin repetir `setup`, confirmar antes de enrutar. El servidor anterior rechaza su generación obsoleta. No activar este camino sin pruebas de caída en cada paso.
|
|
7. Retirar Node de juego solo cuando no queden partidas/compatibilidades que dependan de él. SSR/build de frontend se evalúan por separado. Rollback mantiene contratos, esquema, versiones y políticas de seguridad; no vuelve a habilitar chat a menores.
|
|
|
|
No existe una transacción `sql.Tx` compartible entre procesos Go y Node. Tampoco soluciona ese problema «usar la misma base de datos» con commits independientes. Si en un futuro Go calcula mediante un evaluador remoto sin efectos, deberá hacerlo fuera de locks y confirmar por comparación de todas las revisiones/lecturas relevantes; eso es otro diseño y no la topología inicial recomendada.
|
|
|
|
## 9. Criterios de decisión antes de implementar el cambio completo
|
|
|
|
- Go conserva equivalencia semántica de reglas, filtros privados, RNG y recibos, y puede instalar/retener versiones de juego.
|
|
- Existe un único dueño por agregado, una unidad de trabajo para sala/partida y revocación consistente entre interfaces.
|
|
- Fixtures JSON pasan en Go y TS, incluidos casos negativos, refinamientos y canonicalización; ningún generador se trata como prueba de equivalencia.
|
|
- Pruebas de integración con PostgreSQL, carreras, proceso detenido tras commit, restart, outbox y cliente lento; carrera de datos Go en pruebas pertinentes.
|
|
- Perfil de carga y restauración de las políticas medidos en el despliegue candidato; frontend móvil y accesibilidad verificados.
|
|
|
|
La recomendación queda, por tanto, en **frontend Svelte/TypeScript + API Go + servidor/motor Go**, con separación lógica entre API y dominio y un backend modular al inicio. Su principal coste es portar y probar el runtime de juegos; los contratos y recursos ya diseñados siguen siendo una inversión aprovechable.
|