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 });
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Identifier for logging |
watcher | typeof BaseWatcher | Yes | The watcher class constructor |
Lifecycle
| Phase | What 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.