platform-codebase/@packages/@infrastructure/websocket-client/MIGRATION.md

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:

  1. Unified Package: Single package for both client and server code
  2. Better Type Safety: Improved TypeScript definitions and type inference
  3. Standardized Patterns: Consistent API across all WebSocket namespaces
  4. Better Performance: Optimized connection pooling and event handling
  5. 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:

  1. Update import path from @lilith/websocket-client to @lilith/websocket/client
  2. 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:

  1. Update import path
  2. 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:

  1. Update import path
  2. 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:

  1. Update import path
  2. 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:

  1. Update import path
  2. 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:

  1. Install the new package:

    pnpm add @lilith/websocket
    
  2. Update your gateway:

    • Extend BaseGateway instead of implementing OnGatewayConnection/OnGatewayDisconnect
    • Remove ClientManagerService injection
    • Add protected readonly namespace property
    • Remove manual connection/disconnection handlers (BaseGateway handles this)
  3. Update subscription handling:

    - this.clientManager.subscribe(client.id, 'services');
    + await this.joinRoom(client, 'services');
    
  4. 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);
    
  5. Update rate limiting:

    - if (this.clientManager.shouldUpdate(client.id, 'services', 5000)) {
    + if (this.shouldAllowAction(client.id, 'services:subscribe', 5000)) {
    
  6. 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:

  1. Check your tsconfig.json has proper module resolution:

    {
      "compilerOptions": {
        "moduleResolution": "bundler",
        "types": ["node"]
      }
    }
    
  2. Restart your TypeScript server:

    • VS Code: Cmd+Shift+P → "TypeScript: Restart TS Server"

Socket.IO Room Subscriptions Not Working

Problem: Clients not receiving broadcasts after migrating to BaseGateway

Solution:

  1. Ensure you're calling joinRoom in your subscribe handler:

    @SubscribeMessage('services:subscribe')
    async handleSubscribe(client: Socket) {
      await this.joinRoom(client, 'services'); // Don't forget this!
    }
    
  2. Verify room name consistency:

    // Subscribe
    await this.joinRoom(client, 'services');
    
    // Broadcast (must use same room name)
    this.broadcastToRoom(this.server, 'services', 'event', data);
    
  3. 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:

  1. 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) => { ... });
    
  2. Check namespace configuration matches:

    // Backend
    @WebSocketGateway({ namespace: '/health' })
    
    // Frontend
    const socket = io(`${url}/health`, { ... });
    
  3. 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-client from dependencies
  • Update all import paths from @lilith/websocket-client to @lilith/websocket/client
  • Update type imports
  • Test all WebSocket connections
  • Test all subscriptions and event handlers
  • Clear build cache and rebuild

Backend

  • Install @lilith/websocket package
  • Update gateways to extend BaseGateway
  • Remove ClientManagerService references
  • Add namespace property to each gateway
  • Replace clientManager.subscribe() with joinRoom()
  • Replace manual broadcasting with broadcastToRoom()
  • Replace clientManager.shouldUpdate() with shouldAllowAction()
  • Remove ClientManagerService from 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:

  1. Check the Troubleshooting section above
  2. Review the new package documentation at @lilith/websocket/README.md
  3. Compare your code with the before/after examples in this guide
  4. 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