Capítulo referencia de la guía Neblla — todos los métodos, eventos y propiedades del SDK, y la plantilla completa. El contrato base está en core.txt.

API de referencia

Cada método indica dónde se puede llamar: (CLIENT), (SERVER) o (BOTH).

Métodos — salas, ciclo de vida

neblla.joinRoom(roomName) (CLIENT) — entra en una sala. Dispara el evento joinRoom. Las salas nuevas empiezan en estado gathering — el presentador llama a startRoom() para pasar a running y arrancar room.js.

neblla.leaveRoom() (CLIENT) — sale de la sala actual y vuelve al lobby. Dispara leaveRoom.

neblla.bookRoom({ config?, length? }) (BOTH) — reserva un nombre de sala único. Devuelve Promise<name>. length (3–12, por defecto 5) fija cuántos caracteres tiene el código, p. ej. { length: 3 } si tu UI tiene 3 casillas. No entra en la sala — llama a joinRoom(name) después. El config opcional se persiste en la sala futura y se expone como neblla.room.config dentro de gathering.js y room.js.

neblla.persistRoom({ name, type, whitelist? }) (CLIENT) — sube una sala recién reservada de ephemeral a public o private. Llamar entre bookRoom y el primer joinRoom — una vez alguien entra, el tipo queda fijado. La whitelist (para private) bootstrappea miembros iniciales (el llamador se añade como primer owner). Los ids de la whitelist son per-app userIds (los id de neblla.room.users[i]), no personIds — solo se resuelven contra usuarios online en esta app en ese momento. Los offline se añaden después vía inviteToRoom.

neblla.startRoom(name?) (CLIENT) — solo presentador. Pasa la sala de gathering a running. El SDK elige al server peer y arranca room.js. Con argumento name, hace joinRoom + startRoom en una sola operación.

neblla.createRoom({ length? }) (CLIENT, @legacy) — crea y se une a una sala con nombre único (length igual que en bookRoom). Reservado por compatibilidad con apps anteriores al split bookRoom → joinRoom → startRoom. Para código nuevo usa ese flujo.

Métodos — salas persistentes: listado, inspección, borrado

neblla.listRooms({ type? }) (CLIENT) — lista salas persistentes accesibles al usuario. Cada entrada: { name, type, creator, memberCount, activeUsers, lastActivityAt }.

neblla.getRoom(name?) (CLIENT) — inspecciona una sala por nombre (por defecto, la actual).

neblla.getMembers(name?) (CLIENT) — solo private. Devuelve { members[], invites[] }.

neblla.deleteRoom(name?) (CLIENT) — borra una sala persistente. Moderadores (cualquier sala) y owners de privadas (la suya). Echa a todos al lobby y emite roomDeleted.

neblla.setRoomType(name, type) (CLIENT) — promociona/baja entre public y private. Solo moderadores.

Métodos — salas privadas, membresía

neblla.acceptInvite(roomName) / neblla.rejectInvite(roomName) (CLIENT) — responde a una invitación memberInvited.

neblla.kickFromRoom(targetUserId) (CLIENT) — expulsa de la sala privada actual. Solo owner o moderador.

neblla.abandonRoom(roomName?) (CLIENT) — renuncia a la membresía de una sala privada. Distinto de leaveRoom: leave = "me desconecto ahora", abandon = "quítame de la whitelist".

neblla.promoteOwner(targetUserId) / neblla.demoteOwner(targetUserId) (CLIENT) — gestión de owners. Solo owner. No puedes auto-demotearte si eres el último.

Métodos — historial de chat (persistentes)

neblla.getRoomHistory({ name?, before?, limit? }) (CLIENT) — paginación de chat persistido. Newest-first. Default limit 50, max 200. Los mensajes borrados vienen con body: null y deleted: true.

neblla.deleteMessage(messageId, name?) (CLIENT) — soft-delete. Autor, owner o moderador. Emite messageDeleted a todos.

Métodos — mensajería

neblla.sendMessage(data) (BOTH) — broadcast a TODOS en la sala. Desde CLIENT, también te llega a ti mismo con fromMe: true, y los clientes lo reciben en el evento 'message' (broadcast entre pares). Desde SERVER, llega a todos los clientes en el evento 'messageFromServer' — es la voz autoritaria del juego, no hay un "yo" al que reflejarlo.

neblla.tellPresenter(data) (CLIENT) — mensaje privado al presenter. Solo llega al dispositivo con isPresenter === true, en el evento 'messageToPresenter'. No se difunde al resto.

neblla.tellServer(data) (CLIENT) — mensaje privado al server (el dispositivo que ejecuta el archivo de server/ de la fase actual). Llega en el evento 'messageToServer' — a server/room.js si la sala está en running, a server/gathering.js si está en gathering, a server/lobby.js si estás en el lobby. No se difunde al resto de clientes. No-op si la sala aún no tiene server peer.

neblla.setState(state) (SERVER) — emite el nuevo estado autoritario. Todos los dispositivos de la sala reciben el evento 'state' y neblla.state queda actualizado en cada uno. Esto incluye a los peers de respaldo con room.js cargado, así que si el server-peer cae, su sucesor arranca con el último state ya disponible.

neblla.sendFile(file, opts?) (CLIENT) — transferencia P2P (WebRTC) de un fichero a los demás peers de la sala. Emite los eventos 'fileStart' / 'fileProgress' / 'file' para que los receptores muestren progreso y lean el blob final. No pasa por Neblla — directo entre dispositivos. El emisor manda el SHA-256 del fichero; el receptor lo verifica sobre el blob reensamblado antes de emitir 'file'. Si no cuadra (transferencia truncada/corrupta) emite 'fileError' en vez de 'file' — nunca entrega un blob roto en silencio.

neblla.setRoomData(data) (CLIENT, presenter o server peer) — sustituye neblla.room.data (persistente en salas public/private). Todos los miembros de la sala reciben 'roomDataUpdated'. Sujeto al cap maxDataSize; un exceso emite 'roomDataError'.

Métodos — comunidad (BETA, función community)

Todos devuelven promesa; rechazan con err.code. Detalle en comunidad.txt.

neblla.community.publish({ title, summary?, category?, tags?, lang?, contentType, payload, parentId? }) (CLIENT, login) → entry. Nace published o pending según el modo de moderación. neblla.community.update(entryId, patch) (CLIENT, login) → entry. Solo el autor o un moderador. neblla.community.unpublish(entryId) (CLIENT, login) — el autor retira su entry (borrado real). neblla.community.mine(opts?) (CLIENT, login) → { entries, next }, todos los estados propios. neblla.community.list({ q?, category?, tags?, lang?, contentType?, owner?, parent?, sort?, cursor?, limit? }) (BOTH) → { entries, next } solo published, sin payload. neblla.community.facets(['category'|'tags'|'lang'|'contentType'], { parent? }) (BOTH) → conteos por valor. neblla.community.get(entryId) (BOTH) → { entry, payload }. neblla.community.relate(entryId, { favorite?, rating?, review? }) (CLIENT, login) → { relation, stats }. Merge parcial; null borra una ranura. El autor no valora ni reseña lo suyo. neblla.community.relation(entryId) (CLIENT, login) → { favorite, rating, review, reported }. neblla.community.favorites(opts?) (CLIENT, login) → { entries, next }, mis favoritas publicadas. neblla.community.reviews(entryId, opts?) (BOTH) → { reviews, next }, reseñas públicas. neblla.community.report(entryId, { reason, message? }) (CLIENT, login) → { ok, suspended }. Una por usuario y entry; al umbral la entry vuelve a pending. neblla.community.shareLink(entryId, { uses?, expiresInDays?, deleteEntry? }) (CLIENT, login, autor) → { token, expiresAt, uses }. Enlace privado a una entry (una publish({ ..., visibility: 'private' }) no se lista). neblla.community.openLink(token) (CLIENT, login) → { entry, payload }; gasta un uso. Desconocido, gastado o caducado → link_not_found.

neblla.community.links({ entryId? }) (CLIENT, login) → tus enlaces [{ id, entryId, usesLeft, expiresAt, createdAt, deleteEntry }], los más nuevos primero. id = hash del token (no abre nada).

neblla.community.revokeLink(id) (CLIENT, login) → { revoked: true }. Solo el autor (not_owner); desconocido → link_not_found. Con deleteEntry, revocar el último enlace borra la entry. neblla.community.queue({ status?, cursor? }) / neblla.community.moderate(entryId, { action, reason?, message? }) (CLIENT, moderadores de la app) — cola y acciones approve | reject | remove | restore.

Métodos — identidad y persistencia

neblla.setPublicInfo(info) (CLIENT) — tu perfil público (visible a toda la sala). Nombres, avatares, colores.

neblla.save(json, opts?) (CLIENT) — persiste datos. Local o servidor según login. Por defecto reescribe el objeto entero; opts.merge = 'shallow' | 'deep' | 'crdt' hace que el servidor funda tu escritura con lo guardado ('crdt' = LWW-Map, gana el más nuevo por campo, converge entre dispositivos; null borra un campo). Tras un merge el servidor devuelve el doc resultante → neblla.me.data se actualiza y se dispara 'dataMerged'.

neblla.login(opts?) (CLIENT) — SSO. opts.scopes lista los permisos pedidos: email, friends, discoverable.

neblla.logout() (CLIENT) — cierra sesión de esta app. No cierra la sesión SSO de Neblla.

Métodos — amigos (requiere scope friends)

neblla.requestFriend(target) — petición a un usuario online en esta app. neblla.acceptFriend(requestId) / neblla.rejectFriend(requestId). neblla.removeFriend(friendId) — funciona offline. neblla.inviteToRoom(target) — añade a la whitelist de tu sala privada. neblla.inviteFriend(target) — invita casual a un amigo a tu sala (cualquier tipo). neblla.inviteToApp() — abre un popup de Neblla para invitar a un amigo de Neblla A TU APP (la app no ve la lista de amigos). Requiere login. Fire-and-forget: la invitación va a la bandeja del amigo en play.neblla.com (caduca en 5 min) y lo trae a tu app. No requiere el scope friends. neblla.getFriends() / neblla.getFriendRequests() — refresh manual.

Métodos — eventos

neblla.on(event, callback) (BOTH) — registra listener. '*' para todos. neblla.off(event, callback) (BOTH) — desregistra. '' para todos.

Métodos — IA

neblla.ai({ prompt, preset?, model?, userToken? }) (CLIENT) — devuelve Promise<{ text, usage, quota }>.

Métodos — comunicaciones con el dev

neblla.sendCommunication({ message, data?, dateFrom?, dateTo?, thread? }) (CLIENT) — manda mensaje al backoffice. neblla.updateCommunication(commId, {...}) (CLIENT) — edita (mientras no esté read). neblla.deleteCommunication(id) (CLIENT) — borra propia.

Métodos — compras (BETA)

neblla.buy(productId, amount?) (CLIENT) — abre checkout. neblla.consume(productId, amount) (CLIENT) — consume unidades. neblla.validatePurchase(userId, productId) (BOTH) — verifica posesión. neblla.getProducts() (BOTH) — lista productos.

Métodos — anti-cheat

neblla.confirm(eventName, value) (CLIENT) — observar y reportar. neblla.flag(reason?) (BOTH) — marcar sala como sospechosa.

Eventos — mensajería

'message' (BOTH) — broadcast a la sala. { data, user, fromMe, time, p2p }. Las flags toServer / toPresenter del payload viejo desaparecen — esos mensajes ahora viven en sus propios eventos.

'messageToServer' (SERVER) — mensaje dirigido al server vía tellServer. Solo llega a room.js (o lobby.js / gathering.js según el estado de la sala). No se difunde al resto de clientes. { data, user, time }.

'messageFromServer' (CLIENT) — mensaje del server al cliente. Llega a todos los clientes de la sala. Se distingue de 'message' (broadcast entre pares) porque es la voz autoritaria del juego. { data, time }.

'messageToPresenter' (CLIENT, en el dispositivo del presenter) — mensaje dirigido al presenter vía tellPresenter. Solo llega al cliente que tiene isPresenter === true. { data, user, time }.

'state' (BOTH) — el server emitió un nuevo estado autoritario con setState(). Todos los dispositivos lo reciben (incluidos los backups que tienen room.js cargado, listos para promocionarse). neblla.state queda actualizado automáticamente con el último valor.

Eventos — ciclo de vida de sala

'joinRoom' (CLIENT) — entraste a una sala. { room, isLobby }

'roomStarted' (BOTH) — el presentador llamó a startRoom(). room.js arranca en el server peer. isMe true si tú eres el peer. { room, isMe }

'newServerPeer' (BOTH) — el SDK reasignó el server peer (el anterior se fue). room.js arranca fresco en el nuevo con neblla.state ya cargado. { room, isMe }

'flagged' (CLIENT) — sala marcada como sospechosa. Una vez por sala (anti-troll). { room, reason, flaggerId, userCount }

'leaveRoom' (CLIENT) — { room, user }

'memberJoinedRoom' (BOTH) — { user, room, isLobby, alreadyPresent }. alreadyPresent es true para los que ya estaban cuando tú entraste (snapshot inicial).

'memberLeftRoom' (BOTH) — { user, room }

'memberJoined' / 'memberLeft' (BOTH) — alias compactos. Forma del payload: { room: <nombre>, user }. NO confundir con memberJoinedRoom, que pasa el objeto room completo + isLobby + alreadyPresent. Si te importa esa metadata extra usa memberJoinedRoom.

'gathering' (CLIENT, lobby) — refresh del listado de salas en formación. { rooms: [{ name, config, users }] }. Reflejado en neblla.gathering.

'roomDataUpdated' (BOTH) — alguien llamó setRoomData(...). { room, data }. 'roomDataError' (CLIENT) — tu setRoomData(...) fue rechazado (size cap u otro). 'roomJoinError' (CLIENT) — fallo entrando a una sala (whitelist, kick, etc).

'communityUpdated' (CLIENT) — moderación tocó una entry mía de la comunidad. { entry, action }, action ∈ published | pending | rejected | removed | restored | modified | suspended; entry.moderation trae { reason, message }.

'fileStart' / 'fileProgress' / 'file' / 'fileError' (CLIENT) — transferencia P2P de sendFile. 'file' trae el blob completo, ya verificado por SHA-256. 'fileError' ({ reason: 'checksum_mismatch' | 'size_mismatch', ... }) salta cuando el blob reensamblado no cuadra con lo declarado por el emisor.

Eventos — salas persistentes

'memberInvited' (CLIENT) — owner añadió a invitados pendientes. 'inviteAccepted' / 'inviteRejected' (CLIENT). 'memberKicked' (CLIENT) — quitado de la whitelist por owner/moderador. 'kickedFromRoom' (CLIENT) — push directo al expulsado. 'memberAbandoned' (CLIENT) — se auto-quitó. 'ownerAdded' / 'ownerRemoved' (CLIENT). 'roomDeleted' (CLIENT) — { room, reason, by? }. 'roomTypeChanged' (CLIENT).

Eventos — chat persistido

'messagePersisted' (CLIENT) — eco con el id necesario para borrar. 'messageDeleted' (CLIENT) — render tombstone. 'messageDropped' (CLIENT) — solo al autor, mensaje no persistido (too_large o rate_limited).

Eventos — otros

'newPresenter' (CLIENT) — presentador cambió. { user, isMe, room, isLobby }. 'publicInfo' (CLIENT) — { user, publicInfo }. 'login' (CLIENT) — { type, data, grantedScopes, email? }. 'logout' (CLIENT) — {}. 'friendsUpdated' (CLIENT) — { friends }. 'friendRequest' (CLIENT) — { requestId, from }. 'friendAccepted' / 'friendRemoved' (CLIENT). 'roomInvite' (CLIENT) — { from, roomName, appId, type }. 'appInvite' (CLIENT) — un amigo de Neblla te invitó a esta app (llegaste por el enlace de la bandeja, o te invitó estando ya dentro). { from } con from = { id, name, picture, slug, userId|null }. También rellena neblla.invitedBy. 'dataMerged' (CLIENT) — el servidor resolvió un save(data, { merge }). { data, merge } — data es el doc resultante (ya copiado a neblla.me.data). 'reconnected' (CLIENT) — { room, isLobby }. 'communications' (CLIENT) — sync inicial. 'communicationUpdated' (CLIENT) — { communication, action }. 'ai' (CLIENT) — respuesta de neblla.ai(). 'purchase' (CLIENT) — { userId, productId, type, purchases }. 'sessionExpired' (CLIENT) — {}. 'app_maintenance' (CLIENT) — el dev está migrando la BBDD de la app a otro backend (ver identidad.txt). Tu socket se desconectará en ~2 segundos; el SDK reconecta automáticamente después. Aprovéchalo para mostrar un mensaje tipo "estamos en mantenimiento, vuelve en un momento". { reason: 'byodb_migration', retryInSeconds: 30 }.

Propiedades

neblla.me (CLIENT) Tu objeto user. .data con tus saves. neblla.users (BOTH) Mapa { [id]: user } de la sala. neblla.room (BOTH) { name, users[] } sala actual. neblla.presenter (BOTH) El user presentador actual. neblla.state (BOTH) Último estado emitido por el server con setState(). Útil en peers de respaldo al promocionarse — room.js arranca con el último valor ya disponible. neblla.communications (CLIENT) Array de comunicaciones del usuario. neblla.friends (CLIENT) Amigos visibles en esta app. neblla.friendRequests (CLIENT) Peticiones recibidas. neblla.invitedBy (CLIENT) Quién te invitó a esta app vía bandeja de Neblla, o null. { id, name, picture, slug, userId|null }. neblla.purchases (CLIENT) Shortcut a neblla.me.purchases. neblla.gathering (CLIENT, lobby) Array de salas en formación visibles desde el lobby, cada una con { name, config, users }. Se mantiene sincronizado con el evento 'gathering'.

No existen neblla.server ni user.isServer — el server peer es opaco a propósito.

El objeto user expone { id, publicInfo, isPresenter, isMe }. publicInfo está vacío hasta que el usuario llama a setPublicInfo().

Plantilla completa — turn-based

Esqueleto completo y funcional para el patrón turn-based. Los demás patrones siguen la misma forma con las adaptaciones del final.

index.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Mi App</title>
  <link rel="stylesheet" href="main.css" />
  <script src="https://neblla.com/connect/app/APP_ID"></script>
</head>
<body>
  <button id="loginBtn">Log in</button>
  <button id="logoutBtn" hidden>Log out</button>
  <div id="status">Connecting…</div>
  <div id="roomControls" hidden>
    <button id="createRoomBtn">Create room</button>
    <input id="roomCodeInput" placeholder="code" maxlength="8">
    <button id="joinRoomBtn">Join</button>
    <button id="leaveRoomBtn" hidden>Leave room</button>
  </div>
  <div id="players"></div>
  <div id="game"></div>
  <script src="client.js"></script>
</body>
</html>

main.css


body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; }
#players .player { padding: .25rem .5rem; }
#players .player.presenter::before { content: "★ "; }
#players .player.me { font-weight: bold; }

client.js


let isLobby = true;

window.onNeblla = (neblla) => {
  const $ = (id) => document.getElementById(id);
  $('status').textContent = 'Connected. In: ' + neblla.room.name;
  $('roomControls').hidden = false;

  $('loginBtn').onclick      = () => neblla.login();
  $('logoutBtn').onclick     = () => neblla.logout();
  $('createRoomBtn').onclick = () => neblla.createRoom();
  $('leaveRoomBtn').onclick  = () => neblla.leaveRoom();
  $('joinRoomBtn').onclick   = () => {
    const code = $('roomCodeInput').value.trim();
    if (code) neblla.joinRoom(code);
  };

  neblla.on('joinRoom', (event) => {
    isLobby = event.isLobby;
    $('status').textContent = 'In room: ' + event.room.name;
    $('leaveRoomBtn').hidden = isLobby;
    renderPlayers();

    if (isLobby) {
      $('game').innerHTML = '';
    } else {
      // render game board
    }
  });

  neblla.on('messageFromServer', (event) => {
    if (event.data.type === 'state') {
      // render(event.data.state);
    }
  });

  neblla.on('state', (state) => {
    // render(state);
  });

  neblla.on('memberJoinedRoom', renderPlayers);
  neblla.on('memberLeftRoom',   renderPlayers);
  neblla.on('publicInfo',       renderPlayers);
  neblla.on('newPresenter',     renderPlayers);

  neblla.on('login', () => {
    $('loginBtn').hidden  = true;
    $('logoutBtn').hidden = false;
  });

  neblla.on('logout', () => {
    $('loginBtn').hidden  = false;
    $('logoutBtn').hidden = true;
  });

  function renderPlayers() {
    const el = $('players');
    el.innerHTML = '';
    neblla.room.users.forEach(u => {
      const div = document.createElement('div');
      div.className = 'player'
        + (u.isPresenter ? ' presenter' : '')
        + (u.isMe ? ' me' : '');
      div.textContent = (u.publicInfo.name || u.id.slice(0, 8))
        + (u.isMe ? ' (you)' : '');
      el.appendChild(div);
    });
  }
};

room.js


// Top-level. neblla en scope global. Sin DOM. Sin neblla.me.

let gameState = neblla.state || null;

neblla.on('messageToServer', (event) => {
  if (event.data.type === 'action') {
    gameState = applyAction(gameState, event.user.id, event.data);
    neblla.setState(gameState);
  }
});

neblla.on('memberJoinedRoom', () => {
  if (gameState) neblla.setState(gameState);
});

Adaptaciones