touchdesigner-mcp-server 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +4 -307
- package/README.md +9 -308
- package/dist/cli.js +132 -16
- package/dist/core/logger.js +13 -0
- package/dist/server/touchDesignerServer.js +19 -0
- package/dist/transport/config.js +75 -0
- package/dist/transport/expressHttpManager.js +235 -0
- package/dist/transport/factory.js +198 -0
- package/dist/transport/index.js +12 -0
- package/dist/transport/sessionManager.js +276 -0
- package/dist/transport/transportRegistry.js +272 -0
- package/dist/transport/validator.js +78 -0
- package/package.json +4 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { createErrorResult, createSuccessResult } from "../core/result.js";
|
|
5
|
+
import { isStreamableHttpTransportConfig } from "./config.js";
|
|
6
|
+
import { TransportConfigValidator } from "./validator.js";
|
|
7
|
+
/**
|
|
8
|
+
* Factory for creating MCP transport instances based on configuration
|
|
9
|
+
*
|
|
10
|
+
* This factory implements the Factory pattern to encapsulate transport creation logic.
|
|
11
|
+
* It validates configuration and creates the appropriate transport type (stdio or HTTP).
|
|
12
|
+
*/
|
|
13
|
+
export class TransportFactory {
|
|
14
|
+
/**
|
|
15
|
+
* Reset the internal state of a StreamableHTTPServerTransport instance
|
|
16
|
+
*
|
|
17
|
+
* WARNING: This method manipulates the private field '_initialized' using Reflect.set().
|
|
18
|
+
* This is fragile and may break if the MCP SDK implementation changes.
|
|
19
|
+
* There is currently no public API to reset the initialized state, so this is required
|
|
20
|
+
* to ensure the transport can be reused safely after a session is closed.
|
|
21
|
+
* If the SDK adds a public reset/init method, use that instead.
|
|
22
|
+
* If the SDK changes the field name or its semantics, update this code accordingly.
|
|
23
|
+
*/
|
|
24
|
+
static resetStreamableHttpState(transport) {
|
|
25
|
+
transport.sessionId = undefined;
|
|
26
|
+
Reflect.set(transport, "_initialized", false);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a transport instance from configuration
|
|
30
|
+
*
|
|
31
|
+
* @param config - Transport configuration (stdio or streamable-http)
|
|
32
|
+
* @param logger - Optional logger for HTTP transport session events (ignored for stdio)
|
|
33
|
+
* @param sessionManager - Optional session manager for HTTP transport (ignored for stdio)
|
|
34
|
+
* @returns Result with Transport instance or Error
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // Create stdio transport
|
|
39
|
+
* const stdioResult = TransportFactory.create({ type: 'stdio' });
|
|
40
|
+
*
|
|
41
|
+
* // Create HTTP transport with logger and session manager
|
|
42
|
+
* const httpResult = TransportFactory.create({
|
|
43
|
+
* type: 'streamable-http',
|
|
44
|
+
* port: 6280,
|
|
45
|
+
* host: '127.0.0.1',
|
|
46
|
+
* endpoint: '/mcp'
|
|
47
|
+
* }, logger, sessionManager);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
static create(config, logger, sessionManager) {
|
|
51
|
+
// Validate configuration first
|
|
52
|
+
const validationResult = TransportConfigValidator.validate(config);
|
|
53
|
+
if (!validationResult.success) {
|
|
54
|
+
return validationResult;
|
|
55
|
+
}
|
|
56
|
+
const validatedConfig = validationResult.data;
|
|
57
|
+
// Dispatch to appropriate factory method based on type
|
|
58
|
+
if (isStreamableHttpTransportConfig(validatedConfig)) {
|
|
59
|
+
return TransportFactory.createStreamableHttp(validatedConfig, logger, sessionManager);
|
|
60
|
+
}
|
|
61
|
+
return TransportFactory.createStdio(validatedConfig);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a stdio transport instance
|
|
65
|
+
*
|
|
66
|
+
* @param _config - Stdio transport configuration (unused, kept for API consistency)
|
|
67
|
+
* @returns Result with StdioServerTransport instance
|
|
68
|
+
*/
|
|
69
|
+
static createStdio(_config) {
|
|
70
|
+
try {
|
|
71
|
+
const transport = new StdioServerTransport();
|
|
72
|
+
return createSuccessResult(transport);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
76
|
+
return createErrorResult(new Error(`Failed to create stdio transport: ${err.message}`));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a streamable HTTP transport instance
|
|
81
|
+
*
|
|
82
|
+
* Creates a StreamableHTTPServerTransport with session management support.
|
|
83
|
+
* The transport instance is stateful and manages session lifecycle through callbacks.
|
|
84
|
+
*
|
|
85
|
+
* Note: DNS rebinding protection is handled by Express middleware (createMcpExpressApp)
|
|
86
|
+
* in the ExpressHttpManager, not by the transport itself.
|
|
87
|
+
*
|
|
88
|
+
* @param config - Streamable HTTP transport configuration
|
|
89
|
+
* @param logger - Optional logger for session lifecycle events
|
|
90
|
+
* @param sessionManager - Optional session manager for tracking sessions
|
|
91
|
+
* @returns Result with StreamableHTTPServerTransport instance or Error
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const config: StreamableHttpTransportConfig = {
|
|
96
|
+
* type: 'streamable-http',
|
|
97
|
+
* port: 6280,
|
|
98
|
+
* host: '127.0.0.1',
|
|
99
|
+
* endpoint: '/mcp',
|
|
100
|
+
* sessionConfig: { enabled: true }
|
|
101
|
+
* };
|
|
102
|
+
* const result = TransportFactory.createStreamableHttp(config, logger, sessionManager);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
static createStreamableHttp(config, logger, sessionManager) {
|
|
106
|
+
try {
|
|
107
|
+
let transport = null;
|
|
108
|
+
let suppressCloseEvent = false;
|
|
109
|
+
const handleSessionClosed = config.sessionConfig?.enabled
|
|
110
|
+
? (sessionId) => {
|
|
111
|
+
if (logger) {
|
|
112
|
+
logger.sendLog({
|
|
113
|
+
data: `Session closed: ${sessionId}`,
|
|
114
|
+
level: "info",
|
|
115
|
+
logger: "TransportFactory",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Clean up session from SessionManager
|
|
119
|
+
if (sessionManager) {
|
|
120
|
+
sessionManager.cleanup(sessionId);
|
|
121
|
+
}
|
|
122
|
+
suppressCloseEvent = true;
|
|
123
|
+
if (transport) {
|
|
124
|
+
TransportFactory.resetStreamableHttpState(transport);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
128
|
+
// Create transport with session management only
|
|
129
|
+
// Security (DNS rebinding protection) is handled by Express middleware
|
|
130
|
+
transport = new StreamableHTTPServerTransport({
|
|
131
|
+
// Enable JSON responses for simple request/response scenarios
|
|
132
|
+
enableJsonResponse: false,
|
|
133
|
+
// Session close callback
|
|
134
|
+
// This is called when a session is terminated via DELETE request
|
|
135
|
+
onsessionclosed: handleSessionClosed,
|
|
136
|
+
// Session initialization callback
|
|
137
|
+
// This is called when a new session is created
|
|
138
|
+
onsessioninitialized: config.sessionConfig?.enabled
|
|
139
|
+
? (sessionId) => {
|
|
140
|
+
if (logger) {
|
|
141
|
+
logger.sendLog({
|
|
142
|
+
data: `Session initialized: ${sessionId}`,
|
|
143
|
+
level: "info",
|
|
144
|
+
logger: "TransportFactory",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Register session with SessionManager
|
|
148
|
+
if (sessionManager) {
|
|
149
|
+
sessionManager.register(sessionId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
: undefined,
|
|
153
|
+
// Retry interval for SSE polling behavior (optional)
|
|
154
|
+
retryInterval: config.retryInterval,
|
|
155
|
+
// Use randomUUID for session ID generation if sessions are enabled
|
|
156
|
+
sessionIdGenerator: config.sessionConfig?.enabled
|
|
157
|
+
? () => randomUUID()
|
|
158
|
+
: undefined,
|
|
159
|
+
});
|
|
160
|
+
// Override the transport's close method to handle session cleanup scenarios.
|
|
161
|
+
//
|
|
162
|
+
// Why suppressCloseEvent is necessary:
|
|
163
|
+
// When a session is explicitly closed (via DELETE request), the SDK calls onsessionclosed
|
|
164
|
+
// which triggers our handleSessionClosed callback. We need to reset the transport state
|
|
165
|
+
// without triggering the onclose callback, which would signal a transport-level disconnection.
|
|
166
|
+
//
|
|
167
|
+
// Scenarios that trigger this code path:
|
|
168
|
+
// 1. Client sends DELETE request to close session → onsessionclosed fires → suppressCloseEvent = true
|
|
169
|
+
// 2. Session TTL expires → cleanup triggers close → suppressCloseEvent = true
|
|
170
|
+
//
|
|
171
|
+
// Relationship between suppressCloseEvent and handleSessionClosed:
|
|
172
|
+
// - handleSessionClosed sets suppressCloseEvent = true to indicate a session-level (not transport-level) close
|
|
173
|
+
// - When close() is called with suppressCloseEvent = true, we temporarily remove onclose callback
|
|
174
|
+
// - This prevents the MCP server from treating session cleanup as a transport disconnection
|
|
175
|
+
const originalClose = transport.close.bind(transport);
|
|
176
|
+
transport.close = (async () => {
|
|
177
|
+
if (suppressCloseEvent) {
|
|
178
|
+
const previousOnClose = transport?.onclose;
|
|
179
|
+
transport.onclose = undefined;
|
|
180
|
+
try {
|
|
181
|
+
await originalClose();
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
transport.onclose = previousOnClose;
|
|
185
|
+
suppressCloseEvent = false;
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await originalClose();
|
|
190
|
+
});
|
|
191
|
+
return createSuccessResult(transport);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
195
|
+
return createErrorResult(new Error(`Failed to create streamable HTTP transport: ${err.message}`));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport layer module exports
|
|
3
|
+
*
|
|
4
|
+
* This module provides type-safe configuration and validation for MCP transports:
|
|
5
|
+
* - stdio: Standard input/output transport
|
|
6
|
+
* - streamable-http: HTTP-based transport with SSE streaming
|
|
7
|
+
*/
|
|
8
|
+
export { DEFAULT_HTTP_CONFIG, DEFAULT_SESSION_CONFIG, isStdioTransportConfig, isStreamableHttpTransportConfig, TransportConfigSchema, } from "./config.js";
|
|
9
|
+
export { ExpressHttpManager } from "./expressHttpManager.js";
|
|
10
|
+
export { TransportFactory } from "./factory.js";
|
|
11
|
+
export { SessionManager } from "./sessionManager.js";
|
|
12
|
+
export { TransportConfigValidator } from "./validator.js";
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createErrorResult, createSuccessResult } from "../core/result.js";
|
|
3
|
+
import { DEFAULT_SESSION_CONFIG } from "./config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Session Manager
|
|
6
|
+
*
|
|
7
|
+
* Manages client sessions for Streamable HTTP transport.
|
|
8
|
+
* Provides session creation, validation, TTL-based expiration, and automatic cleanup.
|
|
9
|
+
*
|
|
10
|
+
* Note: Session validation (checking if session ID exists) is now handled by
|
|
11
|
+
* StreamableHTTPServerTransport.handleRequest(). This SessionManager focuses on:
|
|
12
|
+
* - Session creation via SDK callbacks (onsessioninitialized)
|
|
13
|
+
* - Session cleanup via SDK callbacks (onsessionclosed) and TTL-based cleanup
|
|
14
|
+
* - Session tracking for health checks and monitoring
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const sessionManager = new SessionManager(
|
|
19
|
+
* { enabled: true, ttl: 60 * 60 * 1000 }, // 1 hour TTL
|
|
20
|
+
* logger
|
|
21
|
+
* );
|
|
22
|
+
*
|
|
23
|
+
* // Create session (typically called from SDK callback)
|
|
24
|
+
* const sessionId = sessionManager.create({ clientVersion: '1.0' });
|
|
25
|
+
*
|
|
26
|
+
* // Start automatic TTL-based cleanup
|
|
27
|
+
* sessionManager.startTTLCleanup();
|
|
28
|
+
*
|
|
29
|
+
* // Stop cleanup when done
|
|
30
|
+
* sessionManager.stopTTLCleanup();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class SessionManager {
|
|
34
|
+
sessions = new Map();
|
|
35
|
+
config;
|
|
36
|
+
logger;
|
|
37
|
+
cleanupInterval = null;
|
|
38
|
+
onSessionExpired = null;
|
|
39
|
+
constructor(config, logger) {
|
|
40
|
+
// Apply defaults for optional values to ensure TTL cleanup is active by default
|
|
41
|
+
this.config = {
|
|
42
|
+
cleanupInterval: config.cleanupInterval ?? DEFAULT_SESSION_CONFIG.cleanupInterval,
|
|
43
|
+
enabled: config.enabled ?? DEFAULT_SESSION_CONFIG.enabled,
|
|
44
|
+
ttl: config.ttl ?? DEFAULT_SESSION_CONFIG.ttl,
|
|
45
|
+
};
|
|
46
|
+
this.logger = logger;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a new session with optional metadata
|
|
50
|
+
*
|
|
51
|
+
* @param metadata - Optional metadata to associate with the session
|
|
52
|
+
* @returns Session ID (UUID v4)
|
|
53
|
+
*/
|
|
54
|
+
create(metadata) {
|
|
55
|
+
const sessionId = randomUUID();
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const session = {
|
|
58
|
+
createdAt: now,
|
|
59
|
+
id: sessionId,
|
|
60
|
+
lastAccessedAt: now,
|
|
61
|
+
metadata,
|
|
62
|
+
};
|
|
63
|
+
this.sessions.set(sessionId, session);
|
|
64
|
+
this.logger.sendLog({
|
|
65
|
+
data: `Session created: ${sessionId}${metadata ? ` (metadata: ${JSON.stringify(metadata)})` : ""}`,
|
|
66
|
+
level: "info",
|
|
67
|
+
logger: "SessionManager",
|
|
68
|
+
});
|
|
69
|
+
return sessionId;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Update lastAccessedAt when a session receives activity
|
|
73
|
+
*
|
|
74
|
+
* @param sessionId - Session ID to refresh
|
|
75
|
+
*/
|
|
76
|
+
touch(sessionId) {
|
|
77
|
+
const session = this.sessions.get(sessionId);
|
|
78
|
+
if (!session) {
|
|
79
|
+
return createErrorResult(new Error(`Session not found: ${sessionId}`));
|
|
80
|
+
}
|
|
81
|
+
session.lastAccessedAt = Date.now();
|
|
82
|
+
return createSuccessResult(undefined);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Register a callback to run when TTL cleanup expires a session.
|
|
86
|
+
*
|
|
87
|
+
* @param handler - Callback invoked with expired session ID
|
|
88
|
+
*/
|
|
89
|
+
setExpirationHandler(handler) {
|
|
90
|
+
this.onSessionExpired = handler;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Register an SDK-created session for tracking
|
|
94
|
+
*
|
|
95
|
+
* This method registers sessions that were created by the MCP SDK's
|
|
96
|
+
* StreamableHTTPServerTransport. The SDK generates session IDs via the
|
|
97
|
+
* sessionIdGenerator callback, and then calls onsessioninitialized.
|
|
98
|
+
* This method creates a new tracking entry with current timestamps
|
|
99
|
+
* so the session can be managed for TTL cleanup and monitoring.
|
|
100
|
+
*
|
|
101
|
+
* @param sessionId - Session ID generated by SDK
|
|
102
|
+
* @param metadata - Optional metadata to associate with the session
|
|
103
|
+
*/
|
|
104
|
+
register(sessionId, metadata) {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const session = {
|
|
107
|
+
createdAt: now,
|
|
108
|
+
id: sessionId,
|
|
109
|
+
lastAccessedAt: now,
|
|
110
|
+
metadata,
|
|
111
|
+
};
|
|
112
|
+
this.sessions.set(sessionId, session);
|
|
113
|
+
this.logger.sendLog({
|
|
114
|
+
data: `Session registered: ${sessionId}${metadata ? ` (metadata: ${JSON.stringify(metadata)})` : ""}`,
|
|
115
|
+
level: "info",
|
|
116
|
+
logger: "SessionManager",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clean up (delete) a session by ID
|
|
121
|
+
*
|
|
122
|
+
* @param sessionId - Session ID to clean up
|
|
123
|
+
* @returns Result indicating success or failure
|
|
124
|
+
*/
|
|
125
|
+
cleanup(sessionId) {
|
|
126
|
+
const existed = this.sessions.delete(sessionId);
|
|
127
|
+
if (existed) {
|
|
128
|
+
this.logger.sendLog({
|
|
129
|
+
data: `Session cleaned up: ${sessionId}`,
|
|
130
|
+
level: "info",
|
|
131
|
+
logger: "SessionManager",
|
|
132
|
+
});
|
|
133
|
+
return createSuccessResult(undefined);
|
|
134
|
+
}
|
|
135
|
+
return createErrorResult(new Error(`Session not found for cleanup: ${sessionId}`));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* List all active sessions
|
|
139
|
+
*
|
|
140
|
+
* @returns Array of all active sessions
|
|
141
|
+
*/
|
|
142
|
+
list() {
|
|
143
|
+
return Array.from(this.sessions.values());
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Start automatic TTL-based cleanup
|
|
147
|
+
*
|
|
148
|
+
* Runs cleanup task at an interval of TTL/2 to remove expired sessions.
|
|
149
|
+
* Does nothing if TTL is not configured or cleanup is already running.
|
|
150
|
+
*/
|
|
151
|
+
startTTLCleanup() {
|
|
152
|
+
// Don't start if TTL not configured or already running
|
|
153
|
+
if (!this.config.ttl || this.cleanupInterval) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const intervalMs = this.config.cleanupInterval || this.config.ttl / 2;
|
|
157
|
+
this.logger.sendLog({
|
|
158
|
+
data: `Starting TTL cleanup (interval: ${intervalMs}ms, TTL: ${this.config.ttl}ms)`,
|
|
159
|
+
level: "info",
|
|
160
|
+
logger: "SessionManager",
|
|
161
|
+
});
|
|
162
|
+
this.cleanupInterval = setInterval(() => {
|
|
163
|
+
try {
|
|
164
|
+
this.runCleanupTask();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
168
|
+
this.logger.sendLog({
|
|
169
|
+
data: `CRITICAL: TTL cleanup task failed: ${err.message}. Stack: ${err.stack}`,
|
|
170
|
+
level: "error",
|
|
171
|
+
logger: "SessionManager",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}, intervalMs);
|
|
175
|
+
// Don't keep the process alive just for cleanup
|
|
176
|
+
this.cleanupInterval.unref();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Stop automatic TTL-based cleanup
|
|
180
|
+
*/
|
|
181
|
+
stopTTLCleanup() {
|
|
182
|
+
if (this.cleanupInterval) {
|
|
183
|
+
clearInterval(this.cleanupInterval);
|
|
184
|
+
this.cleanupInterval = null;
|
|
185
|
+
this.logger.sendLog({
|
|
186
|
+
data: "Stopped TTL cleanup",
|
|
187
|
+
level: "info",
|
|
188
|
+
logger: "SessionManager",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Run a single cleanup task to remove expired sessions
|
|
194
|
+
*
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
runCleanupTask() {
|
|
198
|
+
const ttl = this.config.ttl;
|
|
199
|
+
if (!ttl) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const expiredIds = [];
|
|
204
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
205
|
+
try {
|
|
206
|
+
const elapsed = now - session.lastAccessedAt;
|
|
207
|
+
if (elapsed > ttl) {
|
|
208
|
+
expiredIds.push(id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
// Log individual session cleanup errors but continue processing others
|
|
213
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
214
|
+
this.logger.sendLog({
|
|
215
|
+
data: `Error cleaning session ${id}: ${err.message}`,
|
|
216
|
+
level: "error",
|
|
217
|
+
logger: "SessionManager",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const sessionId of expiredIds) {
|
|
222
|
+
try {
|
|
223
|
+
const result = this.onSessionExpired?.(sessionId);
|
|
224
|
+
if (result && typeof result.catch === "function") {
|
|
225
|
+
result.catch((error) => {
|
|
226
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
227
|
+
this.logger.sendLog({
|
|
228
|
+
data: `Error running expiration handler for ${sessionId}: ${err.message}`,
|
|
229
|
+
level: "error",
|
|
230
|
+
logger: "SessionManager",
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
237
|
+
this.logger.sendLog({
|
|
238
|
+
data: `Error running expiration handler for ${sessionId}: ${err.message}`,
|
|
239
|
+
level: "error",
|
|
240
|
+
logger: "SessionManager",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Ensure the session is removed from tracking even if handler fails
|
|
244
|
+
this.sessions.delete(sessionId);
|
|
245
|
+
}
|
|
246
|
+
if (expiredIds.length > 0) {
|
|
247
|
+
this.logger.sendLog({
|
|
248
|
+
data: `Cleanup completed: removed ${expiredIds.length} expired session(s): ${expiredIds.join(", ")}`,
|
|
249
|
+
level: "info",
|
|
250
|
+
logger: "SessionManager",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get number of active sessions
|
|
256
|
+
*
|
|
257
|
+
* @returns Number of active sessions
|
|
258
|
+
*/
|
|
259
|
+
getActiveSessionCount() {
|
|
260
|
+
return this.sessions.size;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Clear all sessions
|
|
264
|
+
*
|
|
265
|
+
* Useful for testing or emergency cleanup.
|
|
266
|
+
*/
|
|
267
|
+
clearAll() {
|
|
268
|
+
const count = this.sessions.size;
|
|
269
|
+
this.sessions.clear();
|
|
270
|
+
this.logger.sendLog({
|
|
271
|
+
data: `All sessions cleared: ${count} session(s) removed`,
|
|
272
|
+
level: "info",
|
|
273
|
+
logger: "SessionManager",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|