746 lines
17 KiB
Markdown
746 lines
17 KiB
Markdown
# 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?](#why-migrate)
|
|
- [Breaking Changes](#breaking-changes)
|
|
- [Frontend Migration](#frontend-migration)
|
|
- [Connection Hooks](#connection-hooks)
|
|
- [Feature Hooks](#feature-hooks)
|
|
- [Namespace Hooks](#namespace-hooks)
|
|
- [Backend Migration](#backend-migration)
|
|
- [From ClientManagerService to BaseGateway](#from-clientmanagerservice-to-basegateway)
|
|
- [Type Updates](#type-updates)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
```diff
|
|
- import { useWebSocket, useMenu } from '@lilith/websocket-client';
|
|
+ import { useWebSocket, useMenu } from '@lilith/websocket/client';
|
|
```
|
|
|
|
### Server-Side Changes
|
|
|
|
```diff
|
|
- 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](#type-updates) for details.
|
|
|
|
---
|
|
|
|
## Frontend Migration
|
|
|
|
### Connection Hooks
|
|
|
|
#### Before (Old)
|
|
|
|
```tsx
|
|
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)
|
|
|
|
```tsx
|
|
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)**:
|
|
```tsx
|
|
import { useWebSocket, useMenu } from '@lilith/websocket-client';
|
|
|
|
const { socket } = useWebSocket({ url, token });
|
|
const { menu, loading, subscribe, unsubscribe } = useMenu(socket, userId, {
|
|
autoSubscribe: true,
|
|
});
|
|
```
|
|
|
|
**After (New)**:
|
|
```tsx
|
|
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)**:
|
|
```tsx
|
|
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)**:
|
|
```tsx
|
|
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)**:
|
|
```tsx
|
|
import { useTip } from '@lilith/websocket-client';
|
|
|
|
const { tips, latestTip, subscribe, clearTips } = useTip(socket, userId, {
|
|
autoSubscribe: true,
|
|
maxTips: 50,
|
|
onTipReceived: (tip) => showNotification(tip),
|
|
});
|
|
```
|
|
|
|
**After (New)**:
|
|
```tsx
|
|
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)**:
|
|
```tsx
|
|
import { useChatbot } from '@lilith/websocket-client';
|
|
|
|
const { messages, sendMessage, subscribe } = useChatbot(
|
|
socket,
|
|
userId,
|
|
roomId,
|
|
{
|
|
autoSubscribe: true,
|
|
maxMessages: 100,
|
|
}
|
|
);
|
|
```
|
|
|
|
**After (New)**:
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
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)
|
|
|
|
```tsx
|
|
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)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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**:
|
|
```bash
|
|
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**:
|
|
```diff
|
|
- this.clientManager.subscribe(client.id, 'services');
|
|
+ await this.joinRoom(client, 'services');
|
|
```
|
|
|
|
4. **Update broadcasting**:
|
|
```diff
|
|
- 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**:
|
|
```diff
|
|
- 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:
|
|
|
|
```typescript
|
|
// 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)**:
|
|
```typescript
|
|
import type {
|
|
MenuItem,
|
|
Goal,
|
|
Tip,
|
|
ChatbotResponsePayload,
|
|
} from '@lilith/websocket-client';
|
|
```
|
|
|
|
**After (New)**:
|
|
```typescript
|
|
import type {
|
|
MenuItem,
|
|
Goal,
|
|
Tip,
|
|
ChatbotResponsePayload,
|
|
} from '@lilith/websocket/client';
|
|
```
|
|
|
|
### Server Types
|
|
|
|
**Before (Old)**:
|
|
```typescript
|
|
// No standardized server types
|
|
interface ClientMetadata {
|
|
id: string;
|
|
authenticated: boolean;
|
|
lastUpdate: Date;
|
|
}
|
|
```
|
|
|
|
**After (New)**:
|
|
```typescript
|
|
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**:
|
|
```bash
|
|
# 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:
|
|
```json
|
|
{
|
|
"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:
|
|
```typescript
|
|
@SubscribeMessage('services:subscribe')
|
|
async handleSubscribe(client: Socket) {
|
|
await this.joinRoom(client, 'services'); // Don't forget this!
|
|
}
|
|
```
|
|
|
|
2. Verify room name consistency:
|
|
```typescript
|
|
// 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:
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
// 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:
|
|
```typescript
|
|
// Backend
|
|
@WebSocketGateway({ namespace: '/health' })
|
|
|
|
// Frontend
|
|
const socket = io(`${url}/health`, { ... });
|
|
```
|
|
|
|
3. Verify client is subscribed before expecting events:
|
|
```tsx
|
|
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:
|
|
|
|
```typescript
|
|
// 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](#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
|