Skip to content
Moku
@moku-labs/room pages

stage

The host's facade. Create the room, change the state, receive intents. Added by the app.

On this page

What it does

Added by the app, on the shared screen. stage is a thin surface over the engines. It owns no state, no config and no timers. Every method delegates to session, sync or intent.

How it works

It depends on all four engines and re-declares the six room:* events. A game plugin with depends: [stagePlugin] therefore gets typed hooks for all of them. The facade forwards nothing: the event bus is global, so the engines' emits already reach your hooks.

Config

None. Each option lives on the engine that owns it.

API

Mounted at app.stage, reachable with ctx.require(stagePlugin).

MethodSignatureDescription
createRoom() => RoomDescriptorSynchronous. Returns { code, joinUrl, qr: null, hostToken }. Throws when a room is already active.
qr() => Promise<QrMatrix | null>The matrix for the join URL. null with no room or with generateQr: false.
mutate(ns: string, recipe: (draft: Cells) => Cells) => voidReturns the next cells of a slice. Sent on the next tick. Throws on an unregistered slice.
broadcast() => voidSends pending changes now as a delta. No-op when nothing changed.
onIntent(name: string, handler: (payload: unknown, peerId: PeerId) => void) => () => voidSets the handler of a registered intent. Returns the unsubscribe.
roster() => readonly RosterEntry[]Connected phones, oldest first.

register and registerSlice are not here. Call them on app.intent and app.sync, once, before onIntent, mutate and createRoom.

Events

Emits

None of its own. Re-declares all six room:* events for types.

Listens to

None.

Depends on

transport, session, intent, sync.

Example

src/game.ts
import { createApp, createPlugin, stagePlugin } from "@moku-labs/room";

const gamePlugin = createPlugin("game", {
  depends: [stagePlugin],
  onStart: ctx => {
    const stage = ctx.require(stagePlugin);
    ctx.log.info("game:players", { count: stage.roster().length });
  },
  hooks: ctx => ({
    "room:peer-joined": ({ peerId }) => {
      ctx.require(stagePlugin).mutate("players", draft => ({ ...draft, [peerId]: 0 }));
    },
    "room:peer-left": ({ peerId }) => {
      ctx.require(stagePlugin).mutate("players", draft => ({ ...draft, [peerId]: null }));
    }
  })
});

export const app = createApp({ plugins: [stagePlugin, gamePlugin] });
await app.start();
app.sync.registerSlice("players", {});
app.stage.createRoom();
  1. null deletes a cell on the phones.
  • onIntent before register does nothing. The handler is not stored, and no error is raised. Register first.
  • broadcast sends no snapshot. It flushes a delta. For a full snapshot to one phone, use app.sync.broadcast(peerId).
  • mutate returns, it does not edit. Return a new object. The draft is the live state.