返回 AiToEarn
sse-ping.service.ts
根目录 / project / aitoearn-backend / libs / nest-mcp / src / services / sse-ping.service.ts
1 import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
2 import {
3 Injectable,
4 Logger,
5 OnModuleDestroy,
6 OnModuleInit,
7 } from '@nestjs/common'
8 import { HttpResponse } from '../interfaces/http-adapter.interface'
9
10 /**
11 * Service that implements automatic ping for SSE connections
12 * This prevents browser/client timeouts for long-lived connections
13 */
14 @Injectable()
15 export class SsePingService implements OnModuleInit, OnModuleDestroy {
16 private pingInterval: NodeJS.Timeout | null = null
17 private readonly logger = new Logger(SsePingService.name)
18 private readonly activeConnections = new Map<
19 string,
20 {
21 transport: SSEServerTransport
22 res: HttpResponse
23 }
24 >()
25
26 // Default to 30 seconds - this is a reasonable interval for most clients
27 private pingIntervalMs = 30000
28
29 constructor() {}
30
31 onModuleInit() {
32 this.logger.log('Initializing SSE ping service')
33 }
34
35 onModuleDestroy() {
36 this.stopPingInterval()
37 this.logger.log('SSE ping service stopped')
38 }
39
40 /**
41 * Configure the ping service
42 */
43 configure(options: { pingEnabled?: boolean, pingIntervalMs?: number }) {
44 if (options.pingIntervalMs !== undefined) {
45 this.pingIntervalMs = options.pingIntervalMs
46 }
47
48 if (options.pingEnabled !== false) {
49 this.startPingInterval()
50 }
51 else {
52 this.stopPingInterval()
53 }
54 }
55
56 /**
57 * Register a new SSE connection to receive pings
58 */
59 registerConnection(
60 sessionId: string,
61 transport: SSEServerTransport,
62 res: HttpResponse,
63 ) {
64 this.activeConnections.set(sessionId, { transport, res })
65 this.logger.debug(`SSE connection registered: ${sessionId}`)
66 }
67
68 /**
69 * Remove an SSE connection
70 */
71 removeConnection(sessionId: string) {
72 this.activeConnections.delete(sessionId)
73 this.logger.debug(`SSE connection removed: ${sessionId}`)
74 }
75
76 /**
77 * Start the ping interval timer
78 */
79 private startPingInterval() {
80 if (this.pingInterval) {
81 this.stopPingInterval()
82 }
83
84 this.logger.log(
85 `Starting SSE ping service (interval: ${this.pingIntervalMs}ms)`,
86 )
87 this.pingInterval = setInterval(() => {
88 this.sendPingToAllConnections()
89 }, this.pingIntervalMs)
90 }
91
92 /**
93 * Stop the ping interval timer
94 */
95 private stopPingInterval() {
96 if (this.pingInterval) {
97 clearInterval(this.pingInterval)
98 this.pingInterval = null
99 this.logger.log('SSE ping interval stopped')
100 }
101 }
102
103 /**
104 * Send a ping to all active connections
105 */
106 private sendPingToAllConnections() {
107 const timestamp = Date.now()
108 const connectionCount = this.activeConnections.size
109
110 if (connectionCount === 0) {
111 return
112 }
113
114 this.logger.debug(`Sending SSE ping to ${connectionCount} connections`)
115
116 for (const [sessionId, { res }] of this.activeConnections.entries()) {
117 try {
118 // Send a comment-type SSE message (line starting with ':')
119 // This keeps the connection alive without triggering an event in the client
120 if (!res.closed && res.writable) {
121 res.write(`: ping - ${new Date(timestamp).toISOString()}\n\n`)
122 }
123 else {
124 this.logger.debug(
125 `Connection ${sessionId} is no longer writable, removing`,
126 )
127 // TODO: After non writable connections are discovered it'd be useful to cleanup transports/mcp servers
128 // for that connection in sse.controller.factory.ts
129 this.removeConnection(sessionId)
130 }
131 }
132 catch (error) {
133 this.logger.error(
134 `Error sending ping to connection ${sessionId}`,
135 error,
136 )
137 this.removeConnection(sessionId)
138 }
139 }
140 }
141 }
142
142 lines TYPESCRIPT