wse-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/constants.d.ts +195 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +285 -0
- package/dist/constants.js.map +1 -0
- package/dist/examples/ChatHandlers.d.ts +15 -0
- package/dist/examples/ChatHandlers.d.ts.map +1 -0
- package/dist/examples/ChatHandlers.js +20 -0
- package/dist/examples/ChatHandlers.js.map +1 -0
- package/dist/examples/IoTHandlers.d.ts +12 -0
- package/dist/examples/IoTHandlers.d.ts.map +1 -0
- package/dist/examples/IoTHandlers.js +15 -0
- package/dist/examples/IoTHandlers.js.map +1 -0
- package/dist/handlers/EventHandlers.d.ts +17 -0
- package/dist/handlers/EventHandlers.d.ts.map +1 -0
- package/dist/handlers/EventHandlers.js +20 -0
- package/dist/handlers/EventHandlers.js.map +1 -0
- package/dist/handlers/index.d.ts +14 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +37 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/hooks/useWSE.d.ts +42 -0
- package/dist/hooks/useWSE.d.ts.map +1 -0
- package/dist/hooks/useWSE.js +1224 -0
- package/dist/hooks/useWSE.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/protocols/compression.d.ts +17 -0
- package/dist/protocols/compression.d.ts.map +1 -0
- package/dist/protocols/compression.js +155 -0
- package/dist/protocols/compression.js.map +1 -0
- package/dist/protocols/transformer.d.ts +12 -0
- package/dist/protocols/transformer.d.ts.map +1 -0
- package/dist/protocols/transformer.js +76 -0
- package/dist/protocols/transformer.js.map +1 -0
- package/dist/services/AdaptiveQualityManager.d.ts +44 -0
- package/dist/services/AdaptiveQualityManager.d.ts.map +1 -0
- package/dist/services/AdaptiveQualityManager.js +114 -0
- package/dist/services/AdaptiveQualityManager.js.map +1 -0
- package/dist/services/ConnectionManager.d.ts +92 -0
- package/dist/services/ConnectionManager.d.ts.map +1 -0
- package/dist/services/ConnectionManager.js +852 -0
- package/dist/services/ConnectionManager.js.map +1 -0
- package/dist/services/ConnectionPool.d.ts +74 -0
- package/dist/services/ConnectionPool.d.ts.map +1 -0
- package/dist/services/ConnectionPool.js +430 -0
- package/dist/services/ConnectionPool.js.map +1 -0
- package/dist/services/EventSequencer.d.ts +30 -0
- package/dist/services/EventSequencer.d.ts.map +1 -0
- package/dist/services/EventSequencer.js +152 -0
- package/dist/services/EventSequencer.js.map +1 -0
- package/dist/services/MessageProcessor.d.ts +69 -0
- package/dist/services/MessageProcessor.d.ts.map +1 -0
- package/dist/services/MessageProcessor.js +1050 -0
- package/dist/services/MessageProcessor.js.map +1 -0
- package/dist/services/NetworkMonitor.d.ts +34 -0
- package/dist/services/NetworkMonitor.d.ts.map +1 -0
- package/dist/services/NetworkMonitor.js +143 -0
- package/dist/services/NetworkMonitor.js.map +1 -0
- package/dist/services/OfflineQueue.d.ts +25 -0
- package/dist/services/OfflineQueue.d.ts.map +1 -0
- package/dist/services/OfflineQueue.js +117 -0
- package/dist/services/OfflineQueue.js.map +1 -0
- package/dist/services/RateLimiter.d.ts +25 -0
- package/dist/services/RateLimiter.d.ts.map +1 -0
- package/dist/services/RateLimiter.js +85 -0
- package/dist/services/RateLimiter.js.map +1 -0
- package/dist/stores/useMessageQueueStore.d.ts +23 -0
- package/dist/stores/useMessageQueueStore.d.ts.map +1 -0
- package/dist/stores/useMessageQueueStore.js +188 -0
- package/dist/stores/useMessageQueueStore.js.map +1 -0
- package/dist/stores/useWSEStore.d.ts +54 -0
- package/dist/stores/useWSEStore.d.ts.map +1 -0
- package/dist/stores/useWSEStore.js +464 -0
- package/dist/stores/useWSEStore.js.map +1 -0
- package/dist/types.d.ts +403 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/circuitBreaker.d.ts +35 -0
- package/dist/utils/circuitBreaker.d.ts.map +1 -0
- package/dist/utils/circuitBreaker.js +145 -0
- package/dist/utils/circuitBreaker.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +231 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/security.d.ts +74 -0
- package/dist/utils/security.d.ts.map +1 -0
- package/dist/utils/security.js +383 -0
- package/dist/utils/security.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// WebSocket Event System - Message Processing
|
|
3
|
+
// =============================================================================
|
|
4
|
+
import { MessagePriority, ConnectionQuality, ConnectionState, CircuitBreakerState } from '../types';
|
|
5
|
+
import { useWSEStore } from '../stores/useWSEStore';
|
|
6
|
+
import { useMessageQueueStore } from '../stores/useMessageQueueStore';
|
|
7
|
+
import { EventSequencer } from './EventSequencer';
|
|
8
|
+
import { CompressionManager } from '../protocols/compression';
|
|
9
|
+
import { logger } from '../utils/logger';
|
|
10
|
+
import { WS_CLIENT_VERSION, WS_PROTOCOL_VERSION } from '../constants';
|
|
11
|
+
import { securityManager } from '../utils/security';
|
|
12
|
+
export class MessageProcessor {
|
|
13
|
+
constructor(batchSize = 10, batchTimeout = 100) {
|
|
14
|
+
this.batchSize = batchSize;
|
|
15
|
+
this.batchTimeout = batchTimeout;
|
|
16
|
+
this.batchTimer = null;
|
|
17
|
+
this.processing = false;
|
|
18
|
+
this.connectionManager = null;
|
|
19
|
+
this.isReady = false;
|
|
20
|
+
// Use promise-based queue for race condition prevention
|
|
21
|
+
this.batchPromise = null;
|
|
22
|
+
this.destroyed = false;
|
|
23
|
+
// Add server ready state management
|
|
24
|
+
this.serverReadyProcessed = false;
|
|
25
|
+
this.serverReadyDetails = null;
|
|
26
|
+
// High-frequency event types that use debug-level logging instead of info
|
|
27
|
+
this.highFrequencyTypes = new Set([
|
|
28
|
+
'heartbeat',
|
|
29
|
+
'health_check',
|
|
30
|
+
'health_check_response',
|
|
31
|
+
'metrics_response',
|
|
32
|
+
'PONG',
|
|
33
|
+
]);
|
|
34
|
+
this.sequencer = new EventSequencer();
|
|
35
|
+
this.compression = new CompressionManager();
|
|
36
|
+
this.messageHandlers = new Map();
|
|
37
|
+
this.registerDefaultHandlers();
|
|
38
|
+
}
|
|
39
|
+
setConnectionManager(manager) {
|
|
40
|
+
this.connectionManager = manager;
|
|
41
|
+
// Process pending server ready if we have it
|
|
42
|
+
if (this.serverReadyProcessed && this.serverReadyDetails && manager) {
|
|
43
|
+
logger.info('Processing pending server ready details');
|
|
44
|
+
manager.handleServerReady(this.serverReadyDetails);
|
|
45
|
+
this.serverReadyDetails = null; // Clear after processing
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
setReady(ready) {
|
|
49
|
+
this.isReady = ready;
|
|
50
|
+
if (ready) {
|
|
51
|
+
logger.info('MessageProcessor is now ready to process messages');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Debug
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
debugMessage(type, message, details) {
|
|
58
|
+
// Only log in development or if debug mode is enabled
|
|
59
|
+
if (process.env.NODE_ENV !== 'development' && !window.WSE_DEBUG) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const timestamp = new Date().toISOString();
|
|
63
|
+
const logEntry = {
|
|
64
|
+
timestamp,
|
|
65
|
+
type,
|
|
66
|
+
message: {
|
|
67
|
+
t: message?.t,
|
|
68
|
+
id: message?.id,
|
|
69
|
+
seq: message?.seq,
|
|
70
|
+
hasPayload: !!message?.p,
|
|
71
|
+
payloadType: typeof message?.p,
|
|
72
|
+
payloadKeys: message?.p ? Object.keys(message.p) : [],
|
|
73
|
+
},
|
|
74
|
+
details,
|
|
75
|
+
raw: message
|
|
76
|
+
};
|
|
77
|
+
// Store in session storage for debugging
|
|
78
|
+
try {
|
|
79
|
+
const debugLog = JSON.parse(sessionStorage.getItem('wse_debug_log') || '[]');
|
|
80
|
+
debugLog.push(logEntry);
|
|
81
|
+
// Keep only last 100 entries
|
|
82
|
+
if (debugLog.length > 100) {
|
|
83
|
+
debugLog.shift();
|
|
84
|
+
}
|
|
85
|
+
sessionStorage.setItem('wse_debug_log', JSON.stringify(debugLog));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Ignore storage errors
|
|
89
|
+
}
|
|
90
|
+
// Log to console with color coding
|
|
91
|
+
const color = type.includes('error') ? 'color: red' :
|
|
92
|
+
type.includes('warning') ? 'color: orange' :
|
|
93
|
+
'color: blue';
|
|
94
|
+
console.log(`%c[WSE Debug] ${type}`, color, logEntry);
|
|
95
|
+
}
|
|
96
|
+
// Add this static method to enable/disable debug mode
|
|
97
|
+
static enableDebugMode(enabled = true) {
|
|
98
|
+
window.WSE_DEBUG = enabled;
|
|
99
|
+
if (enabled) {
|
|
100
|
+
logger.info('WSE Debug mode enabled. Messages will be logged to console and sessionStorage.');
|
|
101
|
+
logger.info('View debug log with: JSON.parse(sessionStorage.getItem("wse_debug_log"))');
|
|
102
|
+
logger.info('Clear debug log with: sessionStorage.removeItem("wse_debug_log")');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
logger.info('WSE Debug mode disabled.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Event Versioning
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
handleEventVersion(message) {
|
|
112
|
+
// Client supports event versions 1-2
|
|
113
|
+
const CLIENT_SUPPORTED_VERSION = 2;
|
|
114
|
+
if (message.event_version && message.event_version > CLIENT_SUPPORTED_VERSION) {
|
|
115
|
+
logger.warn(`Received event with version ${message.event_version} (type: ${message.t}), ` +
|
|
116
|
+
`but client only supports up to version ${CLIENT_SUPPORTED_VERSION}. ` +
|
|
117
|
+
`Some fields may not be handled correctly. Consider upgrading the client.`, { eventType: message.t, version: message.event_version });
|
|
118
|
+
}
|
|
119
|
+
// Log version info for debugging (only once per event type)
|
|
120
|
+
if (message.event_version) {
|
|
121
|
+
const key = `${message.t}_v${message.event_version}`;
|
|
122
|
+
if (!this.sequencer.isDuplicate(key)) {
|
|
123
|
+
logger.debug(`Event type '${message.t}' using schema version ${message.event_version}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
logPerformanceMetrics(message) {
|
|
128
|
+
// Log performance metrics for observability and monitoring
|
|
129
|
+
const metrics = [];
|
|
130
|
+
if (message.latency_ms !== undefined) {
|
|
131
|
+
metrics.push(`event_latency=${message.latency_ms}ms`);
|
|
132
|
+
// Warn on high end-to-end latency
|
|
133
|
+
if (message.latency_ms > 1000) {
|
|
134
|
+
logger.warn(`High event latency detected: ${message.latency_ms}ms for event type '${message.t}'`, { eventType: message.t, latency: message.latency_ms });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (message.wse_processing_ms !== undefined) {
|
|
138
|
+
metrics.push(`wse_processing=${message.wse_processing_ms}ms`);
|
|
139
|
+
if (message.wse_processing_ms > 150) {
|
|
140
|
+
logger.warn(`High backend WSE processing time: ${message.wse_processing_ms}ms for event type '${message.t}'`, {
|
|
141
|
+
eventType: message.t,
|
|
142
|
+
backendWseProcessing: message.wse_processing_ms,
|
|
143
|
+
note: 'This is backend processing time, not frontend processing'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (message.trace_id) {
|
|
148
|
+
metrics.push(`trace_id=${message.trace_id}`);
|
|
149
|
+
}
|
|
150
|
+
// Log metrics if any are present
|
|
151
|
+
if (metrics.length > 0) {
|
|
152
|
+
logger.debug(`Event '${message.t}' metrics: ${metrics.join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Message Processing
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
async processIncoming(data) {
|
|
159
|
+
if (this.destroyed)
|
|
160
|
+
return;
|
|
161
|
+
const store = useWSEStore.getState();
|
|
162
|
+
store.incrementMetric('messagesReceived');
|
|
163
|
+
try {
|
|
164
|
+
let message;
|
|
165
|
+
if (data instanceof ArrayBuffer) {
|
|
166
|
+
message = await this.processBinaryMessage(data);
|
|
167
|
+
store.incrementMetric('bytesReceived', data.byteLength);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
message = this.processTextMessage(data);
|
|
171
|
+
store.incrementMetric('bytesReceived', new TextEncoder().encode(data).byteLength);
|
|
172
|
+
}
|
|
173
|
+
if (!message)
|
|
174
|
+
return;
|
|
175
|
+
// Log every message for debugging
|
|
176
|
+
logger.debug(`Received message type: ${message.t}`, message);
|
|
177
|
+
// Check event version compatibility
|
|
178
|
+
this.handleEventVersion(message);
|
|
179
|
+
// Log performance metrics for observability
|
|
180
|
+
this.logPerformanceMetrics(message);
|
|
181
|
+
// Check for duplicate
|
|
182
|
+
if (message.id && this.sequencer.isDuplicate(message.id)) {
|
|
183
|
+
logger.debug(`Duplicate message ignored: ${message.id}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Record sequence if present
|
|
187
|
+
if (message.seq !== undefined) {
|
|
188
|
+
this.sequencer.recordSequence(message.seq);
|
|
189
|
+
}
|
|
190
|
+
// Route message
|
|
191
|
+
await this.routeMessage(message);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
logger.error('Error processing message:', error);
|
|
195
|
+
store.setLastError('Message processing error');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
processTextMessage(data) {
|
|
199
|
+
// Handle special text messages from the backend
|
|
200
|
+
// WSE:PING from backend - respond with JSON PONG
|
|
201
|
+
if (data.toUpperCase().startsWith('WSE:PING') || data.toUpperCase().startsWith('PING')) {
|
|
202
|
+
const parts = data.split(':');
|
|
203
|
+
const serverTimestamp = parts.length > 1 ? parseInt(parts[parts.length - 1], 10) : Date.now();
|
|
204
|
+
if (this.connectionManager?.ws?.readyState === WebSocket.OPEN) {
|
|
205
|
+
try {
|
|
206
|
+
const pongMessage = {
|
|
207
|
+
t: 'PONG',
|
|
208
|
+
p: {
|
|
209
|
+
server_timestamp: serverTimestamp,
|
|
210
|
+
client_timestamp: Date.now()
|
|
211
|
+
},
|
|
212
|
+
v: WS_PROTOCOL_VERSION
|
|
213
|
+
};
|
|
214
|
+
this.connectionManager.ws.send(`WSE${JSON.stringify(pongMessage)}`);
|
|
215
|
+
logger.debug(`Responded to WSE:PING with JSON PONG`);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
logger.error('Failed to send PONG:', error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
// WSE:PONG text response (legacy, keep for backward compatibility)
|
|
224
|
+
if (data.startsWith('WSE:PONG:') || data.startsWith('PONG:')) {
|
|
225
|
+
const timestamp = parseInt(data.split(':').pop() || '0', 10);
|
|
226
|
+
const latency = Date.now() - timestamp;
|
|
227
|
+
const store = useWSEStore.getState();
|
|
228
|
+
store.recordLatency(latency);
|
|
229
|
+
logger.debug(`WSE:PONG latency: ${latency}ms`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// Parse message category prefix (WSE=System, S=Snapshot, U=Update)
|
|
233
|
+
let msgCategory = null;
|
|
234
|
+
let jsonData = data;
|
|
235
|
+
if (data.startsWith('WSE{')) {
|
|
236
|
+
msgCategory = 'WSE';
|
|
237
|
+
jsonData = data.substring(3);
|
|
238
|
+
}
|
|
239
|
+
else if (data.startsWith('S{')) {
|
|
240
|
+
msgCategory = 'S';
|
|
241
|
+
jsonData = data.substring(1);
|
|
242
|
+
}
|
|
243
|
+
else if (data.startsWith('U{')) {
|
|
244
|
+
msgCategory = 'U';
|
|
245
|
+
jsonData = data.substring(1);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const parsed = JSON.parse(jsonData);
|
|
249
|
+
if (msgCategory) {
|
|
250
|
+
parsed._msg_cat = msgCategory;
|
|
251
|
+
}
|
|
252
|
+
return parsed;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
logger.error('Invalid JSON message:', error);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async processBinaryMessage(data) {
|
|
260
|
+
const view = new Uint8Array(data);
|
|
261
|
+
// Log binary message details for debugging
|
|
262
|
+
logger.debug('Binary message received:', {
|
|
263
|
+
length: data.byteLength,
|
|
264
|
+
first10Bytes: Array.from(view.slice(0, 10)),
|
|
265
|
+
first2Chars: view.length >= 2 ? String.fromCharCode(view[0], view[1]) : 'N/A',
|
|
266
|
+
hexFirst10: Array.from(view.slice(0, 10)).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
|
267
|
+
});
|
|
268
|
+
// Check for zlib magic bytes FIRST
|
|
269
|
+
if (view.length > 2 && view[0] === 0x78) {
|
|
270
|
+
const zlibCompressionMethods = [0x01, 0x5E, 0x9C, 0xDA];
|
|
271
|
+
if (zlibCompressionMethods.includes(view[1])) {
|
|
272
|
+
try {
|
|
273
|
+
logger.debug('Detected raw zlib compressed data (0x78 header)');
|
|
274
|
+
const decompressed = this.compression.decompress(data);
|
|
275
|
+
const text = new TextDecoder().decode(decompressed);
|
|
276
|
+
const parsed = JSON.parse(text);
|
|
277
|
+
logger.debug('Decompressed raw zlib message:', {
|
|
278
|
+
type: parsed.t,
|
|
279
|
+
originalSize: data.byteLength,
|
|
280
|
+
decompressedSize: decompressed.byteLength
|
|
281
|
+
});
|
|
282
|
+
const store = useWSEStore.getState();
|
|
283
|
+
store.incrementMetric('compressionHits');
|
|
284
|
+
return parsed;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
logger.error('Failed to decompress raw zlib data:', error);
|
|
288
|
+
logger.error('Data info:', {
|
|
289
|
+
length: data.byteLength,
|
|
290
|
+
first20Hex: Array.from(view.slice(0, 20)).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
|
291
|
+
});
|
|
292
|
+
// Don't return here - try other methods
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// 1. Check compression header 'C':
|
|
297
|
+
if (view.length >= 2 && view[0] === 67 && view[1] === 58) { // 'C:'
|
|
298
|
+
const store = useWSEStore.getState();
|
|
299
|
+
store.incrementMetric('compressionHits');
|
|
300
|
+
try {
|
|
301
|
+
const compressed = data.slice(2);
|
|
302
|
+
const decompressed = this.compression.decompress(compressed);
|
|
303
|
+
let text = new TextDecoder().decode(decompressed);
|
|
304
|
+
// Strip WSE/S/U prefix from decompressed text
|
|
305
|
+
if (text.startsWith('WSE{')) {
|
|
306
|
+
text = text.substring(3);
|
|
307
|
+
}
|
|
308
|
+
else if (text.startsWith('S{') || text.startsWith('U{')) {
|
|
309
|
+
text = text.substring(1);
|
|
310
|
+
}
|
|
311
|
+
const parsed = JSON.parse(text);
|
|
312
|
+
logger.info('=== DECOMPRESSED MESSAGE WITH C: HEADER ===');
|
|
313
|
+
logger.info('Type:', parsed.t);
|
|
314
|
+
return parsed;
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
logger.error('Failed to process compressed message with C: header:', error);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// 2. Check MessagePack header 'M':
|
|
322
|
+
if (view.length >= 2 && view[0] === 77 && view[1] === 58) { // 'M:'
|
|
323
|
+
try {
|
|
324
|
+
return this.compression.unpackMsgPack(data.slice(2));
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
logger.error('Failed to unpack MessagePack:', error);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// 3. Check encryption header 'E:' -- decrypt via SecurityManager
|
|
332
|
+
if (view.length >= 2 && view[0] === 69 && view[1] === 58) { // 'E:'
|
|
333
|
+
if (!securityManager.isEncryptionEnabled()) {
|
|
334
|
+
logger.warn('Encrypted message received but encryption not enabled - message dropped. Size:', data.byteLength);
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const decrypted = await securityManager.decryptFromTransport(data);
|
|
339
|
+
if (!decrypted) {
|
|
340
|
+
logger.error('Decryption returned null for encrypted message');
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return this.processTextMessage(decrypted);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
logger.error('Failed to decrypt message:', error);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// 4. Try as JSON first
|
|
351
|
+
try {
|
|
352
|
+
const text = new TextDecoder().decode(data);
|
|
353
|
+
if (text.startsWith('{') || text.startsWith('[')) {
|
|
354
|
+
const parsed = JSON.parse(text);
|
|
355
|
+
logger.debug('Parsed as plain JSON:', parsed.t);
|
|
356
|
+
return parsed;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Not JSON, continue to other methods
|
|
361
|
+
}
|
|
362
|
+
// 5. Try as raw MessagePack
|
|
363
|
+
try {
|
|
364
|
+
const unpacked = this.compression.unpackMsgPack(data);
|
|
365
|
+
logger.debug('Parsed as raw MessagePack');
|
|
366
|
+
return unpacked;
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
// Not MessagePack either
|
|
370
|
+
}
|
|
371
|
+
// 6. Last attempt - unknown format
|
|
372
|
+
logger.error('Failed to parse binary message in any known format');
|
|
373
|
+
logger.error('Message info:', {
|
|
374
|
+
length: data.byteLength,
|
|
375
|
+
first20Bytes: Array.from(view.slice(0, Math.min(20, view.length))),
|
|
376
|
+
first20Hex: Array.from(view.slice(0, Math.min(20, view.length))).map(b => `0x${b.toString(16).padStart(2, '0')}`).join(' '),
|
|
377
|
+
asText: (() => {
|
|
378
|
+
try {
|
|
379
|
+
const text = new TextDecoder().decode(data.slice(0, Math.min(100, data.byteLength)));
|
|
380
|
+
return text.replace(/[^\x20-\x7E]/g, '.');
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return 'Not text data';
|
|
384
|
+
}
|
|
385
|
+
})()
|
|
386
|
+
});
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Message Routing
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
async routeMessage(message) {
|
|
393
|
+
const type = message.t;
|
|
394
|
+
// Handle JSON PING from backend - respond with JSON PONG
|
|
395
|
+
if (type === 'PING' || type === 'ping') {
|
|
396
|
+
if (this.connectionManager?.ws?.readyState === WebSocket.OPEN) {
|
|
397
|
+
try {
|
|
398
|
+
const serverTimestamp = message.p?.timestamp || Date.now();
|
|
399
|
+
const pongMessage = {
|
|
400
|
+
t: 'PONG',
|
|
401
|
+
p: {
|
|
402
|
+
server_timestamp: serverTimestamp,
|
|
403
|
+
client_timestamp: Date.now()
|
|
404
|
+
},
|
|
405
|
+
v: WS_PROTOCOL_VERSION
|
|
406
|
+
};
|
|
407
|
+
this.connectionManager.ws.send(`WSE${JSON.stringify(pongMessage)}`);
|
|
408
|
+
logger.debug(`Responded to JSON PING with PONG`);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
logger.error('Failed to send PONG:', error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// Log messages (use debug for high-frequency event types)
|
|
417
|
+
if (this.highFrequencyTypes.has(type)) {
|
|
418
|
+
logger.debug(`Routing: ${type}`);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
logger.info(`Routing message: ${type}`);
|
|
422
|
+
}
|
|
423
|
+
// Check for a registered handler
|
|
424
|
+
const handler = this.messageHandlers.get(type);
|
|
425
|
+
if (handler) {
|
|
426
|
+
try {
|
|
427
|
+
if (this.highFrequencyTypes.has(type)) {
|
|
428
|
+
logger.debug(`Handler: ${type}`);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
logger.info(`Executing handler for ${type}`);
|
|
432
|
+
}
|
|
433
|
+
handler(message);
|
|
434
|
+
if (!this.highFrequencyTypes.has(type)) {
|
|
435
|
+
logger.info(`Handler executed successfully for ${type}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
logger.error(`Error in handler for ${type}:`, error);
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
// If no handler found
|
|
444
|
+
logger.warn(`NO HANDLER FOUND for message type: ${type}`);
|
|
445
|
+
logger.warn(`Registered handlers:`, Array.from(this.messageHandlers.keys()));
|
|
446
|
+
}
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
// Message Handlers
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
registerDefaultHandlers() {
|
|
451
|
+
logger.info('Registering default message handlers');
|
|
452
|
+
// System message handlers
|
|
453
|
+
this.messageHandlers.set('server_ready', (msg) => this.handleServerReady(msg));
|
|
454
|
+
this.messageHandlers.set('server_hello', (msg) => this.handleServerHello(msg));
|
|
455
|
+
this.messageHandlers.set('subscription_update', (msg) => this.handleSubscriptionUpdate(msg));
|
|
456
|
+
this.messageHandlers.set('error', (msg) => this.handleError(msg));
|
|
457
|
+
this.messageHandlers.set('connection_state_change', (msg) => this.handleConnectionStateChange(msg));
|
|
458
|
+
this.messageHandlers.set('health_check', (msg) => this.handleHealthCheck(msg));
|
|
459
|
+
this.messageHandlers.set('health_check_response', (msg) => this.handleHealthCheckResponse(msg));
|
|
460
|
+
this.messageHandlers.set('rate_limit_warning', (msg) => this.handleRateLimitWarning(msg));
|
|
461
|
+
this.messageHandlers.set('connection_quality', (msg) => this.handleConnectionQuality(msg));
|
|
462
|
+
this.messageHandlers.set('snapshot_complete', (msg) => this.handleSnapshotComplete(msg));
|
|
463
|
+
this.messageHandlers.set('heartbeat', () => this.handleHeartbeat());
|
|
464
|
+
this.messageHandlers.set('PONG', () => { }); // PONG is handled in processTextMessage
|
|
465
|
+
// Debug handlers
|
|
466
|
+
this.messageHandlers.set('debug_response', (msg) => this.handleDebugResponse(msg));
|
|
467
|
+
this.messageHandlers.set('sequence_stats_response', (msg) => this.handleSequenceStatsResponse(msg));
|
|
468
|
+
// Configuration handlers
|
|
469
|
+
this.messageHandlers.set('config_response', (msg) => this.handleConfigResponse(msg));
|
|
470
|
+
this.messageHandlers.set('config_update_response', (msg) => this.handleConfigUpdateResponse(msg));
|
|
471
|
+
// Encryption handlers
|
|
472
|
+
this.messageHandlers.set('encryption_response', (msg) => this.handleEncryptionResponse(msg));
|
|
473
|
+
this.messageHandlers.set('key_rotation_response', (msg) => this.handleKeyRotationResponse(msg));
|
|
474
|
+
// Batch handlers
|
|
475
|
+
this.messageHandlers.set('batch', (msg) => this.handleBatchMessage(msg));
|
|
476
|
+
this.messageHandlers.set('batch_message_result', (msg) => this.handleBatchMessageResult(msg));
|
|
477
|
+
// Metrics handler
|
|
478
|
+
this.messageHandlers.set('metrics_response', (msg) => this.handleMetricsResponse(msg));
|
|
479
|
+
// Connection state handler
|
|
480
|
+
this.messageHandlers.set('connection_state_response', (msg) => this.handleConnectionStateResponse(msg));
|
|
481
|
+
// Sync request handler
|
|
482
|
+
this.messageHandlers.set('sync_request', (msg) => {
|
|
483
|
+
const store = useWSEStore.getState();
|
|
484
|
+
this.queueOutgoing({
|
|
485
|
+
t: 'sync_response',
|
|
486
|
+
p: {
|
|
487
|
+
client_version: WS_CLIENT_VERSION,
|
|
488
|
+
protocol_version: WS_PROTOCOL_VERSION,
|
|
489
|
+
sequence: this.sequencer.getCurrentSequence(),
|
|
490
|
+
subscriptions: store.activeTopics,
|
|
491
|
+
last_update: Date.now(),
|
|
492
|
+
}
|
|
493
|
+
}, MessagePriority.HIGH);
|
|
494
|
+
});
|
|
495
|
+
// Config update handler
|
|
496
|
+
this.messageHandlers.set('config_update', (msg) => {
|
|
497
|
+
const config = msg.p;
|
|
498
|
+
const store = useWSEStore.getState();
|
|
499
|
+
if (config.compression_enabled !== undefined) {
|
|
500
|
+
store.updateConfig({ compressionEnabled: config.compression_enabled });
|
|
501
|
+
}
|
|
502
|
+
if (config.batching_enabled !== undefined) {
|
|
503
|
+
store.updateConfig({ batchingEnabled: config.batching_enabled });
|
|
504
|
+
if (config.batch_size) {
|
|
505
|
+
this.batchSize = config.batch_size;
|
|
506
|
+
}
|
|
507
|
+
if (config.batch_timeout) {
|
|
508
|
+
this.batchTimeout = config.batch_timeout;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (config.max_queue_size !== undefined) {
|
|
512
|
+
const queueStore = useMessageQueueStore.getState();
|
|
513
|
+
queueStore.setCapacity(config.max_queue_size);
|
|
514
|
+
}
|
|
515
|
+
logger.info('Configuration updated:', config);
|
|
516
|
+
});
|
|
517
|
+
// Metrics request handler
|
|
518
|
+
this.messageHandlers.set('metrics_request', (msg) => {
|
|
519
|
+
const store = useWSEStore.getState();
|
|
520
|
+
const queueStore = useMessageQueueStore.getState();
|
|
521
|
+
this.queueOutgoing({
|
|
522
|
+
t: 'metrics_response',
|
|
523
|
+
p: {
|
|
524
|
+
client_version: WS_CLIENT_VERSION,
|
|
525
|
+
connection_stats: store.metrics,
|
|
526
|
+
queue_stats: queueStore.stats,
|
|
527
|
+
diagnostics: store.diagnostics,
|
|
528
|
+
circuit_breaker: store.circuitBreaker,
|
|
529
|
+
security: store.security,
|
|
530
|
+
subscriptions: {
|
|
531
|
+
active: store.activeTopics,
|
|
532
|
+
pending: store.subscriptions.pendingSubscriptions,
|
|
533
|
+
failed: store.subscriptions.failedSubscriptions,
|
|
534
|
+
},
|
|
535
|
+
event_sequencer: this.sequencer.getStats(),
|
|
536
|
+
timestamp: new Date().toISOString(),
|
|
537
|
+
}
|
|
538
|
+
}, MessagePriority.HIGH);
|
|
539
|
+
});
|
|
540
|
+
// Priority message handler
|
|
541
|
+
this.messageHandlers.set('priority_message', (msg) => {
|
|
542
|
+
const payload = msg.p;
|
|
543
|
+
const priority = payload.priority || MessagePriority.NORMAL;
|
|
544
|
+
logger.info(`Priority message received with priority ${priority}:`, payload);
|
|
545
|
+
if (payload.type && this.messageHandlers.has(payload.type)) {
|
|
546
|
+
const handler = this.messageHandlers.get(payload.type);
|
|
547
|
+
handler({ ...msg, p: payload.content || payload });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
logger.info('Default handlers registered');
|
|
551
|
+
}
|
|
552
|
+
handleServerReady(message) {
|
|
553
|
+
const payload = message.p;
|
|
554
|
+
logger.info('Server ready:', payload);
|
|
555
|
+
const store = useWSEStore.getState();
|
|
556
|
+
// Set connection state to CONNECTED
|
|
557
|
+
store.setConnectionState(ConnectionState.CONNECTED);
|
|
558
|
+
store.updateMetrics({ connectedSince: Date.now() });
|
|
559
|
+
// Store server ready details for later processing
|
|
560
|
+
this.serverReadyDetails = payload.details || payload;
|
|
561
|
+
this.serverReadyProcessed = true;
|
|
562
|
+
// Process when connection manager is available
|
|
563
|
+
if (this.connectionManager) {
|
|
564
|
+
this.connectionManager.handleServerReady(this.serverReadyDetails);
|
|
565
|
+
this.serverReadyDetails = null;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
logger.info('Connection manager not yet available, storing server ready details for later');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
processPendingServerReady() {
|
|
572
|
+
if (this.serverReadyProcessed && this.serverReadyDetails && this.connectionManager) {
|
|
573
|
+
this.connectionManager.handleServerReady(this.serverReadyDetails);
|
|
574
|
+
this.serverReadyDetails = null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
resetServerReadyFlag() {
|
|
578
|
+
this.serverReadyProcessed = false;
|
|
579
|
+
this.serverReadyDetails = null;
|
|
580
|
+
}
|
|
581
|
+
handleServerHello(message) {
|
|
582
|
+
const payload = message.p;
|
|
583
|
+
logger.info('Server hello received:', payload);
|
|
584
|
+
const store = useWSEStore.getState();
|
|
585
|
+
if (payload.features) {
|
|
586
|
+
store.updateConfig({
|
|
587
|
+
compressionEnabled: payload.features.compression ?? true,
|
|
588
|
+
batchingEnabled: payload.features.batching ?? true,
|
|
589
|
+
offlineModeEnabled: payload.features.offline_queue ?? true,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (payload.limits) {
|
|
593
|
+
const queueStore = useMessageQueueStore.getState();
|
|
594
|
+
if (payload.limits.max_queue_size) {
|
|
595
|
+
queueStore.setCapacity(payload.limits.max_queue_size);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
handleConnectionStateChange(message) {
|
|
600
|
+
const payload = message.p;
|
|
601
|
+
logger.info('Connection state change:', payload);
|
|
602
|
+
const store = useWSEStore.getState();
|
|
603
|
+
if (payload.new_state) {
|
|
604
|
+
const stateMap = {
|
|
605
|
+
'pending': ConnectionState.PENDING,
|
|
606
|
+
'connecting': ConnectionState.CONNECTING,
|
|
607
|
+
'connected': ConnectionState.CONNECTED,
|
|
608
|
+
'reconnecting': ConnectionState.RECONNECTING,
|
|
609
|
+
'disconnected': ConnectionState.DISCONNECTED,
|
|
610
|
+
'error': ConnectionState.ERROR,
|
|
611
|
+
'degraded': ConnectionState.DEGRADED,
|
|
612
|
+
};
|
|
613
|
+
const newState = stateMap[payload.new_state.toLowerCase()];
|
|
614
|
+
if (newState) {
|
|
615
|
+
store.setConnectionState(newState);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
window.dispatchEvent(new CustomEvent('connectionStateChange', {
|
|
619
|
+
detail: {
|
|
620
|
+
oldState: payload.old_state,
|
|
621
|
+
newState: payload.new_state,
|
|
622
|
+
connectionId: payload.connection_id,
|
|
623
|
+
timestamp: payload.timestamp || new Date().toISOString(),
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
handleSubscriptionUpdate(message) {
|
|
628
|
+
const payload = message.p;
|
|
629
|
+
const store = useWSEStore.getState();
|
|
630
|
+
logger.info('=== SUBSCRIPTION UPDATE RECEIVED ===');
|
|
631
|
+
logger.info('Payload:', payload);
|
|
632
|
+
if (payload.success) {
|
|
633
|
+
payload.success_topics?.forEach((topic) => {
|
|
634
|
+
store.confirmSubscription(topic);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (payload.failed_topics) {
|
|
638
|
+
payload.failed_topics.forEach((topic) => {
|
|
639
|
+
store.failSubscription(topic);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
window.dispatchEvent(new CustomEvent('subscriptionUpdate', {
|
|
643
|
+
detail: payload
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
handleError(message) {
|
|
647
|
+
const store = useWSEStore.getState();
|
|
648
|
+
logger.error('Server error received:', {
|
|
649
|
+
type: message.t,
|
|
650
|
+
payload: message.p,
|
|
651
|
+
fullMessage: JSON.stringify(message, null, 2)
|
|
652
|
+
});
|
|
653
|
+
const errorData = message.p || {};
|
|
654
|
+
const errorMessage = errorData.message || 'Unknown error';
|
|
655
|
+
const errorCode = errorData.code || 'UNKNOWN_ERROR';
|
|
656
|
+
const recoverable = errorData.recoverable !== false;
|
|
657
|
+
const details = errorData.details || {};
|
|
658
|
+
logger.error(`Processed error - Code: ${errorCode}, Message: ${errorMessage}`, {
|
|
659
|
+
errorData,
|
|
660
|
+
recoverable,
|
|
661
|
+
details
|
|
662
|
+
});
|
|
663
|
+
store.setLastError(errorMessage, typeof errorCode === 'number' ? errorCode :
|
|
664
|
+
errorCode === 'AUTH_FAILED' ? 401 :
|
|
665
|
+
errorCode === 'INIT_ERROR' ? 500 : 500);
|
|
666
|
+
if (errorCode === 'AUTH_FAILED') {
|
|
667
|
+
logger.error('Authentication failed:', details);
|
|
668
|
+
window.dispatchEvent(new CustomEvent('wseAuthFailed', {
|
|
669
|
+
detail: { message: errorMessage, code: errorCode, details }
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
if (errorCode === 'INIT_ERROR' && !recoverable) {
|
|
673
|
+
logger.error('Critical initialization error:', details);
|
|
674
|
+
window.dispatchEvent(new CustomEvent('wseInitializationError', {
|
|
675
|
+
detail: { message: errorMessage, code: errorCode, recoverable: false, details }
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
if (errorCode === 'SERVER_ERROR') {
|
|
679
|
+
logger.error('Server error:', details);
|
|
680
|
+
window.dispatchEvent(new CustomEvent('wseServerError', {
|
|
681
|
+
detail: { message: errorMessage, code: errorCode, details }
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
if (errorCode === 'CIRCUIT_BREAKER_OPEN') {
|
|
685
|
+
logger.error('Circuit breaker activated');
|
|
686
|
+
store.updateCircuitBreaker({
|
|
687
|
+
state: CircuitBreakerState.OPEN,
|
|
688
|
+
lastFailureTime: Date.now()
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (errorCode === 'RATE_LIMIT_EXCEEDED' || errorMessage.includes('Rate limit')) {
|
|
692
|
+
logger.warn('Rate limit exceeded');
|
|
693
|
+
window.dispatchEvent(new CustomEvent('rateLimitExceeded', {
|
|
694
|
+
detail: {
|
|
695
|
+
message: errorMessage,
|
|
696
|
+
retryAfter: errorData.retry_after || errorData.retryAfter,
|
|
697
|
+
...details
|
|
698
|
+
}
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
if (errorCode === 'SUBSCRIPTION_FAILED') {
|
|
702
|
+
logger.warn('Subscription failed:', details);
|
|
703
|
+
window.dispatchEvent(new CustomEvent('subscriptionFailed', {
|
|
704
|
+
detail: {
|
|
705
|
+
message: errorMessage,
|
|
706
|
+
topics: errorData.topics || [],
|
|
707
|
+
...details
|
|
708
|
+
}
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
window.dispatchEvent(new CustomEvent('serverError', {
|
|
712
|
+
detail: {
|
|
713
|
+
message: errorMessage,
|
|
714
|
+
code: errorCode,
|
|
715
|
+
details: errorData,
|
|
716
|
+
recoverable,
|
|
717
|
+
timestamp: errorData.timestamp || new Date().toISOString(),
|
|
718
|
+
severity: errorData.severity || 'error',
|
|
719
|
+
context: {
|
|
720
|
+
messageType: message.t,
|
|
721
|
+
messageId: message.id,
|
|
722
|
+
sequence: message.seq
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}));
|
|
726
|
+
if (!recoverable || errorCode === 'PROTOCOL_ERROR' || errorCode === 'INVALID_MESSAGE') {
|
|
727
|
+
logger.error('Critical error detected, connection may need to be reset');
|
|
728
|
+
window.dispatchEvent(new CustomEvent('criticalError', {
|
|
729
|
+
detail: {
|
|
730
|
+
code: errorCode,
|
|
731
|
+
message: errorMessage,
|
|
732
|
+
shouldReconnect: errorData.shouldReconnect !== false
|
|
733
|
+
}
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
handleHealthCheck(message) {
|
|
738
|
+
const store = useWSEStore.getState();
|
|
739
|
+
store.updateMetrics({ lastHealthCheck: Date.now() });
|
|
740
|
+
this.queueOutgoing({
|
|
741
|
+
t: 'health_check_response',
|
|
742
|
+
p: {
|
|
743
|
+
client_version: WS_CLIENT_VERSION,
|
|
744
|
+
stats: store.metrics,
|
|
745
|
+
diagnostics: store.diagnostics,
|
|
746
|
+
queue_size: useMessageQueueStore.getState().size,
|
|
747
|
+
}
|
|
748
|
+
}, MessagePriority.CRITICAL);
|
|
749
|
+
}
|
|
750
|
+
handleHealthCheckResponse(message) {
|
|
751
|
+
const payload = message.p;
|
|
752
|
+
logger.info('Health check response received:', payload);
|
|
753
|
+
if (payload.diagnostics) {
|
|
754
|
+
const store = useWSEStore.getState();
|
|
755
|
+
store.updateDiagnostics(payload.diagnostics);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
handleRateLimitWarning(message) {
|
|
759
|
+
const warning = message.p;
|
|
760
|
+
logger.warn('Rate limit warning:', warning);
|
|
761
|
+
const store = useWSEStore.getState();
|
|
762
|
+
store.setLastError(`Rate limit: ${warning.message}`, 429);
|
|
763
|
+
if (warning.retry_after) {
|
|
764
|
+
logger.info(`Should retry after ${warning.retry_after} seconds`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
handleConnectionQuality(message) {
|
|
768
|
+
const payload = message.p;
|
|
769
|
+
logger.info('Connection quality update:', payload);
|
|
770
|
+
const store = useWSEStore.getState();
|
|
771
|
+
if (payload.suggestions && payload.suggestions.length > 0) {
|
|
772
|
+
const currentDiagnostics = store.diagnostics || {
|
|
773
|
+
quality: ConnectionQuality.UNKNOWN,
|
|
774
|
+
stability: 100,
|
|
775
|
+
jitter: 0,
|
|
776
|
+
packetLoss: 0,
|
|
777
|
+
roundTripTime: 0,
|
|
778
|
+
suggestions: [],
|
|
779
|
+
lastAnalysis: null,
|
|
780
|
+
};
|
|
781
|
+
store.updateDiagnostics({
|
|
782
|
+
...currentDiagnostics,
|
|
783
|
+
suggestions: payload.suggestions,
|
|
784
|
+
quality: payload.quality || currentDiagnostics.quality,
|
|
785
|
+
lastAnalysis: Date.now(),
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (payload.recommended_settings) {
|
|
789
|
+
const settings = payload.recommended_settings;
|
|
790
|
+
if (settings.compression !== undefined) {
|
|
791
|
+
store.updateConfig({ compressionEnabled: settings.compression });
|
|
792
|
+
}
|
|
793
|
+
if (settings.batch_size !== undefined) {
|
|
794
|
+
this.batchSize = settings.batch_size;
|
|
795
|
+
}
|
|
796
|
+
if (settings.batch_timeout !== undefined) {
|
|
797
|
+
this.batchTimeout = settings.batch_timeout;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
handleSnapshotComplete(message) {
|
|
802
|
+
logger.info('Snapshot complete:', message.p);
|
|
803
|
+
window.dispatchEvent(new CustomEvent('snapshotComplete', {
|
|
804
|
+
detail: message.p
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
handleHeartbeat() {
|
|
808
|
+
const store = useWSEStore.getState();
|
|
809
|
+
store.updateMetrics({ lastHealthCheck: Date.now() });
|
|
810
|
+
}
|
|
811
|
+
handleDebugResponse(message) {
|
|
812
|
+
logger.info('Debug response received:', message.p);
|
|
813
|
+
window.dispatchEvent(new CustomEvent('debugResponse', {
|
|
814
|
+
detail: message.p
|
|
815
|
+
}));
|
|
816
|
+
}
|
|
817
|
+
handleSequenceStatsResponse(message) {
|
|
818
|
+
logger.info('Sequence stats received:', message.p);
|
|
819
|
+
window.dispatchEvent(new CustomEvent('sequenceStatsResponse', {
|
|
820
|
+
detail: message.p
|
|
821
|
+
}));
|
|
822
|
+
}
|
|
823
|
+
handleConfigResponse(message) {
|
|
824
|
+
logger.info('Configuration response:', message.p);
|
|
825
|
+
window.dispatchEvent(new CustomEvent('configResponse', {
|
|
826
|
+
detail: message.p
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
handleConfigUpdateResponse(message) {
|
|
830
|
+
logger.info('Configuration update response:', message.p);
|
|
831
|
+
window.dispatchEvent(new CustomEvent('configUpdateResponse', {
|
|
832
|
+
detail: message.p
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
handleEncryptionResponse(message) {
|
|
836
|
+
logger.info('Encryption response:', message.p);
|
|
837
|
+
const store = useWSEStore.getState();
|
|
838
|
+
if (message.p.enabled !== undefined) {
|
|
839
|
+
store.updateSecurity({
|
|
840
|
+
encryptionEnabled: message.p.enabled,
|
|
841
|
+
encryptionAlgorithm: message.p.algorithms?.encryption || null
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
window.dispatchEvent(new CustomEvent('encryptionResponse', {
|
|
845
|
+
detail: message.p
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
handleKeyRotationResponse(message) {
|
|
849
|
+
logger.info('Key rotation response:', message.p);
|
|
850
|
+
const store = useWSEStore.getState();
|
|
851
|
+
if (message.p.success) {
|
|
852
|
+
store.updateSecurity({
|
|
853
|
+
lastKeyRotation: Date.now()
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
window.dispatchEvent(new CustomEvent('keyRotationResponse', {
|
|
857
|
+
detail: message.p
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
async handleBatchMessage(message) {
|
|
861
|
+
const payload = message.p;
|
|
862
|
+
logger.info(`Batch message received with ${payload.count} messages`);
|
|
863
|
+
if (payload.messages && Array.isArray(payload.messages)) {
|
|
864
|
+
for (const msg of payload.messages) {
|
|
865
|
+
await this.routeMessage(msg);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
handleBatchMessageResult(message) {
|
|
870
|
+
logger.info('Batch message result:', message.p);
|
|
871
|
+
window.dispatchEvent(new CustomEvent('batchMessageResult', {
|
|
872
|
+
detail: message.p
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
handleMetricsResponse(message) {
|
|
876
|
+
logger.info('Metrics response received:', message.p);
|
|
877
|
+
window.dispatchEvent(new CustomEvent('metricsResponse', {
|
|
878
|
+
detail: message.p
|
|
879
|
+
}));
|
|
880
|
+
}
|
|
881
|
+
handleConnectionStateResponse(message) {
|
|
882
|
+
const payload = message.p;
|
|
883
|
+
logger.info('Connection state response:', payload);
|
|
884
|
+
const store = useWSEStore.getState();
|
|
885
|
+
if (payload.metrics) {
|
|
886
|
+
store.updateMetrics(payload.metrics);
|
|
887
|
+
}
|
|
888
|
+
window.dispatchEvent(new CustomEvent('connectionStateResponse', {
|
|
889
|
+
detail: payload
|
|
890
|
+
}));
|
|
891
|
+
}
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// Outgoing Messages with Race Condition Fix
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
queueOutgoing(message, priority = MessagePriority.NORMAL) {
|
|
896
|
+
if (this.destroyed)
|
|
897
|
+
return false;
|
|
898
|
+
const queueStore = useMessageQueueStore.getState();
|
|
899
|
+
const queuedMessage = {
|
|
900
|
+
id: message.id || crypto.randomUUID(),
|
|
901
|
+
type: message.t || 'unknown',
|
|
902
|
+
payload: message.p || {},
|
|
903
|
+
priority,
|
|
904
|
+
timestamp: Date.now(),
|
|
905
|
+
retries: 0,
|
|
906
|
+
};
|
|
907
|
+
const queued = queueStore.enqueue(queuedMessage);
|
|
908
|
+
if (queued) {
|
|
909
|
+
this.scheduleBatch();
|
|
910
|
+
}
|
|
911
|
+
return queued;
|
|
912
|
+
}
|
|
913
|
+
scheduleBatch() {
|
|
914
|
+
if (this.destroyed || this.batchPromise)
|
|
915
|
+
return;
|
|
916
|
+
this.batchPromise = new Promise((resolve) => {
|
|
917
|
+
const timer = setTimeout(() => {
|
|
918
|
+
if (this.destroyed) {
|
|
919
|
+
resolve();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
this.processBatchSafe()
|
|
923
|
+
.then(resolve)
|
|
924
|
+
.catch((error) => {
|
|
925
|
+
logger.error('Batch processing error:', error);
|
|
926
|
+
resolve();
|
|
927
|
+
})
|
|
928
|
+
.finally(() => {
|
|
929
|
+
this.batchPromise = null;
|
|
930
|
+
});
|
|
931
|
+
}, this.batchTimeout);
|
|
932
|
+
if (this.batchTimer)
|
|
933
|
+
clearTimeout(this.batchTimer);
|
|
934
|
+
this.batchTimer = timer;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async processBatchSafe() {
|
|
938
|
+
if (this.processing || this.destroyed)
|
|
939
|
+
return;
|
|
940
|
+
this.processing = true;
|
|
941
|
+
try {
|
|
942
|
+
await this.processBatch();
|
|
943
|
+
}
|
|
944
|
+
finally {
|
|
945
|
+
this.processing = false;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async processBatch() {
|
|
949
|
+
if (this.destroyed)
|
|
950
|
+
return [];
|
|
951
|
+
const queueStore = useMessageQueueStore.getState();
|
|
952
|
+
queueStore.setProcessing(true);
|
|
953
|
+
try {
|
|
954
|
+
const messages = queueStore.dequeue(this.batchSize);
|
|
955
|
+
if (messages.length === 0) {
|
|
956
|
+
return [];
|
|
957
|
+
}
|
|
958
|
+
messages.sort((a, b) => {
|
|
959
|
+
if (a.priority !== b.priority) {
|
|
960
|
+
return b.priority - a.priority;
|
|
961
|
+
}
|
|
962
|
+
return a.timestamp - b.timestamp;
|
|
963
|
+
});
|
|
964
|
+
const wsMessages = messages.map(msg => ({
|
|
965
|
+
id: msg.id,
|
|
966
|
+
t: msg.type,
|
|
967
|
+
p: msg.payload,
|
|
968
|
+
v: WS_PROTOCOL_VERSION,
|
|
969
|
+
seq: this.sequencer.getNextSequence(),
|
|
970
|
+
ts: new Date().toISOString(),
|
|
971
|
+
pri: msg.priority,
|
|
972
|
+
}));
|
|
973
|
+
const store = useWSEStore.getState();
|
|
974
|
+
store.incrementMetric('messagesSent', wsMessages.length);
|
|
975
|
+
return wsMessages;
|
|
976
|
+
}
|
|
977
|
+
finally {
|
|
978
|
+
queueStore.setProcessing(false);
|
|
979
|
+
if (queueStore.size > 0 && !this.destroyed) {
|
|
980
|
+
this.scheduleBatch();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// ---------------------------------------------------------------------------
|
|
985
|
+
// Public API
|
|
986
|
+
// ---------------------------------------------------------------------------
|
|
987
|
+
/**
|
|
988
|
+
* Register an event type as high-frequency (uses debug-level logging).
|
|
989
|
+
* High-frequency types are logged at debug level to reduce noise.
|
|
990
|
+
*/
|
|
991
|
+
registerHighFrequencyType(type) {
|
|
992
|
+
this.highFrequencyTypes.add(type);
|
|
993
|
+
}
|
|
994
|
+
registerHandler(type, handler) {
|
|
995
|
+
logger.info(`Registering handler for message type: ${type}`);
|
|
996
|
+
this.messageHandlers.set(type, handler);
|
|
997
|
+
}
|
|
998
|
+
unregisterHandler(type) {
|
|
999
|
+
logger.info(`Unregistering handler for message type: ${type}`);
|
|
1000
|
+
this.messageHandlers.delete(type);
|
|
1001
|
+
}
|
|
1002
|
+
clearHandlers() {
|
|
1003
|
+
logger.info('Clearing all message handlers');
|
|
1004
|
+
this.messageHandlers.clear();
|
|
1005
|
+
this.registerDefaultHandlers();
|
|
1006
|
+
}
|
|
1007
|
+
getRegisteredHandlers() {
|
|
1008
|
+
return Array.from(this.messageHandlers.keys());
|
|
1009
|
+
}
|
|
1010
|
+
isHandlerRegistered(type) {
|
|
1011
|
+
return this.messageHandlers.has(type);
|
|
1012
|
+
}
|
|
1013
|
+
waitForHandlers(requiredHandlers, timeout = 5000) {
|
|
1014
|
+
return new Promise((resolve) => {
|
|
1015
|
+
const checkHandlers = () => {
|
|
1016
|
+
const allRegistered = requiredHandlers.every(h => this.isHandlerRegistered(h));
|
|
1017
|
+
if (allRegistered) {
|
|
1018
|
+
resolve(true);
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
return false;
|
|
1022
|
+
};
|
|
1023
|
+
if (checkHandlers())
|
|
1024
|
+
return;
|
|
1025
|
+
const startTime = Date.now();
|
|
1026
|
+
const interval = setInterval(() => {
|
|
1027
|
+
if (checkHandlers()) {
|
|
1028
|
+
clearInterval(interval);
|
|
1029
|
+
}
|
|
1030
|
+
else if (Date.now() - startTime > timeout) {
|
|
1031
|
+
clearInterval(interval);
|
|
1032
|
+
resolve(requiredHandlers.every(h => this.isHandlerRegistered(h)));
|
|
1033
|
+
}
|
|
1034
|
+
}, 100);
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
destroy() {
|
|
1038
|
+
this.destroyed = true;
|
|
1039
|
+
if (this.batchTimer) {
|
|
1040
|
+
clearTimeout(this.batchTimer);
|
|
1041
|
+
this.batchTimer = null;
|
|
1042
|
+
}
|
|
1043
|
+
this.batchPromise = null;
|
|
1044
|
+
this.clearHandlers();
|
|
1045
|
+
this.sequencer.destroy();
|
|
1046
|
+
this.serverReadyProcessed = false;
|
|
1047
|
+
this.serverReadyDetails = null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
//# sourceMappingURL=MessageProcessor.js.map
|