@africode/core 5.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.
Files changed (136) hide show
  1. package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
  2. package/LICENSE +623 -0
  3. package/README.md +442 -0
  4. package/bin/africode.js +73 -0
  5. package/bin/africode.js.1758507140 +343 -0
  6. package/bin/cli.ts +83 -0
  7. package/bin/create-africode.js +158 -0
  8. package/bin/scaffold.ts +219 -0
  9. package/components/accordion.js +183 -0
  10. package/components/alert.js +131 -0
  11. package/components/auth.js +172 -0
  12. package/components/avatar.js +117 -0
  13. package/components/badge.js +104 -0
  14. package/components/base.d.ts +139 -0
  15. package/components/base.js +184 -0
  16. package/components/button.js +164 -0
  17. package/components/card.js +137 -0
  18. package/components/cultural-card.js +243 -0
  19. package/components/divider.js +83 -0
  20. package/components/dropdown.js +171 -0
  21. package/components/error-boundary.js +155 -0
  22. package/components/form.js +131 -0
  23. package/components/grid.js +273 -0
  24. package/components/hero.js +138 -0
  25. package/components/icon.js +36 -0
  26. package/components/index.js +57 -0
  27. package/components/input.js +256 -0
  28. package/components/kanga-card.js +185 -0
  29. package/components/language-switcher.js +108 -0
  30. package/components/loader.js +80 -0
  31. package/components/modal.js +262 -0
  32. package/components/motion.js +84 -0
  33. package/components/navbar.js +236 -0
  34. package/components/pattern-showcase.js +225 -0
  35. package/components/progress.js +134 -0
  36. package/components/react.js +111 -0
  37. package/components/section.js +54 -0
  38. package/components/select.js +322 -0
  39. package/components/sidebar.js +180 -0
  40. package/components/skeleton.js +85 -0
  41. package/components/table.js +181 -0
  42. package/components/tabs.js +202 -0
  43. package/components/theme-toggle.js +82 -0
  44. package/components/toast.js +139 -0
  45. package/components/tooltip.js +167 -0
  46. package/core/a2ui-schema-manager.js +344 -0
  47. package/core/a2ui.js +431 -0
  48. package/core/bun-runtime.js +799 -0
  49. package/core/cli/commands/add.js +23 -0
  50. package/core/cli/commands/audit.js +58 -0
  51. package/core/cli/commands/build.js +137 -0
  52. package/core/cli/commands/create-plugin.js +241 -0
  53. package/core/cli/commands/dev.js +228 -0
  54. package/core/cli/commands/lint.js +23 -0
  55. package/core/cli/commands/test.js +34 -0
  56. package/core/cli/migrator.js +71 -0
  57. package/core/cli/ui.js +46 -0
  58. package/core/compliance.js +628 -0
  59. package/core/config.js +263 -0
  60. package/core/db-advanced.js +481 -0
  61. package/core/db.js +284 -0
  62. package/core/enhanced-hmr.js +404 -0
  63. package/core/errors.js +222 -0
  64. package/core/file-router.js +290 -0
  65. package/core/heartbeat.js +64 -0
  66. package/core/hmr-client.js +204 -0
  67. package/core/hmr.js +196 -0
  68. package/core/html.d.ts +116 -0
  69. package/core/html.js +160 -0
  70. package/core/hydration.js +52 -0
  71. package/core/lipa-namba-journey.js +572 -0
  72. package/core/motion.js +106 -0
  73. package/core/nida-cig-middleware.js +455 -0
  74. package/core/patterns.d.ts +124 -0
  75. package/core/patterns.js +833 -0
  76. package/core/plugins/index.js +312 -0
  77. package/core/router.js +387 -0
  78. package/core/sdk-client.js +62 -0
  79. package/core/sdk.d.ts +133 -0
  80. package/core/sdk.js +123 -0
  81. package/core/seo.js +76 -0
  82. package/core/server/auth-endpoints.js +339 -0
  83. package/core/server/auth.js +180 -0
  84. package/core/server/csrf.js +206 -0
  85. package/core/server/db.js +39 -0
  86. package/core/server/middleware.js +324 -0
  87. package/core/server/rate-limit.js +238 -0
  88. package/core/server/render.js +69 -0
  89. package/core/server/router.js +120 -0
  90. package/core/shim.js +28 -0
  91. package/core/state.d.ts +86 -0
  92. package/core/state.js +242 -0
  93. package/core/store.d.ts +122 -0
  94. package/core/store.js +61 -0
  95. package/core/validation.d.ts +233 -0
  96. package/core/validation.js +590 -0
  97. package/core/websocket.js +639 -0
  98. package/dist/africode.js +2905 -0
  99. package/dist/africode.js.map +61 -0
  100. package/dist/build-info.json +23 -0
  101. package/dist/components.js +2888 -0
  102. package/dist/components.js.map +58 -0
  103. package/dist/styles/africanity.css +322 -0
  104. package/dist/styles/typography.css +141 -0
  105. package/docs/IDE-Guide.md +50 -0
  106. package/package.json +110 -0
  107. package/src/index.ts +196 -0
  108. package/styles/africanity.css +322 -0
  109. package/styles/typography.css +141 -0
  110. package/templates/starter/.env.example +15 -0
  111. package/templates/starter/africode.config.js +40 -0
  112. package/templates/starter/package.json +14 -0
  113. package/templates/starter/src/pages/index.html +46 -0
  114. package/templates/starter/src/pages/index.js +32 -0
  115. package/templates/starter/src/styles/main.css +4 -0
  116. package/templates/starter-3d/.env.example +7 -0
  117. package/templates/starter-3d/africode.config.js +29 -0
  118. package/templates/starter-3d/components/af-model-viewer.js +125 -0
  119. package/templates/starter-3d/package.json +15 -0
  120. package/templates/starter-3d/src/pages/index.html +46 -0
  121. package/templates/starter-3d/src/pages/index.js +50 -0
  122. package/templates/starter-3d/src/styles/main.css +4 -0
  123. package/templates/starter-react/.env.example +15 -0
  124. package/templates/starter-react/africode.config.js +40 -0
  125. package/templates/starter-react/package.json +16 -0
  126. package/templates/starter-react/src/pages/index.html +46 -0
  127. package/templates/starter-react/src/pages/index.js +68 -0
  128. package/templates/starter-react/src/styles/main.css +4 -0
  129. package/templates/starter-tailwind/.env.example +15 -0
  130. package/templates/starter-tailwind/africode.config.js +40 -0
  131. package/templates/starter-tailwind/package.json +20 -0
  132. package/templates/starter-tailwind/src/pages/index.html +46 -0
  133. package/templates/starter-tailwind/src/pages/index.js +37 -0
  134. package/templates/starter-tailwind/src/styles/main.css +4 -0
  135. package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
  136. package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
@@ -0,0 +1,639 @@
1
+ /**
2
+ * AfriCode WebSocket Support
3
+ *
4
+ * Real-time bidirectional communication with:
5
+ * - Persistent WebSocket connections
6
+ * - Room/channel management
7
+ * - Event broadcasting
8
+ * - Message queueing
9
+ * - Presence tracking
10
+ * - Automatic reconnection
11
+ */
12
+
13
+ /**
14
+ * WebSocket Server Manager
15
+ * Handles connections, rooms, and broadcasting
16
+ */
17
+ export class WebSocketServer {
18
+ constructor() {
19
+ this.connections = new Map(); // connection_id -> WebSocket
20
+ this.rooms = new Map(); // room_name -> Set of connection_ids
21
+ this.messageHandlers = new Map(); // event_type -> handler function
22
+ this.userConnections = new Map(); // user_id -> Set of connection_ids
23
+ this.messageQueue = []; // For offline message queueing
24
+ this.maxQueueSize = 1000;
25
+ this.connectionCounter = 0;
26
+ }
27
+
28
+ /**
29
+ * Register a message handler for a specific event type
30
+ */
31
+ on(eventType, handler) {
32
+ if (!this.messageHandlers.has(eventType)) {
33
+ this.messageHandlers.set(eventType, []);
34
+ }
35
+ this.messageHandlers.get(eventType).push(handler);
36
+ return this;
37
+ }
38
+
39
+ /**
40
+ * Handle new WebSocket connection
41
+ */
42
+ handleConnection(ws, userId = null) {
43
+ const connectionId = `conn_${++this.connectionCounter}`;
44
+ this.connections.set(connectionId, {
45
+ ws,
46
+ userId,
47
+ connectedAt: Date.now(),
48
+ lastHeartbeat: Date.now(),
49
+ rooms: new Set()
50
+ });
51
+
52
+ if (userId) {
53
+ if (!this.userConnections.has(userId)) {
54
+ this.userConnections.set(userId, new Set());
55
+ }
56
+ this.userConnections.get(userId).add(connectionId);
57
+ this.broadcast('user:online', { userId, connectionId }, null, 'presence');
58
+ }
59
+
60
+ // Setup message handler
61
+ ws.onmessage = (event) => this.handleMessage(connectionId, event);
62
+
63
+ // Setup close handler
64
+ ws.onclose = () => this.handleDisconnection(connectionId);
65
+
66
+ // Send welcome message
67
+ this.send(connectionId, 'connection:established', {
68
+ connectionId,
69
+ userId,
70
+ serverTime: Date.now()
71
+ });
72
+
73
+ // Start heartbeat
74
+ this.startHeartbeat(connectionId);
75
+
76
+ return connectionId;
77
+ }
78
+
79
+ /**
80
+ * Handle incoming messages
81
+ */
82
+ handleMessage(connectionId, event) {
83
+ try {
84
+ const data = JSON.parse(event.data);
85
+ const { type, payload, roomId } = data;
86
+
87
+ const connInfo = this.connections.get(connectionId);
88
+ if (!connInfo) return;
89
+
90
+ connInfo.lastHeartbeat = Date.now();
91
+
92
+ // Handle special system messages
93
+ if (type === 'ping') {
94
+ this.send(connectionId, 'pong', { timestamp: Date.now() });
95
+ return;
96
+ }
97
+
98
+ if (type === 'join:room') {
99
+ this.joinRoom(connectionId, payload.roomId, payload.userId);
100
+ return;
101
+ }
102
+
103
+ if (type === 'leave:room') {
104
+ this.leaveRoom(connectionId, payload.roomId);
105
+ return;
106
+ }
107
+
108
+ // Call registered handlers
109
+ const handlers = this.messageHandlers.get(type) || [];
110
+ for (const handler of handlers) {
111
+ try {
112
+ handler({
113
+ connectionId,
114
+ userId: connInfo.userId,
115
+ payload,
116
+ roomId,
117
+ timestamp: Date.now()
118
+ });
119
+ } catch (error) {
120
+ console.error(`Error in handler for ${type}:`, error);
121
+ }
122
+ }
123
+ } catch (error) {
124
+ console.error('Error handling message:', error);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Handle disconnection
130
+ */
131
+ handleDisconnection(connectionId) {
132
+ const connInfo = this.connections.get(connectionId);
133
+ if (!connInfo) return;
134
+
135
+ const { userId } = connInfo;
136
+
137
+ // Remove from all rooms
138
+ for (const roomId of connInfo.rooms) {
139
+ this.leaveRoom(connectionId, roomId);
140
+ }
141
+
142
+ // Remove from user connections
143
+ if (userId) {
144
+ const userConns = this.userConnections.get(userId);
145
+ if (userConns) {
146
+ userConns.delete(connectionId);
147
+ if (userConns.size === 0) {
148
+ this.userConnections.delete(userId);
149
+ this.broadcast('user:offline', { userId }, null, 'presence');
150
+ }
151
+ }
152
+ }
153
+
154
+ this.connections.delete(connectionId);
155
+ }
156
+
157
+ /**
158
+ * Join a room
159
+ */
160
+ joinRoom(connectionId, roomId, userId = null) {
161
+ if (!this.rooms.has(roomId)) {
162
+ this.rooms.set(roomId, new Set());
163
+ }
164
+
165
+ const room = this.rooms.get(roomId);
166
+ room.add(connectionId);
167
+
168
+ const connInfo = this.connections.get(connectionId);
169
+ if (connInfo) {
170
+ connInfo.rooms.add(roomId);
171
+ }
172
+
173
+ // Broadcast user joined to room
174
+ this.broadcast(
175
+ 'room:user:joined',
176
+ { userId, connectionId, roomId },
177
+ roomId,
178
+ 'room'
179
+ );
180
+
181
+ // Send room members list to new member
182
+ const members = Array.from(room).map(cid => {
183
+ const ci = this.connections.get(cid);
184
+ return { connectionId: cid, userId: ci?.userId };
185
+ });
186
+
187
+ this.send(connectionId, 'room:members', { roomId, members });
188
+ }
189
+
190
+ /**
191
+ * Leave a room
192
+ */
193
+ leaveRoom(connectionId, roomId) {
194
+ const room = this.rooms.get(roomId);
195
+ if (!room) return;
196
+
197
+ room.delete(connectionId);
198
+
199
+ const connInfo = this.connections.get(connectionId);
200
+ if (connInfo) {
201
+ connInfo.rooms.delete(roomId);
202
+ }
203
+
204
+ // Broadcast user left to room
205
+ this.broadcast(
206
+ 'room:user:left',
207
+ { connectionId, roomId },
208
+ roomId,
209
+ 'room'
210
+ );
211
+
212
+ // Clean up empty rooms
213
+ if (room.size === 0) {
214
+ this.rooms.delete(roomId);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Send message to specific connection
220
+ */
221
+ send(connectionId, type, payload) {
222
+ const connInfo = this.connections.get(connectionId);
223
+ if (!connInfo) return false;
224
+
225
+ try {
226
+ connInfo.ws.send(JSON.stringify({
227
+ type,
228
+ payload,
229
+ timestamp: Date.now()
230
+ }));
231
+ return true;
232
+ } catch (error) {
233
+ console.error(`Error sending to ${connectionId}:`, error);
234
+ return false;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Broadcast message to all connections in a room or globally
240
+ */
241
+ broadcast(type, payload, roomId = null, scope = 'global') {
242
+ const timestamp = Date.now();
243
+
244
+ if (scope === 'room' && roomId) {
245
+ const room = this.rooms.get(roomId);
246
+ if (room) {
247
+ for (const connectionId of room) {
248
+ const connInfo = this.connections.get(connectionId);
249
+ if (connInfo) {
250
+ try {
251
+ connInfo.ws.send(JSON.stringify({
252
+ type,
253
+ payload,
254
+ roomId,
255
+ timestamp
256
+ }));
257
+ } catch (error) {
258
+ console.error(`Error broadcasting to ${connectionId}:`, error);
259
+ }
260
+ }
261
+ }
262
+ }
263
+ } else if (scope === 'user' && payload.userId) {
264
+ const userConns = this.userConnections.get(payload.userId);
265
+ if (userConns) {
266
+ for (const connectionId of userConns) {
267
+ this.send(connectionId, type, payload);
268
+ }
269
+ }
270
+ } else if (scope === 'presence') {
271
+ // Broadcast presence to all connections
272
+ for (const [connId, connInfo] of this.connections.entries()) {
273
+ try {
274
+ connInfo.ws.send(JSON.stringify({
275
+ type,
276
+ payload,
277
+ timestamp
278
+ }));
279
+ } catch (error) {
280
+ console.error(`Error broadcasting presence to ${connId}:`, error);
281
+ }
282
+ }
283
+ } else {
284
+ // Global broadcast
285
+ for (const [connId, connInfo] of this.connections.entries()) {
286
+ try {
287
+ connInfo.ws.send(JSON.stringify({
288
+ type,
289
+ payload,
290
+ timestamp
291
+ }));
292
+ } catch (error) {
293
+ console.error(`Error broadcasting to ${connId}:`, error);
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Send message to all users in a room
301
+ */
302
+ toRoom(roomId, type, payload, excludeConnectionId = null) {
303
+ const room = this.rooms.get(roomId);
304
+ if (!room) return;
305
+
306
+ for (const connectionId of room) {
307
+ if (excludeConnectionId === connectionId) continue;
308
+ this.send(connectionId, type, payload);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Send message to specific user (all their connections)
314
+ */
315
+ toUser(userId, type, payload) {
316
+ const userConns = this.userConnections.get(userId);
317
+ if (!userConns) return;
318
+
319
+ for (const connectionId of userConns) {
320
+ this.send(connectionId, type, payload);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Queue message for offline user
326
+ */
327
+ queueMessage(userId, type, payload) {
328
+ if (this.messageQueue.length >= this.maxQueueSize) {
329
+ this.messageQueue.shift(); // Remove oldest
330
+ }
331
+
332
+ this.messageQueue.push({
333
+ userId,
334
+ type,
335
+ payload,
336
+ queuedAt: Date.now()
337
+ });
338
+ }
339
+
340
+ /**
341
+ * Deliver queued messages to user
342
+ */
343
+ deliverQueuedMessages(userId) {
344
+ const queued = this.messageQueue.filter(msg => msg.userId === userId);
345
+
346
+ for (const msg of queued) {
347
+ this.toUser(userId, msg.type, msg.payload);
348
+ this.messageQueue = this.messageQueue.filter(m => m !== msg);
349
+ }
350
+
351
+ return queued.length;
352
+ }
353
+
354
+ /**
355
+ * Start heartbeat for connection
356
+ */
357
+ startHeartbeat(connectionId) {
358
+ const interval = setInterval(() => {
359
+ const connInfo = this.connections.get(connectionId);
360
+ if (!connInfo) {
361
+ clearInterval(interval);
362
+ return;
363
+ }
364
+
365
+ const timeSinceLastHeartbeat = Date.now() - connInfo.lastHeartbeat;
366
+ if (timeSinceLastHeartbeat > 60000) {
367
+ // No heartbeat for 60 seconds, consider dead
368
+ try {
369
+ connInfo.ws.close(1000, 'Heartbeat timeout');
370
+ } catch (e) {
371
+ // Already closed
372
+ }
373
+ clearInterval(interval);
374
+ return;
375
+ }
376
+
377
+ this.send(connectionId, 'ping', { timestamp: Date.now() });
378
+ }, 30000); // Every 30 seconds
379
+
380
+ return interval;
381
+ }
382
+
383
+ /**
384
+ * Get room information
385
+ */
386
+ getRoomInfo(roomId) {
387
+ const room = this.rooms.get(roomId);
388
+ if (!room) return null;
389
+
390
+ const members = Array.from(room).map(connId => {
391
+ const connInfo = this.connections.get(connId);
392
+ return {
393
+ connectionId: connId,
394
+ userId: connInfo?.userId,
395
+ connectedAt: connInfo?.connectedAt
396
+ };
397
+ });
398
+
399
+ return {
400
+ roomId,
401
+ memberCount: room.size,
402
+ members,
403
+ createdAt: Math.min(...members.map(m => m.connectedAt))
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Get user presence information
409
+ */
410
+ getUserPresence(userId) {
411
+ const userConns = this.userConnections.get(userId);
412
+ if (!userConns) return null;
413
+
414
+ const connections = Array.from(userConns).map(connId => {
415
+ const connInfo = this.connections.get(connId);
416
+ return {
417
+ connectionId: connId,
418
+ connectedAt: connInfo?.connectedAt,
419
+ rooms: Array.from(connInfo?.rooms || [])
420
+ };
421
+ });
422
+
423
+ return {
424
+ userId,
425
+ connectionCount: userConns.size,
426
+ connections,
427
+ isOnline: userConns.size > 0
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Get all active rooms
433
+ */
434
+ getAllRooms() {
435
+ const roomList = [];
436
+
437
+ for (const [roomId, members] of this.rooms.entries()) {
438
+ roomList.push({
439
+ roomId,
440
+ memberCount: members.size
441
+ });
442
+ }
443
+
444
+ return roomList;
445
+ }
446
+
447
+ /**
448
+ * Get connection statistics
449
+ */
450
+ getStats() {
451
+ return {
452
+ activeConnections: this.connections.size,
453
+ activeRooms: this.rooms.size,
454
+ activeUsers: this.userConnections.size,
455
+ queuedMessages: this.messageQueue.length,
456
+ messageHandlers: this.messageHandlers.size
457
+ };
458
+ }
459
+
460
+ /**
461
+ * Close all connections gracefully
462
+ */
463
+ shutdown() {
464
+ for (const [connId, connInfo] of this.connections.entries()) {
465
+ try {
466
+ connInfo.ws.close(1001, 'Server shutting down');
467
+ } catch (e) {
468
+ // Already closed
469
+ }
470
+ }
471
+ this.connections.clear();
472
+ this.rooms.clear();
473
+ this.userConnections.clear();
474
+ this.messageHandlers.clear();
475
+ this.messageQueue = [];
476
+ }
477
+ }
478
+
479
+ /**
480
+ * WebSocket Client Helper
481
+ * For use in Bun HTTP server context
482
+ */
483
+ export class WebSocketClient {
484
+ constructor(url, userId = null) {
485
+ this.url = url;
486
+ this.userId = userId;
487
+ this.ws = null;
488
+ this.reconnectAttempts = 0;
489
+ this.maxReconnectAttempts = 10;
490
+ this.reconnectDelay = 1000; // Start at 1 second
491
+ this.messageHandlers = new Map();
492
+ this.roomSubscriptions = new Set();
493
+ this.isConnecting = false;
494
+ }
495
+
496
+ /**
497
+ * Connect to WebSocket server
498
+ */
499
+ connect() {
500
+ if (this.isConnecting || this.ws) return;
501
+ this.isConnecting = true;
502
+
503
+ try {
504
+ this.ws = new WebSocket(this.url);
505
+
506
+ this.ws.onopen = () => {
507
+ console.log('Connected to WebSocket server');
508
+ this.reconnectAttempts = 0;
509
+ this.reconnectDelay = 1000;
510
+ this.emit('connected', { userId: this.userId });
511
+ };
512
+
513
+ this.ws.onmessage = (event) => {
514
+ const data = JSON.parse(event.data);
515
+ this.emit(data.type, data.payload);
516
+ };
517
+
518
+ this.ws.onerror = (error) => {
519
+ console.error('WebSocket error:', error);
520
+ this.emit('error', error);
521
+ };
522
+
523
+ this.ws.onclose = () => {
524
+ console.log('Disconnected from WebSocket');
525
+ this.ws = null;
526
+ this.isConnecting = false;
527
+ this.attemptReconnect();
528
+ };
529
+ } catch (error) {
530
+ console.error('Failed to connect to WebSocket:', error);
531
+ this.isConnecting = false;
532
+ this.attemptReconnect();
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Attempt to reconnect with exponential backoff
538
+ */
539
+ attemptReconnect() {
540
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
541
+ this.emit('reconnect:failed', {
542
+ attempts: this.reconnectAttempts
543
+ });
544
+ return;
545
+ }
546
+
547
+ this.reconnectAttempts++;
548
+ const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
549
+
550
+ console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
551
+
552
+ setTimeout(() => {
553
+ this.connect();
554
+ }, delay);
555
+ }
556
+
557
+ /**
558
+ * Register message handler
559
+ */
560
+ on(type, handler) {
561
+ if (!this.messageHandlers.has(type)) {
562
+ this.messageHandlers.set(type, []);
563
+ }
564
+ this.messageHandlers.get(type).push(handler);
565
+ return this;
566
+ }
567
+
568
+ /**
569
+ * Emit event to handlers
570
+ */
571
+ emit(type, payload) {
572
+ const handlers = this.messageHandlers.get(type) || [];
573
+ for (const handler of handlers) {
574
+ try {
575
+ handler(payload);
576
+ } catch (error) {
577
+ console.error(`Error in handler for ${type}:`, error);
578
+ }
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Send message to server
584
+ */
585
+ send(type, payload) {
586
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
587
+ console.warn('WebSocket not connected');
588
+ return false;
589
+ }
590
+
591
+ try {
592
+ this.ws.send(JSON.stringify({
593
+ type,
594
+ payload,
595
+ timestamp: Date.now()
596
+ }));
597
+ return true;
598
+ } catch (error) {
599
+ console.error('Error sending message:', error);
600
+ return false;
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Join a room
606
+ */
607
+ joinRoom(roomId) {
608
+ this.roomSubscriptions.add(roomId);
609
+ return this.send('join:room', {
610
+ roomId,
611
+ userId: this.userId
612
+ });
613
+ }
614
+
615
+ /**
616
+ * Leave a room
617
+ */
618
+ leaveRoom(roomId) {
619
+ this.roomSubscriptions.delete(roomId);
620
+ return this.send('leave:room', {
621
+ roomId
622
+ });
623
+ }
624
+
625
+ /**
626
+ * Disconnect from server
627
+ */
628
+ disconnect() {
629
+ if (this.ws) {
630
+ this.ws.close(1000, 'Client closing');
631
+ this.ws = null;
632
+ }
633
+ }
634
+ }
635
+
636
+ export default {
637
+ WebSocketServer,
638
+ WebSocketClient
639
+ };