WebSockets i Server-Sent Events (SSE) to dwie fundamentalne technologie umożliwiające komunikację w czasie rzeczywistym między klientem a serwerem. W tej lekcji poznamy zaawansowane techniki implementacji obu rozwiązań w aplikacjach React.
1// hooks/useWebSocket.js
2import { useState, useEffect, useRef, useCallback } from 'react';
3
4const useWebSocket = (url, options = {}) => {
5 const [socket, setSocket] = useState(null);
6 const [lastMessage, setLastMessage] = useState(null);
7 const [readyState, setReadyState] = useState(0);
8 const [connectionError, setConnectionError] = useState(null);
9
10 // Ref dla callbacks aby uniknąć re-renderów
11 const messageListeners = useRef(new Set());
12 const retryTimeoutRef = useRef(null);
13 const reconnectAttemptsRef = useRef(0);
14
15 const {
16 reconnectAttempts = 5,
17 reconnectInterval = 3000,
18 heartbeatInterval = 30000,
19 onOpen,
20 onClose,
21 onError,
22 onMessage,
23 protocols = []
24 } = options;
25
26 // Funkcja do tworzenia połączenia
27 const connect = useCallback(() => {
28 try {
29 const ws = new WebSocket(url, protocols);
30
31 ws.onopen = (event) => {
32 console.log('WebSocket połączony:', url);
33 setReadyState(1); // OPEN
34 setConnectionError(null);
35 reconnectAttemptsRef.current = 0;
36 onOpen?.(event);
37 };
38
39 ws.onmessage = (event) => {
40 let parsedData;
41 try {
42 parsedData = JSON.parse(event.data);
43 } catch {
44 parsedData = event.data;
45 }
46
47 setLastMessage({
48 data: parsedData,
49 timestamp: Date.now(),
50 raw: event.data
51 });
52
53 // Wywołaj wszystkie zarejestrowane listenery
54 messageListeners.current.forEach(listener => {
55 listener(parsedData, event);
56 });
57
58 onMessage?.(parsedData, event);
59 };
60
61 ws.onclose = (event) => {
62 console.log('WebSocket rozłączony:', event.code, event.reason);
63 setReadyState(3); // CLOSED
64 setSocket(null);
65 onClose?.(event);
66
67 // Automatyczne ponowne łączenie
68 if (!event.wasClean && reconnectAttemptsRef.current < reconnectAttempts) {
69 reconnectAttemptsRef.current++;
70 console.log(`Ponowne łączenie (${reconnectAttemptsRef.current}/${reconnectAttempts})...`);
71
72 retryTimeoutRef.current = setTimeout(() => {
73 connect();
74 }, reconnectInterval * reconnectAttemptsRef.current);
75 }
76 };
77
78 ws.onerror = (error) => {
79 console.error('WebSocket błąd:', error);
80 setConnectionError(error);
81 onError?.(error);
82 };
83
84 setSocket(ws);
85 setReadyState(0); // CONNECTING
86
87 } catch (error) {
88 console.error('Nie udało się utworzyć WebSocket:', error);
89 setConnectionError(error);
90 }
91 }, [url, protocols, reconnectAttempts, reconnectInterval, onOpen, onClose, onError, onMessage]);
92
93 // Wysyłanie wiadomości
94 const sendMessage = useCallback((message) => {
95 if (socket && socket.readyState === WebSocket.OPEN) {
96 const dataToSend = typeof message === 'string' ? message : JSON.stringify(message);
97 socket.send(dataToSend);
98 return true;
99 } else {
100 console.warn('WebSocket nie jest połączony');
101 return false;
102 }
103 }, [socket]);
104
105 // Dodawanie listenera wiadomości
106 const addMessageListener = useCallback((listener) => {
107 messageListeners.current.add(listener);
108 return () => messageListeners.current.delete(listener);
109 }, []);
110
111 // Ręczne połączenie
112 const reconnect = useCallback(() => {
113 if (socket) {
114 socket.close();
115 }
116 reconnectAttemptsRef.current = 0;
117 connect();
118 }, [socket, connect]);
119
120 // Rozłączenie
121 const disconnect = useCallback(() => {
122 if (retryTimeoutRef.current) {
123 clearTimeout(retryTimeoutRef.current);
124 }
125 if (socket) {
126 socket.close(1000, 'Zamknięte przez użytkownika');
127 }
128 }, [socket]);
129
130 // Automatyczne połączenie przy mount
131 useEffect(() => {
132 connect();
133
134 return () => {
135 if (retryTimeoutRef.current) {
136 clearTimeout(retryTimeoutRef.current);
137 }
138 if (socket) {
139 socket.close();
140 }
141 };
142 }, [connect]);
143
144 // Heartbeat - ping/pong
145 useEffect(() => {
146 if (socket && readyState === 1 && heartbeatInterval > 0) {
147 const interval = setInterval(() => {
148 if (socket.readyState === WebSocket.OPEN) {
149 sendMessage({ type: 'ping', timestamp: Date.now() });
150 }
151 }, heartbeatInterval);
152
153 return () => clearInterval(interval);
154 }
155 }, [socket, readyState, heartbeatInterval, sendMessage]);
156
157 return {
158 socket,
159 lastMessage,
160 readyState,
161 connectionError,
162 sendMessage,
163 addMessageListener,
164 reconnect,
165 disconnect,
166 isConnecting: readyState === 0,
167 isOpen: readyState === 1,
168 isClosing: readyState === 2,
169 isClosed: readyState === 3
170 };
171};
172
173export default useWebSocket;1// components/ChatRoom.jsx
2import React, { useState, useEffect, useRef } from 'react';
3import useWebSocket from '../hooks/useWebSocket';
4
5const ChatRoom = ({ roomId, userId, userName }) => {
6 const [messages, setMessages] = useState([]);
7 const [newMessage, setNewMessage] = useState('');
8 const [typingUsers, setTypingUsers] = useState(new Set());
9 const [onlineUsers, setOnlineUsers] = useState(new Map());
10 const messagesEndRef = useRef(null);
11 const typingTimeoutRef = useRef(null);
12
13 const {
14 sendMessage,
15 lastMessage,
16 isOpen,
17 connectionError,
18 addMessageListener
19 } = useWebSocket(`ws://localhost:8080/chat/${roomId}`, {
20 onOpen: () => {
21 // Dołącz do pokoju
22 sendMessage({
23 type: 'join',
24 roomId,
25 userId,
26 userName
27 });
28 }
29 });
30
31 // Obsługa otrzymanych wiadomości
32 useEffect(() => {
33 if (!lastMessage) return;
34
35 const { data } = lastMessage;
36
37 switch (data.type) {
38 case 'message':
39 setMessages(prev => [...prev, {
40 id: data.id,
41 text: data.text,
42 userId: data.userId,
43 userName: data.userName,
44 timestamp: data.timestamp
45 }]);
46 break;
47
48 case 'typing':
49 setTypingUsers(prev => {
50 const newSet = new Set(prev);
51 if (data.isTyping && data.userId !== userId) {
52 newSet.add(data.userName);
53 } else {
54 newSet.delete(data.userName);
55 }
56 return newSet;
57 });
58 break;
59
60 case 'userJoined':
61 setOnlineUsers(prev => new Map(prev).set(data.userId, data.userName));
62 setMessages(prev => [...prev, {
63 id: `system-${Date.now()}`,
64 text: `${data.userName} dołączył do czatu`,
65 type: 'system',
66 timestamp: Date.now()
67 }]);
68 break;
69
70 case 'userLeft':
71 setOnlineUsers(prev => {
72 const newMap = new Map(prev);
73 newMap.delete(data.userId);
74 return newMap;
75 });
76 setMessages(prev => [...prev, {
77 id: `system-${Date.now()}`,
78 text: `${data.userName} opuścił czat`,
79 type: 'system',
80 timestamp: Date.now()
81 }]);
82 break;
83
84 case 'messageDeleted':
85 setMessages(prev => prev.filter(msg => msg.id !== data.messageId));
86 break;
87
88 case 'messageEdited':
89 setMessages(prev => prev.map(msg =>
90 msg.id === data.messageId
91 ? { ...msg, text: data.newText, edited: true }
92 : msg
93 ));
94 break;
95 }
96 }, [lastMessage, userId]);
97
98 // Auto-scroll do najnowszej wiadomości
99 useEffect(() => {
100 messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
101 }, [messages]);
102
103 // Wysyłanie wiadomości
104 const handleSendMessage = (e) => {
105 e.preventDefault();
106
107 if (newMessage.trim() && isOpen) {
108 sendMessage({
109 type: 'message',
110 text: newMessage.trim(),
111 roomId,
112 userId,
113 userName
114 });
115
116 setNewMessage('');
117 handleStopTyping();
118 }
119 };
120
121 // Obsługa pisania
122 const handleTyping = (e) => {
123 setNewMessage(e.target.value);
124
125 if (!typingTimeoutRef.current) {
126 sendMessage({
127 type: 'typing',
128 isTyping: true,
129 userId,
130 userName,
131 roomId
132 });
133 }
134
135 clearTimeout(typingTimeoutRef.current);
136 typingTimeoutRef.current = setTimeout(() => {
137 handleStopTyping();
138 }, 2000);
139 };
140
141 const handleStopTyping = () => {
142 if (typingTimeoutRef.current) {
143 clearTimeout(typingTimeoutRef.current);
144 typingTimeoutRef.current = null;
145
146 sendMessage({
147 type: 'typing',
148 isTyping: false,
149 userId,
150 userName,
151 roomId
152 });
153 }
154 };
155
156 // Usuwanie wiadomości
157 const handleDeleteMessage = (messageId) => {
158 sendMessage({
159 type: 'deleteMessage',
160 messageId,
161 userId
162 });
163 };
164
165 if (connectionError) {
166 return (
167 <div className="chat-error">
168 <h3>Błąd połączenia z czatem</h3>
169 <p>{connectionError.message}</p>
170 <button onClick={() => window.location.reload()}>
171 Spróbuj ponownie
172 </button>
173 </div>
174 );
175 }
176
177 return (
178 <div className="chat-room">
179 <div className="chat-header">
180 <h2>Pokój: {roomId}</h2>
181 <div className="online-users">
182 Online ({onlineUsers.size}): {Array.from(onlineUsers.values()).join(', ')}
183 </div>
184 <div className="connection-status">
185 Status: {isOpen ? '🟢 Połączony' : '🔴 Rozłączony'}
186 </div>
187 </div>
188
189 <div className="chat-messages">
190 {messages.map(message => (
191 <div
192 key={message.id}
193 className={`message ${message.type === 'system' ? 'system' : ''} ${message.userId === userId ? 'own' : ''}`}
194 >
195 {message.type !== 'system' && (
196 <div className="message-header">
197 <span className="username">{message.userName}</span>
198 <span className="timestamp">
199 {new Date(message.timestamp).toLocaleTimeString()}
200 </span>
201 {message.userId === userId && (
202 <button
203 onClick={() => handleDeleteMessage(message.id)}
204 className="delete-btn"
205 >
206 🗑️
207 </button>
208 )}
209 </div>
210 )}
211 <div className="message-text">
212 {message.text}
213 {message.edited && <span className="edited"> (edytowane)</span>}
214 </div>
215 </div>
216 ))}
217
218 {typingUsers.size > 0 && (
219 <div className="typing-indicator">
220 {Array.from(typingUsers).join(', ')} pisze...
221 </div>
222 )}
223
224 <div ref={messagesEndRef} />
225 </div>
226
227 <form onSubmit={handleSendMessage} className="chat-input">
228 <input
229 type="text"
230 value={newMessage}
231 onChange={handleTyping}
232 onBlur={handleStopTyping}
233 disabled={!isOpen}
234 maxLength={500}
235 />
236 <button type="submit" disabled={!isOpen || !newMessage.trim()}>
237 Wyślij
238 </button>
239 </form>
240 </div>
241 );
242};
243
244export default ChatRoom;1// utils/websocketMiddleware.js
2class WebSocketMiddleware {
3 constructor() {
4 this.middlewares = [];
5 }
6
7 use(middleware) {
8 this.middlewares.push(middleware);
9 }
10
11 async processIncoming(message, socket) {
12 let processedMessage = message;
13
14 for (const middleware of this.middlewares) {
15 try {
16 processedMessage = await middleware.incoming(processedMessage, socket);
17 if (!processedMessage) break; // Middleware może zatrzymać przetwarzanie
18 } catch (error) {
19 console.error('Middleware error (incoming):', error);
20 break;
21 }
22 }
23
24 return processedMessage;
25 }
26
27 async processOutgoing(message, socket) {
28 let processedMessage = message;
29
30 for (const middleware of this.middlewares) {
31 try {
32 processedMessage = await middleware.outgoing(processedMessage, socket);
33 if (!processedMessage) break;
34 } catch (error) {
35 console.error('Middleware error (outgoing):', error);
36 break;
37 }
38 }
39
40 return processedMessage;
41 }
42}
43
44// Middleware do autentykacji
45const authMiddleware = {
46 incoming: async (message, socket) => {
47 if (message.type === 'auth') {
48 const token = message.token;
49 const isValid = await validateToken(token);
50
51 if (isValid) {
52 socket.isAuthenticated = true;
53 socket.userId = getUserIdFromToken(token);
54 return { type: 'authSuccess', userId: socket.userId };
55 } else {
56 socket.close(4001, 'Unauthorized');
57 return null;
58 }
59 }
60
61 if (!socket.isAuthenticated && message.type !== 'auth') {
62 socket.send(JSON.stringify({
63 type: 'error',
64 message: 'Authentication required'
65 }));
66 return null;
67 }
68
69 return message;
70 },
71
72 outgoing: async (message, socket) => {
73 if (!socket.isAuthenticated && message.type !== 'authRequired') {
74 return null;
75 }
76 return message;
77 }
78};
79
80// Middleware do rate limiting
81const rateLimitMiddleware = {
82 incoming: async (message, socket) => {
83 if (!socket.messageCount) {
84 socket.messageCount = 0;
85 socket.lastReset = Date.now();
86 }
87
88 const now = Date.now();
89 const timePassed = now - socket.lastReset;
90
91 // Reset co minutę
92 if (timePassed > 60000) {
93 socket.messageCount = 0;
94 socket.lastReset = now;
95 }
96
97 socket.messageCount++;
98
99 // Maksymalnie 100 wiadomości na minutę
100 if (socket.messageCount > 100) {
101 socket.send(JSON.stringify({
102 type: 'error',
103 message: 'Rate limit exceeded'
104 }));
105 return null;
106 }
107
108 return message;
109 },
110
111 outgoing: async (message, socket) => message
112};
113
114// Hook z middleware
115const useAdvancedWebSocket = (url, options = {}) => {
116 const [socket, setSocket] = useState(null);
117 const middleware = useRef(new WebSocketMiddleware());
118
119 useEffect(() => {
120 // Konfiguracja middleware
121 middleware.current.use(authMiddleware);
122 middleware.current.use(rateLimitMiddleware);
123 }, []);
124
125 const { sendMessage: baseSendMessage, ...rest } = useWebSocket(url, {
126 ...options,
127 onMessage: async (data, event) => {
128 const processed = await middleware.current.processIncoming(data, event.target);
129 if (processed) {
130 options.onMessage?.(processed, event);
131 }
132 }
133 });
134
135 const sendMessage = useCallback(async (message) => {
136 if (socket) {
137 const processed = await middleware.current.processOutgoing(message, socket);
138 if (processed) {
139 return baseSendMessage(processed);
140 }
141 }
142 return false;
143 }, [socket, baseSendMessage]);
144
145 return {
146 ...rest,
147 sendMessage,
148 socket
149 };
150};1// hooks/useServerSentEvents.js
2import { useState, useEffect, useRef, useCallback } from 'react';
3
4const useServerSentEvents = (url, options = {}) => {
5 const [data, setData] = useState(null);
6 const [readyState, setReadyState] = useState(0);
7 const [error, setError] = useState(null);
8 const eventSourceRef = useRef(null);
9 const listenersRef = useRef(new Map());
10
11 const {
12 withCredentials = false,
13 retryTime = 3000,
14 maxRetries = 5,
15 onOpen,
16 onError,
17 onMessage
18 } = options;
19
20 const connect = useCallback(() => {
21 try {
22 const eventSource = new EventSource(url, { withCredentials });
23 eventSourceRef.current = eventSource;
24
25 eventSource.onopen = (event) => {
26 console.log('SSE połączone:', url);
27 setReadyState(1); // OPEN
28 setError(null);
29 onOpen?.(event);
30 };
31
32 eventSource.onmessage = (event) => {
33 let parsedData;
34 try {
35 parsedData = JSON.parse(event.data);
36 } catch {
37 parsedData = event.data;
38 }
39
40 setData({
41 data: parsedData,
42 timestamp: Date.now(),
43 id: event.lastEventId,
44 type: event.type
45 });
46
47 onMessage?.(parsedData, event);
48 };
49
50 eventSource.onerror = (event) => {
51 console.error('SSE błąd:', event);
52 setReadyState(2); // CLOSED
53 setError(event);
54 onError?.(event);
55 };
56
57 // Dodaj custom event listeners
58 listenersRef.current.forEach((listener, eventType) => {
59 eventSource.addEventListener(eventType, listener);
60 });
61
62 } catch (err) {
63 console.error('Nie udało się utworzyć EventSource:', err);
64 setError(err);
65 }
66 }, [url, withCredentials, onOpen, onMessage, onError]);
67
68 const addEventListener = useCallback((eventType, listener) => {
69 listenersRef.current.set(eventType, listener);
70
71 if (eventSourceRef.current) {
72 eventSourceRef.current.addEventListener(eventType, listener);
73 }
74
75 return () => {
76 listenersRef.current.delete(eventType);
77 if (eventSourceRef.current) {
78 eventSourceRef.current.removeEventListener(eventType, listener);
79 }
80 };
81 }, []);
82
83 const close = useCallback(() => {
84 if (eventSourceRef.current) {
85 eventSourceRef.current.close();
86 eventSourceRef.current = null;
87 setReadyState(2);
88 }
89 }, []);
90
91 useEffect(() => {
92 connect();
93
94 return () => {
95 close();
96 };
97 }, [connect, close]);
98
99 return {
100 data,
101 readyState,
102 error,
103 addEventListener,
104 close,
105 reconnect: connect,
106 isConnecting: readyState === 0,
107 isOpen: readyState === 1,
108 isClosed: readyState === 2
109 };
110};
111
112export default useServerSentEvents;1// components/LiveDashboard.jsx
2import React, { useState, useEffect } from 'react';
3import useServerSentEvents from '../hooks/useServerSentEvents';
4
5const LiveDashboard = () => {
6 const [metrics, setMetrics] = useState({
7 activeUsers: 0,
8 serverLoad: 0,
9 memoryUsage: 0,
10 requestsPerSecond: 0
11 });
12
13 const [notifications, setNotifications] = useState([]);
14 const [systemStatus, setSystemStatus] = useState('healthy');
15 const [lastUpdate, setLastUpdate] = useState(null);
16
17 const { data, isOpen, error, addEventListener } = useServerSentEvents(
18 '/api/dashboard/stream',
19 {
20 onMessage: (data) => {
21 setLastUpdate(new Date());
22 }
23 }
24 );
25
26 // Handler dla różnych typów eventów
27 useEffect(() => {
28 const removeMetricsListener = addEventListener('metrics', (event) => {
29 const metricsData = JSON.parse(event.data);
30 setMetrics(metricsData);
31 });
32
33 const removeNotificationListener = addEventListener('notification', (event) => {
34 const notification = JSON.parse(event.data);
35 setNotifications(prev => [notification, ...prev.slice(0, 9)]); // Maksymalnie 10 notyfikacji
36 });
37
38 const removeStatusListener = addEventListener('status', (event) => {
39 const status = JSON.parse(event.data);
40 setSystemStatus(status.level);
41 });
42
43 const removeAlertListener = addEventListener('alert', (event) => {
44 const alert = JSON.parse(event.data);
45
46 // Pokazuj alert jako browser notification
47 if (Notification.permission === 'granted') {
48 new Notification(`System Alert: ${alert.type}`, {
49 body: alert.message,
50 icon: '/icons/alert.png'
51 });
52 }
53
54 setNotifications(prev => [{
55 ...alert,
56 type: 'alert',
57 timestamp: Date.now()
58 }, ...prev.slice(0, 9)]);
59 });
60
61 return () => {
62 removeMetricsListener();
63 removeNotificationListener();
64 removeStatusListener();
65 removeAlertListener();
66 };
67 }, [addEventListener]);
68
69 // Request notification permission
70 useEffect(() => {
71 if (Notification.permission === 'default') {
72 Notification.requestPermission();
73 }
74 }, []);
75
76 const getStatusColor = (status) => {
77 switch (status) {
78 case 'healthy': return '#4CAF50';
79 case 'warning': return '#FF9800';
80 case 'critical': return '#F44336';
81 default: return '#9E9E9E';
82 }
83 };
84
85 const getMetricColor = (value, thresholds) => {
86 if (value >= thresholds.critical) return '#F44336';
87 if (value >= thresholds.warning) return '#FF9800';
88 return '#4CAF50';
89 };
90
91 if (error) {
92 return (
93 <div className="dashboard-error">
94 <h2>Błąd połączenia z dashboard</h2>
95 <p>Nie można nawiązać połączenia z serwerem monitoringu.</p>
96 <button onClick={() => window.location.reload()}>
97 Odśwież stronę
98 </button>
99 </div>
100 );
101 }
102
103 return (
104 <div className="live-dashboard">
105 <header className="dashboard-header">
106 <h1>Live Dashboard</h1>
107 <div className="connection-info">
108 <span
109 className={`status-indicator ${isOpen ? 'connected' : 'disconnected'}`}
110 style={{ backgroundColor: isOpen ? '#4CAF50' : '#F44336' }}
111 />
112 {isOpen ? 'Połączony' : 'Rozłączony'}
113 {lastUpdate && (
114 <span className="last-update">
115 Ostatnia aktualizacja: {lastUpdate.toLocaleTimeString()}
116 </span>
117 )}
118 </div>
119 </header>
120
121 <div className="metrics-grid">
122 <div className="metric-card">
123 <h3>Aktywni użytkownicy</h3>
124 <div
125 className="metric-value"
126 style={{ color: getMetricColor(metrics.activeUsers, { warning: 1000, critical: 1500 }) }}
127 >
128 {metrics.activeUsers.toLocaleString()}
129 </div>
130 </div>
131
132 <div className="metric-card">
133 <h3>Obciążenie serwera</h3>
134 <div
135 className="metric-value"
136 style={{ color: getMetricColor(metrics.serverLoad, { warning: 70, critical: 90 }) }}
137 >
138 {metrics.serverLoad}%
139 </div>
140 <div className="progress-bar">
141 <div
142 className="progress-fill"
143 style={{
144 width: `${metrics.serverLoad}%`,
145 backgroundColor: getMetricColor(metrics.serverLoad, { warning: 70, critical: 90 })
146 }}
147 />
148 </div>
149 </div>
150
151 <div className="metric-card">
152 <h3>Użycie pamięci</h3>
153 <div
154 className="metric-value"
155 style={{ color: getMetricColor(metrics.memoryUsage, { warning: 80, critical: 95 }) }}
156 >
157 {metrics.memoryUsage}%
158 </div>
159 <div className="progress-bar">
160 <div
161 className="progress-fill"
162 style={{
163 width: `${metrics.memoryUsage}%`,
164 backgroundColor: getMetricColor(metrics.memoryUsage, { warning: 80, critical: 95 })
165 }}
166 />
167 </div>
168 </div>
169
170 <div className="metric-card">
171 <h3>Żądania/sek</h3>
172 <div
173 className="metric-value"
174 style={{ color: getMetricColor(metrics.requestsPerSecond, { warning: 1000, critical: 2000 }) }}
175 >
176 {metrics.requestsPerSecond}
177 </div>
178 </div>
179 </div>
180
181 <div className="system-status">
182 <h3>Status systemu</h3>
183 <div
184 className="status-badge"
185 style={{
186 backgroundColor: getStatusColor(systemStatus),
187 color: 'white',
188 padding: '8px 16px',
189 borderRadius: '20px',
190 display: 'inline-block'
191 }}
192 >
193 {systemStatus.toUpperCase()}
194 </div>
195 </div>
196
197 <div className="notifications-panel">
198 <h3>Powiadomienia ({notifications.length})</h3>
199 <div className="notifications-list">
200 {notifications.map((notification, index) => (
201 <div
202 key={index}
203 className={`notification ${notification.type || 'info'}`}
204 >
205 <div className="notification-content">
206 <strong>{notification.title || notification.type}</strong>
207 <p>{notification.message}</p>
208 </div>
209 <div className="notification-time">
210 {new Date(notification.timestamp).toLocaleTimeString()}
211 </div>
212 </div>
213 ))}
214 </div>
215 </div>
216 </div>
217 );
218};
219
220export default LiveDashboard;1// Przykład: Aplikacja do kolaboracji w czasie rzeczywistym
2const CollaborativeEditor = () => {
3 const { sendMessage, lastMessage, isOpen } = useWebSocket('/ws/document/123');
4 const [documentContent, setDocumentContent] = useState('');
5 const [cursors, setCursors] = useState(new Map());
6
7 const handleTextChange = (newContent, position) => {
8 setDocumentContent(newContent);
9
10 // Wyślij zmianę do innych użytkowników
11 sendMessage({
12 type: 'content_change',
13 content: newContent,
14 position,
15 userId: currentUser.id
16 });
17 };
18
19 const handleCursorMove = (position) => {
20 sendMessage({
21 type: 'cursor_move',
22 position,
23 userId: currentUser.id
24 });
25 };
26
27 useEffect(() => {
28 if (lastMessage) {
29 switch (lastMessage.data.type) {
30 case 'content_change':
31 setDocumentContent(lastMessage.data.content);
32 break;
33 case 'cursor_move':
34 setCursors(prev => new Map(prev).set(
35 lastMessage.data.userId,
36 lastMessage.data.position
37 ));
38 break;
39 }
40 }
41 }, [lastMessage]);
42
43 return (
44 <div className="collaborative-editor">
45 {/* Edytor z real-time collaboration */}
46 </div>
47 );
48};1// Przykład: Live feed aktualności
2const NewsFeed = () => {
3 const [articles, setArticles] = useState([]);
4 const [newArticleCount, setNewArticleCount] = useState(0);
5
6 const { data, addEventListener } = useServerSentEvents('/api/news/stream');
7
8 useEffect(() => {
9 const removeArticleListener = addEventListener('new_article', (event) => {
10 const article = JSON.parse(event.data);
11 setArticles(prev => [article, ...prev]);
12 setNewArticleCount(prev => prev + 1);
13 });
14
15 const removeUpdateListener = addEventListener('article_update', (event) => {
16 const updatedArticle = JSON.parse(event.data);
17 setArticles(prev => prev.map(article =>
18 article.id === updatedArticle.id ? updatedArticle : article
19 ));
20 });
21
22 return () => {
23 removeArticleListener();
24 removeUpdateListener();
25 };
26 }, [addEventListener]);
27
28 return (
29 <div className="news-feed">
30 {newArticleCount > 0 && (
31 <div className="new-articles-banner">
32 {newArticleCount} nowych artykułów
33 <button onClick={() => setNewArticleCount(0)}>
34 Oznacz jako przeczytane
35 </button>
36 </div>
37 )}
38
39 {articles.map(article => (
40 <ArticleCard key={article.id} article={article} />
41 ))}
42 </div>
43 );
44};1// Utility do wyboru odpowiedniej technologii
2const useRealTimeConnection = (url, options = {}) => {
3 const {
4 bidirectional = false,
5 authentication = false,
6 highFrequency = false,
7 binaryData = false
8 } = options;
9
10 // Decyzja o technologii na podstawie wymagań
11 const shouldUseWebSocket =
12 bidirectional ||
13 authentication ||
14 highFrequency ||
15 binaryData;
16
17 if (shouldUseWebSocket) {
18 return {
19 type: 'websocket',
20 connection: useWebSocket(url.replace('http', 'ws'), options)
21 };
22 } else {
23 return {
24 type: 'sse',
25 connection: useServerSentEvents(url, options)
26 };
27 }
28};
29
30// Przykład użycia
31const SmartComponent = () => {
32 const { type, connection } = useRealTimeConnection('/api/data', {
33 bidirectional: true, // Wymaga WebSocket
34 authentication: true
35 });
36
37 if (type === 'websocket') {
38 const { sendMessage, lastMessage } = connection;
39 // Logika dla WebSocket
40 } else {
41 const { data, addEventListener } = connection;
42 // Logika dla SSE
43 }
44
45 return <div>Smart real-time component</div>;
46};WebSockets i Server-Sent Events to komplementarne technologie dla komunikacji w czasie rzeczywistym. WebSockets oferują pełną dwukierunkową komunikację i są idealne dla aplikacji wymagających intensywnej interakcji, podczas gdy SSE zapewniają prostsze, jednokierunkowe strumieniowanie danych od serwera do klienta z automatycznym ponownym łączeniem.