@prmichaelsen/mcp-auth 7.0.4 → 7.2.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.md +167 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wrapper/config.d.ts +20 -2
- package/dist/wrapper/config.d.ts.map +1 -1
- package/dist/wrapper/index.d.ts +10 -30
- package/dist/wrapper/index.d.ts.map +1 -1
- package/dist/wrapper/index.js +6 -5
- package/dist/wrapper/index.js.map +3 -3
- package/dist/wrapper/instance-pool-manager.d.ts +119 -0
- package/dist/wrapper/instance-pool-manager.d.ts.map +1 -0
- package/dist/wrapper/instance-pool-manager.js +225 -0
- package/dist/wrapper/instance-pool-manager.js.map +7 -0
- package/dist/wrapper/progress-manager.d.ts +101 -0
- package/dist/wrapper/progress-manager.d.ts.map +1 -0
- package/dist/wrapper/progress-manager.js +241 -0
- package/dist/wrapper/progress-manager.js.map +7 -0
- package/dist/wrapper/server-wrapper.d.ts +36 -0
- package/dist/wrapper/server-wrapper.d.ts.map +1 -1
- package/dist/wrapper/server-wrapper.js +196 -5
- package/dist/wrapper/server-wrapper.js.map +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
class InstancePoolManager {
|
|
2
|
+
instances;
|
|
3
|
+
config;
|
|
4
|
+
logger;
|
|
5
|
+
cleanupTimer;
|
|
6
|
+
constructor(config, logger) {
|
|
7
|
+
this.instances = /* @__PURE__ */ new Map();
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
this.startCleanupTimer();
|
|
11
|
+
this.logger.info("InstancePoolManager initialized", {
|
|
12
|
+
maxSize: config.maxSize,
|
|
13
|
+
idleTimeout: config.idleTimeout,
|
|
14
|
+
maxLifetime: config.maxLifetime
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get or create a server instance for a user
|
|
19
|
+
*
|
|
20
|
+
* If a valid instance exists for the user, it will be reused.
|
|
21
|
+
* Otherwise, a new instance will be created using the factory function.
|
|
22
|
+
*
|
|
23
|
+
* @param userId - User identifier
|
|
24
|
+
* @param accessToken - Access token for the user
|
|
25
|
+
* @param factory - Factory function to create new server instances
|
|
26
|
+
* @returns Server instance (existing or newly created)
|
|
27
|
+
*/
|
|
28
|
+
async getInstance(userId, accessToken, factory) {
|
|
29
|
+
const existing = this.instances.get(userId);
|
|
30
|
+
if (existing && this.isValid(existing)) {
|
|
31
|
+
this.logger.debug("Reusing pooled instance", { userId });
|
|
32
|
+
existing.lastUsed = Date.now();
|
|
33
|
+
return existing.server;
|
|
34
|
+
}
|
|
35
|
+
if (existing) {
|
|
36
|
+
this.logger.debug("Removing invalid instance", {
|
|
37
|
+
userId,
|
|
38
|
+
age: Date.now() - existing.createdAt,
|
|
39
|
+
idle: Date.now() - existing.lastUsed
|
|
40
|
+
});
|
|
41
|
+
await this.removeInstance(userId);
|
|
42
|
+
}
|
|
43
|
+
if (this.instances.size >= this.config.maxSize) {
|
|
44
|
+
this.logger.debug("Pool full, evicting LRU instance", {
|
|
45
|
+
size: this.instances.size,
|
|
46
|
+
maxSize: this.config.maxSize
|
|
47
|
+
});
|
|
48
|
+
await this.evictLeastRecentlyUsed();
|
|
49
|
+
}
|
|
50
|
+
this.logger.debug("Creating new pooled instance", { userId });
|
|
51
|
+
const server = await factory(accessToken, userId);
|
|
52
|
+
this.instances.set(userId, {
|
|
53
|
+
server,
|
|
54
|
+
userId,
|
|
55
|
+
accessToken,
|
|
56
|
+
createdAt: Date.now(),
|
|
57
|
+
lastUsed: Date.now()
|
|
58
|
+
});
|
|
59
|
+
this.logger.info("Pooled instance created", {
|
|
60
|
+
userId,
|
|
61
|
+
poolSize: this.instances.size
|
|
62
|
+
});
|
|
63
|
+
return server;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if an instance is still valid
|
|
67
|
+
*
|
|
68
|
+
* An instance is valid if:
|
|
69
|
+
* - It hasn't exceeded its maximum lifetime
|
|
70
|
+
* - It hasn't been idle for too long
|
|
71
|
+
*
|
|
72
|
+
* @param instance - Instance metadata to check
|
|
73
|
+
* @returns true if instance is valid, false otherwise
|
|
74
|
+
*/
|
|
75
|
+
isValid(instance) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const age = now - instance.createdAt;
|
|
78
|
+
const idle = now - instance.lastUsed;
|
|
79
|
+
const valid = age < this.config.maxLifetime && idle < this.config.idleTimeout;
|
|
80
|
+
if (!valid) {
|
|
81
|
+
this.logger.debug("Instance invalid", {
|
|
82
|
+
userId: instance.userId,
|
|
83
|
+
age,
|
|
84
|
+
idle,
|
|
85
|
+
maxLifetime: this.config.maxLifetime,
|
|
86
|
+
idleTimeout: this.config.idleTimeout,
|
|
87
|
+
reason: age >= this.config.maxLifetime ? "max lifetime exceeded" : "idle timeout exceeded"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return valid;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Evict the least recently used instance
|
|
94
|
+
*
|
|
95
|
+
* Finds the instance with the oldest lastUsed timestamp
|
|
96
|
+
* and removes it from the pool.
|
|
97
|
+
*/
|
|
98
|
+
async evictLeastRecentlyUsed() {
|
|
99
|
+
let oldestUserId = null;
|
|
100
|
+
let oldestTime = Date.now();
|
|
101
|
+
for (const [userId, instance] of this.instances.entries()) {
|
|
102
|
+
if (instance.lastUsed < oldestTime) {
|
|
103
|
+
oldestTime = instance.lastUsed;
|
|
104
|
+
oldestUserId = userId;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (oldestUserId) {
|
|
108
|
+
this.logger.info("Evicting LRU instance", {
|
|
109
|
+
userId: oldestUserId,
|
|
110
|
+
lastUsed: new Date(oldestTime).toISOString(),
|
|
111
|
+
idleTime: Date.now() - oldestTime
|
|
112
|
+
});
|
|
113
|
+
await this.removeInstance(oldestUserId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Remove an instance from the pool
|
|
118
|
+
*
|
|
119
|
+
* Closes the server instance (if it has a close method)
|
|
120
|
+
* and removes it from the pool.
|
|
121
|
+
*
|
|
122
|
+
* @param userId - User ID of the instance to remove
|
|
123
|
+
*/
|
|
124
|
+
async removeInstance(userId) {
|
|
125
|
+
const instance = this.instances.get(userId);
|
|
126
|
+
if (instance) {
|
|
127
|
+
try {
|
|
128
|
+
if ("close" in instance.server && typeof instance.server.close === "function") {
|
|
129
|
+
await instance.server.close();
|
|
130
|
+
this.logger.debug("Server instance closed", { userId });
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
134
|
+
this.logger.error(`Error closing instance for user ${userId}: ${errorMessage}`);
|
|
135
|
+
}
|
|
136
|
+
this.instances.delete(userId);
|
|
137
|
+
this.logger.debug("Instance removed from pool", {
|
|
138
|
+
userId,
|
|
139
|
+
poolSize: this.instances.size
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Start periodic cleanup timer
|
|
145
|
+
*
|
|
146
|
+
* Runs cleanup every minute to remove expired instances.
|
|
147
|
+
*/
|
|
148
|
+
startCleanupTimer() {
|
|
149
|
+
this.cleanupTimer = setInterval(() => {
|
|
150
|
+
this.cleanupExpiredInstances();
|
|
151
|
+
}, 6e4);
|
|
152
|
+
if (this.cleanupTimer.unref) {
|
|
153
|
+
this.cleanupTimer.unref();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Clean up expired instances
|
|
158
|
+
*
|
|
159
|
+
* Removes all instances that are no longer valid
|
|
160
|
+
* (exceeded max lifetime or idle timeout).
|
|
161
|
+
*/
|
|
162
|
+
async cleanupExpiredInstances() {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
const toRemove = [];
|
|
165
|
+
for (const [userId, instance] of this.instances.entries()) {
|
|
166
|
+
if (!this.isValid(instance)) {
|
|
167
|
+
toRemove.push(userId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (toRemove.length > 0) {
|
|
171
|
+
this.logger.info("Cleaning up expired instances", {
|
|
172
|
+
count: toRemove.length,
|
|
173
|
+
userIds: toRemove
|
|
174
|
+
});
|
|
175
|
+
for (const userId of toRemove) {
|
|
176
|
+
await this.removeInstance(userId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Close all pooled instances
|
|
182
|
+
*
|
|
183
|
+
* Closes all server instances and clears the pool.
|
|
184
|
+
* Should be called when shutting down the server.
|
|
185
|
+
*/
|
|
186
|
+
async closeAll() {
|
|
187
|
+
this.logger.info("Closing all pooled instances", {
|
|
188
|
+
count: this.instances.size
|
|
189
|
+
});
|
|
190
|
+
if (this.cleanupTimer) {
|
|
191
|
+
clearInterval(this.cleanupTimer);
|
|
192
|
+
this.cleanupTimer = void 0;
|
|
193
|
+
}
|
|
194
|
+
const userIds = Array.from(this.instances.keys());
|
|
195
|
+
for (const userId of userIds) {
|
|
196
|
+
await this.removeInstance(userId);
|
|
197
|
+
}
|
|
198
|
+
this.logger.info("All pooled instances closed");
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get pool statistics
|
|
202
|
+
*
|
|
203
|
+
* Returns current state of the pool for monitoring and debugging.
|
|
204
|
+
*
|
|
205
|
+
* @returns Pool statistics including size and instance details
|
|
206
|
+
*/
|
|
207
|
+
getStats() {
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
return {
|
|
210
|
+
size: this.instances.size,
|
|
211
|
+
maxSize: this.config.maxSize,
|
|
212
|
+
instances: Array.from(this.instances.values()).map((i) => ({
|
|
213
|
+
userId: i.userId,
|
|
214
|
+
age: now - i.createdAt,
|
|
215
|
+
idle: now - i.lastUsed,
|
|
216
|
+
createdAt: new Date(i.createdAt).toISOString(),
|
|
217
|
+
lastUsed: new Date(i.lastUsed).toISOString()
|
|
218
|
+
}))
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
InstancePoolManager
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=instance-pool-manager.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/wrapper/instance-pool-manager.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Instance Pool Manager\n * \n * Manages lifecycle of pooled server instances with configurable\n * size limits, idle timeouts, and maximum lifetimes.\n */\n\nimport type { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport type { InstancePoolConfig } from '../types.js';\nimport type { Logger } from '../utils/logger.js';\n\n/**\n * Metadata for a pooled server instance\n */\ninterface InstanceMetadata {\n /** The MCP server instance */\n server: Server;\n \n /** User ID this instance belongs to */\n userId: string;\n \n /** Access token used to create this instance */\n accessToken: string;\n \n /** Timestamp when instance was created */\n createdAt: number;\n \n /** Timestamp when instance was last used */\n lastUsed: number;\n}\n\n/**\n * Instance Pool Manager\n * \n * Manages a pool of server instances with automatic lifecycle management:\n * - Reuses instances for the same user\n * - Enforces maximum pool size with LRU eviction\n * - Automatically closes idle instances\n * - Forces refresh of old instances\n * - Periodic cleanup of expired instances\n * \n * @example\n * ```typescript\n * const poolManager = new InstancePoolManager({\n * maxSize: 10,\n * idleTimeout: 300000,\n * maxLifetime: 3600000\n * }, logger);\n * \n * const server = await poolManager.getInstance(\n * 'user123',\n * 'token456',\n * (token, userId) => createMyServer(token, userId)\n * );\n * ```\n */\nexport class InstancePoolManager {\n private instances: Map<string, InstanceMetadata>;\n private config: InstancePoolConfig;\n private logger: Logger;\n private cleanupTimer?: NodeJS.Timeout;\n \n constructor(config: InstancePoolConfig, logger: Logger) {\n this.instances = new Map();\n this.config = config;\n this.logger = logger;\n this.startCleanupTimer();\n \n this.logger.info('InstancePoolManager initialized', {\n maxSize: config.maxSize,\n idleTimeout: config.idleTimeout,\n maxLifetime: config.maxLifetime\n });\n }\n \n /**\n * Get or create a server instance for a user\n * \n * If a valid instance exists for the user, it will be reused.\n * Otherwise, a new instance will be created using the factory function.\n * \n * @param userId - User identifier\n * @param accessToken - Access token for the user\n * @param factory - Factory function to create new server instances\n * @returns Server instance (existing or newly created)\n */\n async getInstance(\n userId: string,\n accessToken: string,\n factory: (accessToken: string, userId: string) => Server | Promise<Server>\n ): Promise<Server> {\n // Check if instance exists and is valid\n const existing = this.instances.get(userId);\n if (existing && this.isValid(existing)) {\n this.logger.debug('Reusing pooled instance', { userId });\n existing.lastUsed = Date.now();\n return existing.server;\n }\n \n // Remove invalid instance if exists\n if (existing) {\n this.logger.debug('Removing invalid instance', { \n userId,\n age: Date.now() - existing.createdAt,\n idle: Date.now() - existing.lastUsed\n });\n await this.removeInstance(userId);\n }\n \n // Check pool size limit\n if (this.instances.size >= this.config.maxSize) {\n this.logger.debug('Pool full, evicting LRU instance', { \n size: this.instances.size,\n maxSize: this.config.maxSize\n });\n await this.evictLeastRecentlyUsed();\n }\n \n // Create new instance\n this.logger.debug('Creating new pooled instance', { userId });\n const server = await factory(accessToken, userId);\n \n this.instances.set(userId, {\n server,\n userId,\n accessToken,\n createdAt: Date.now(),\n lastUsed: Date.now()\n });\n \n this.logger.info('Pooled instance created', {\n userId,\n poolSize: this.instances.size\n });\n \n return server;\n }\n \n /**\n * Check if an instance is still valid\n * \n * An instance is valid if:\n * - It hasn't exceeded its maximum lifetime\n * - It hasn't been idle for too long\n * \n * @param instance - Instance metadata to check\n * @returns true if instance is valid, false otherwise\n */\n private isValid(instance: InstanceMetadata): boolean {\n const now = Date.now();\n const age = now - instance.createdAt;\n const idle = now - instance.lastUsed;\n \n const valid = age < this.config.maxLifetime && \n idle < this.config.idleTimeout;\n \n if (!valid) {\n this.logger.debug('Instance invalid', {\n userId: instance.userId,\n age,\n idle,\n maxLifetime: this.config.maxLifetime,\n idleTimeout: this.config.idleTimeout,\n reason: age >= this.config.maxLifetime ? 'max lifetime exceeded' : 'idle timeout exceeded'\n });\n }\n \n return valid;\n }\n \n /**\n * Evict the least recently used instance\n * \n * Finds the instance with the oldest lastUsed timestamp\n * and removes it from the pool.\n */\n private async evictLeastRecentlyUsed(): Promise<void> {\n let oldestUserId: string | null = null;\n let oldestTime = Date.now();\n \n for (const [userId, instance] of this.instances.entries()) {\n if (instance.lastUsed < oldestTime) {\n oldestTime = instance.lastUsed;\n oldestUserId = userId;\n }\n }\n \n if (oldestUserId) {\n this.logger.info('Evicting LRU instance', { \n userId: oldestUserId,\n lastUsed: new Date(oldestTime).toISOString(),\n idleTime: Date.now() - oldestTime\n });\n await this.removeInstance(oldestUserId);\n }\n }\n \n /**\n * Remove an instance from the pool\n * \n * Closes the server instance (if it has a close method)\n * and removes it from the pool.\n * \n * @param userId - User ID of the instance to remove\n */\n private async removeInstance(userId: string): Promise<void> {\n const instance = this.instances.get(userId);\n if (instance) {\n try {\n // Close server if it has a close method\n if ('close' in instance.server && typeof instance.server.close === 'function') {\n await instance.server.close();\n this.logger.debug('Server instance closed', { userId });\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n this.logger.error(`Error closing instance for user ${userId}: ${errorMessage}`);\n }\n this.instances.delete(userId);\n this.logger.debug('Instance removed from pool', {\n userId,\n poolSize: this.instances.size\n });\n }\n }\n \n /**\n * Start periodic cleanup timer\n * \n * Runs cleanup every minute to remove expired instances.\n */\n private startCleanupTimer(): void {\n // Run cleanup every minute\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpiredInstances();\n }, 60000);\n \n // Don't keep the process alive just for this timer\n if (this.cleanupTimer.unref) {\n this.cleanupTimer.unref();\n }\n }\n \n /**\n * Clean up expired instances\n * \n * Removes all instances that are no longer valid\n * (exceeded max lifetime or idle timeout).\n */\n private async cleanupExpiredInstances(): Promise<void> {\n const now = Date.now();\n const toRemove: string[] = [];\n \n for (const [userId, instance] of this.instances.entries()) {\n if (!this.isValid(instance)) {\n toRemove.push(userId);\n }\n }\n \n if (toRemove.length > 0) {\n this.logger.info('Cleaning up expired instances', { \n count: toRemove.length,\n userIds: toRemove\n });\n \n for (const userId of toRemove) {\n await this.removeInstance(userId);\n }\n }\n }\n \n /**\n * Close all pooled instances\n * \n * Closes all server instances and clears the pool.\n * Should be called when shutting down the server.\n */\n async closeAll(): Promise<void> {\n this.logger.info('Closing all pooled instances', { \n count: this.instances.size \n });\n \n // Stop cleanup timer\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n \n // Close all instances\n const userIds = Array.from(this.instances.keys());\n for (const userId of userIds) {\n await this.removeInstance(userId);\n }\n \n this.logger.info('All pooled instances closed');\n }\n \n /**\n * Get pool statistics\n * \n * Returns current state of the pool for monitoring and debugging.\n * \n * @returns Pool statistics including size and instance details\n */\n getStats() {\n const now = Date.now();\n return {\n size: this.instances.size,\n maxSize: this.config.maxSize,\n instances: Array.from(this.instances.values()).map(i => ({\n userId: i.userId,\n age: now - i.createdAt,\n idle: now - i.lastUsed,\n createdAt: new Date(i.createdAt).toISOString(),\n lastUsed: new Date(i.lastUsed).toISOString()\n }))\n };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress notification manager for multi-tenant progress streaming
|
|
3
|
+
*
|
|
4
|
+
* Manages routing of progress notifications from wrapped MCP servers
|
|
5
|
+
* to the correct clients in multi-tenant deployments.
|
|
6
|
+
*/
|
|
7
|
+
import { Logger } from '../utils/logger.js';
|
|
8
|
+
import type { ProgressNotification } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Progress stream metadata
|
|
11
|
+
*/
|
|
12
|
+
interface ProgressStream {
|
|
13
|
+
userId: string;
|
|
14
|
+
progressToken: string | number;
|
|
15
|
+
startTime: number;
|
|
16
|
+
lastUpdate: number;
|
|
17
|
+
messageCount: number;
|
|
18
|
+
bytesTransferred?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Progress notification callback
|
|
22
|
+
*/
|
|
23
|
+
export type ProgressCallback = (notification: ProgressNotification) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Manages progress notification routing for multi-tenant deployments
|
|
26
|
+
*/
|
|
27
|
+
export declare class ProgressManager {
|
|
28
|
+
private streams;
|
|
29
|
+
private callbacks;
|
|
30
|
+
private logger;
|
|
31
|
+
constructor(logger: Logger);
|
|
32
|
+
/**
|
|
33
|
+
* Register a progress stream for a user
|
|
34
|
+
*/
|
|
35
|
+
registerStream(userId: string, progressToken: string | number, callback: ProgressCallback): void;
|
|
36
|
+
/**
|
|
37
|
+
* Forward a progress notification to the correct client
|
|
38
|
+
*/
|
|
39
|
+
forwardNotification(notification: ProgressNotification): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Unregister a progress stream
|
|
42
|
+
*/
|
|
43
|
+
unregisterStream(progressToken: string | number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get active streams for a user
|
|
46
|
+
*/
|
|
47
|
+
getUserStreams(userId: string): ProgressStream[];
|
|
48
|
+
/**
|
|
49
|
+
* Clean up stale streams (no updates for 5 minutes)
|
|
50
|
+
*/
|
|
51
|
+
cleanupStaleStreams(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Get statistics about active streams
|
|
54
|
+
*/
|
|
55
|
+
getStats(): {
|
|
56
|
+
activeStreams: number;
|
|
57
|
+
totalMessages: number;
|
|
58
|
+
userCount: number;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Get detailed metrics for a specific stream
|
|
62
|
+
*/
|
|
63
|
+
getStreamMetrics(progressToken: string | number): ProgressStreamMetrics | null;
|
|
64
|
+
/**
|
|
65
|
+
* Get metrics for all active streams
|
|
66
|
+
*/
|
|
67
|
+
getAllMetrics(): ProgressStreamMetrics[];
|
|
68
|
+
/**
|
|
69
|
+
* Get aggregated metrics by user
|
|
70
|
+
*/
|
|
71
|
+
getUserMetrics(userId: string): {
|
|
72
|
+
activeStreams: number;
|
|
73
|
+
totalMessages: number;
|
|
74
|
+
totalBytes: number;
|
|
75
|
+
oldestStreamAge: number;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Check health of all active streams
|
|
79
|
+
*/
|
|
80
|
+
checkHealth(): {
|
|
81
|
+
healthy: boolean;
|
|
82
|
+
issues: string[];
|
|
83
|
+
warnings: string[];
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Detailed progress stream metrics
|
|
88
|
+
*/
|
|
89
|
+
export interface ProgressStreamMetrics {
|
|
90
|
+
userId: string;
|
|
91
|
+
progressToken: string | number;
|
|
92
|
+
startTime: number;
|
|
93
|
+
lastUpdate: number;
|
|
94
|
+
duration: number;
|
|
95
|
+
messageCount: number;
|
|
96
|
+
bytesTransferred: number;
|
|
97
|
+
averageMessageSize: number;
|
|
98
|
+
messagesPerSecond: number;
|
|
99
|
+
}
|
|
100
|
+
export {};
|
|
101
|
+
//# sourceMappingURL=progress-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress-manager.d.ts","sourceRoot":"","sources":["../../src/wrapper/progress-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD;;GAEG;AACH,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,YAAY,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE5E;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,MAAM;IAM1B;;OAEG;IACH,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GAAG,MAAM,EAC9B,QAAQ,EAAE,gBAAgB,GACzB,IAAI;IAkBP;;OAEG;IACH,mBAAmB,CAAC,YAAY,EAAE,oBAAoB,GAAG,OAAO;IAkEhE;;OAEG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAkBtD;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE;IAYhD;;OAEG;IACH,mBAAmB,IAAI,IAAI;IAiB3B;;OAEG;IACH,QAAQ,IAAI;QACV,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;KACnB;IAgBD;;OAEG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,qBAAqB,GAAG,IAAI;IAuB9E;;OAEG;IACH,aAAa,IAAI,qBAAqB,EAAE;IAaxC;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;QAC9B,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;KACzB;IAyBD;;OAEG;IACH,WAAW,IAAI;QACb,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB;CAyCF;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;CAC3B"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
class ProgressManager {
|
|
2
|
+
streams;
|
|
3
|
+
callbacks;
|
|
4
|
+
logger;
|
|
5
|
+
constructor(logger) {
|
|
6
|
+
this.streams = /* @__PURE__ */ new Map();
|
|
7
|
+
this.callbacks = /* @__PURE__ */ new Map();
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Register a progress stream for a user
|
|
12
|
+
*/
|
|
13
|
+
registerStream(userId, progressToken, callback) {
|
|
14
|
+
const stream = {
|
|
15
|
+
userId,
|
|
16
|
+
progressToken,
|
|
17
|
+
startTime: Date.now(),
|
|
18
|
+
lastUpdate: Date.now(),
|
|
19
|
+
messageCount: 0
|
|
20
|
+
};
|
|
21
|
+
this.streams.set(progressToken, stream);
|
|
22
|
+
this.callbacks.set(progressToken, callback);
|
|
23
|
+
this.logger.debug("Progress stream registered", {
|
|
24
|
+
userId,
|
|
25
|
+
progressToken
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Forward a progress notification to the correct client
|
|
30
|
+
*/
|
|
31
|
+
forwardNotification(notification) {
|
|
32
|
+
const { progressToken, progress, total, message } = notification;
|
|
33
|
+
const stream = this.streams.get(progressToken);
|
|
34
|
+
const callback = this.callbacks.get(progressToken);
|
|
35
|
+
if (!stream || !callback) {
|
|
36
|
+
this.logger.warn("Progress notification for unknown token", {
|
|
37
|
+
progressToken,
|
|
38
|
+
hasStream: !!stream,
|
|
39
|
+
hasCallback: !!callback
|
|
40
|
+
});
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
stream.lastUpdate = Date.now();
|
|
44
|
+
stream.messageCount++;
|
|
45
|
+
if (message) {
|
|
46
|
+
const bytes = Buffer.byteLength(message, "utf8");
|
|
47
|
+
stream.bytesTransferred = (stream.bytesTransferred || 0) + bytes;
|
|
48
|
+
}
|
|
49
|
+
if (progress !== void 0 && total !== void 0) {
|
|
50
|
+
const percentage = progress / total * 100;
|
|
51
|
+
if ([25, 50, 75, 100].includes(Math.floor(percentage))) {
|
|
52
|
+
this.logger.info("Progress milestone", {
|
|
53
|
+
userId: stream.userId,
|
|
54
|
+
progressToken,
|
|
55
|
+
percentage: Math.floor(percentage),
|
|
56
|
+
progress,
|
|
57
|
+
total
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
callback(notification);
|
|
63
|
+
this.logger.debug("Progress notification forwarded", {
|
|
64
|
+
userId: stream.userId,
|
|
65
|
+
progressToken,
|
|
66
|
+
messageCount: stream.messageCount,
|
|
67
|
+
progress,
|
|
68
|
+
total,
|
|
69
|
+
messageLength: message?.length || 0
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
this.logger.error("Error forwarding progress notification", error, {
|
|
74
|
+
userId: stream.userId,
|
|
75
|
+
progressToken,
|
|
76
|
+
messageCount: stream.messageCount
|
|
77
|
+
});
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Unregister a progress stream
|
|
83
|
+
*/
|
|
84
|
+
unregisterStream(progressToken) {
|
|
85
|
+
const stream = this.streams.get(progressToken);
|
|
86
|
+
if (stream) {
|
|
87
|
+
const duration = Date.now() - stream.startTime;
|
|
88
|
+
this.logger.debug("Progress stream unregistered", {
|
|
89
|
+
userId: stream.userId,
|
|
90
|
+
progressToken,
|
|
91
|
+
duration,
|
|
92
|
+
messageCount: stream.messageCount
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
this.streams.delete(progressToken);
|
|
96
|
+
this.callbacks.delete(progressToken);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get active streams for a user
|
|
100
|
+
*/
|
|
101
|
+
getUserStreams(userId) {
|
|
102
|
+
const streams = [];
|
|
103
|
+
for (const stream of this.streams.values()) {
|
|
104
|
+
if (stream.userId === userId) {
|
|
105
|
+
streams.push(stream);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return streams;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Clean up stale streams (no updates for 5 minutes)
|
|
112
|
+
*/
|
|
113
|
+
cleanupStaleStreams() {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const staleThreshold = 5 * 60 * 1e3;
|
|
116
|
+
const toRemove = [];
|
|
117
|
+
for (const [token, stream] of this.streams.entries()) {
|
|
118
|
+
if (now - stream.lastUpdate > staleThreshold) {
|
|
119
|
+
toRemove.push(token);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const token of toRemove) {
|
|
123
|
+
this.unregisterStream(token);
|
|
124
|
+
this.logger.warn("Cleaned up stale progress stream", { progressToken: token });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get statistics about active streams
|
|
129
|
+
*/
|
|
130
|
+
getStats() {
|
|
131
|
+
const userIds = /* @__PURE__ */ new Set();
|
|
132
|
+
let totalMessages = 0;
|
|
133
|
+
for (const stream of this.streams.values()) {
|
|
134
|
+
userIds.add(stream.userId);
|
|
135
|
+
totalMessages += stream.messageCount;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
activeStreams: this.streams.size,
|
|
139
|
+
totalMessages,
|
|
140
|
+
userCount: userIds.size
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get detailed metrics for a specific stream
|
|
145
|
+
*/
|
|
146
|
+
getStreamMetrics(progressToken) {
|
|
147
|
+
const stream = this.streams.get(progressToken);
|
|
148
|
+
if (!stream) return null;
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const duration = now - stream.startTime;
|
|
151
|
+
const messagesPerSecond = duration > 0 ? stream.messageCount / (duration / 1e3) : 0;
|
|
152
|
+
return {
|
|
153
|
+
userId: stream.userId,
|
|
154
|
+
progressToken: stream.progressToken,
|
|
155
|
+
startTime: stream.startTime,
|
|
156
|
+
lastUpdate: stream.lastUpdate,
|
|
157
|
+
duration,
|
|
158
|
+
messageCount: stream.messageCount,
|
|
159
|
+
bytesTransferred: stream.bytesTransferred || 0,
|
|
160
|
+
averageMessageSize: stream.bytesTransferred && stream.messageCount > 0 ? stream.bytesTransferred / stream.messageCount : 0,
|
|
161
|
+
messagesPerSecond
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get metrics for all active streams
|
|
166
|
+
*/
|
|
167
|
+
getAllMetrics() {
|
|
168
|
+
const metrics = [];
|
|
169
|
+
for (const token of this.streams.keys()) {
|
|
170
|
+
const metric = this.getStreamMetrics(token);
|
|
171
|
+
if (metric) {
|
|
172
|
+
metrics.push(metric);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return metrics;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get aggregated metrics by user
|
|
179
|
+
*/
|
|
180
|
+
getUserMetrics(userId) {
|
|
181
|
+
const streams = this.getUserStreams(userId);
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
let totalMessages = 0;
|
|
184
|
+
let totalBytes = 0;
|
|
185
|
+
let oldestStreamAge = 0;
|
|
186
|
+
for (const stream of streams) {
|
|
187
|
+
totalMessages += stream.messageCount;
|
|
188
|
+
totalBytes += stream.bytesTransferred || 0;
|
|
189
|
+
const age = now - stream.startTime;
|
|
190
|
+
if (age > oldestStreamAge) {
|
|
191
|
+
oldestStreamAge = age;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
activeStreams: streams.length,
|
|
196
|
+
totalMessages,
|
|
197
|
+
totalBytes,
|
|
198
|
+
oldestStreamAge
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check health of all active streams
|
|
203
|
+
*/
|
|
204
|
+
checkHealth() {
|
|
205
|
+
const issues = [];
|
|
206
|
+
const warnings = [];
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
for (const [token, stream] of this.streams.entries()) {
|
|
209
|
+
const age = now - stream.startTime;
|
|
210
|
+
const timeSinceUpdate = now - stream.lastUpdate;
|
|
211
|
+
if (age > 36e5) {
|
|
212
|
+
warnings.push(
|
|
213
|
+
`Stream ${token} for user ${stream.userId} is very old (${Math.floor(age / 6e4)} minutes)`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (timeSinceUpdate > 3e5) {
|
|
217
|
+
issues.push(
|
|
218
|
+
`Stream ${token} for user ${stream.userId} is stale (${Math.floor(timeSinceUpdate / 6e4)} minutes since update)`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const duration = age / 1e3;
|
|
222
|
+
if (duration > 0) {
|
|
223
|
+
const rate = stream.messageCount / duration;
|
|
224
|
+
if (rate > 100) {
|
|
225
|
+
warnings.push(
|
|
226
|
+
`Stream ${token} has high message rate (${rate.toFixed(1)}/sec)`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
healthy: issues.length === 0,
|
|
233
|
+
issues,
|
|
234
|
+
warnings
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export {
|
|
239
|
+
ProgressManager
|
|
240
|
+
};
|
|
241
|
+
//# sourceMappingURL=progress-manager.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/wrapper/progress-manager.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Progress notification manager for multi-tenant progress streaming\n * \n * Manages routing of progress notifications from wrapped MCP servers\n * to the correct clients in multi-tenant deployments.\n */\n\nimport { Logger } from '../utils/logger.js';\nimport type { ProgressNotification } from '../types.js';\n\n/**\n * Progress stream metadata\n */\ninterface ProgressStream {\n userId: string;\n progressToken: string | number;\n startTime: number;\n lastUpdate: number;\n messageCount: number;\n bytesTransferred?: number;\n}\n\n/**\n * Progress notification callback\n */\nexport type ProgressCallback = (notification: ProgressNotification) => void;\n\n/**\n * Manages progress notification routing for multi-tenant deployments\n */\nexport class ProgressManager {\n private streams: Map<string | number, ProgressStream>;\n private callbacks: Map<string | number, ProgressCallback>;\n private logger: Logger;\n \n constructor(logger: Logger) {\n this.streams = new Map();\n this.callbacks = new Map();\n this.logger = logger;\n }\n \n /**\n * Register a progress stream for a user\n */\n registerStream(\n userId: string,\n progressToken: string | number,\n callback: ProgressCallback\n ): void {\n const stream: ProgressStream = {\n userId,\n progressToken,\n startTime: Date.now(),\n lastUpdate: Date.now(),\n messageCount: 0\n };\n \n this.streams.set(progressToken, stream);\n this.callbacks.set(progressToken, callback);\n \n this.logger.debug('Progress stream registered', {\n userId,\n progressToken\n });\n }\n \n /**\n * Forward a progress notification to the correct client\n */\n forwardNotification(notification: ProgressNotification): boolean {\n const { progressToken, progress, total, message } = notification;\n \n const stream = this.streams.get(progressToken);\n const callback = this.callbacks.get(progressToken);\n \n if (!stream || !callback) {\n this.logger.warn('Progress notification for unknown token', {\n progressToken,\n hasStream: !!stream,\n hasCallback: !!callback\n });\n return false;\n }\n \n // Update stream metadata\n stream.lastUpdate = Date.now();\n stream.messageCount++;\n \n // Track bytes if message provided\n if (message) {\n const bytes = Buffer.byteLength(message, 'utf8');\n stream.bytesTransferred = (stream.bytesTransferred || 0) + bytes;\n }\n \n // Log progress milestones\n if (progress !== undefined && total !== undefined) {\n const percentage = (progress / total) * 100;\n \n // Log at 25%, 50%, 75%, 100%\n if ([25, 50, 75, 100].includes(Math.floor(percentage))) {\n this.logger.info('Progress milestone', {\n userId: stream.userId,\n progressToken,\n percentage: Math.floor(percentage),\n progress,\n total\n });\n }\n }\n \n // Forward to client\n try {\n callback(notification);\n \n // Debug logging for every notification\n this.logger.debug('Progress notification forwarded', {\n userId: stream.userId,\n progressToken,\n messageCount: stream.messageCount,\n progress,\n total,\n messageLength: message?.length || 0\n });\n \n return true;\n } catch (error) {\n this.logger.error('Error forwarding progress notification', error as Error, {\n userId: stream.userId,\n progressToken,\n messageCount: stream.messageCount\n });\n return false;\n }\n }\n \n /**\n * Unregister a progress stream\n */\n unregisterStream(progressToken: string | number): void {\n const stream = this.streams.get(progressToken);\n \n if (stream) {\n const duration = Date.now() - stream.startTime;\n \n this.logger.debug('Progress stream unregistered', {\n userId: stream.userId,\n progressToken,\n duration,\n messageCount: stream.messageCount\n });\n }\n \n this.streams.delete(progressToken);\n this.callbacks.delete(progressToken);\n }\n \n /**\n * Get active streams for a user\n */\n getUserStreams(userId: string): ProgressStream[] {\n const streams: ProgressStream[] = [];\n \n for (const stream of this.streams.values()) {\n if (stream.userId === userId) {\n streams.push(stream);\n }\n }\n \n return streams;\n }\n \n /**\n * Clean up stale streams (no updates for 5 minutes)\n */\n cleanupStaleStreams(): void {\n const now = Date.now();\n const staleThreshold = 5 * 60 * 1000; // 5 minutes\n const toRemove: (string | number)[] = [];\n \n for (const [token, stream] of this.streams.entries()) {\n if (now - stream.lastUpdate > staleThreshold) {\n toRemove.push(token);\n }\n }\n \n for (const token of toRemove) {\n this.unregisterStream(token);\n this.logger.warn('Cleaned up stale progress stream', { progressToken: token });\n }\n }\n \n /**\n * Get statistics about active streams\n */\n getStats(): {\n activeStreams: number;\n totalMessages: number;\n userCount: number;\n } {\n const userIds = new Set<string>();\n let totalMessages = 0;\n \n for (const stream of this.streams.values()) {\n userIds.add(stream.userId);\n totalMessages += stream.messageCount;\n }\n \n return {\n activeStreams: this.streams.size,\n totalMessages,\n userCount: userIds.size\n };\n }\n \n /**\n * Get detailed metrics for a specific stream\n */\n getStreamMetrics(progressToken: string | number): ProgressStreamMetrics | null {\n const stream = this.streams.get(progressToken);\n if (!stream) return null;\n \n const now = Date.now();\n const duration = now - stream.startTime;\n const messagesPerSecond = duration > 0 ? stream.messageCount / (duration / 1000) : 0;\n \n return {\n userId: stream.userId,\n progressToken: stream.progressToken,\n startTime: stream.startTime,\n lastUpdate: stream.lastUpdate,\n duration,\n messageCount: stream.messageCount,\n bytesTransferred: stream.bytesTransferred || 0,\n averageMessageSize: stream.bytesTransferred && stream.messageCount > 0\n ? stream.bytesTransferred / stream.messageCount\n : 0,\n messagesPerSecond\n };\n }\n \n /**\n * Get metrics for all active streams\n */\n getAllMetrics(): ProgressStreamMetrics[] {\n const metrics: ProgressStreamMetrics[] = [];\n \n for (const token of this.streams.keys()) {\n const metric = this.getStreamMetrics(token);\n if (metric) {\n metrics.push(metric);\n }\n }\n \n return metrics;\n }\n \n /**\n * Get aggregated metrics by user\n */\n getUserMetrics(userId: string): {\n activeStreams: number;\n totalMessages: number;\n totalBytes: number;\n oldestStreamAge: number;\n } {\n const streams = this.getUserStreams(userId);\n const now = Date.now();\n \n let totalMessages = 0;\n let totalBytes = 0;\n let oldestStreamAge = 0;\n \n for (const stream of streams) {\n totalMessages += stream.messageCount;\n totalBytes += stream.bytesTransferred || 0;\n const age = now - stream.startTime;\n if (age > oldestStreamAge) {\n oldestStreamAge = age;\n }\n }\n \n return {\n activeStreams: streams.length,\n totalMessages,\n totalBytes,\n oldestStreamAge\n };\n }\n \n /**\n * Check health of all active streams\n */\n checkHealth(): {\n healthy: boolean;\n issues: string[];\n warnings: string[];\n } {\n const issues: string[] = [];\n const warnings: string[] = [];\n const now = Date.now();\n \n for (const [token, stream] of this.streams.entries()) {\n const age = now - stream.startTime;\n const timeSinceUpdate = now - stream.lastUpdate;\n \n // Check for very old streams (>1 hour)\n if (age > 3600000) {\n warnings.push(\n `Stream ${token} for user ${stream.userId} is very old (${Math.floor(age / 60000)} minutes)`\n );\n }\n \n // Check for stale streams (>5 minutes since update)\n if (timeSinceUpdate > 300000) {\n issues.push(\n `Stream ${token} for user ${stream.userId} is stale (${Math.floor(timeSinceUpdate / 60000)} minutes since update)`\n );\n }\n \n // Check for high message rate (>100/sec)\n const duration = age / 1000;\n if (duration > 0) {\n const rate = stream.messageCount / duration;\n if (rate > 100) {\n warnings.push(\n `Stream ${token} has high message rate (${rate.toFixed(1)}/sec)`\n );\n }\n }\n }\n \n return {\n healthy: issues.length === 0,\n issues,\n warnings\n };\n }\n}\n\n/**\n * Detailed progress stream metrics\n */\nexport interface ProgressStreamMetrics {\n userId: string;\n progressToken: string | number;\n startTime: number;\n lastUpdate: number;\n duration: number;\n messageCount: number;\n bytesTransferred: number;\n averageMessageSize: number;\n messagesPerSecond: number;\n}\n"],
|
|
5
|
+
"mappings": "AA8BO,MAAM,gBAAgB;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAAgB;AAC1B,SAAK,UAAU,oBAAI,IAAI;AACvB,SAAK,YAAY,oBAAI,IAAI;AACzB,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,eACE,QACA,eACA,UACM;AACN,UAAM,SAAyB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,WAAW,KAAK,IAAI;AAAA,MACpB,YAAY,KAAK,IAAI;AAAA,MACrB,cAAc;AAAA,IAChB;AAEA,SAAK,QAAQ,IAAI,eAAe,MAAM;AACtC,SAAK,UAAU,IAAI,eAAe,QAAQ;AAE1C,SAAK,OAAO,MAAM,8BAA8B;AAAA,MAC9C;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAoB,cAA6C;AAC/D,UAAM,EAAE,eAAe,UAAU,OAAO,QAAQ,IAAI;AAEpD,UAAM,SAAS,KAAK,QAAQ,IAAI,aAAa;AAC7C,UAAM,WAAW,KAAK,UAAU,IAAI,aAAa;AAEjD,QAAI,CAAC,UAAU,CAAC,UAAU;AACxB,WAAK,OAAO,KAAK,2CAA2C;AAAA,QAC1D;AAAA,QACA,WAAW,CAAC,CAAC;AAAA,QACb,aAAa,CAAC,CAAC;AAAA,MACjB,CAAC;AACD,aAAO;AAAA,IACT;AAGA,WAAO,aAAa,KAAK,IAAI;AAC7B,WAAO;AAGP,QAAI,SAAS;AACX,YAAM,QAAQ,OAAO,WAAW,SAAS,MAAM;AAC/C,aAAO,oBAAoB,OAAO,oBAAoB,KAAK;AAAA,IAC7D;AAGA,QAAI,aAAa,UAAa,UAAU,QAAW;AACjD,YAAM,aAAc,WAAW,QAAS;AAGxC,UAAI,CAAC,IAAI,IAAI,IAAI,GAAG,EAAE,SAAS,KAAK,MAAM,UAAU,CAAC,GAAG;AACtD,aAAK,OAAO,KAAK,sBAAsB;AAAA,UACrC,QAAQ,OAAO;AAAA,UACf;AAAA,UACA,YAAY,KAAK,MAAM,UAAU;AAAA,UACjC;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI;AACF,eAAS,YAAY;AAGrB,WAAK,OAAO,MAAM,mCAAmC;AAAA,QACnD,QAAQ,OAAO;AAAA,QACf;AAAA,QACA,cAAc,OAAO;AAAA,QACrB;AAAA,QACA;AAAA,QACA,eAAe,SAAS,UAAU;AAAA,MACpC,CAAC;AAED,aAAO;AAAA,IACT,SAAS,OAAO;AACd,WAAK,OAAO,MAAM,0CAA0C,OAAgB;AAAA,QAC1E,QAAQ,OAAO;AAAA,QACf;AAAA,QACA,cAAc,OAAO;AAAA,MACvB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,eAAsC;AACrD,UAAM,SAAS,KAAK,QAAQ,IAAI,aAAa;AAE7C,QAAI,QAAQ;AACV,YAAM,WAAW,KAAK,IAAI,IAAI,OAAO;AAErC,WAAK,OAAO,MAAM,gCAAgC;AAAA,QAChD,QAAQ,OAAO;AAAA,QACf;AAAA,QACA;AAAA,QACA,cAAc,OAAO;AAAA,MACvB,CAAC;AAAA,IACH;AAEA,SAAK,QAAQ,OAAO,aAAa;AACjC,SAAK,UAAU,OAAO,aAAa;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,QAAkC;AAC/C,UAAM,UAA4B,CAAC;AAEnC,eAAW,UAAU,KAAK,QAAQ,OAAO,GAAG;AAC1C,UAAI,OAAO,WAAW,QAAQ;AAC5B,gBAAQ,KAAK,MAAM;AAAA,MACrB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,sBAA4B;AAC1B,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,iBAAiB,IAAI,KAAK;AAChC,UAAM,WAAgC,CAAC;AAEvC,eAAW,CAAC,OAAO,MAAM,KAAK,KAAK,QAAQ,QAAQ,GAAG;AACpD,UAAI,MAAM,OAAO,aAAa,gBAAgB;AAC5C,iBAAS,KAAK,KAAK;AAAA,MACrB;AAAA,IACF;AAEA,eAAW,SAAS,UAAU;AAC5B,WAAK,iBAAiB,KAAK;AAC3B,WAAK,OAAO,KAAK,oCAAoC,EAAE,eAAe,MAAM,CAAC;AAAA,IAC/E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,WAIE;AACA,UAAM,UAAU,oBAAI,IAAY;AAChC,QAAI,gBAAgB;AAEpB,eAAW,UAAU,KAAK,QAAQ,OAAO,GAAG;AAC1C,cAAQ,IAAI,OAAO,MAAM;AACzB,uBAAiB,OAAO;AAAA,IAC1B;AAEA,WAAO;AAAA,MACL,eAAe,KAAK,QAAQ;AAAA,MAC5B;AAAA,MACA,WAAW,QAAQ;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,eAA8D;AAC7E,UAAM,SAAS,KAAK,QAAQ,IAAI,aAAa;AAC7C,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAAW,MAAM,OAAO;AAC9B,UAAM,oBAAoB,WAAW,IAAI,OAAO,gBAAgB,WAAW,OAAQ;AAEnF,WAAO;AAAA,MACL,QAAQ,OAAO;AAAA,MACf,eAAe,OAAO;AAAA,MACtB,WAAW,OAAO;AAAA,MAClB,YAAY,OAAO;AAAA,MACnB;AAAA,MACA,cAAc,OAAO;AAAA,MACrB,kBAAkB,OAAO,oBAAoB;AAAA,MAC7C,oBAAoB,OAAO,oBAAoB,OAAO,eAAe,IACjE,OAAO,mBAAmB,OAAO,eACjC;AAAA,MACJ;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAyC;AACvC,UAAM,UAAmC,CAAC;AAE1C,eAAW,SAAS,KAAK,QAAQ,KAAK,GAAG;AACvC,YAAM,SAAS,KAAK,iBAAiB,KAAK;AAC1C,UAAI,QAAQ;AACV,gBAAQ,KAAK,MAAM;AAAA,MACrB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,QAKb;AACA,UAAM,UAAU,KAAK,eAAe,MAAM;AAC1C,UAAM,MAAM,KAAK,IAAI;AAErB,QAAI,gBAAgB;AACpB,QAAI,aAAa;AACjB,QAAI,kBAAkB;AAEtB,eAAW,UAAU,SAAS;AAC5B,uBAAiB,OAAO;AACxB,oBAAc,OAAO,oBAAoB;AACzC,YAAM,MAAM,MAAM,OAAO;AACzB,UAAI,MAAM,iBAAiB;AACzB,0BAAkB;AAAA,MACpB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,eAAe,QAAQ;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,cAIE;AACA,UAAM,SAAmB,CAAC;AAC1B,UAAM,WAAqB,CAAC;AAC5B,UAAM,MAAM,KAAK,IAAI;AAErB,eAAW,CAAC,OAAO,MAAM,KAAK,KAAK,QAAQ,QAAQ,GAAG;AACpD,YAAM,MAAM,MAAM,OAAO;AACzB,YAAM,kBAAkB,MAAM,OAAO;AAGrC,UAAI,MAAM,MAAS;AACjB,iBAAS;AAAA,UACP,UAAU,KAAK,aAAa,OAAO,MAAM,iBAAiB,KAAK,MAAM,MAAM,GAAK,CAAC;AAAA,QACnF;AAAA,MACF;AAGA,UAAI,kBAAkB,KAAQ;AAC5B,eAAO;AAAA,UACL,UAAU,KAAK,aAAa,OAAO,MAAM,cAAc,KAAK,MAAM,kBAAkB,GAAK,CAAC;AAAA,QAC5F;AAAA,MACF;AAGA,YAAM,WAAW,MAAM;AACvB,UAAI,WAAW,GAAG;AAChB,cAAM,OAAO,OAAO,eAAe;AACnC,YAAI,OAAO,KAAK;AACd,mBAAS;AAAA,YACP,UAAU,KAAK,2BAA2B,KAAK,QAAQ,CAAC,CAAC;AAAA,UAC3D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS,OAAO,WAAW;AAAA,MAC3B;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|