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 });
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Property name on appHandle |
manager | typeof BaseManager | Yes | The 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() });
}