Middleware
Middleware functions intercept every incoming client event before it reaches instance handlers. They follow the (context, next) pattern.
Signature
type Middleware = (
context: MiddlewareContext,
next: () => Promise<void>
) => void | Promise<void>;
interface MiddlewareContext {
event: string;
payload: unknown;
session: Session;
mesh: PlayMesh;
}
Logging
mesh.use(async (context, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
if (ms > 100) console.warn(`Slow event: ${context.event} — ${ms}ms`);
});
Rate limiting
const rateLimiter = new Map<string, number[]>();
mesh.use(async (context, next) => {
if (context.event !== 'chat') return next();
const history = rateLimiter.get(context.session.userId) ?? [];
const recent = history.filter(t => Date.now() - t < 1000);
if (recent.length >= 5) {
context.session.send('rate-limited', { event: context.event });
return; // Don't call next() — drop the event
}
recent.push(Date.now());
rateLimiter.set(context.session.userId, recent);
await next();
});
Authorization
const ADMIN_EVENTS = new Set(['ban-player', 'broadcast-message']);
mesh.use(async (context, next) => {
if (ADMIN_EVENTS.has(context.event) && !context.session.data.isAdmin) {
context.session.send('unauthorized', { event: context.event });
return;
}
await next();
});
⚠Calling next()multiple times in the same middleware throws a "next() called multiple times" error.
Plugins
Plugins are objects with an install(mesh) method. They receive the PlayMesh instance and can register hooks, middleware, domains, and anything else.
Plugin interface
interface Plugin {
name?: string;
install(mesh: PlayMesh): void | Promise<void>;
}
Writing a plugin
import type { PlayMesh, Plugin } from '@playmesh/server';
class MatchmakingPlugin implements Plugin {
readonly name = 'matchmaking';
private queue: string[] = [];
constructor(private readonly minPlayers: number) {}
async install(mesh: PlayMesh) {
const domain = mesh.createDomain('ranked');
const lobby = domain.createInstance('lobby');
mesh.onSessionCreate(async session => {
this.queue.push(session.id);
if (this.queue.length >= this.minPlayers) {
const players = this.queue.splice(0, this.minPlayers);
const match = domain.createInstance(`match-${Date.now()}`);
for (const id of players) {
const s = mesh.sessions.find(s => s.id === id);
if (s) {
await s.leave(lobby);
await s.join(match);
}
}
}
});
mesh.onDisconnect(session => {
this.queue = this.queue.filter(id => id !== session.id);
});
}
}
Using a plugin
const mesh = new PlayMesh({ port: 4000 });
mesh.use(new MatchmakingPlugin(2));
await mesh.start();
✦Plugins are installed before bootstrap hooks run, so a plugin can safely register its own bootstrap hooks.