PlayMesh/Docs
HomeGitHubnpm

Middleware & Plugins

Intercept events with middleware and extend PlayMesh with reusable plugins.

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 (built in)

PlayMesh ships a rateLimit middleware with per-session, per-event token buckets. '*' catches events without their own limit; over-limit events are rejected with an error the client sees via onError.
import { rateLimit } from '@playmesh/server';

mesh.use(rateLimit({ 'player:move': 30, chat: 2, '*': 100 })); // events/second

Role-based access (built in)

requireRole protects events by role - a session needs at least one of the listed roles (granted by your authenticate hook). Events without a rule stay open. Denied events reach the client via onError.
import { requireRole } from '@playmesh/server';

mesh.use(requireRole({
  'kick-user': ['admin'],
  'mute-user': ['admin', 'moderator'],
}));

Custom authorization

const ADMIN_EVENTS = new Set(['ban-player', 'broadcast-message']);

mesh.use(async (context, next) => {
  if (ADMIN_EVENTS.has(context.event) && !context.session.hasRole('admin')) {
    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.