websocket-realtime-builder

安装量: 99
排名: #8338

安装

npx skills add https://github.com/patricio0312rev/skills --skill websocket-realtime-builder

WebSocket Realtime Builder

Build real-time applications with WebSockets and Socket.io.

Core Workflow Choose library: Socket.io vs native WebSocket Setup server: Configure WebSocket server Add authentication: Validate connections Implement rooms: Group connections Handle events: Define event handlers Add reconnection: Handle disconnects gracefully Installation

Server

npm install socket.io

Client

npm install socket.io-client

Server Setup Basic Socket.io Server // server.ts import express from 'express'; import { createServer } from 'http'; import { Server, Socket } from 'socket.io'; import { verifyToken } from './auth';

const app = express(); const httpServer = createServer(app);

const io = new Server(httpServer, { cors: { origin: process.env.CLIENT_URL, credentials: true, }, pingInterval: 25000, pingTimeout: 60000, });

// Authentication middleware io.use(async (socket, next) => { const token = socket.handshake.auth.token;

if (!token) { return next(new Error('Authentication required')); }

try { const user = await verifyToken(token); socket.data.user = user; next(); } catch (err) { next(new Error('Invalid token')); } });

io.on('connection', (socket: Socket) => { const user = socket.data.user; console.log(User connected: ${user.id});

// Join user's personal room socket.join(user:${user.id});

// Handle events socket.on('disconnect', () => { console.log(User disconnected: ${user.id}); }); });

httpServer.listen(3001, () => { console.log('Socket.io server running on port 3001'); });

export { io };

Namespaces and Rooms // namespaces/chat.ts import { Server, Socket } from 'socket.io';

export function setupChatNamespace(io: Server) { const chatNamespace = io.of('/chat');

chatNamespace.on('connection', (socket: Socket) => { const user = socket.data.user;

// Join a chat room
socket.on('join-room', async (roomId: string) => {
  // Validate user can access this room
  const canAccess = await canAccessRoom(user.id, roomId);
  if (!canAccess) {
    socket.emit('error', { message: 'Access denied' });
    return;
  }

  socket.join(`room:${roomId}`);
  socket.to(`room:${roomId}`).emit('user-joined', {
    userId: user.id,
    name: user.name,
  });
});

// Leave a chat room
socket.on('leave-room', (roomId: string) => {
  socket.leave(`room:${roomId}`);
  socket.to(`room:${roomId}`).emit('user-left', {
    userId: user.id,
  });
});

// Send message
socket.on('send-message', async (data: { roomId: string; content: string }) => {
  const { roomId, content } = data;

  // Save to database
  const message = await db.message.create({
    data: {
      roomId,
      authorId: user.id,
      content,
    },
    include: { author: true },
  });

  // Broadcast to room
  chatNamespace.to(`room:${roomId}`).emit('new-message', {
    id: message.id,
    content: message.content,
    author: {
      id: user.id,
      name: user.name,
    },
    createdAt: message.createdAt,
  });
});

// Typing indicator
socket.on('typing-start', (roomId: string) => {
  socket.to(`room:${roomId}`).emit('user-typing', {
    userId: user.id,
    name: user.name,
  });
});

socket.on('typing-stop', (roomId: string) => {
  socket.to(`room:${roomId}`).emit('user-stopped-typing', {
    userId: user.id,
  });
});

});

return chatNamespace; }

Event Emitters // services/notifications.ts import { io } from '../server';

export class NotificationService { // Send to specific user static sendToUser(userId: string, event: string, data: any) { io.to(user:${userId}).emit(event, data); }

// Send to multiple users static sendToUsers(userIds: string[], event: string, data: any) { userIds.forEach((userId) => { io.to(user:${userId}).emit(event, data); }); }

// Broadcast to all connected users static broadcast(event: string, data: any) { io.emit(event, data); }

// Send to room static sendToRoom(roomId: string, event: string, data: any) { io.to(room:${roomId}).emit(event, data); }

// Notify new order static notifyNewOrder(order: Order) { // Notify customer this.sendToUser(order.customerId, 'order:created', { orderId: order.id, status: order.status, });

// Notify admins
io.to('role:admin').emit('admin:new-order', {
  orderId: order.id,
  customer: order.customerName,
  total: order.total,
});

} }

Client Setup React Client Hook // hooks/useSocket.ts import { useEffect, useRef, useState, useCallback } from 'react'; import { io, Socket } from 'socket.io-client'; import { useAuth } from './useAuth';

interface UseSocketOptions { namespace?: string; autoConnect?: boolean; }

export function useSocket(options: UseSocketOptions = {}) { const { namespace = '/', autoConnect = true } = options; const { token } = useAuth(); const socketRef = useRef(null); const [isConnected, setIsConnected] = useState(false); const [error, setError] = useState(null);

useEffect(() => { if (!token || !autoConnect) return;

const socket = io(`${process.env.NEXT_PUBLIC_WS_URL}${namespace}`, {
  auth: { token },
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
});

socket.on('connect', () => {
  setIsConnected(true);
  setError(null);
});

socket.on('disconnect', () => {
  setIsConnected(false);
});

socket.on('connect_error', (err) => {
  setError(err.message);
  setIsConnected(false);
});

socketRef.current = socket;

return () => {
  socket.disconnect();
};

}, [token, namespace, autoConnect]);

const emit = useCallback((event: string, data?: any) => { socketRef.current?.emit(event, data); }, []);

const on = useCallback((event: string, handler: (...args: any[]) => void) => { socketRef.current?.on(event, handler); return () => { socketRef.current?.off(event, handler); }; }, []);

const off = useCallback((event: string, handler?: (...args: any[]) => void) => { socketRef.current?.off(event, handler); }, []);

return { socket: socketRef.current, isConnected, error, emit, on, off, }; }

Chat Hook // hooks/useChat.ts import { useEffect, useState, useCallback } from 'react'; import { useSocket } from './useSocket';

interface Message { id: string; content: string; author: { id: string; name: string }; createdAt: string; }

interface TypingUser { userId: string; name: string; }

export function useChat(roomId: string) { const { socket, isConnected, emit, on } = useSocket({ namespace: '/chat' }); const [messages, setMessages] = useState([]); const [typingUsers, setTypingUsers] = useState([]);

// Join room on connect useEffect(() => { if (isConnected && roomId) { emit('join-room', roomId);

  return () => {
    emit('leave-room', roomId);
  };
}

}, [isConnected, roomId, emit]);

// Listen for messages useEffect(() => { const unsubMessage = on('new-message', (message: Message) => { setMessages((prev) => [...prev, message]); });

const unsubTyping = on('user-typing', (user: TypingUser) => {
  setTypingUsers((prev) => {
    if (prev.some((u) => u.userId === user.userId)) return prev;
    return [...prev, user];
  });
});

const unsubStopTyping = on('user-stopped-typing', ({ userId }: { userId: string }) => {
  setTypingUsers((prev) => prev.filter((u) => u.userId !== userId));
});

return () => {
  unsubMessage();
  unsubTyping();
  unsubStopTyping();
};

}, [on]);

const sendMessage = useCallback((content: string) => { emit('send-message', { roomId, content }); }, [emit, roomId]);

const startTyping = useCallback(() => { emit('typing-start', roomId); }, [emit, roomId]);

const stopTyping = useCallback(() => { emit('typing-stop', roomId); }, [emit, roomId]);

return { messages, typingUsers, sendMessage, startTyping, stopTyping, isConnected, }; }

Chat Component // components/ChatRoom.tsx 'use client';

import { useState, useRef, useEffect } from 'react'; import { useChat } from '@/hooks/useChat';

export function ChatRoom({ roomId }: { roomId: string }) { const { messages, typingUsers, sendMessage, startTyping, stopTyping, isConnected, } = useChat(roomId);

const [input, setInput] = useState(''); const messagesEndRef = useRef(null); const typingTimeoutRef = useRef();

// Auto-scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);

const handleInputChange = (e: React.ChangeEvent) => { setInput(e.target.value);

// Debounced typing indicator
startTyping();
clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = setTimeout(stopTyping, 2000);

};

const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!input.trim()) return;

sendMessage(input);
setInput('');
stopTyping();

};

if (!isConnected) { return

Connecting...
; }

return (

{messages.map((message) => (
{message.author.name}: {message.content}
))}

  {typingUsers.length > 0 && (
    <div className="px-4 py-2 text-sm text-gray-500">
      {typingUsers.map((u) => u.name).join(', ')}{' '}
      {typingUsers.length === 1 ? 'is' : 'are'} typing...
    </div>
  )}

  <form onSubmit={handleSubmit} className="p-4 border-t">
    <input
      type="text"
      value={input}
      onChange={handleInputChange}
      placeholder="Type a message..."
      className="w-full px-4 py-2 border rounded"
    />
  </form>
</div>

); }

Real-time Notifications // hooks/useNotifications.ts import { useEffect, useState } from 'react'; import { useSocket } from './useSocket'; import { toast } from 'sonner';

interface Notification { id: string; type: string; title: string; message: string; createdAt: string; read: boolean; }

export function useNotifications() { const { on, isConnected } = useSocket(); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0);

useEffect(() => { const unsubscribe = on('notification', (notification: Notification) => { setNotifications((prev) => [notification, ...prev]); setUnreadCount((prev) => prev + 1);

  // Show toast
  toast(notification.title, {
    description: notification.message,
  });
});

return unsubscribe;

}, [on]);

const markAsRead = (id: string) => { setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)) ); setUnreadCount((prev) => Math.max(0, prev - 1)); };

const markAllAsRead = () => { setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); setUnreadCount(0); };

return { notifications, unreadCount, markAsRead, markAllAsRead, isConnected, }; }

Presence System // server/presence.ts import { Server, Socket } from 'socket.io';

interface UserPresence { odlineserId: string; name: string; status: 'online' | 'away' | 'busy'; lastSeen: Date; }

const onlineUsers = new Map();

export function setupPresence(io: Server) { io.on('connection', (socket: Socket) => { const user = socket.data.user;

// User comes online
onlineUsers.set(user.id, {
  odlineserId: user.id,
  name: user.name,
  status: 'online',
  lastSeen: new Date(),
});

// Broadcast to all users
io.emit('presence:update', {
  userId: user.id,
  status: 'online',
});

// Send current online users to new connection
socket.emit('presence:list', Array.from(onlineUsers.values()));

// Handle status changes
socket.on('presence:status', (status: 'online' | 'away' | 'busy') => {
  const presence = onlineUsers.get(user.id);
  if (presence) {
    presence.status = status;
    io.emit('presence:update', { userId: user.id, status });
  }
});

// Handle disconnect
socket.on('disconnect', () => {
  onlineUsers.delete(user.id);
  io.emit('presence:update', {
    userId: user.id,
    status: 'offline',
  });
});

}); }

Error Handling & Reconnection // Client-side error handling const socket = io(WS_URL, { auth: { token }, reconnection: true, reconnectionAttempts: 10, reconnectionDelay: 1000, reconnectionDelayMax: 10000, timeout: 20000, });

socket.on('connect_error', (error) => { if (error.message === 'Authentication required') { // Redirect to login router.push('/login'); } else { console.error('Connection error:', error); } });

socket.on('reconnect', (attemptNumber) => { console.log(Reconnected after ${attemptNumber} attempts); // Re-sync state socket.emit('sync-state'); });

socket.on('reconnect_failed', () => { console.error('Failed to reconnect'); // Show user notification toast.error('Connection lost. Please refresh the page.'); });

Best Practices Authenticate connections: Validate tokens on connect Use rooms: Group users logically Handle reconnection: Re-sync state after reconnect Debounce events: Prevent flooding (e.g., typing indicator) Clean up listeners: Remove on component unmount Acknowledge important events: Confirm critical messages Use namespaces: Separate concerns (chat, notifications) Scale with Redis: Use Redis adapter for multiple servers Output Checklist

Every WebSocket implementation should include:

Server with authentication middleware Client hook with reconnection handling Room/namespace organization Event type definitions Error handling and recovery Typing indicators for chat Presence system (online/offline) Clean disconnect handling Rate limiting for events Redis adapter for scaling (production)

返回排行榜