Components

Managers

Shared singleton logic with BaseManager — the dependency injection mechanism in IOServer.

Managers

Managers are shared singletons. They are the only component type instantiated synchronously at registration time, before server.start() is called. Every component registered after a manager can access it via this.appHandle.managerName.

Base class

import { BaseManager, AppHandle } from "ioserver";

abstract class BaseManager {
  protected appHandle: AppHandle;
  constructor(appHandle: AppHandle);
}

Creating a manager

import { BaseManager } from "ioserver";

export class SessionManager extends BaseManager {
  private sessions: Map<string, { userId: string; expires: number }> = new Map();

  // Optional lifecycle hook — called automatically (non-blocking) after instantiation
  async start(): Promise<void> {
    this.appHandle.log(6, "SessionManager started — scheduling cleanup");
    // Perform one-time async setup here (e.g. connect to Redis)
  }

  create(userId: string, ttlMs: number = 3_600_000): string {
    const token = Math.random().toString(36).slice(2);
    this.sessions.set(token, { userId, expires: Date.now() + ttlMs });
    return token;
  }

  validate(token: string): string | null {
    const session = this.sessions.get(token);
    if (!session) return null;
    if (Date.now() > session.expires) {
      this.sessions.delete(token);
      return null;
    }
    return session.userId;
  }

  revoke(token: string): void {
    this.sessions.delete(token);
  }
}

Registering a manager

// Always register managers BEFORE any component that uses them
server.addManager({ name: "sessionManager", manager: SessionManager });
server.addManager({ name: "statsManager", manager: StatsManager });

// Then register services, controllers, and watchers
server.addService({ name: "auth", service: AuthService });
server.addController({ name: "api", controller: ApiController });
OptionTypeRequiredDescription
namestringYesProperty name on appHandle
managertypeof BaseManagerYesThe manager class constructor
The name must be unique and cannot be "send", "log", or "verbose" (reserved by the framework). Names shorter than 2 characters are also rejected.

Using a manager from other components

// From a service
export class AuthService extends BaseService {
  async login(socket: any, data: { token: string }, callback?: Function): Promise<void> {
    const userId = this.appHandle.sessionManager.validate(data.token);
    if (!userId) {
      if (callback) return callback({ status: "error", message: "Invalid session" });
      return;
    }
    // ...
  }
}

// From a controller
export class ProfileController extends BaseController {
  async getProfile(request: any, reply: any): Promise<void> {
    const token = request.headers.authorization;
    const userId = this.appHandle.sessionManager.validate(token);
    if (!userId) throw new IOServerError("Unauthorized", 401);
    // ...
  }
}

// From a watcher
export class CleanupWatcher extends BaseWatcher {
  async watch(): Promise<void> {
    setInterval(() => {
      this.appHandle.statsManager.reset();
    }, 24 * 60 * 60 * 1000);
  }
  stop(): void { /* clear interval */ }
}

The start() lifecycle hook

If a manager defines async start(): Promise<void>, IOServer calls it automatically right after new ManagerClass(appHandle) — before any other component is registered. The call is non-blocking: errors are caught, logged at level 3 (ERROR), and do not abort the server.

export class DatabaseManager extends BaseManager {
  private db!: Database;

  async start(): Promise<void> {
    this.db = await connectToDatabase(process.env.DB_URL!);
    this.appHandle.log(6, "Database connected");
  }

  async query(sql: string, params: any[]): Promise<any[]> {
    return this.db.query(sql, params);
  }
}
start() is fire-and-forget — IOServer does not await it. If your other components depend on the manager being fully ready (e.g. database connected), use a readiness flag or consider confirming connectivity in your first service/controller call.

Real-world example

StatsManager from the chat application (examples/chat-app/managers/StatsManager.ts):

import { BaseManager } from "ioserver";

interface Stats {
  totalUsers: number;
  activeRooms: number;
  messagesCount: number;
  peakConcurrentUsers: number;
}

export class StatsManager extends BaseManager {
  private stats: Stats = { totalUsers: 0, activeRooms: 0, messagesCount: 0, peakConcurrentUsers: 0 };
  private startTime = new Date();

  incrementUsers(): void {
    this.stats.totalUsers++;
    if (this.stats.totalUsers > this.stats.peakConcurrentUsers) {
      this.stats.peakConcurrentUsers = this.stats.totalUsers;
    }
  }

  decrementUsers(): void {
    if (this.stats.totalUsers > 0) this.stats.totalUsers--;
  }

  incrementMessages(): void {
    this.stats.messagesCount++;
  }

  setActiveRooms(count: number): void {
    this.stats.activeRooms = count;
  }

  getStats(): Stats & { uptime: string } {
    const diffMs = Date.now() - this.startTime.getTime();
    const h = Math.floor(diffMs / 3_600_000);
    const m = Math.floor((diffMs % 3_600_000) / 60_000);
    const s = Math.floor((diffMs % 60_000) / 1_000);
    return { ...this.stats, uptime: `${h}h ${m}m ${s}s` };
  }
}

Used in ChatService:

this.appHandle.statsManager.incrementUsers();
this.appHandle.statsManager.incrementMessages();

And exposed via ApiController:

async getStats(request: any, reply: any): Promise<void> {
  reply.send({ status: "OK", data: this.appHandle.statsManager.getStats() });
}
Copyright © 2026