Components

Watchers

Long-running background tasks with BaseWatcher.

Watchers

Watchers run background tasks alongside the server — periodic cleanup, health monitoring, stats aggregation, polling. Both abstract methods must be implemented.

Base class

import { BaseWatcher, AppHandle } from "ioserver";

abstract class BaseWatcher {
  protected appHandle: AppHandle;
  constructor(appHandle: AppHandle);

  abstract watch(): Promise<void>;  // Called at server.start()
  abstract stop(): void;            // Called at server.stop()
}

Creating a watcher

import { BaseWatcher } from "ioserver";

export class MetricsWatcher extends BaseWatcher {
  private intervalId: ReturnType<typeof setInterval> | null = null;

  async watch(): Promise<void> {
    this.appHandle.log(6, "MetricsWatcher started");

    this.intervalId = setInterval(() => {
      this.collectMetrics();
    }, 60_000); // every minute
  }

  stop(): void {
    if (this.intervalId !== null) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
    this.appHandle.log(6, "MetricsWatcher stopped");
  }

  private collectMetrics(): void {
    const mem = process.memoryUsage();
    const heapMb = Math.round(mem.heapUsed / 1_048_576);
    this.appHandle.log(6, `Heap: ${heapMb}MB`);

    if (heapMb > 500) {
      this.appHandle.log(4, `High memory usage: ${heapMb}MB`);
    }
  }
}

Registering a watcher

server.addWatcher({ name: "metricsWatcher", watcher: MetricsWatcher });
OptionTypeRequiredDescription
namestringYesIdentifier for logging
watchertypeof BaseWatcherYesThe watcher class constructor

Lifecycle

PhaseWhat happens
addWatcher(...)Watcher class is instantiated
server.start()watch() is called on all watchers. Calls run in parallel (non-blocking). Errors are logged, not thrown
server.stop()stop() is called on all watchers before closing the server
watch() is called with Promise.all but errors do not abort start(). If a watch() call throws, the error is logged and startup continues. Always handle your own errors inside watch().

Managing multiple intervals

Use an array to track interval IDs and clear them all in stop():

export class ChatWatcher extends BaseWatcher {
  private intervals: ReturnType<typeof setInterval>[] = [];

  async watch(): Promise<void> {
    // Cleanup every 30 minutes
    this.intervals.push(setInterval(() => this.cleanup(), 30 * 60_000));

    // Stats every 5 minutes
    this.intervals.push(setInterval(() => this.logStats(), 5 * 60_000));

    // Health check every minute
    this.intervals.push(setInterval(() => this.checkHealth(), 60_000));

    this.appHandle.log(6, "ChatWatcher started");
  }

  stop(): void {
    this.intervals.forEach(clearInterval);
    this.intervals = [];
    this.appHandle.log(6, "ChatWatcher stopped");
  }

  private logStats(): void {
    const stats = this.appHandle.statsManager?.getStats();
    if (stats) {
      this.appHandle.log(6, `Stats — users: ${stats.totalUsers}, messages: ${stats.messagesCount}`);
    }
  }

  private cleanup(): void {
    this.appHandle.log(6, "Running scheduled cleanup");
  }

  private checkHealth(): void {
    const heapMb = Math.round(process.memoryUsage().heapUsed / 1_048_576);
    if (heapMb > 500) {
      this.appHandle.log(4, `High memory alert: ${heapMb}MB`);
    }
  }
}

Accessing managers from watchers

Watchers receive appHandle just like all other components:

export class ReportWatcher extends BaseWatcher {
  private intervalId: ReturnType<typeof setInterval> | null = null;

  async watch(): Promise<void> {
    this.intervalId = setInterval(async () => {
      const report = this.appHandle.reportManager.generate();
      this.appHandle.send({
        namespace: "admin",
        event: "daily_report",
        data: report,
      });
    }, 24 * 60 * 60_000);
  }

  stop(): void {
    if (this.intervalId !== null) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }
}

Graceful shutdown pattern

When using process.on("SIGINT") in your entry point, call server.stop() to ensure all watchers clean up properly:

process.on("SIGINT", async () => {
  await server.stop();
  process.exit(0);
});

server.stop() calls watcher.stop() for every registered watcher before closing the HTTP server.

Copyright © 2026