create-fluxstack 1.0.13 → 1.0.15
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/.env.example +29 -29
- package/app/client/README.md +69 -69
- package/app/client/index.html +14 -13
- package/app/client/src/App.tsx +157 -524
- package/app/client/src/components/ErrorBoundary.tsx +107 -0
- package/app/client/src/components/ErrorDisplay.css +365 -0
- package/app/client/src/components/ErrorDisplay.tsx +258 -0
- package/app/client/src/components/FluxStackConfig.tsx +1321 -0
- package/app/client/src/components/HybridLiveCounter.tsx +140 -0
- package/app/client/src/components/LiveClock.tsx +286 -0
- package/app/client/src/components/MainLayout.tsx +390 -0
- package/app/client/src/components/SidebarNavigation.tsx +391 -0
- package/app/client/src/components/StateDemo.tsx +178 -0
- package/app/client/src/components/SystemMonitor.tsx +1038 -0
- package/app/client/src/components/Teste.tsx +104 -0
- package/app/client/src/components/UserProfile.tsx +809 -0
- package/app/client/src/hooks/useAuth.ts +39 -0
- package/app/client/src/hooks/useNotifications.ts +56 -0
- package/app/client/src/lib/eden-api.ts +189 -53
- package/app/client/src/lib/errors.ts +340 -0
- package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
- package/app/client/src/lib/index.ts +45 -0
- package/app/client/src/main.tsx +3 -2
- package/app/client/src/pages/ApiDocs.tsx +182 -0
- package/app/client/src/pages/Demo.tsx +174 -0
- package/app/client/src/pages/HybridLive.tsx +263 -0
- package/app/client/src/pages/Overview.tsx +155 -0
- package/app/client/src/store/README.md +43 -0
- package/app/client/src/store/index.ts +16 -0
- package/app/client/src/store/slices/uiSlice.ts +151 -0
- package/app/client/src/store/slices/userSlice.ts +161 -0
- package/app/client/src/test/README.md +257 -0
- package/app/client/src/test/setup.ts +70 -0
- package/app/client/src/test/types.ts +12 -0
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +44 -43
- package/app/client/tsconfig.json +7 -7
- package/app/client/tsconfig.node.json +25 -25
- package/app/client/zustand-setup.md +65 -0
- package/app/server/controllers/users.controller.ts +68 -68
- package/app/server/index.ts +9 -1
- package/app/server/live/CounterComponent.ts +191 -0
- package/app/server/live/FluxStackConfig.ts +529 -0
- package/app/server/live/LiveClockComponent.ts +214 -0
- package/app/server/live/SidebarNavigation.ts +156 -0
- package/app/server/live/SystemMonitor.ts +594 -0
- package/app/server/live/SystemMonitorIntegration.ts +151 -0
- package/app/server/live/TesteComponent.ts +87 -0
- package/app/server/live/UserProfileComponent.ts +135 -0
- package/app/server/live/register-components.ts +28 -0
- package/app/server/middleware/auth.ts +136 -0
- package/app/server/middleware/errorHandling.ts +250 -0
- package/app/server/middleware/index.ts +10 -0
- package/app/server/middleware/rateLimit.ts +193 -0
- package/app/server/middleware/requestLogging.ts +215 -0
- package/app/server/middleware/validation.ts +270 -0
- package/app/server/routes/index.ts +14 -2
- package/app/server/routes/upload.ts +92 -0
- package/app/server/routes/users.routes.ts +2 -9
- package/app/server/services/NotificationService.ts +302 -0
- package/app/server/services/UserService.ts +222 -0
- package/app/server/services/index.ts +46 -0
- package/core/cli/commands/plugin-deps.ts +263 -0
- package/core/cli/generators/README.md +339 -0
- package/core/cli/generators/component.ts +770 -0
- package/core/cli/generators/controller.ts +299 -0
- package/core/cli/generators/index.ts +144 -0
- package/core/cli/generators/interactive.ts +228 -0
- package/core/cli/generators/prompts.ts +83 -0
- package/core/cli/generators/route.ts +513 -0
- package/core/cli/generators/service.ts +465 -0
- package/core/cli/generators/template-engine.ts +154 -0
- package/core/cli/generators/types.ts +71 -0
- package/core/cli/generators/utils.ts +192 -0
- package/core/cli/index.ts +69 -0
- package/core/cli/plugin-discovery.ts +16 -85
- package/core/client/fluxstack.ts +17 -0
- package/core/client/hooks/index.ts +7 -0
- package/core/client/hooks/state-validator.ts +130 -0
- package/core/client/hooks/useAuth.ts +49 -0
- package/core/client/hooks/useChunkedUpload.ts +258 -0
- package/core/client/hooks/useHybridLiveComponent.ts +967 -0
- package/core/client/hooks/useWebSocket.ts +373 -0
- package/core/client/index.ts +47 -0
- package/core/client/state/createStore.ts +193 -0
- package/core/client/state/index.ts +15 -0
- package/core/config/env-dynamic.ts +1 -1
- package/core/config/env.ts +2 -1
- package/core/config/runtime-config.ts +3 -3
- package/core/config/schema.ts +84 -49
- package/core/framework/server.ts +30 -0
- package/core/index.ts +25 -0
- package/core/live/ComponentRegistry.ts +399 -0
- package/core/live/types.ts +164 -0
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
- package/core/plugins/built-in/live-components/index.ts +27 -0
- package/core/plugins/built-in/logger/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +1 -1
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +1 -1
- package/core/plugins/built-in/vite/index.ts +1 -1
- package/core/plugins/dependency-manager.ts +384 -0
- package/core/plugins/index.ts +5 -1
- package/core/plugins/manager.ts +7 -3
- package/core/plugins/registry.ts +88 -10
- package/core/plugins/types.ts +11 -11
- package/core/server/framework.ts +43 -0
- package/core/server/index.ts +11 -1
- package/core/server/live/ComponentRegistry.ts +1017 -0
- package/core/server/live/FileUploadManager.ts +272 -0
- package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +644 -0
- package/core/server/live/WebSocketConnectionManager.ts +688 -0
- package/core/server/live/websocket-plugin.ts +435 -0
- package/core/server/middleware/errorHandling.ts +141 -0
- package/core/server/middleware/index.ts +16 -0
- package/core/server/plugins/static-files-plugin.ts +232 -0
- package/core/server/services/BaseService.ts +95 -0
- package/core/server/services/ServiceContainer.ts +144 -0
- package/core/server/services/index.ts +9 -0
- package/core/templates/create-project.ts +196 -33
- package/core/testing/index.ts +10 -0
- package/core/testing/setup.ts +74 -0
- package/core/types/build.ts +38 -14
- package/core/types/types.ts +319 -0
- package/core/utils/env-runtime.ts +7 -0
- package/core/utils/errors/handlers.ts +264 -39
- package/core/utils/errors/index.ts +528 -18
- package/core/utils/errors/middleware.ts +114 -0
- package/core/utils/logger/formatters.ts +222 -0
- package/core/utils/logger/index.ts +167 -48
- package/core/utils/logger/middleware.ts +253 -0
- package/core/utils/logger/performance.ts +384 -0
- package/core/utils/logger/transports.ts +365 -0
- package/create-fluxstack.ts +296 -296
- package/fluxstack.config.ts +17 -1
- package/package-template.json +66 -66
- package/package.json +31 -6
- package/public/README.md +16 -0
- package/vite.config.ts +29 -14
- package/.claude/settings.local.json +0 -74
- package/.github/workflows/ci-build-tests.yml +0 -480
- package/.github/workflows/dependency-management.yml +0 -324
- package/.github/workflows/release-validation.yml +0 -355
- package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
- package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
- package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
- package/CLAUDE.md +0 -200
- package/Dockerfile +0 -58
- package/Dockerfile.backend +0 -52
- package/Dockerfile.frontend +0 -54
- package/README-Docker.md +0 -85
- package/ai-context/00-QUICK-START.md +0 -86
- package/ai-context/README.md +0 -88
- package/ai-context/development/eden-treaty-guide.md +0 -362
- package/ai-context/development/patterns.md +0 -382
- package/ai-context/development/plugins-guide.md +0 -572
- package/ai-context/examples/crud-complete.md +0 -626
- package/ai-context/project/architecture.md +0 -399
- package/ai-context/project/overview.md +0 -213
- package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
- package/ai-context/recent-changes/type-inference-fix.md +0 -223
- package/ai-context/reference/environment-vars.md +0 -384
- package/ai-context/reference/troubleshooting.md +0 -407
- package/app/client/src/components/TestPage.tsx +0 -453
- package/bun.lock +0 -1063
- package/bunfig.toml +0 -16
- package/core/__tests__/integration.test.ts +0 -227
- package/core/build/index.ts +0 -186
- package/core/config/__tests__/config-loader.test.ts +0 -554
- package/core/config/__tests__/config-merger.test.ts +0 -657
- package/core/config/__tests__/env-converter.test.ts +0 -372
- package/core/config/__tests__/env-processor.test.ts +0 -431
- package/core/config/__tests__/env.test.ts +0 -452
- package/core/config/__tests__/integration.test.ts +0 -418
- package/core/config/__tests__/loader.test.ts +0 -331
- package/core/config/__tests__/schema.test.ts +0 -129
- package/core/config/__tests__/validator.test.ts +0 -318
- package/core/framework/__tests__/server.test.ts +0 -233
- package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
- package/core/plugins/__tests__/manager.test.ts +0 -398
- package/core/plugins/__tests__/monitoring.test.ts +0 -401
- package/core/plugins/__tests__/registry.test.ts +0 -335
- package/core/utils/__tests__/errors.test.ts +0 -139
- package/core/utils/__tests__/helpers.test.ts +0 -297
- package/core/utils/__tests__/logger.test.ts +0 -141
- package/create-test-app.ts +0 -156
- package/docker-compose.microservices.yml +0 -75
- package/docker-compose.simple.yml +0 -57
- package/docker-compose.yml +0 -71
- package/eslint.config.js +0 -23
- package/flux-cli.ts +0 -214
- package/nginx-lb.conf +0 -37
- package/publish.sh +0 -63
- package/run-clean.ts +0 -26
- package/run-env-tests.ts +0 -313
- package/tailwind.config.js +0 -34
- package/tests/__mocks__/api.ts +0 -56
- package/tests/fixtures/users.ts +0 -69
- package/tests/integration/api/users.routes.test.ts +0 -221
- package/tests/setup.ts +0 -29
- package/tests/unit/app/client/App-simple.test.tsx +0 -56
- package/tests/unit/app/client/App.test.tsx.skip +0 -237
- package/tests/unit/app/client/eden-api.test.ts +0 -186
- package/tests/unit/app/client/simple.test.tsx +0 -23
- package/tests/unit/app/controllers/users.controller.test.ts +0 -150
- package/tests/unit/core/create-project.test.ts.skip +0 -95
- package/tests/unit/core/framework.test.ts +0 -144
- package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
- package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
- package/tests/utils/test-helpers.ts +0 -61
- package/vitest.config.ts +0 -50
- package/workspace.json +0 -6
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
// 🔌 FluxStack Enhanced WebSocket Connection Manager
|
|
2
|
+
// Advanced connection management with pooling, load balancing, and health monitoring
|
|
3
|
+
|
|
4
|
+
import { EventEmitter } from 'events'
|
|
5
|
+
|
|
6
|
+
export interface ConnectionConfig {
|
|
7
|
+
maxConnections: number
|
|
8
|
+
connectionTimeout: number
|
|
9
|
+
heartbeatInterval: number
|
|
10
|
+
reconnectAttempts: number
|
|
11
|
+
reconnectDelay: number
|
|
12
|
+
maxReconnectDelay: number
|
|
13
|
+
jitterFactor: number
|
|
14
|
+
loadBalancing: 'round-robin' | 'least-connections' | 'random'
|
|
15
|
+
healthCheckInterval: number
|
|
16
|
+
messageQueueSize: number
|
|
17
|
+
offlineQueueEnabled: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ConnectionMetrics {
|
|
21
|
+
id: string
|
|
22
|
+
connectedAt: Date
|
|
23
|
+
lastActivity: Date
|
|
24
|
+
messagesSent: number
|
|
25
|
+
messagesReceived: number
|
|
26
|
+
bytesTransferred: number
|
|
27
|
+
latency: number
|
|
28
|
+
status: 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'error'
|
|
29
|
+
errorCount: number
|
|
30
|
+
reconnectCount: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConnectionHealth {
|
|
34
|
+
id: string
|
|
35
|
+
status: 'healthy' | 'degraded' | 'unhealthy'
|
|
36
|
+
lastCheck: Date
|
|
37
|
+
issues: string[]
|
|
38
|
+
metrics: ConnectionMetrics
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface QueuedMessage {
|
|
42
|
+
id: string
|
|
43
|
+
message: any
|
|
44
|
+
timestamp: number
|
|
45
|
+
priority: number
|
|
46
|
+
retryCount: number
|
|
47
|
+
maxRetries: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface LoadBalancerStats {
|
|
51
|
+
strategy: string
|
|
52
|
+
totalConnections: number
|
|
53
|
+
activeConnections: number
|
|
54
|
+
averageLatency: number
|
|
55
|
+
messageDistribution: Record<string, number>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class WebSocketConnectionManager extends EventEmitter {
|
|
59
|
+
private connections = new Map<string, any>() // connectionId -> websocket
|
|
60
|
+
private connectionMetrics = new Map<string, ConnectionMetrics>()
|
|
61
|
+
private connectionPools = new Map<string, Set<string>>() // poolId -> connectionIds
|
|
62
|
+
private messageQueues = new Map<string, QueuedMessage[]>() // connectionId -> queued messages
|
|
63
|
+
private healthCheckInterval: NodeJS.Timeout
|
|
64
|
+
private config: ConnectionConfig
|
|
65
|
+
private loadBalancerIndex = 0
|
|
66
|
+
|
|
67
|
+
constructor(config?: Partial<ConnectionConfig>) {
|
|
68
|
+
super()
|
|
69
|
+
|
|
70
|
+
this.config = {
|
|
71
|
+
maxConnections: 10000,
|
|
72
|
+
connectionTimeout: 30000,
|
|
73
|
+
heartbeatInterval: 30000,
|
|
74
|
+
reconnectAttempts: 5,
|
|
75
|
+
reconnectDelay: 1000,
|
|
76
|
+
maxReconnectDelay: 30000,
|
|
77
|
+
jitterFactor: 0.1,
|
|
78
|
+
loadBalancing: 'round-robin',
|
|
79
|
+
healthCheckInterval: 60000,
|
|
80
|
+
messageQueueSize: 1000,
|
|
81
|
+
offlineQueueEnabled: true,
|
|
82
|
+
...config
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.setupHealthMonitoring()
|
|
86
|
+
this.setupHeartbeat()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a new WebSocket connection
|
|
91
|
+
*/
|
|
92
|
+
registerConnection(ws: any, connectionId: string, poolId?: string): void {
|
|
93
|
+
if (this.connections.size >= this.config.maxConnections) {
|
|
94
|
+
throw new Error('Maximum connections exceeded')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create connection metrics
|
|
98
|
+
const metrics: ConnectionMetrics = {
|
|
99
|
+
id: connectionId,
|
|
100
|
+
connectedAt: new Date(),
|
|
101
|
+
lastActivity: new Date(),
|
|
102
|
+
messagesSent: 0,
|
|
103
|
+
messagesReceived: 0,
|
|
104
|
+
bytesTransferred: 0,
|
|
105
|
+
latency: 0,
|
|
106
|
+
status: 'connected',
|
|
107
|
+
errorCount: 0,
|
|
108
|
+
reconnectCount: 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.connections.set(connectionId, ws)
|
|
112
|
+
this.connectionMetrics.set(connectionId, metrics)
|
|
113
|
+
|
|
114
|
+
// Add to pool if specified
|
|
115
|
+
if (poolId) {
|
|
116
|
+
this.addToPool(connectionId, poolId)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Initialize message queue
|
|
120
|
+
this.messageQueues.set(connectionId, [])
|
|
121
|
+
|
|
122
|
+
// Setup connection event handlers
|
|
123
|
+
this.setupConnectionHandlers(ws, connectionId)
|
|
124
|
+
|
|
125
|
+
console.log(`🔌 Connection registered: ${connectionId} (Pool: ${poolId || 'default'})`)
|
|
126
|
+
this.emit('connectionRegistered', { connectionId, poolId })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Setup connection event handlers
|
|
131
|
+
*/
|
|
132
|
+
private setupConnectionHandlers(ws: any, connectionId: string): void {
|
|
133
|
+
const metrics = this.connectionMetrics.get(connectionId)
|
|
134
|
+
if (!metrics) return
|
|
135
|
+
|
|
136
|
+
// Handle incoming messages
|
|
137
|
+
ws.on('message', (data: any) => {
|
|
138
|
+
metrics.messagesReceived++
|
|
139
|
+
metrics.lastActivity = new Date()
|
|
140
|
+
metrics.bytesTransferred += Buffer.byteLength(data)
|
|
141
|
+
|
|
142
|
+
this.emit('messageReceived', { connectionId, data })
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Handle connection close
|
|
146
|
+
ws.on('close', () => {
|
|
147
|
+
metrics.status = 'disconnected'
|
|
148
|
+
this.handleConnectionClose(connectionId)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Handle connection errors
|
|
152
|
+
ws.on('error', (error: Error) => {
|
|
153
|
+
metrics.errorCount++
|
|
154
|
+
metrics.status = 'error'
|
|
155
|
+
this.handleConnectionError(connectionId, error)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Handle pong responses for latency measurement
|
|
159
|
+
ws.on('pong', () => {
|
|
160
|
+
const now = Date.now()
|
|
161
|
+
const pingTime = (ws as any)._pingTime
|
|
162
|
+
if (pingTime) {
|
|
163
|
+
metrics.latency = now - pingTime
|
|
164
|
+
delete (ws as any)._pingTime
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Add connection to a pool
|
|
171
|
+
*/
|
|
172
|
+
addToPool(connectionId: string, poolId: string): void {
|
|
173
|
+
if (!this.connectionPools.has(poolId)) {
|
|
174
|
+
this.connectionPools.set(poolId, new Set())
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.connectionPools.get(poolId)!.add(connectionId)
|
|
178
|
+
console.log(`🏊 Connection ${connectionId} added to pool ${poolId}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Remove connection from pool
|
|
183
|
+
*/
|
|
184
|
+
removeFromPool(connectionId: string, poolId: string): void {
|
|
185
|
+
const pool = this.connectionPools.get(poolId)
|
|
186
|
+
if (pool) {
|
|
187
|
+
pool.delete(connectionId)
|
|
188
|
+
if (pool.size === 0) {
|
|
189
|
+
this.connectionPools.delete(poolId)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Send message with load balancing and queuing
|
|
196
|
+
*/
|
|
197
|
+
async sendMessage(
|
|
198
|
+
message: any,
|
|
199
|
+
target?: { connectionId?: string; poolId?: string },
|
|
200
|
+
options?: { priority?: number; maxRetries?: number; queueIfOffline?: boolean }
|
|
201
|
+
): Promise<boolean> {
|
|
202
|
+
const { priority = 1, maxRetries = 3, queueIfOffline = true } = options || {}
|
|
203
|
+
|
|
204
|
+
let targetConnections: string[] = []
|
|
205
|
+
|
|
206
|
+
if (target?.connectionId) {
|
|
207
|
+
// Send to specific connection
|
|
208
|
+
targetConnections = [target.connectionId]
|
|
209
|
+
} else if (target?.poolId) {
|
|
210
|
+
// Send to pool using load balancing
|
|
211
|
+
targetConnections = this.selectConnectionsFromPool(target.poolId, 1)
|
|
212
|
+
} else {
|
|
213
|
+
// Broadcast to all connections
|
|
214
|
+
targetConnections = Array.from(this.connections.keys())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let successCount = 0
|
|
218
|
+
|
|
219
|
+
for (const connectionId of targetConnections) {
|
|
220
|
+
const success = await this.sendToConnection(connectionId, message, {
|
|
221
|
+
priority,
|
|
222
|
+
maxRetries,
|
|
223
|
+
queueIfOffline
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if (success) successCount++
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return successCount > 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Send message to specific connection
|
|
234
|
+
*/
|
|
235
|
+
private async sendToConnection(
|
|
236
|
+
connectionId: string,
|
|
237
|
+
message: any,
|
|
238
|
+
options: { priority: number; maxRetries: number; queueIfOffline: boolean }
|
|
239
|
+
): Promise<boolean> {
|
|
240
|
+
const ws = this.connections.get(connectionId)
|
|
241
|
+
const metrics = this.connectionMetrics.get(connectionId)
|
|
242
|
+
|
|
243
|
+
if (!ws || !metrics) {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check if connection is ready
|
|
248
|
+
if (ws.readyState !== 1) { // WebSocket.OPEN
|
|
249
|
+
if (options.queueIfOffline && this.config.offlineQueueEnabled) {
|
|
250
|
+
return this.queueMessage(connectionId, message, options)
|
|
251
|
+
}
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const serializedMessage = JSON.stringify(message)
|
|
257
|
+
ws.send(serializedMessage)
|
|
258
|
+
|
|
259
|
+
// Update metrics
|
|
260
|
+
metrics.messagesSent++
|
|
261
|
+
metrics.lastActivity = new Date()
|
|
262
|
+
metrics.bytesTransferred += Buffer.byteLength(serializedMessage)
|
|
263
|
+
|
|
264
|
+
return true
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error(`❌ Failed to send message to ${connectionId}:`, error)
|
|
267
|
+
|
|
268
|
+
// Queue message for retry if enabled
|
|
269
|
+
if (options.queueIfOffline) {
|
|
270
|
+
return this.queueMessage(connectionId, message, options)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Queue message for offline delivery
|
|
279
|
+
*/
|
|
280
|
+
private queueMessage(
|
|
281
|
+
connectionId: string,
|
|
282
|
+
message: any,
|
|
283
|
+
options: { priority: number; maxRetries: number }
|
|
284
|
+
): boolean {
|
|
285
|
+
const queue = this.messageQueues.get(connectionId)
|
|
286
|
+
if (!queue) return false
|
|
287
|
+
|
|
288
|
+
// Check queue size limit
|
|
289
|
+
if (queue.length >= this.config.messageQueueSize) {
|
|
290
|
+
// Remove oldest low-priority message
|
|
291
|
+
const lowPriorityIndex = queue.findIndex(msg => msg.priority <= options.priority)
|
|
292
|
+
if (lowPriorityIndex !== -1) {
|
|
293
|
+
queue.splice(lowPriorityIndex, 1)
|
|
294
|
+
} else {
|
|
295
|
+
return false // Queue full with higher priority messages
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const queuedMessage: QueuedMessage = {
|
|
300
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
301
|
+
message,
|
|
302
|
+
timestamp: Date.now(),
|
|
303
|
+
priority: options.priority,
|
|
304
|
+
retryCount: 0,
|
|
305
|
+
maxRetries: options.maxRetries
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Insert message in priority order
|
|
309
|
+
const insertIndex = queue.findIndex(msg => msg.priority < options.priority)
|
|
310
|
+
if (insertIndex === -1) {
|
|
311
|
+
queue.push(queuedMessage)
|
|
312
|
+
} else {
|
|
313
|
+
queue.splice(insertIndex, 0, queuedMessage)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log(`📬 Message queued for ${connectionId}: ${queuedMessage.id}`)
|
|
317
|
+
return true
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Process queued messages for a connection
|
|
322
|
+
*/
|
|
323
|
+
private async processMessageQueue(connectionId: string): Promise<void> {
|
|
324
|
+
const queue = this.messageQueues.get(connectionId)
|
|
325
|
+
const ws = this.connections.get(connectionId)
|
|
326
|
+
|
|
327
|
+
if (!queue || !ws || ws.readyState !== 1) return
|
|
328
|
+
|
|
329
|
+
const messagesToProcess = [...queue]
|
|
330
|
+
queue.length = 0 // Clear queue
|
|
331
|
+
|
|
332
|
+
for (const queuedMessage of messagesToProcess) {
|
|
333
|
+
try {
|
|
334
|
+
const success = await this.sendToConnection(connectionId, queuedMessage.message, {
|
|
335
|
+
priority: queuedMessage.priority,
|
|
336
|
+
maxRetries: queuedMessage.maxRetries - queuedMessage.retryCount,
|
|
337
|
+
queueIfOffline: false
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
if (!success) {
|
|
341
|
+
queuedMessage.retryCount++
|
|
342
|
+
if (queuedMessage.retryCount < queuedMessage.maxRetries) {
|
|
343
|
+
// Re-queue for retry
|
|
344
|
+
queue.push(queuedMessage)
|
|
345
|
+
} else {
|
|
346
|
+
console.warn(`❌ Message ${queuedMessage.id} exceeded max retries`)
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
console.log(`✅ Queued message delivered: ${queuedMessage.id}`)
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error(`❌ Error processing queued message ${queuedMessage.id}:`, error)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Select connections from pool using load balancing strategy
|
|
359
|
+
*/
|
|
360
|
+
private selectConnectionsFromPool(poolId: string, count: number = 1): string[] {
|
|
361
|
+
const pool = this.connectionPools.get(poolId)
|
|
362
|
+
if (!pool || pool.size === 0) return []
|
|
363
|
+
|
|
364
|
+
const availableConnections = Array.from(pool).filter(connectionId => {
|
|
365
|
+
const ws = this.connections.get(connectionId)
|
|
366
|
+
return ws && ws.readyState === 1 // WebSocket.OPEN
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
if (availableConnections.length === 0) return []
|
|
370
|
+
|
|
371
|
+
switch (this.config.loadBalancing) {
|
|
372
|
+
case 'round-robin':
|
|
373
|
+
return this.roundRobinSelection(availableConnections, count)
|
|
374
|
+
|
|
375
|
+
case 'least-connections':
|
|
376
|
+
return this.leastConnectionsSelection(availableConnections, count)
|
|
377
|
+
|
|
378
|
+
case 'random':
|
|
379
|
+
return this.randomSelection(availableConnections, count)
|
|
380
|
+
|
|
381
|
+
default:
|
|
382
|
+
return this.roundRobinSelection(availableConnections, count)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Round-robin load balancing
|
|
388
|
+
*/
|
|
389
|
+
private roundRobinSelection(connections: string[], count: number): string[] {
|
|
390
|
+
const selected: string[] = []
|
|
391
|
+
|
|
392
|
+
for (let i = 0; i < count && i < connections.length; i++) {
|
|
393
|
+
const index = (this.loadBalancerIndex + i) % connections.length
|
|
394
|
+
selected.push(connections[index])
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.loadBalancerIndex = (this.loadBalancerIndex + count) % connections.length
|
|
398
|
+
return selected
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Least connections load balancing
|
|
403
|
+
*/
|
|
404
|
+
private leastConnectionsSelection(connections: string[], count: number): string[] {
|
|
405
|
+
const connectionLoads = connections.map(connectionId => {
|
|
406
|
+
const metrics = this.connectionMetrics.get(connectionId)
|
|
407
|
+
const queueSize = this.messageQueues.get(connectionId)?.length || 0
|
|
408
|
+
return {
|
|
409
|
+
connectionId,
|
|
410
|
+
load: (metrics?.messagesSent || 0) + queueSize
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
connectionLoads.sort((a, b) => a.load - b.load)
|
|
415
|
+
return connectionLoads.slice(0, count).map(item => item.connectionId)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Random load balancing
|
|
420
|
+
*/
|
|
421
|
+
private randomSelection(connections: string[], count: number): string[] {
|
|
422
|
+
const shuffled = [...connections].sort(() => Math.random() - 0.5)
|
|
423
|
+
return shuffled.slice(0, count)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Handle connection close
|
|
428
|
+
*/
|
|
429
|
+
private handleConnectionClose(connectionId: string): void {
|
|
430
|
+
console.log(`🔌 Connection closed: ${connectionId}`)
|
|
431
|
+
|
|
432
|
+
// Update metrics
|
|
433
|
+
const metrics = this.connectionMetrics.get(connectionId)
|
|
434
|
+
if (metrics) {
|
|
435
|
+
metrics.status = 'disconnected'
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Remove from pools
|
|
439
|
+
for (const [poolId, pool] of this.connectionPools) {
|
|
440
|
+
if (pool.has(connectionId)) {
|
|
441
|
+
this.removeFromPool(connectionId, poolId)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
this.emit('connectionClosed', { connectionId })
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Handle connection error
|
|
450
|
+
*/
|
|
451
|
+
private handleConnectionError(connectionId: string, error: Error): void {
|
|
452
|
+
console.error(`❌ Connection error for ${connectionId}:`, error.message)
|
|
453
|
+
|
|
454
|
+
const metrics = this.connectionMetrics.get(connectionId)
|
|
455
|
+
if (metrics) {
|
|
456
|
+
metrics.errorCount++
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
this.emit('connectionError', { connectionId, error })
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Setup health monitoring
|
|
464
|
+
*/
|
|
465
|
+
private setupHealthMonitoring(): void {
|
|
466
|
+
this.healthCheckInterval = setInterval(() => {
|
|
467
|
+
this.performHealthChecks()
|
|
468
|
+
}, this.config.healthCheckInterval)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Setup heartbeat/ping mechanism
|
|
473
|
+
*/
|
|
474
|
+
private setupHeartbeat(): void {
|
|
475
|
+
setInterval(() => {
|
|
476
|
+
this.sendHeartbeat()
|
|
477
|
+
}, this.config.heartbeatInterval)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Send heartbeat to all connections
|
|
482
|
+
*/
|
|
483
|
+
private sendHeartbeat(): void {
|
|
484
|
+
for (const [connectionId, ws] of this.connections) {
|
|
485
|
+
if (ws.readyState === 1) { // WebSocket.OPEN
|
|
486
|
+
try {
|
|
487
|
+
(ws as any)._pingTime = Date.now()
|
|
488
|
+
ws.ping()
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error(`❌ Heartbeat failed for ${connectionId}:`, error)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Perform health checks on all connections
|
|
498
|
+
*/
|
|
499
|
+
private async performHealthChecks(): Promise<void> {
|
|
500
|
+
const healthChecks: ConnectionHealth[] = []
|
|
501
|
+
const now = Date.now()
|
|
502
|
+
|
|
503
|
+
for (const [connectionId, metrics] of this.connectionMetrics) {
|
|
504
|
+
const ws = this.connections.get(connectionId)
|
|
505
|
+
const issues: string[] = []
|
|
506
|
+
let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy'
|
|
507
|
+
|
|
508
|
+
// Check connection state
|
|
509
|
+
if (!ws || ws.readyState !== 1) {
|
|
510
|
+
issues.push('Connection not open')
|
|
511
|
+
status = 'unhealthy'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check activity
|
|
515
|
+
const timeSinceActivity = now - metrics.lastActivity.getTime()
|
|
516
|
+
if (timeSinceActivity > this.config.heartbeatInterval * 2) {
|
|
517
|
+
issues.push('No activity for extended period')
|
|
518
|
+
status = status === 'healthy' ? 'degraded' : 'unhealthy'
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check error rate
|
|
522
|
+
if (metrics.errorCount > 10) {
|
|
523
|
+
issues.push('High error rate')
|
|
524
|
+
status = 'unhealthy'
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check latency
|
|
528
|
+
if (metrics.latency > 5000) { // 5 seconds
|
|
529
|
+
issues.push('High latency detected')
|
|
530
|
+
status = status === 'healthy' ? 'degraded' : status
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
healthChecks.push({
|
|
534
|
+
id: connectionId,
|
|
535
|
+
status,
|
|
536
|
+
lastCheck: new Date(),
|
|
537
|
+
issues,
|
|
538
|
+
metrics: { ...metrics }
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Handle unhealthy connections
|
|
543
|
+
const unhealthyConnections = healthChecks.filter(hc => hc.status === 'unhealthy')
|
|
544
|
+
for (const unhealthy of unhealthyConnections) {
|
|
545
|
+
await this.handleUnhealthyConnection(unhealthy.id)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
this.emit('healthCheckCompleted', { healthChecks, unhealthyCount: unhealthyConnections.length })
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Handle unhealthy connection
|
|
553
|
+
*/
|
|
554
|
+
private async handleUnhealthyConnection(connectionId: string): Promise<void> {
|
|
555
|
+
console.warn(`⚠️ Handling unhealthy connection: ${connectionId}`)
|
|
556
|
+
|
|
557
|
+
const ws = this.connections.get(connectionId)
|
|
558
|
+
if (ws) {
|
|
559
|
+
try {
|
|
560
|
+
ws.close()
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error(`❌ Error closing unhealthy connection ${connectionId}:`, error)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this.cleanupConnection(connectionId)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Cleanup connection resources
|
|
571
|
+
*/
|
|
572
|
+
cleanupConnection(connectionId: string): void {
|
|
573
|
+
// Process any remaining queued messages
|
|
574
|
+
this.processMessageQueue(connectionId)
|
|
575
|
+
|
|
576
|
+
// Remove from all data structures
|
|
577
|
+
this.connections.delete(connectionId)
|
|
578
|
+
this.connectionMetrics.delete(connectionId)
|
|
579
|
+
this.messageQueues.delete(connectionId)
|
|
580
|
+
|
|
581
|
+
// Remove from pools
|
|
582
|
+
for (const [poolId, pool] of this.connectionPools) {
|
|
583
|
+
if (pool.has(connectionId)) {
|
|
584
|
+
this.removeFromPool(connectionId, poolId)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
console.log(`🧹 Connection cleaned up: ${connectionId}`)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Get connection metrics
|
|
593
|
+
*/
|
|
594
|
+
getConnectionMetrics(connectionId: string): ConnectionMetrics | null {
|
|
595
|
+
return this.connectionMetrics.get(connectionId) || null
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Get all connection metrics
|
|
600
|
+
*/
|
|
601
|
+
getAllConnectionMetrics(): ConnectionMetrics[] {
|
|
602
|
+
return Array.from(this.connectionMetrics.values())
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get pool statistics
|
|
607
|
+
*/
|
|
608
|
+
getPoolStats(poolId: string): LoadBalancerStats | null {
|
|
609
|
+
const pool = this.connectionPools.get(poolId)
|
|
610
|
+
if (!pool) return null
|
|
611
|
+
|
|
612
|
+
const connections = Array.from(pool)
|
|
613
|
+
const activeConnections = connections.filter(connectionId => {
|
|
614
|
+
const ws = this.connections.get(connectionId)
|
|
615
|
+
return ws && ws.readyState === 1
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
const totalLatency = activeConnections.reduce((sum, connectionId) => {
|
|
619
|
+
const metrics = this.connectionMetrics.get(connectionId)
|
|
620
|
+
return sum + (metrics?.latency || 0)
|
|
621
|
+
}, 0)
|
|
622
|
+
|
|
623
|
+
const messageDistribution: Record<string, number> = {}
|
|
624
|
+
for (const connectionId of connections) {
|
|
625
|
+
const metrics = this.connectionMetrics.get(connectionId)
|
|
626
|
+
messageDistribution[connectionId] = metrics?.messagesSent || 0
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
strategy: this.config.loadBalancing,
|
|
631
|
+
totalConnections: connections.length,
|
|
632
|
+
activeConnections: activeConnections.length,
|
|
633
|
+
averageLatency: activeConnections.length > 0 ? totalLatency / activeConnections.length : 0,
|
|
634
|
+
messageDistribution
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Get overall system stats
|
|
640
|
+
*/
|
|
641
|
+
getSystemStats() {
|
|
642
|
+
const totalConnections = this.connections.size
|
|
643
|
+
const activeConnections = Array.from(this.connections.values()).filter(ws => ws.readyState === 1).length
|
|
644
|
+
const totalPools = this.connectionPools.size
|
|
645
|
+
const totalQueuedMessages = Array.from(this.messageQueues.values()).reduce((sum, queue) => sum + queue.length, 0)
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
totalConnections,
|
|
649
|
+
activeConnections,
|
|
650
|
+
totalPools,
|
|
651
|
+
totalQueuedMessages,
|
|
652
|
+
maxConnections: this.config.maxConnections,
|
|
653
|
+
connectionUtilization: (totalConnections / this.config.maxConnections) * 100
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Shutdown connection manager
|
|
659
|
+
*/
|
|
660
|
+
shutdown(): void {
|
|
661
|
+
console.log('🔌 Shutting down WebSocket Connection Manager...')
|
|
662
|
+
|
|
663
|
+
// Clear intervals
|
|
664
|
+
if (this.healthCheckInterval) {
|
|
665
|
+
clearInterval(this.healthCheckInterval)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Close all connections
|
|
669
|
+
for (const [connectionId, ws] of this.connections) {
|
|
670
|
+
try {
|
|
671
|
+
ws.close()
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error(`❌ Error closing connection ${connectionId}:`, error)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Clear all data
|
|
678
|
+
this.connections.clear()
|
|
679
|
+
this.connectionMetrics.clear()
|
|
680
|
+
this.connectionPools.clear()
|
|
681
|
+
this.messageQueues.clear()
|
|
682
|
+
|
|
683
|
+
console.log('✅ WebSocket Connection Manager shutdown complete')
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Global connection manager instance
|
|
688
|
+
export const connectionManager = new WebSocketConnectionManager()
|