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`. `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: , 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 ```html Mi App
Connecting…
``` ### main.css ```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 ```javascript 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 ```javascript // 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 - **Host + controllers**: en `client.js` ramificas con `if (neblla.me.isPresenter) renderPresenterView(); else renderControllerView();`. - **Collab realtime**: `room.js` vacío. Todos los peers iguales. `sendMessage` para cambios locales, aplicar entrantes en el handler. - **Solo + save**: `room.js` vacío, sin salas. Solo `neblla.save()` y `neblla.me.data`.