Quick start
A shared screen and a phone: create a room, send an intent, read the synced state.
A room in five steps
-
Install.
@moku-labs/coreand@moku-labs/commonare peer dependencies.trysteroandqrcodecome with Room.install bun add @moku-labs/room @moku-labs/core @moku-labs/common -
Write the stage. The screen everyone looks at. It registers its state and its intents, then creates the room.
-
Write the phone. It joins with the code from the URL, renders the state it receives, and sends intents.
-
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.
-
Open the stage, scan the code. The phone joins the room directly over WebRTC.
The code
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}`);- Register before
createRoom. A phone gets every registered slice in its first snapshot.
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 });- 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.
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", {});- One adapter instance, shared by both apps.
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
- How it works: the star, the two planes, the join.
- Joining a room: the QR code and the join errors.
- Failures and reloads: what to show when the network breaks.