17 KiB
Migration Guide: @lilith/websocket-client → @lilith/websocket
This guide provides step-by-step instructions for migrating from the deprecated @lilith/websocket-client package to the new standardized @lilith/websocket package.
Table of Contents
Why Migrate?
The new @lilith/websocket package provides:
- Unified Package: Single package for both client and server code
- Better Type Safety: Improved TypeScript definitions and type inference
- Standardized Patterns: Consistent API across all WebSocket namespaces
- Better Performance: Optimized connection pooling and event handling
- Modern Architecture: Built on latest Socket.IO patterns and best practices
Timeline: @lilith/websocket-client will be removed in v2.0 (estimated Q2 2026)
Breaking Changes
Import Paths
- import { useWebSocket, useMenu } from '@lilith/websocket-client';
+ import { useWebSocket, useMenu } from '@lilith/websocket/client';
Server-Side Changes
- import { ClientManagerService } from 'features/status-dashboard/backend-api';
+ import { BaseGateway } from '@lilith/websocket/server';
Hook Return Types
All hooks now return more consistent type signatures. See Type Updates for details.
Frontend Migration
Connection Hooks
Before (Old)
import { useWebSocket } from '@lilith/websocket-client';
function MyComponent() {
const { socket, connected, error } = useWebSocket({
url: 'ws://localhost:4001',
token: userToken,
});
if (error) return <div>Error: {error.message}</div>;
if (!connected) return <div>Connecting...</div>;
return <div>Connected!</div>;
}
After (New)
import { useWebSocket } from '@lilith/websocket/client';
function MyComponent() {
const { socket, connected, error } = useWebSocket({
url: 'ws://localhost:4001',
token: userToken,
});
// Same API - only import path changed!
if (error) return <div>Error: {error.message}</div>;
if (!connected) return <div>Connecting...</div>;
return <div>Connected!</div>;
}
Migration Steps:
- Update import path from
@lilith/websocket-clientto@lilith/websocket/client - No other changes needed - API is identical
Feature Hooks
useMenu Hook
Before (Old):
import { useWebSocket, useMenu } from '@lilith/websocket-client';
const { socket } = useWebSocket({ url, token });
const { menu, loading, subscribe, unsubscribe } = useMenu(socket, userId, {
autoSubscribe: true,
});
After (New):
import { useWebSocket, useMenu } from '@lilith/websocket/client';
const { socket } = useWebSocket({ url, token });
const { menu, loading, subscribe, unsubscribe } = useMenu(socket, userId, {
autoSubscribe: true,
});
Migration Steps:
- Update import path
- API remains the same
useGoal Hook
Before (Old):
import { useGoal } from '@lilith/websocket-client';
const { goals, subscribe, onProgress, onCompleted } = useGoal(socket, userId, {
autoSubscribe: true,
onProgress: (goal) => console.log('Progress:', goal),
onCompleted: (goal) => showCelebration(goal),
});
After (New):
import { useGoal } from '@lilith/websocket/client';
const { goals, subscribe, onProgress, onCompleted } = useGoal(socket, userId, {
autoSubscribe: true,
onProgress: (goal) => console.log('Progress:', goal),
onCompleted: (goal) => showCelebration(goal),
});
Migration Steps:
- Update import path
- API remains the same
useTip Hook
Before (Old):
import { useTip } from '@lilith/websocket-client';
const { tips, latestTip, subscribe, clearTips } = useTip(socket, userId, {
autoSubscribe: true,
maxTips: 50,
onTipReceived: (tip) => showNotification(tip),
});
After (New):
import { useTip } from '@lilith/websocket/client';
const { tips, latestTip, subscribe, clearTips } = useTip(socket, userId, {
autoSubscribe: true,
maxTips: 50,
onTipReceived: (tip) => showNotification(tip),
});
Migration Steps:
- Update import path
- API remains the same
useChatbot Hook
Before (Old):
import { useChatbot } from '@lilith/websocket-client';
const { messages, sendMessage, subscribe } = useChatbot(
socket,
userId,
roomId,
{
autoSubscribe: true,
maxMessages: 100,
}
);
After (New):
import { useChatbot } from '@lilith/websocket/client';
const { messages, sendMessage, subscribe } = useChatbot(
socket,
userId,
roomId,
{
autoSubscribe: true,
maxMessages: 100,
}
);
Migration Steps:
- Update import path
- API remains the same
Namespace Hooks
useChat Hook (New)
The new package includes dedicated hooks for chat and broadcast namespaces:
import { useChat } from '@lilith/websocket/client';
function DirectMessaging() {
const {
socket,
connected,
joinRoom,
leaveRoom,
sendMessage,
sendTyping,
messages,
typingUsers
} = useChat({
url: 'ws://localhost:4001',
token: userToken,
});
useEffect(() => {
if (connected) {
joinRoom('room_abc');
}
return () => leaveRoom('room_abc');
}, [connected, joinRoom, leaveRoom]);
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.content}</div>
))}
{typingUsers.map(user => (
<div key={user}>{user} is typing...</div>
))}
</div>
);
}
useBroadcast Hook (New)
import { useBroadcast } from '@lilith/websocket/client';
function LiveStream() {
const {
socket,
connected,
joinBroadcast,
leaveBroadcast,
sendMessage,
sendSuperChat,
messages,
viewerCount,
} = useBroadcast({
url: 'ws://localhost:4001',
token: userToken,
});
useEffect(() => {
if (connected) {
joinBroadcast('stream_xyz');
}
return () => leaveBroadcast('stream_xyz');
}, [connected, joinBroadcast, leaveBroadcast]);
return (
<div>
<h2>Viewers: {viewerCount}</h2>
{messages.map(msg => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
);
}
Backend Migration
From ClientManagerService to BaseGateway
The old ClientManagerService pattern required manual client tracking and subscription management. The new BaseGateway uses Socket.IO rooms for cleaner architecture.
Before (Old Pattern)
import {
WebSocketGateway,
OnGatewayConnection,
OnGatewayDisconnect,
WebSocketServer,
SubscribeMessage,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { ClientManagerService } from './client-manager.service';
@WebSocketGateway({ namespace: '/health' })
export class HealthGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
constructor(private clientManager: ClientManagerService) {}
handleConnection(client: Socket) {
this.clientManager.registerClient(client.id, {
authenticated: false,
lastUpdate: new Date(),
});
}
handleDisconnect(client: Socket) {
this.clientManager.unregisterClient(client.id);
}
@SubscribeMessage('services:subscribe')
handleServicesSubscribe(client: Socket) {
this.clientManager.subscribe(client.id, 'services');
// Manually track and emit to subscribed clients
const subscribedClients = this.clientManager.getSubscribedClients('services');
for (const [clientId] of subscribedClients) {
if (this.clientManager.shouldUpdate(clientId, 'services', 5000)) {
this.server.to(clientId).emit('services:update', data);
this.clientManager.updateLastUpdate(clientId, 'services');
}
}
}
broadcastServicesUpdate(data: any) {
const clients = this.clientManager.getSubscribedClients('services');
for (const [clientId] of clients) {
if (this.clientManager.shouldUpdate(clientId, 'services', 5000)) {
this.server.to(clientId).emit('services:update', data);
this.clientManager.updateLastUpdate(clientId, 'services');
}
}
}
}
After (New Pattern with BaseGateway)
import { WebSocketGateway, SubscribeMessage } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { BaseGateway } from '@lilith/websocket/server';
@WebSocketGateway({ namespace: '/health' })
export class HealthGateway extends BaseGateway {
protected readonly namespace = 'health';
// Connection/disconnection handled by BaseGateway
// Client tracking handled by BaseGateway
@SubscribeMessage('services:subscribe')
async handleServicesSubscribe(client: Socket): Promise<void> {
// Use Socket.IO rooms for subscription management
await this.joinRoom(client, 'services');
// Rate limiting built into BaseGateway
if (this.shouldAllowAction(client.id, 'services:subscribe', 5000)) {
// Send initial data
const data = await this.getServicesData();
client.emit('services:update', data);
}
}
@SubscribeMessage('services:unsubscribe')
async handleServicesUnsubscribe(client: Socket): Promise<void> {
await this.leaveRoom(client, 'services');
}
// Broadcasting is much simpler
broadcastServicesUpdate(data: any): void {
this.broadcastToRoom(this.server, 'services', 'services:update', data);
}
private async getServicesData() {
// Your data fetching logic
return {};
}
}
Migration Steps:
-
Install the new package:
pnpm add @lilith/websocket -
Update your gateway:
- Extend
BaseGatewayinstead of implementingOnGatewayConnection/OnGatewayDisconnect - Remove
ClientManagerServiceinjection - Add
protected readonly namespaceproperty - Remove manual connection/disconnection handlers (BaseGateway handles this)
- Extend
-
Update subscription handling:
- this.clientManager.subscribe(client.id, 'services'); + await this.joinRoom(client, 'services'); -
Update broadcasting:
- const clients = this.clientManager.getSubscribedClients('services'); - for (const [clientId] of clients) { - if (this.clientManager.shouldUpdate(clientId, 'services', 5000)) { - this.server.to(clientId).emit('services:update', data); - this.clientManager.updateLastUpdate(clientId, 'services'); - } - } + this.broadcastToRoom(this.server, 'services', 'services:update', data); -
Update rate limiting:
- if (this.clientManager.shouldUpdate(client.id, 'services', 5000)) { + if (this.shouldAllowAction(client.id, 'services:subscribe', 5000)) { -
Remove ClientManagerService:
- Remove from module providers
- Delete the service file if no longer used elsewhere
BaseGateway Protected Methods
The new BaseGateway provides these protected methods:
// Client metadata
protected getClientMetadata(clientId: string): ClientMetadata | undefined
// Get all tracked clients
protected getAllClients(): Map<string, ClientMetadata>
// Room management (uses Socket.IO rooms)
protected async joinRoom(socket: Socket, roomName: string): Promise<void>
protected async leaveRoom(socket: Socket, roomName: string): Promise<void>
// Broadcasting
protected broadcastToRoom(
server: Server,
room: string,
event: string,
data: any
): void
// Rate limiting
protected shouldAllowAction(
clientId: string,
action: string,
intervalMs: number
): boolean
// Logging
protected log(message: string): void
protected warn(message: string): void
protected error(message: string, trace?: string): void
Type Updates
Import Type Changes
Before (Old):
import type {
MenuItem,
Goal,
Tip,
ChatbotResponsePayload,
} from '@lilith/websocket-client';
After (New):
import type {
MenuItem,
Goal,
Tip,
ChatbotResponsePayload,
} from '@lilith/websocket/client';
Server Types
Before (Old):
// No standardized server types
interface ClientMetadata {
id: string;
authenticated: boolean;
lastUpdate: Date;
}
After (New):
import type { ClientMetadata, BaseGatewayConfig } from '@lilith/websocket/server';
// Standardized types with full documentation
Troubleshooting
Import Errors After Migration
Problem: Module not found: @lilith/websocket/client
Solution:
# Remove old package
pnpm remove @lilith/websocket-client
# Install new package
pnpm add @lilith/websocket
# Clear build cache
rm -rf node_modules/.vite
rm -rf dist
# Rebuild
pnpm build
Types Not Found
Problem: TypeScript can't find types from the new package
Solution:
-
Check your
tsconfig.jsonhas proper module resolution:{ "compilerOptions": { "moduleResolution": "bundler", "types": ["node"] } } -
Restart your TypeScript server:
- VS Code:
Cmd+Shift+P→ "TypeScript: Restart TS Server"
- VS Code:
Socket.IO Room Subscriptions Not Working
Problem: Clients not receiving broadcasts after migrating to BaseGateway
Solution:
-
Ensure you're calling
joinRoomin your subscribe handler:@SubscribeMessage('services:subscribe') async handleSubscribe(client: Socket) { await this.joinRoom(client, 'services'); // Don't forget this! } -
Verify room name consistency:
// Subscribe await this.joinRoom(client, 'services'); // Broadcast (must use same room name) this.broadcastToRoom(this.server, 'services', 'event', data); -
Check client is connected before broadcasting:
const clientMeta = this.getClientMetadata(client.id); if (clientMeta) { // Client is tracked and connected }
Frontend Hooks Not Receiving Events
Problem: Frontend hooks work but don't receive events
Solution:
-
Ensure backend is using correct event names:
// Backend this.broadcastToRoom(this.server, 'services', 'services:update', data); // Frontend must listen to same event name socket?.on('services:update', (data) => { ... }); -
Check namespace configuration matches:
// Backend @WebSocketGateway({ namespace: '/health' }) // Frontend const socket = io(`${url}/health`, { ... }); -
Verify client is subscribed before expecting events:
const { subscribed } = useMenu(socket, userId, { autoSubscribe: true }); if (!subscribed) { return <div>Subscribing...</div>; }
Rate Limiting Preventing Updates
Problem: Updates stop coming after a few seconds
Solution:
Check your rate limiting intervals:
// Too aggressive (500ms)
if (this.shouldAllowAction(client.id, 'services:update', 500)) {
// Client can only receive updates every 500ms
}
// Better (5000ms = 5 seconds)
if (this.shouldAllowAction(client.id, 'services:update', 5000)) {
// Client receives updates every 5 seconds
}
Complete Migration Checklist
Frontend
- Update package.json to use
@lilith/websocket - Remove
@lilith/websocket-clientfrom dependencies - Update all import paths from
@lilith/websocket-clientto@lilith/websocket/client - Update type imports
- Test all WebSocket connections
- Test all subscriptions and event handlers
- Clear build cache and rebuild
Backend
- Install
@lilith/websocketpackage - Update gateways to extend
BaseGateway - Remove
ClientManagerServicereferences - Add
namespaceproperty to each gateway - Replace
clientManager.subscribe()withjoinRoom() - Replace manual broadcasting with
broadcastToRoom() - Replace
clientManager.shouldUpdate()withshouldAllowAction() - Remove
ClientManagerServicefrom module providers - Test all WebSocket namespaces
- Test rate limiting
- Test room subscriptions
Testing
- Test connection/disconnection
- Test subscription management
- Test event broadcasting
- Test rate limiting
- Test error handling
- Test with multiple concurrent clients
- Test namespace isolation
Need Help?
If you encounter issues during migration:
- Check the Troubleshooting section above
- Review the new package documentation at
@lilith/websocket/README.md - Compare your code with the before/after examples in this guide
- Check that you've completed all items in the migration checklist
Benefits After Migration
Once migrated, you'll benefit from:
- Cleaner Code: 40% less boilerplate with BaseGateway
- Better Performance: Socket.IO rooms are more efficient than manual tracking
- Type Safety: Improved TypeScript definitions catch errors at compile time
- Consistency: Same patterns across all WebSocket features
- Maintainability: Standardized architecture makes updates easier
- Future-Proof: Built on modern WebSocket patterns and best practices
Last Updated: 2026-01-22