@mexty/multiplayer
v0.1.0
Published
Real-time multiplayer state for Mexty blocks, synced over LiveKit data channels with Yjs
Readme
@mexty/multiplayer
Real-time multiplayer state for Mexty blocks — CRDT state sync (Yjs) carried over LiveKit data channels. One realtime infrastructure for video, audio, and block state: a block embedded in a meeting shares the meeting's LiveKit room, so multiplayer inside live sessions works with no extra servers.
This package replaces @mexty/realtime (Yjs over a self-hosted Hocuspocus
websocket server, now removed). The hook API is drop-in compatible.
How it works
┌────────────┐ Yjs sync protocol ┌────────────┐
│ Client A │ ◄───────────────────► │ Client B │
│ Y.Doc │ LiveKit data channel │ Y.Doc │
└─────┬──────┘ (reliable, topic └──────┬─────┘
│ "_mexty_mp") │
└──────────► LiveKit SFU ◄─────────────┘
(wss://livekit.mext.app)- Each collaborative space (a
useCollabSpacecall) is a Yjs document. - Documents sync peer-to-peer through the room's data channel using the
standard Yjs sync protocol (
y-protocols/sync): joiners broadcast their state vector, peers reply with the missing updates, and every local change is broadcast as an incremental update. - Multiple spaces share one room connection (frames are prefixed with the space id), and the room connection is shared and refcounted across hooks.
- Payloads above LiveKit's ~15 KiB data-packet cap are chunked and reassembled transparently.
- Conflicts merge via CRDT semantics: concurrent edits to different keys both win, concurrent appends to the same array merge, concurrent writes to the same scalar resolve last-writer-wins.
- State lives with the participants (every client holds the full doc); a late
joiner catches up from any present peer. When the room empties, state is
gone — seed it via
initialStateor persist externally.
Authentication and room access run through the Mexty backend
(POST /api/livekit/token), which mints LiveKit tokens with
canPublishData for the authenticated user.
Usage
Basic — shared state for a block
import { useCollabSpace } from "@mexty/multiplayer";
function ScoreBoard({ blockId }: { blockId: string }) {
const { state, update, isConnected, connectionStatus, userId, peers } =
useCollabSpace(`game:${blockId}`, {
score: { blue: 0, red: 0 },
players: [] as string[],
});
// Partial update: merges at the top level
const resetScore = () => update({ score: { blue: 0, red: 0 } });
// Functional update: receives the previous state
const addPoint = (team: "blue" | "red") =>
update((prev) => ({
...prev,
score: { ...prev.score, [team]: prev.score[team] + 1 },
}));
return <div>…</div>;
}Configuration (once, at app startup)
import { configure } from "@mexty/multiplayer";
configure({
serverUrl: import.meta.env.VITE_API_URL || "https://api.mexty.ai",
enableLogging: import.meta.env.DEV,
});Custom auth? Replace token fetching entirely:
configure({
tokenProvider: async (roomName) => {
const res = await myApi.post("/livekit/token", { roomId: roomName });
return { token: res.data.token, serverUrl: res.data.serverUrl };
},
});Inside a meeting — share the video call's room
By default each space connects to a room named after the space id. To make
block state ride an existing meeting, scope it with MultiplayerRoomProvider:
import { MultiplayerRoomProvider } from "@mexty/multiplayer";
// Option A: by room name (the package manages its own connection)
<MultiplayerRoomProvider room={meetingUuid}>
<EmbeddedBlock blockId={blockId} />
</MultiplayerRoomProvider>
// Option B: reuse the already-connected LiveKit Room instance
// (e.g. from @livekit/components-react's useRoomContext())
<MultiplayerRoomProvider livekitRoom={room}>
<EmbeddedBlock blockId={blockId} />
</MultiplayerRoomProvider>Or per hook call: useCollabSpace(spaceId, initial, { room: meetingUuid }).
API
useCollabSpace<T>(spaceId, initialState, options?)
| Return field | Description |
| ------------------ | -------------------------------------------------------- |
| state: T | Current synchronized state |
| update(u) | Partial<T> (top-level merge) or (prev: T) => T |
| isConnected | LiveKit room connection is up |
| connectionStatus | connecting | connected | reconnecting | disconnected |
| userId | LiveKit identity once connected (persistent anon id before) |
| peers | Identities of the other participants in the room |
initialState is captured on first render and seeds keys the room doesn't
have yet; it never clobbers state already present.
Semantics & limits
- State must be JSON (plain objects, arrays, primitives) — no class
instances,
Date,Map, functions. - Merging: unchanged paths are untouched; concurrent appends to the same array merge; reorders/splices and scalar conflicts are last-writer-wins.
- No persistence: state exists while at least one participant is in the room. Persist externally if a session must survive everyone leaving.
Development
npm install
npm run typecheck
npm test
npm run build