Capítulo servidor de la guía Neblla — modos de ejecución, estado autoritario, handover y anti-cheat. El contrato base está en core.txt.

El servidor

Tres modos de ejecución

Los archivos de server/ corren en una de tres "casas". Se cambia con un selector en Settings → Server mode, sin tocar la lógica del juego:

1. peer + JavaScript (default) — Neblla elige uno de los dispositivos conectados para ejecutar room.js. Ese dispositivo es el servidor durante la partida; al terminar la sala, desaparece. Coste cero. Riesgo: el dueño del dispositivo puede inspeccionar la lógica en DevTools y manipularla. 2. peer + WebAssembly — room.ts en AssemblyScript (subset estricto de TypeScript) se compila a .wasm en el navegador del dev al subirlo — sin build server. La lógica sigue en un peer, pero el estado vive en memoria lineal de WASM, opaca a DevTools. Detiene al hacker casual. La traducción JS→AssemblyScript es mecánica (el contrato del servidor es estricto: solo eventos, sin DOM, sin métodos de usuario) — una IA puede hacerla desde el editor integrado o vía MCP. 3. infraestructura de Neblla — un proceso por sala, estado persistente durante toda la sesión, clientes que no pueden inspeccionar ni impersonar el servidor. Mismo código fuente.

El contrato que hace posible la portabilidad (se paga escribiendo así desde el día 1):

Elección del server peer

El SDK elige al server peer al hacer startRoom(). Su identidad es opaca: no hay neblla.server ni user.isServer. Nunca escribas código que inspeccione el id del server peer.

Estado autoritario — setState

neblla.setState(state) (SERVER) emite el estado a todos los dispositivos de la sala — incluidos los peers de respaldo que tienen room.js cargado en standby:


// room.js (corre en el dispositivo-servidor)
let state = neblla.state || { tablero: initialBoard(), turno: 0 };

neblla.on('messageToServer', event => {
  if (esLegal(state, event.data)) {
    state.tablero = aplicar(state.tablero, event.data);
    state.turno++;
    neblla.setState(state);   // emite 'state' a todos, replica a backups
  }
});

// client.js
neblla.on('state', state => render(state));
console.log(neblla.state);   // siempre el último valor

Sincronizar a nuevos jugadores:


// room.js
neblla.on('memberJoinedRoom', () => {
  neblla.setState(gameState);   // emite 'state' a todos
});

Usa setState, no sendMessage, para el estado:


// ❌ Mal — mensaje genérico:
neblla.sendMessage({ type: 'state', state: newState });

// ✅ Bien — primitivo de estado:
neblla.setState({ tablero, turno });

setState() replica a los backups y emite 'state' en una sola llamada. Si el server-peer cae, el handover es instantáneo porque los backups ya tienen el último state.

Handover y sucesión (modo peer)

El peer-servidor puede caerse en cualquier momento (pestaña cerrada, túnel, batería). Tres piezas lo resuelven:

1. setState() replica el estado a los peers de respaldo (arriba). 2. La sucesión está preacordada: cada dispositivo sabe quién es el siguiente en línea. Sin elección ni negociación. 3. La transición no espera al disconnect: cuando la conectividad del servidor pasa a inactive, el handover se dispara inmediatamente. El sucesor arranca room.js fresco con el último neblla.state ya en mano. Evento 'newServerPeer' { room, isMe } (isMe true si tú eres el nuevo peer). La pausa es imperceptible.

El usuario que perdió la conexión conserva su periodo de gracia (más largo que el del servidor): si vuelve a tiempo, reentra como miembro normal, sin ser ya el servidor.

En infraestructura nada de esto hace falta: el proceso es único y persistente. setState() sigue funcionando igual (emite el evento), pero no hay backups que mantener.

Server sin DOM


// ❌ Mal — room.js toca el DOM:
document.getElementById('counter').textContent = state.turno;

// ✅ Bien — room.js solo cambia estado, el cliente reacciona:
// room.js
neblla.setState({ ...state, turno: state.turno + 1 });

// client.js
neblla.on('state', state => {
  document.getElementById('counter').textContent = state.turno;
});

document y window están sombreados en server-side: el código mal escrito se rompe en desarrollo, no al cambiar de modo. La UI vive en client.js.

tellServer, no broadcast filtrado


// ❌ Mal — broadcast con filtro manual:
neblla.sendMessage({ type: 'move', square: 'e4', forServer: true });

// ✅ Bien — primitivo dedicado:
// cliente
neblla.tellServer({ move: 'e4' });

// room.js
neblla.on('messageToServer', event => {
  // solo llegan aquí los mensajes para el servidor
});

tellServer no se difunde al resto de clientes. tellServer es no-op si la sala aún no tiene server peer.

Protecciones siempre activas

Anti-cheat opt-in

Relevante en modo peer: el peer-servidor podría anunciar resultados falsos (corre en un dispositivo que su dueño controla). En infraestructura no aplica.

neblla.confirm(eventName, value) (CLIENT) — observar y reportar. Cada cliente llama a confirm cuando observa un evento crítico. El servidor recoge los reportes durante unos 3 segundos y comprueba:

Si las dos respuestas son sí, silencio. Si alguna falla, el SDK del servidor llama solo a neblla.flag(). Un peer-servidor mentiroso queda atrapado porque el client.js de cada testigo reporta su propio estado local.

El value tiene que ser primitivo (string, number finito, boolean). No vale objeto, array, null, undefined ni NaN — cada afirmación debe ser un dato inequívoco que la sala pueda votar.


// client.js, al detectar el ganador anunciado por el server
neblla.on('messageFromServer', event => {
  if (event.data.type === 'state' && event.data.state.ganador) {
    neblla.confirm('ganador-final', event.data.state.ganador);
  }
});

// ❌ Mal — pasa un objeto:
neblla.confirm('estado', { tablero, turno });  // el SDK lo rechaza

// ✅ Bien — afirmaciones inequívocas:
neblla.confirm('ganador', userId);
neblla.confirm('score-final', 1250);

neblla.flag(reason?) (BOTH) — marca la sala como sospechosa. Se llama automáticamente cuando confirm falla; también a mano desde cualquier cliente o server-side. Una vez marcada, los siguientes flag() en la misma sala son no-ops (anti-troll). El flagCount sube en todos los usuarios de la sala (logueados lo persisten, anónimos en sesión) y se ve en el backoffice. Evento 'flagged' { room, reason, flaggerId, userCount } para que la app reaccione, una vez por sala.

Consejo práctico. Lanza en modo peer todo lo que no sea dinero real. Pon confirm() en los eventos que importan (ganador, score final, envío a leaderboard). Pasa a infraestructura solo cuando cada resultado tenga que ser infalsificable en tiempo real. Al cambiar, los confirm() se quedan donde están: en infra son redundantes y el SDK los ignora sin coste de bandwidth.