Components

Services

Building WebSocket event handlers with BaseService.

Services

Services handle real-time communication over WebSocket. Every public method on a service class (not starting with _, not constructor) is automatically registered as a Socket.IO event handler when the server starts.

Base class

import { BaseService, AppHandle } from "ioserver";

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

Creating a service

import { BaseService } from "ioserver";

export class NotificationService extends BaseService {
  // Called as: socket.emit("subscribe", { topic: "news" }, callback)
  async subscribe(
    socket: any,
    data: { topic: string },
    callback?: Function
  ): Promise<void> {
    if (!data.topic) {
      if (callback) return callback({ status: "error", message: "Topic required" });
      return socket.emit("error", { message: "Topic required" });
    }

    await socket.join(data.topic);
    this.appHandle.log(6, `Socket ${socket.id} subscribed to ${data.topic}`);

    if (callback) callback({ status: "success", topic: data.topic });
  }

  // Called as: socket.emit("unsubscribe", { topic: "news" })
  async unsubscribe(socket: any, data: { topic: string }): Promise<void> {
    await socket.leave(data.topic);
    socket.emit("unsubscribed", { topic: data.topic });
  }

  // NOT exposed — underscore prefix
  private _isValidTopic(topic: string): boolean {
    return typeof topic === "string" && topic.length > 0 && topic.length < 64;
  }
}

Registering a service

// Default namespace "/"
server.addService({ service: NotificationService });

// Named namespace "/notifications"
server.addService({ name: "notifications", service: NotificationService });

// With Socket.IO middleware
server.addService({
  name: "notifications",
  service: NotificationService,
  middlewares: [AuthMiddleware],
});
OptionTypeRequiredDescription
servicetypeof BaseServiceYesThe service class constructor
namestringNoNamespace name. Defaults to "/"
middlewares(typeof BaseMiddleware)[]NoSocket.IO namespace middleware

Method signature

All three parameters are always passed by the framework:

async methodName(
  socket: any,          // Socket.IO socket — the connection to this client
  data: any,            // Payload from the client
  callback?: Function   // Optional acknowledgement callback
): Promise<void>

Always check whether callback is defined before calling it — some clients emit without callbacks.

Error handling

Throw an IOServerError (or any Error) from a service method. IOServer catches it and delivers a structured error response:

import { BaseService, IOServerError } from "ioserver";

export class ChatService extends BaseService {
  async login(socket: any, data: { username: string }, callback?: Function): Promise<void> {
    if (!data.username?.trim()) {
      throw new IOServerError("Username is required", 400);
    }
    // The framework converts this to:
    // callback({ status: "error", type: "IOServerError", message: "Username is required", statusCode: 400 })
    // or:
    // socket.emit("error", { status: "error", ... })
  }
}

Accessing managers

Use this.appHandle to access registered managers and shared utilities:

export class ChatService extends BaseService {
  async login(socket: any, data: { username: string }, callback?: Function): Promise<void> {
    // Access a manager
    this.appHandle.statsManager.incrementUsers();

    // Log a message (level 6 = INFORMATION)
    this.appHandle.log(6, `User ${data.username} logged in`);

    // Push an event to other clients
    this.appHandle.send({
      namespace: "chat",
      event: "user_joined",
      data: { username: data.username },
    });
  }
}

Real-world example

The chat application's ChatService at examples/chat-app/services/ChatService.ts demonstrates:

  • Managing connected users in a Map<string, User> (in-memory state on the service)
  • Room join/leave with socket.join() / socket.leave()
  • Broadcasting system messages: socket.to(room).emit(...)
  • Typing indicators with no callback required
  • Graceful disconnect handling
// Excerpt from ChatService
async send_message(
  socket: any,
  data: { room: string; content: string },
  callback?: Function
): Promise<void> {
  const user = this.users.get(socket.id);
  if (!user) {
    if (callback) return callback({ status: "error", message: "Not logged in" });
    return;
  }

  const message: Message = {
    id: Date.now().toString(),
    username: user.username,
    room: data.room,
    content: data.content,
    timestamp: new Date(),
    type: "message",
  };

  this.messageHistory.get(data.room)?.push(message);

  // Broadcast to room (including sender)
  socket.nsp.to(data.room).emit("new_message", message);

  this.appHandle.statsManager?.incrementMessages();

  if (callback) callback({ status: "success", messageId: message.id });
}
Copyright © 2026