Examples
Full working examples showing common PlayMesh patterns.
Chat server
The canonical PlayMesh example — a multiplayer chat lobby. Included in
examples/chat in the repository.server.ts
◆examples/chat/server.ts
import { PlayMesh } from '@playmesh/server';
const port = Number(process.env.PORT ?? 4000);
const mesh = new PlayMesh({ port });
mesh.onAuthenticate(async request => {
const username = String(request.auth.username ?? '').trim();
if (!username) throw new Error('A username is required');
return { userId: username };
});
mesh.bootstrap(async ({ mesh }) => {
mesh.createDomain('social').createInstance('lobby');
});
mesh.onAdmission(async () => ({ instances: ['social/lobby'] }));
mesh.onStarted(() => {
const lobby = mesh.domain('social').instance('lobby');
lobby.onJoin(session => {
lobby.broadcast('system', { text: `${session.userId} joined` });
});
lobby.onLeave(session => {
lobby.broadcast('system', { text: `${session.userId} left` });
});
lobby.on('chat', (session, payload) => {
lobby.broadcast('chat', {
sender: session.userId,
message: (payload as { message: string }).message,
});
});
});
await mesh.start();client.ts
◆examples/chat/client.ts
import { createInterface } from 'node:readline';
import { PlayMeshClient } from '@playmesh/client';
const username = process.argv[2] ?? `guest-${Math.floor(Math.random() * 1000)}`;
const client = new PlayMeshClient({
url: `http://localhost:${process.env.PORT ?? 4000}`,
auth: { username },
});
client.on('chat', payload => {
const { sender, message } = payload as { sender: string; message: string };
console.log(`${sender}: ${message}`);
});
client.on('system', payload => {
console.log(`* ${(payload as { text: string }).text}`);
});
const session = await client.connect();
console.log(`Connected as ${session.userId}`);
const rl = createInterface({ input: process.stdin });
rl.on('line', line => {
if (line.trim()) client.emit('chat', { message: line.trim() });
});Run it
cd examples/chat && npm install
# Terminal 1
npx tsx server.ts
# Terminal 2
npx tsx client.ts alice
# Terminal 3
npx tsx client.ts bobMulti-zone world
Players move between zones. Each zone is an instance. A player is always in exactly one zone.
const ZONES = ['forest', 'village', 'dungeon', 'market'];
mesh.bootstrap(async ({ mesh }) => {
const world = mesh.createDomain('world');
for (const zone of ZONES) {
const instance = world.createInstance(zone);
instance.onJoin(session => {
instance.broadcast('player-entered', { userId: session.userId, zone });
});
instance.onLeave(session => {
instance.broadcast('player-left', { userId: session.userId, zone });
});
instance.on('chat', (session, payload) => {
instance.broadcast('chat', { zone, sender: session.userId, ...payload });
});
}
});
mesh.onAdmission(async request => {
const player = await db.players.findById(request.userId);
return { instances: [`world/${player.currentZone ?? 'village'}`] };
});
mesh.onStarted(() => {
const world = mesh.domain('world');
for (const instance of world.instances) {
instance.on('travel-to', async (session, payload) => {
const { zone } = payload as { zone: string };
if (!ZONES.includes(zone)) return;
for (const current of session.instances) {
if (current.domain === world) await session.leave(current);
}
await session.join(world.instance(zone));
await db.players.setZone(session.userId, zone);
});
}
});Guild chat (multi-instance membership)
Players are in the world zone and their guild chat simultaneously.
mesh.bootstrap(async ({ mesh }) => {
const world = mesh.createDomain('world');
const guilds = mesh.createDomain('guild');
world.createInstance('city');
const allGuilds = await db.guilds.findAll();
for (const guild of allGuilds) {
const instance = guilds.createInstance(guild.id);
instance.on('guild-chat', (session, payload) => {
instance.broadcast('guild-chat', {
sender: session.userId,
guildId: guild.id,
message: (payload as { message: string }).message,
});
});
}
});
mesh.onAdmission(async request => {
const player = await db.players.findById(request.userId);
return {
instances: [
'world/city',
...(player.guildId ? [`guild/${player.guildId}`] : []),
],
};
});Matchmaking with queues
Uses BullMQ to form 2-player matches. Requires Redis.
const mesh = new PlayMesh({
port: 4000,
redis: { host: 'localhost', port: 6379 },
});
mesh.bootstrap(async ({ mesh }) => {
const ranked = mesh.createDomain('ranked');
const lobby = ranked.createInstance('lobby');
lobby.on('join-queue', async (session, payload) => {
await mesh.queues.queue('matchmaking').add('find-match', {
userId: session.userId,
rank: (payload as { rank: number }).rank,
});
session.send('queued', {});
});
});
mesh.onStarted(() => {
mesh.queues.worker('matchmaking', async job => {
const { userId, rank } = job.data;
const opponent = await matchmakingService.findOpponent(userId, rank);
if (!opponent) return;
const ranked = mesh.domain('ranked');
const match = ranked.createInstance(`match-${Date.now()}`);
for (const uid of [userId, opponent.userId]) {
const s = mesh.sessions.find(s => s.userId === uid);
if (s) {
await s.leave(ranked.instance('lobby'));
await s.join(match);
}
}
match.broadcast('match-started', { players: [userId, opponent.userId] });
});
});
await mesh.start();✦
For production matchmaking, use a skill-based algorithm and a dedicated service. This example illustrates the queue integration pattern only.