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],
});
| Option | Type | Required | Description |
|---|---|---|---|
service | typeof BaseService | Yes | The service class constructor |
name | string | No | Namespace name. Defaults to "/" |
middlewares | (typeof BaseMiddleware)[] | No | Socket.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 });
}