Skip to content
Moku
@moku-labs/room pages

Quick start

A shared screen and a phone: create a room, send an intent, read the synced state.

On this page

A room in five steps

  1. Install. @moku-labs/core and @moku-labs/common are peer dependencies. trystero and qrcode come with Room.

    install
    bun add @moku-labs/room @moku-labs/core @moku-labs/common
  2. Write the stage. The screen everyone looks at. It registers its state and its intents, then creates the room.

  3. Write the phone. It joins with the code from the URL, renders the state it receives, and sends intents.

  4. Serve both pages. Room is a browser library. Bundle the two files with any tool and serve them on one origin the phones can reach.

  5. Open the stage, scan the code. The phone joins the room directly over WebRTC.

The code

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

const app = createApp({ plugins: [stagePlugin] });
await app.start();

app.sync.registerSlice("scores", {});
app.intent.register("score", {
  fields: { points: { type: "number", min: 1, max: 10 } },
  additionalFields: false
});

app.stage.onIntent("score", (payload, peerId) => {
  const { points } = payload as { points: number };
  app.stage.mutate("scores", draft => ({
    ...draft,
    [peerId]: Number(draft[peerId] ?? 0) + points
  }));
});

const { code, joinUrl } = app.stage.createRoom();
console.log(`Join ${code} at ${joinUrl}`);
  1. Register before createRoom. A phone gets every registered slice in its first snapshot.
src/phone.ts
import { controllerPlugin, createApp } from "@moku-labs/room";

const app = createApp({ plugins: [controllerPlugin] });
await app.start();

const code = new URLSearchParams(location.search).get("room") ?? "";
await app.controller.joinRoom(code);

app.controller.on("scores", scores => console.log(scores));
app.controller.intent("score", { points: 1 });
  1. Room does not read the URL. The join URL ends in ?room=CODE, and your page passes the code on.

Try it without phones

inMemory() connects apps in one process, with no WebRTC. This script runs under bun.

try.ts
import { controllerPlugin, createApp, inMemory, stagePlugin } from "@moku-labs/room";

const signaling = inMemory();
const transport = { signaling, iceServers: [] };

const stage = createApp({ plugins: [stagePlugin], pluginConfigs: { transport } });
await stage.start();
stage.sync.registerSlice("scores", {});
stage.intent.register("score", { fields: {}, additionalFields: false });
stage.stage.onIntent("score", (_payload, peerId) => {
  stage.stage.mutate("scores", draft => ({ ...draft, [peerId]: 1 }));
});
const { code } = stage.stage.createRoom();

const phone = createApp({ plugins: [controllerPlugin], pluginConfigs: { transport } });
await phone.start();
await phone.controller.joinRoom(code);

phone.controller.on("scores", scores => console.log(scores));
phone.controller.intent("score", {});
  1. One adapter instance, shared by both apps.
output
bun try.ts
{}
{ "a0a03192-f54c-4bc6-9a4b-952ec1c0bd7c": 1 }

The first line is the snapshot the phone got on join. The second is the delta after the intent. The script keeps running until you stop the apps: the heartbeat and the broadcast loop hold it open.

Next