servcraft 0.1.0 → 0.1.3

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 (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,692 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { EventEmitter } from 'events';
3
+ import { Server } from 'socket.io';
4
+ import type { Server as HTTPServer } from 'http';
5
+ import { createAdapter } from '@socket.io/redis-adapter';
6
+ import { Redis } from 'ioredis';
7
+ import { logger } from '../../core/logger.js';
8
+ import type {
9
+ WebSocketConfig,
10
+ SocketUser,
11
+ Room,
12
+ Message,
13
+ BroadcastOptions,
14
+ ConnectionStats,
15
+ RoomStats,
16
+ AuthenticatedSocket,
17
+ } from './types.js';
18
+
19
+ // In-memory storage (replace with Redis in production)
20
+ const connectedUsers = new Map<string, SocketUser>();
21
+ const rooms = new Map<string, Room>();
22
+ const messages = new Map<string, Message[]>();
23
+ const userSockets = new Map<string, Set<string>>();
24
+
25
+ /**
26
+ * WebSocket Service
27
+ * Manages real-time connections with Socket.io
28
+ *
29
+ * Now using real Socket.io with Redis adapter for multi-instance support.
30
+ */
31
+ export class WebSocketService extends EventEmitter {
32
+ private config: WebSocketConfig;
33
+ private io: Server | null = null;
34
+ private pubClient: Redis | null = null;
35
+ private subClient: Redis | null = null;
36
+ private stats: ConnectionStats = {
37
+ totalConnections: 0,
38
+ activeConnections: 0,
39
+ totalRooms: 0,
40
+ messagesPerMinute: 0,
41
+ bytesPerMinute: 0,
42
+ avgLatency: 0,
43
+ };
44
+
45
+ constructor(config?: WebSocketConfig) {
46
+ super();
47
+ this.config = {
48
+ path: '/socket.io',
49
+ pingTimeout: 60000,
50
+ pingInterval: 25000,
51
+ maxHttpBufferSize: 1e6,
52
+ ...config,
53
+ };
54
+
55
+ logger.info('WebSocket service initialized');
56
+ }
57
+
58
+ /**
59
+ * Initialize Socket.io server
60
+ * Pass an HTTP server instance to enable WebSocket support
61
+ */
62
+ initialize(httpServer?: HTTPServer): void {
63
+ if (!httpServer) {
64
+ logger.warn('No HTTP server provided - WebSocket service running in mock mode');
65
+ return;
66
+ }
67
+
68
+ try {
69
+ // Create Socket.io server
70
+ this.io = new Server(httpServer, {
71
+ path: this.config.path,
72
+ pingTimeout: this.config.pingTimeout,
73
+ pingInterval: this.config.pingInterval,
74
+ maxHttpBufferSize: this.config.maxHttpBufferSize,
75
+ cors: this.config.cors || {
76
+ origin: '*',
77
+ credentials: true,
78
+ },
79
+ });
80
+
81
+ // Setup Redis adapter if configured
82
+ if (this.config.redis) {
83
+ this.setupRedisAdapter();
84
+ }
85
+
86
+ // Setup connection handlers
87
+ this.setupConnectionHandlers();
88
+
89
+ logger.info({ path: this.config.path }, 'Socket.io server initialized');
90
+ } catch (error) {
91
+ logger.error({ err: error }, 'Failed to initialize Socket.io server');
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Setup Redis adapter for multi-instance support
98
+ */
99
+ private setupRedisAdapter(): void {
100
+ if (!this.config.redis || !this.io) {
101
+ return;
102
+ }
103
+
104
+ try {
105
+ const redisConfig = this.config.redis;
106
+
107
+ // Create pub/sub Redis clients
108
+ this.pubClient = new Redis({
109
+ host: redisConfig.host,
110
+ port: redisConfig.port,
111
+ password: redisConfig.password,
112
+ });
113
+
114
+ this.subClient = new Redis({
115
+ host: redisConfig.host,
116
+ port: redisConfig.port,
117
+ password: redisConfig.password,
118
+ });
119
+
120
+ // Setup event handlers
121
+ this.pubClient.on('connect', () => {
122
+ logger.info('Socket.io Redis pub client connected');
123
+ });
124
+
125
+ this.pubClient.on('error', (error: Error) => {
126
+ logger.error({ err: error }, 'Socket.io Redis pub client error');
127
+ });
128
+
129
+ this.subClient.on('connect', () => {
130
+ logger.info('Socket.io Redis sub client connected');
131
+ });
132
+
133
+ this.subClient.on('error', (error: Error) => {
134
+ logger.error({ err: error }, 'Socket.io Redis sub client error');
135
+ });
136
+
137
+ // Attach Redis adapter to Socket.io
138
+ this.io.adapter(createAdapter(this.pubClient, this.subClient));
139
+
140
+ logger.info(
141
+ { host: redisConfig.host, port: redisConfig.port },
142
+ 'Socket.io Redis adapter configured'
143
+ );
144
+ } catch (error) {
145
+ logger.error({ err: error }, 'Failed to setup Redis adapter');
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Setup Socket.io connection handlers
151
+ */
152
+ private setupConnectionHandlers(): void {
153
+ if (!this.io) {
154
+ return;
155
+ }
156
+
157
+ this.io.on('connection', (socket) => {
158
+ const authenticatedSocket = socket as unknown as AuthenticatedSocket;
159
+
160
+ // Handle connection
161
+ this.handleConnection(authenticatedSocket).catch((error) => {
162
+ logger.error({ err: error, socketId: socket.id }, 'Error handling connection');
163
+ });
164
+
165
+ // Handle disconnection
166
+ socket.on('disconnect', (reason) => {
167
+ this.handleDisconnection(socket.id, reason).catch((error) => {
168
+ logger.error({ err: error, socketId: socket.id }, 'Error handling disconnection');
169
+ });
170
+ });
171
+
172
+ // Handle room join
173
+ socket.on('room:join', async (data: { roomId: string }) => {
174
+ try {
175
+ await this.joinRoom(socket.id, data.roomId);
176
+ socket.join(data.roomId);
177
+ } catch (error) {
178
+ logger.error({ err: error, socketId: socket.id }, 'Error joining room');
179
+ socket.emit('error', { message: 'Failed to join room' });
180
+ }
181
+ });
182
+
183
+ // Handle room leave
184
+ socket.on('room:leave', async (data: { roomId: string }) => {
185
+ try {
186
+ await this.leaveRoom(socket.id, data.roomId);
187
+ socket.leave(data.roomId);
188
+ } catch (error) {
189
+ logger.error({ err: error, socketId: socket.id }, 'Error leaving room');
190
+ }
191
+ });
192
+
193
+ // Handle message
194
+ socket.on(
195
+ 'message',
196
+ async (data: {
197
+ roomId: string;
198
+ content: string;
199
+ type?: Message['type'];
200
+ metadata?: Record<string, unknown>;
201
+ }) => {
202
+ try {
203
+ const user = this.getUser(socket.id);
204
+ if (!user) {
205
+ socket.emit('error', { message: 'User not found' });
206
+ return;
207
+ }
208
+
209
+ const message = await this.sendMessage(
210
+ data.roomId,
211
+ user.id,
212
+ data.content,
213
+ data.type,
214
+ data.metadata
215
+ );
216
+
217
+ // Broadcast to room
218
+ this.io?.to(data.roomId).emit('message', message);
219
+ } catch (error) {
220
+ logger.error({ err: error, socketId: socket.id }, 'Error sending message');
221
+ socket.emit('error', { message: 'Failed to send message' });
222
+ }
223
+ }
224
+ );
225
+
226
+ // Handle typing indicator
227
+ socket.on('typing', (data: { roomId: string; isTyping: boolean }) => {
228
+ const user = this.getUser(socket.id);
229
+ if (user) {
230
+ socket.to(data.roomId).emit('typing', {
231
+ userId: user.id,
232
+ username: user.username,
233
+ roomId: data.roomId,
234
+ isTyping: data.isTyping,
235
+ timestamp: new Date(),
236
+ });
237
+ }
238
+ });
239
+ });
240
+
241
+ logger.info('Socket.io connection handlers configured');
242
+ }
243
+
244
+ /**
245
+ * Handle user connection
246
+ */
247
+ async handleConnection(socket: AuthenticatedSocket): Promise<void> {
248
+ const userId = socket.userId || socket.id;
249
+
250
+ const user: SocketUser = {
251
+ id: userId,
252
+ socketId: socket.id,
253
+ username: socket.handshake.query.username as string,
254
+ email: socket.handshake.query.email as string,
255
+ connectedAt: new Date(),
256
+ lastActivity: new Date(),
257
+ };
258
+
259
+ connectedUsers.set(socket.id, user);
260
+
261
+ // Track user sockets
262
+ if (!userSockets.has(userId)) {
263
+ userSockets.set(userId, new Set());
264
+ }
265
+ userSockets.get(userId)!.add(socket.id);
266
+
267
+ this.stats.totalConnections++;
268
+ this.stats.activeConnections++;
269
+
270
+ this.emit('connection', user);
271
+
272
+ logger.info({ userId, socketId: socket.id, username: user.username }, 'User connected');
273
+ }
274
+
275
+ /**
276
+ * Handle user disconnection
277
+ */
278
+ async handleDisconnection(socketId: string, reason?: string): Promise<void> {
279
+ const user = connectedUsers.get(socketId);
280
+
281
+ if (user) {
282
+ connectedUsers.delete(socketId);
283
+
284
+ // Remove from user sockets
285
+ const sockets = userSockets.get(user.id);
286
+ if (sockets) {
287
+ sockets.delete(socketId);
288
+ if (sockets.size === 0) {
289
+ userSockets.delete(user.id);
290
+ }
291
+ }
292
+
293
+ this.stats.activeConnections--;
294
+
295
+ this.emit('disconnect', user, reason);
296
+
297
+ logger.info(
298
+ { userId: user.id, socketId, username: user.username, reason },
299
+ 'User disconnected'
300
+ );
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Get connected user
306
+ */
307
+ getUser(socketId: string): SocketUser | undefined {
308
+ return connectedUsers.get(socketId);
309
+ }
310
+
311
+ /**
312
+ * Get all connected users
313
+ */
314
+ getConnectedUsers(): SocketUser[] {
315
+ return Array.from(connectedUsers.values());
316
+ }
317
+
318
+ /**
319
+ * Get user by ID (may have multiple sockets)
320
+ */
321
+ getUserSockets(userId: string): string[] {
322
+ return Array.from(userSockets.get(userId) || []);
323
+ }
324
+
325
+ /**
326
+ * Check if user is online
327
+ */
328
+ isUserOnline(userId: string): boolean {
329
+ return userSockets.has(userId) && userSockets.get(userId)!.size > 0;
330
+ }
331
+
332
+ /**
333
+ * Create a room
334
+ */
335
+ async createRoom(
336
+ name: string,
337
+ namespace = 'default',
338
+ createdBy?: string,
339
+ metadata?: Record<string, unknown>
340
+ ): Promise<Room> {
341
+ const room: Room = {
342
+ id: randomUUID(),
343
+ name,
344
+ namespace,
345
+ members: new Set(),
346
+ metadata,
347
+ createdAt: new Date(),
348
+ createdBy,
349
+ };
350
+
351
+ rooms.set(room.id, room);
352
+ messages.set(room.id, []);
353
+ this.stats.totalRooms++;
354
+
355
+ logger.info({ roomId: room.id, name, namespace }, 'Room created');
356
+
357
+ return room;
358
+ }
359
+
360
+ /**
361
+ * Get room
362
+ */
363
+ getRoom(roomId: string): Room | undefined {
364
+ return rooms.get(roomId);
365
+ }
366
+
367
+ /**
368
+ * List rooms
369
+ */
370
+ listRooms(namespace?: string): Room[] {
371
+ const allRooms = Array.from(rooms.values());
372
+
373
+ if (namespace) {
374
+ return allRooms.filter((room) => room.namespace === namespace);
375
+ }
376
+
377
+ return allRooms;
378
+ }
379
+
380
+ /**
381
+ * Join room
382
+ */
383
+ async joinRoom(socketId: string, roomId: string): Promise<void> {
384
+ const user = connectedUsers.get(socketId);
385
+ const room = rooms.get(roomId);
386
+
387
+ if (!user || !room) {
388
+ throw new Error('User or room not found');
389
+ }
390
+
391
+ room.members.add(socketId);
392
+
393
+ this.emit('room:join', { user, room });
394
+
395
+ logger.info({ userId: user.id, socketId, roomId, roomName: room.name }, 'User joined room');
396
+ }
397
+
398
+ /**
399
+ * Leave room
400
+ */
401
+ async leaveRoom(socketId: string, roomId: string): Promise<void> {
402
+ const user = connectedUsers.get(socketId);
403
+ const room = rooms.get(roomId);
404
+
405
+ if (!user || !room) {
406
+ return;
407
+ }
408
+
409
+ room.members.delete(socketId);
410
+
411
+ this.emit('room:leave', { user, room });
412
+
413
+ logger.info({ userId: user.id, socketId, roomId, roomName: room.name }, 'User left room');
414
+ }
415
+
416
+ /**
417
+ * Delete room
418
+ */
419
+ async deleteRoom(roomId: string): Promise<void> {
420
+ const room = rooms.get(roomId);
421
+
422
+ if (room) {
423
+ rooms.delete(roomId);
424
+ messages.delete(roomId);
425
+ this.stats.totalRooms--;
426
+
427
+ logger.info({ roomId, roomName: room.name }, 'Room deleted');
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Get room members
433
+ */
434
+ getRoomMembers(roomId: string): SocketUser[] {
435
+ const room = rooms.get(roomId);
436
+
437
+ if (!room) {
438
+ return [];
439
+ }
440
+
441
+ return Array.from(room.members)
442
+ .map((socketId) => connectedUsers.get(socketId))
443
+ .filter((user): user is SocketUser => user !== undefined);
444
+ }
445
+
446
+ /**
447
+ * Send message to room
448
+ */
449
+ async sendMessage(
450
+ roomId: string,
451
+ userId: string,
452
+ content: string,
453
+ type: Message['type'] = 'text',
454
+ metadata?: Record<string, unknown>
455
+ ): Promise<Message> {
456
+ const room = rooms.get(roomId);
457
+
458
+ if (!room) {
459
+ throw new Error('Room not found');
460
+ }
461
+
462
+ const user = Array.from(connectedUsers.values()).find((u) => u.id === userId);
463
+
464
+ const message: Message = {
465
+ id: randomUUID(),
466
+ roomId,
467
+ userId,
468
+ username: user?.username,
469
+ content,
470
+ type,
471
+ metadata,
472
+ timestamp: new Date(),
473
+ };
474
+
475
+ const roomMessages = messages.get(roomId) || [];
476
+ roomMessages.push(message);
477
+ messages.set(roomId, roomMessages);
478
+
479
+ this.emit('message', message);
480
+
481
+ logger.debug({ messageId: message.id, roomId, userId }, 'Message sent');
482
+
483
+ return message;
484
+ }
485
+
486
+ /**
487
+ * Get room messages
488
+ */
489
+ getRoomMessages(roomId: string, limit = 50, offset = 0): Message[] {
490
+ const roomMessages = messages.get(roomId) || [];
491
+
492
+ return roomMessages.slice(-limit - offset, offset > 0 ? -offset : undefined).reverse();
493
+ }
494
+
495
+ /**
496
+ * Broadcast event to room
497
+ */
498
+ async broadcastToRoom(
499
+ roomId: string,
500
+ event: string,
501
+ data: unknown,
502
+ options?: BroadcastOptions
503
+ ): Promise<void> {
504
+ const room = rooms.get(roomId);
505
+
506
+ if (!room) {
507
+ throw new Error('Room not found');
508
+ }
509
+
510
+ if (this.io) {
511
+ // Use Socket.io room broadcasting
512
+ const except = options?.except || [];
513
+ if (except.length > 0) {
514
+ // Emit to room except specific sockets
515
+ for (const socketId of room.members) {
516
+ if (!except.includes(socketId)) {
517
+ this.io.to(socketId).emit(event, data);
518
+ }
519
+ }
520
+ } else {
521
+ // Emit to entire room
522
+ this.io.to(roomId).emit(event, data);
523
+ }
524
+
525
+ logger.debug({ event, roomId, memberCount: room.members.size }, 'Broadcast to room');
526
+ } else {
527
+ logger.debug({ event, roomId }, 'WebSocket not initialized - skipping broadcast');
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Broadcast to specific users
533
+ */
534
+ async broadcastToUsers(userIds: string[], event: string, data: unknown): Promise<void> {
535
+ if (this.io) {
536
+ for (const userId of userIds) {
537
+ const socketIds = userSockets.get(userId);
538
+
539
+ if (socketIds) {
540
+ for (const socketId of socketIds) {
541
+ this.io.to(socketId).emit(event, data);
542
+ logger.debug({ socketId, userId, event }, 'Broadcasting to user');
543
+ }
544
+ }
545
+ }
546
+
547
+ logger.debug({ event, userCount: userIds.length }, 'Broadcast to users');
548
+ } else {
549
+ logger.debug(
550
+ { event, userCount: userIds.length },
551
+ 'WebSocket not initialized - skipping broadcast'
552
+ );
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Broadcast to all connected users
558
+ */
559
+ async broadcastToAll(event: string, data: unknown, options?: BroadcastOptions): Promise<void> {
560
+ if (this.io) {
561
+ const except = options?.except || [];
562
+
563
+ if (except.length > 0) {
564
+ // Emit to all except specific sockets
565
+ for (const socketId of connectedUsers.keys()) {
566
+ if (!except.includes(socketId)) {
567
+ this.io.to(socketId).emit(event, data);
568
+ }
569
+ }
570
+ } else {
571
+ // Emit to all connected sockets
572
+ this.io.emit(event, data);
573
+ }
574
+
575
+ logger.debug({ event, totalUsers: connectedUsers.size }, 'Broadcast to all users');
576
+ } else {
577
+ logger.debug({ event }, 'WebSocket not initialized - skipping broadcast');
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Emit event to specific socket
583
+ */
584
+ async emitToSocket(socketId: string, event: string, data: unknown): Promise<void> {
585
+ if (this.io) {
586
+ this.io.to(socketId).emit(event, data);
587
+ logger.debug({ socketId, event }, 'Emit to socket');
588
+ } else {
589
+ logger.debug({ socketId, event }, 'WebSocket not initialized - skipping emit');
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Get connection statistics
595
+ */
596
+ getStats(): ConnectionStats {
597
+ return { ...this.stats };
598
+ }
599
+
600
+ /**
601
+ * Get room statistics
602
+ */
603
+ getRoomStats(roomId: string): RoomStats | null {
604
+ const room = rooms.get(roomId);
605
+ const roomMessages = messages.get(roomId);
606
+
607
+ if (!room) {
608
+ return null;
609
+ }
610
+
611
+ const lastMessage = roomMessages?.[roomMessages.length - 1];
612
+
613
+ return {
614
+ roomId: room.id,
615
+ memberCount: room.members.size,
616
+ messageCount: roomMessages?.length || 0,
617
+ createdAt: room.createdAt,
618
+ lastActivity: lastMessage?.timestamp || room.createdAt,
619
+ };
620
+ }
621
+
622
+ /**
623
+ * Disconnect user
624
+ */
625
+ async disconnectUser(userId: string, reason?: string): Promise<void> {
626
+ const socketIds = userSockets.get(userId);
627
+
628
+ if (socketIds && this.io) {
629
+ for (const socketId of socketIds) {
630
+ const socket = this.io.sockets.sockets.get(socketId);
631
+ if (socket) {
632
+ socket.disconnect(true);
633
+ }
634
+ await this.handleDisconnection(socketId, reason);
635
+ }
636
+ }
637
+
638
+ logger.info({ userId, reason }, 'User forcefully disconnected');
639
+ }
640
+
641
+ /**
642
+ * Cleanup inactive connections
643
+ */
644
+ async cleanup(inactiveMinutes = 30): Promise<number> {
645
+ const cutoff = new Date(Date.now() - inactiveMinutes * 60 * 1000);
646
+ let cleaned = 0;
647
+
648
+ for (const [socketId, user] of connectedUsers.entries()) {
649
+ if (user.lastActivity < cutoff) {
650
+ await this.handleDisconnection(socketId, 'inactive');
651
+ cleaned++;
652
+ }
653
+ }
654
+
655
+ logger.info({ cleaned, inactiveMinutes }, 'Cleaned up inactive connections');
656
+
657
+ return cleaned;
658
+ }
659
+
660
+ /**
661
+ * Shutdown service
662
+ */
663
+ async shutdown(): Promise<void> {
664
+ // Disconnect all users
665
+ for (const socketId of connectedUsers.keys()) {
666
+ await this.handleDisconnection(socketId, 'server_shutdown');
667
+ }
668
+
669
+ // Close Redis clients
670
+ if (this.pubClient) {
671
+ await this.pubClient.quit();
672
+ this.pubClient = null;
673
+ }
674
+
675
+ if (this.subClient) {
676
+ await this.subClient.quit();
677
+ this.subClient = null;
678
+ }
679
+
680
+ // Close Socket.io server
681
+ if (this.io) {
682
+ await new Promise<void>((resolve) => {
683
+ this.io?.close(() => {
684
+ resolve();
685
+ });
686
+ });
687
+ this.io = null;
688
+ }
689
+
690
+ logger.info('WebSocket service shut down');
691
+ }
692
+ }