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

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.