diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b7980fadb..3e7eb6a46 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,9 +2,11 @@ export * from './auth/errors.js'; export * from './shared/auth.js'; export * from './shared/authUtils.js'; export * from './shared/metadataUtils.js'; +export * from './shared/progressManager.js'; export * from './shared/protocol.js'; export * from './shared/responseMessage.js'; export * from './shared/stdio.js'; +export * from './shared/timeoutManager.js'; export * from './shared/toolNameValidation.js'; export * from './shared/transport.js'; export * from './shared/uriTemplate.js'; diff --git a/packages/core/src/shared/progressManager.ts b/packages/core/src/shared/progressManager.ts new file mode 100644 index 000000000..7d9af19a6 --- /dev/null +++ b/packages/core/src/shared/progressManager.ts @@ -0,0 +1,172 @@ +/** + * Progress Manager + * + * Manages progress tracking for the Protocol class. + * Extracted from Protocol to follow Single Responsibility Principle. + */ + +import type { Progress, ProgressNotification } from '../types/types.js'; + +/** + * Callback for progress notifications. + */ +export type ProgressCallback = (progress: Progress) => void; + +/** + * Manages progress tracking for requests. + * + * This class handles registration, lookup, and invocation of progress callbacks, + * as well as task-to-progress-token associations for long-running task operations. + * + * @example + * ```typescript + * const progressManager = new ProgressManager(); + * + * // Register a progress handler for a request + * progressManager.registerHandler(messageId, (progress) => { + * console.log(`Progress: ${progress.progress}/${progress.total}`); + * }); + * + * // Handle incoming progress notification + * progressManager.handleProgress(notification); + * + * // Clean up when done + * progressManager.removeHandler(messageId); + * ``` + */ +export class ProgressManager { + /** + * Maps message IDs to progress callbacks. + */ + #progressHandlers: Map = new Map(); + + /** + * Maps task IDs to progress tokens to keep handlers alive after CreateTaskResult. + */ + #taskProgressTokens: Map = new Map(); + + /** + * Registers a progress callback for a message. + * + * @param messageId - The message ID (used as progress token) + * @param callback - The callback to invoke when progress is received + */ + registerHandler(messageId: number, callback: ProgressCallback): void { + this.#progressHandlers.set(messageId, callback); + } + + /** + * Gets the progress callback for a message. + * + * @param messageId - The message ID + * @returns The progress callback or undefined if not registered + */ + getHandler(messageId: number): ProgressCallback | undefined { + return this.#progressHandlers.get(messageId); + } + + /** + * Removes the progress callback for a message. + * + * @param messageId - The message ID + */ + removeHandler(messageId: number): void { + this.#progressHandlers.delete(messageId); + } + + /** + * Checks if a progress handler exists for the given message ID. + * + * @param messageId - The message ID + * @returns true if a handler is registered, false otherwise + */ + hasHandler(messageId: number): boolean { + return this.#progressHandlers.has(messageId); + } + + /** + * Handles an incoming progress notification by invoking the registered callback. + * Returns true if the progress was handled, false if no handler was found. + * + * @param notification - The progress notification + * @returns true if handled, false otherwise + */ + handleProgress(notification: ProgressNotification): boolean { + const token = notification.params.progressToken; + if (typeof token !== 'number') { + // Token must be a number for our internal tracking + return false; + } + + const callback = this.#progressHandlers.get(token); + if (callback) { + callback({ + progress: notification.params.progress, + total: notification.params.total, + message: notification.params.message + }); + return true; + } + + return false; + } + + /** + * Links a task ID to a progress token. + * This keeps the progress handler alive after CreateTaskResult is returned, + * allowing progress notifications to continue for long-running tasks. + * + * @param taskId - The task identifier + * @param progressToken - The progress token (message ID) + */ + linkTaskToProgressToken(taskId: string, progressToken: number): void { + this.#taskProgressTokens.set(taskId, progressToken); + } + + /** + * Gets the progress token associated with a task. + * + * @param taskId - The task identifier + * @returns The progress token or undefined if not linked + */ + getTaskProgressToken(taskId: string): number | undefined { + return this.#taskProgressTokens.get(taskId); + } + + /** + * Cleans up the progress handler associated with a task. + * Should be called when a task reaches a terminal status. + * + * @param taskId - The task identifier + */ + cleanupTaskProgressHandler(taskId: string): void { + const progressToken = this.#taskProgressTokens.get(taskId); + if (progressToken !== undefined) { + this.#progressHandlers.delete(progressToken); + this.#taskProgressTokens.delete(taskId); + } + } + + /** + * Clears all progress handlers and task progress tokens. + * Typically called when the connection is closed. + */ + clear(): void { + this.#progressHandlers.clear(); + this.#taskProgressTokens.clear(); + } + + /** + * Gets the number of active progress handlers. + */ + get handlerCount(): number { + return this.#progressHandlers.size; + } + + /** + * Gets the number of active task-to-progress-token links. + */ + get taskTokenCount(): number { + return this.#taskProgressTokens.size; + } +} diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 3d6c7d187..5f154496f 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -16,7 +16,6 @@ import type { Notification, NotificationMethod, NotificationTypeMap, - Progress, ProgressNotification, RelatedTaskMetadata, Request, @@ -52,14 +51,12 @@ import { import type { AnySchema, SchemaOutput } from '../util/zodCompat.js'; import { safeParse } from '../util/zodCompat.js'; import { parseWithCompat } from '../util/zodJsonSchemaCompat.js'; +import type { ProgressCallback } from './progressManager.js'; +import { ProgressManager } from './progressManager.js'; import type { ResponseMessage } from './responseMessage.js'; +import { TimeoutManager } from './timeoutManager.js'; import type { Transport, TransportSendOptions } from './transport.js'; -/** - * Callback for progress notifications. - */ -export type ProgressCallback = (progress: Progress) => void; - /** * Additional initialization options. */ @@ -313,18 +310,6 @@ export type RequestHandlerExtra void; }; -/** - * Information about a request's timeout state - */ -type TimeoutInfo = { - timeoutId: ReturnType; - startTime: number; - timeout: number; - maxTotalTimeout?: number; - resetTimeoutOnProgress: boolean; - onTimeout: () => void; -}; - /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. @@ -339,13 +324,10 @@ export abstract class Protocol = new Map(); private _notificationHandlers: Map Promise> = new Map(); private _responseHandlers: Map void> = new Map(); - private _progressHandlers: Map = new Map(); - private _timeoutInfo: Map = new Map(); + readonly #progressManager: ProgressManager = new ProgressManager(); + readonly #timeoutManager: TimeoutManager = new TimeoutManager(); private _pendingDebouncedNotifications = new Set(); - // Maps task IDs to progress tokens to keep handlers alive after CreateTaskResult - private _taskProgressTokens: Map = new Map(); - private _taskStore?: TaskStore; private _taskMessageQueue?: TaskMessageQueue; @@ -570,49 +552,6 @@ export abstract class Protocol void, - resetTimeoutOnProgress: boolean = false - ) { - this._timeoutInfo.set(messageId, { - timeoutId: setTimeout(onTimeout, timeout), - startTime: Date.now(), - timeout, - maxTotalTimeout, - resetTimeoutOnProgress, - onTimeout - }); - } - - private _resetTimeout(messageId: number): boolean { - const info = this._timeoutInfo.get(messageId); - if (!info) return false; - - const totalElapsed = Date.now() - info.startTime; - if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { - this._timeoutInfo.delete(messageId); - throw McpError.fromError(ErrorCode.RequestTimeout, 'Maximum total timeout exceeded', { - maxTotalTimeout: info.maxTotalTimeout, - totalElapsed - }); - } - - clearTimeout(info.timeoutId); - info.timeoutId = setTimeout(info.onTimeout, info.timeout); - return true; - } - - private _cleanupTimeout(messageId: number) { - const info = this._timeoutInfo.get(messageId); - if (info) { - clearTimeout(info.timeoutId); - this._timeoutInfo.delete(messageId); - } - } - /** * Attaches to the given transport, starts it, and starts listening for messages. * @@ -655,8 +594,7 @@ export abstract class Protocol; if (typeof task.taskId === 'string') { isTaskResponse = true; - this._taskProgressTokens.set(task.taskId, messageId); + this.#progressManager.linkTaskToProgressToken(task.taskId, messageId); } } } if (!isTaskResponse) { - this._progressHandlers.delete(messageId); + this.#progressManager.removeHandler(messageId); } if (isJSONRPCResultResponse(response)) { @@ -924,6 +864,22 @@ export abstract class Protocol { this._responseHandlers.delete(messageId); - this._progressHandlers.delete(messageId); - this._cleanupTimeout(messageId); + this.#progressManager.removeHandler(messageId); + this.#timeoutManager.cleanup(messageId); this._transport ?.send( @@ -1214,7 +1170,12 @@ export abstract class Protocol cancel(McpError.fromError(ErrorCode.RequestTimeout, 'Request timed out', { timeout })); - this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false); + this.#timeoutManager.setup(messageId, { + timeout, + maxTotalTimeout: options?.maxTotalTimeout, + resetTimeoutOnProgress: options?.resetTimeoutOnProgress ?? false, + onTimeout: timeoutHandler + }); // Queue request if related to a task const relatedTaskId = relatedTask?.taskId; @@ -1236,7 +1197,7 @@ export abstract class Protocol { - this._cleanupTimeout(messageId); + this.#timeoutManager.cleanup(messageId); reject(error); }); @@ -1245,7 +1206,7 @@ export abstract class Protocol { - this._cleanupTimeout(messageId); + this.#timeoutManager.cleanup(messageId); reject(error); }); } @@ -1475,11 +1436,7 @@ export abstract class Protocol; + + /** + * The time when the timeout was started (in milliseconds since epoch). + */ + startTime: number; + + /** + * The timeout duration in milliseconds. + */ + timeout: number; + + /** + * Maximum total time allowed in milliseconds (optional). + */ + maxTotalTimeout?: number; + + /** + * Whether to reset the timeout when progress is received. + */ + resetTimeoutOnProgress: boolean; + + /** + * Callback to invoke when the timeout expires. + */ + onTimeout: () => void; +} + +/** + * Options for setting up a timeout. + */ +export interface TimeoutOptions { + /** + * The timeout duration in milliseconds. + */ + timeout: number; + + /** + * Maximum total time allowed in milliseconds (optional). + * If set, the timeout cannot be reset beyond this total duration. + */ + maxTotalTimeout?: number; + + /** + * Whether to reset the timeout when progress is received. + * @default false + */ + resetTimeoutOnProgress?: boolean; + + /** + * Callback to invoke when the timeout expires. + */ + onTimeout: () => void; +} + +/** + * Result of a timeout reset attempt. + */ +export interface TimeoutResetResult { + /** + * Whether the reset was successful. + */ + success: boolean; + + /** + * If reset failed due to max total timeout being exceeded, this contains + * the elapsed time and max total timeout for error reporting. + */ + maxTotalTimeoutExceeded?: { + elapsed: number; + maxTotalTimeout: number; + }; +} + +/** + * Manages request timeouts for outgoing requests. + * + * This class handles setting up, resetting, and cleaning up timeouts for + * individual messages. It supports both simple timeouts and progress-aware + * timeouts that can be reset when progress notifications are received. + * + * @example + * ```typescript + * const timeoutManager = new TimeoutManager(); + * + * // Set up a timeout + * timeoutManager.setup(messageId, { + * timeout: 30000, + * maxTotalTimeout: 300000, + * resetTimeoutOnProgress: true, + * onTimeout: () => console.log('Request timed out') + * }); + * + * // Reset timeout when progress is received + * const result = timeoutManager.reset(messageId); + * if (!result.success && result.maxTotalTimeoutExceeded) { + * // Handle max total timeout exceeded + * } + * + * // Clean up when response received + * timeoutManager.cleanup(messageId); + * ``` + */ +export class TimeoutManager { + /** + * Maps message IDs to their timeout information. + */ + #timeoutInfo: Map = new Map(); + + /** + * Sets up a timeout for a message. + * + * @param messageId - The unique identifier for the message + * @param options - Timeout configuration options + */ + setup(messageId: number, options: TimeoutOptions): void { + const { timeout, maxTotalTimeout, resetTimeoutOnProgress, onTimeout } = options; + + this.#timeoutInfo.set(messageId, { + timeoutId: setTimeout(onTimeout, timeout), + startTime: Date.now(), + timeout, + maxTotalTimeout, + resetTimeoutOnProgress: resetTimeoutOnProgress ?? false, + onTimeout + }); + } + + /** + * Resets the timeout for a message (e.g., when progress is received). + * + * The reset will fail if: + * - No timeout exists for the message + * - The timeout is not configured for reset on progress + * - The max total timeout has been exceeded + * + * When reset succeeds, the timeout is reset to its original duration. + * The maxTotalTimeout check happens when reset is called, not by setting + * a shorter timeout - this allows progress notifications to be processed + * and the caller to handle the max total timeout exceeded condition. + * + * @param messageId - The message ID whose timeout should be reset + * @returns A result object indicating success or failure with details + */ + reset(messageId: number): TimeoutResetResult { + const info = this.#timeoutInfo.get(messageId); + if (!info || !info.resetTimeoutOnProgress) { + return { success: false }; + } + + const elapsed = Date.now() - info.startTime; + + // Check if max total timeout has been exceeded + if (info.maxTotalTimeout !== undefined && elapsed >= info.maxTotalTimeout) { + return { + success: false, + maxTotalTimeoutExceeded: { + elapsed, + maxTotalTimeout: info.maxTotalTimeout + } + }; + } + + // Reset to the original timeout duration + clearTimeout(info.timeoutId); + info.timeoutId = setTimeout(info.onTimeout, info.timeout); + + return { success: true }; + } + + /** + * Cleans up the timeout for a message (e.g., when a response is received). + * + * @param messageId - The message ID whose timeout should be cleaned up + */ + cleanup(messageId: number): void { + const info = this.#timeoutInfo.get(messageId); + if (info) { + clearTimeout(info.timeoutId); + this.#timeoutInfo.delete(messageId); + } + } + + /** + * Gets the timeout info for a message. + * + * @param messageId - The message ID + * @returns The timeout info or undefined if not found + */ + get(messageId: number): TimeoutInfo | undefined { + return this.#timeoutInfo.get(messageId); + } + + /** + * Checks if a timeout exists for a message. + * + * @param messageId - The message ID + * @returns true if a timeout exists + */ + has(messageId: number): boolean { + return this.#timeoutInfo.has(messageId); + } + + /** + * Gets the elapsed time for a message's timeout. + * + * @param messageId - The message ID + * @returns The elapsed time in milliseconds, or undefined if not found + */ + getElapsed(messageId: number): number | undefined { + const info = this.#timeoutInfo.get(messageId); + if (!info) { + return undefined; + } + return Date.now() - info.startTime; + } + + /** + * Clears all timeouts. + * Typically called when the connection is closed. + */ + clearAll(): void { + for (const info of this.#timeoutInfo.values()) { + clearTimeout(info.timeoutId); + } + this.#timeoutInfo.clear(); + } + + /** + * Gets the number of active timeouts. + */ + get size(): number { + return this.#timeoutInfo.size; + } +} diff --git a/packages/core/test/shared/protocol.test.ts b/packages/core/test/shared/protocol.test.ts index b5f6f40cb..91c5744a7 100644 --- a/packages/core/test/shared/protocol.test.ts +++ b/packages/core/test/shared/protocol.test.ts @@ -36,7 +36,10 @@ interface TestProtocol { _taskMessageQueue?: TaskMessageQueue; _requestResolvers: Map void>; _responseHandlers: Map void>; - _taskProgressTokens: Map; + getProgressManager(): { + getTaskProgressToken(taskId: string): number | undefined; + cleanupTaskProgressHandler(taskId: string): void; + }; _clearTaskQueue: (taskId: string, sessionId?: string) => Promise; requestTaskStore: (request: Request, authInfo: unknown) => TaskStore; // Protected task methods (exposed for testing) @@ -1034,7 +1037,7 @@ describe('Task-based execution', () => { void protocol .request(request, resultSchema, { task: { - ttl: 30000, + ttl: 30_000, pollInterval: 1000 } }) @@ -1048,7 +1051,7 @@ describe('Task-based execution', () => { params: { name: 'test-tool', task: { - ttl: 30000, + ttl: 30_000, pollInterval: 1000 } } @@ -1077,7 +1080,7 @@ describe('Task-based execution', () => { void protocol .request(request, resultSchema, { task: { - ttl: 60000 + ttl: 60_000 } }) .catch(() => { @@ -1092,7 +1095,7 @@ describe('Task-based execution', () => { customField: 'customValue' }, task: { - ttl: 60000 + ttl: 60_000 } } }), @@ -1114,7 +1117,7 @@ describe('Task-based execution', () => { const resultPromise = protocol.request(request, resultSchema, { task: { - ttl: 30000 + ttl: 30_000 } }); @@ -1204,7 +1207,7 @@ describe('Task-based execution', () => { void protocol .request(request, resultSchema, { task: { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }, relatedTask: { @@ -1232,7 +1235,7 @@ describe('Task-based execution', () => { expect(queuedMessage.message.params).toMatchObject({ name: 'test-tool', task: { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }, _meta: { @@ -1269,7 +1272,7 @@ describe('Task-based execution', () => { protocol.setRequestHandler('tools/call', async request => { // Tool implementor can access task creation parameters from request.params.task expect(request.params.task).toEqual({ - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }); return { content: [{ type: 'text', text: 'success' }] }; @@ -1283,7 +1286,7 @@ describe('Task-based execution', () => { name: 'test', arguments: {}, task: { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 } } @@ -1315,7 +1318,7 @@ describe('Task-based execution', () => { const task2 = await mockTaskStore.createTask( { - ttl: 60000, + ttl: 60_000, pollInterval: 1000 }, 2, @@ -1361,7 +1364,7 @@ describe('Task-based execution', () => { { taskId: task2.taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: expect.any(String), lastUpdatedAt: expect.any(String), pollInterval: 1000 @@ -1492,7 +1495,7 @@ describe('Task-based execution', () => { expect(sentMessage.jsonrpc).toBe('2.0'); expect(sentMessage.id).toBe(4); expect(sentMessage.error).toBeDefined(); - expect(sentMessage.error.code).toBe(-32602); // InvalidParams error code + expect(sentMessage.error.code).toBe(-32_602); // InvalidParams error code expect(sentMessage.error.message).toContain('Failed to list tasks'); expect(sentMessage.error.message).toContain('Invalid cursor'); }); @@ -1552,7 +1555,7 @@ describe('Task-based execution', () => { { taskId: 'task-11', status: 'working', - ttl: 30000, + ttl: 30_000, createdAt: '2024-01-01T00:00:00Z', lastUpdatedAt: '2024-01-01T00:00:00Z', pollInterval: 1000 @@ -1671,7 +1674,7 @@ describe('Task-based execution', () => { expect(sentMessage.jsonrpc).toBe('2.0'); expect(sentMessage.id).toBe(6); expect(sentMessage.error).toBeDefined(); - expect(sentMessage.error.code).toBe(-32602); // InvalidParams error code + expect(sentMessage.error.code).toBe(-32_602); // InvalidParams error code expect(sentMessage.error.message).toContain('Task not found'); }); @@ -1719,7 +1722,7 @@ describe('Task-based execution', () => { expect(sentMessage.jsonrpc).toBe('2.0'); expect(sentMessage.id).toBe(7); expect(sentMessage.error).toBeDefined(); - expect(sentMessage.error.code).toBe(-32602); // InvalidParams error code + expect(sentMessage.error.code).toBe(-32_602); // InvalidParams error code expect(sentMessage.error.message).toContain('Cannot cancel task in terminal status'); }); @@ -1737,7 +1740,7 @@ describe('Task-based execution', () => { _meta: {}, taskId: 'task-to-delete', status: 'cancelled', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString() } @@ -2137,7 +2140,7 @@ describe('Request Cancellation vs Task Cancellation', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2169,7 +2172,7 @@ describe('Request Cancellation vs Task Cancellation', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2203,7 +2206,7 @@ describe('Request Cancellation vs Task Cancellation', () => { const sendSpy = vi.spyOn(transport, 'send'); // Create a task and mark it as completed - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2275,7 +2278,7 @@ describe('Request Cancellation vs Task Cancellation', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'test/method', params: {} }); @@ -2311,7 +2314,7 @@ describe('Request Cancellation vs Task Cancellation', () => { }); // Create a task (simulating a long-running tools/call) - const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', { + const task = await taskStore.createTask({ ttl: 60_000 }, 'req-1', { method: 'tools/call', params: { name: 'long-running-tool', arguments: {} } }); @@ -2404,7 +2407,7 @@ describe('Progress notification support for tasks', () => { // Start a task-augmented request with progress callback void protocol .request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }) .catch(() => { @@ -2431,7 +2434,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2482,7 +2485,7 @@ describe('Progress notification support for tasks', () => { // Set up a request handler that will complete the task protocol.setRequestHandler('tools/call', async (_request, extra) => { if (extra.taskStore) { - const task = await extra.taskStore.createTask({ ttl: 60000 }); + const task = await extra.taskStore.createTask({ ttl: 60_000 }); // Simulate async work then complete the task setTimeout(async () => { @@ -2514,7 +2517,7 @@ describe('Progress notification support for tasks', () => { // Start a task-augmented request with progress callback void protocol .request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }) .catch(() => { @@ -2529,7 +2532,7 @@ describe('Progress notification support for tasks', () => { const progressToken = sentRequest.params._meta.progressToken; // Create a task in the mock store first so it exists when we try to get it later - const createdTask = await taskStore.createTask({ ttl: 60000 }, messageId, request); + const createdTask = await taskStore.createTask({ ttl: 60_000 }, messageId, request); const taskId = createdTask.taskId; // Simulate CreateTaskResult response @@ -2564,9 +2567,8 @@ describe('Progress notification support for tasks', () => { expect(progressCallback).toHaveBeenCalledTimes(1); // Verify the task-progress association was created - const taskProgressTokens = (protocol as unknown as TestProtocol)._taskProgressTokens as Map; - expect(taskProgressTokens.has(taskId)).toBe(true); - expect(taskProgressTokens.get(taskId)).toBe(progressToken); + const progressManager = (protocol as unknown as TestProtocol).getProgressManager(); + expect(progressManager.getTaskProgressToken(taskId)).toBe(progressToken); // Simulate task completion by calling through the protocol's task store // This will trigger the cleanup logic @@ -2578,7 +2580,7 @@ describe('Progress notification support for tasks', () => { await new Promise(resolve => setTimeout(resolve, 50)); // Verify the association was cleaned up - expect(taskProgressTokens.has(taskId)).toBe(false); + expect(progressManager.getTaskProgressToken(taskId)).toBeUndefined(); // Try to send progress notification after task completion - should be ignored progressCallback.mockClear(); @@ -2630,7 +2632,7 @@ describe('Progress notification support for tasks', () => { }); void protocol.request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }); @@ -2648,7 +2650,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2671,7 +2673,7 @@ describe('Progress notification support for tasks', () => { params: { taskId, status: 'failed', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString(), statusMessage: 'Task failed' @@ -2728,7 +2730,7 @@ describe('Progress notification support for tasks', () => { }); void protocol.request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }); @@ -2746,7 +2748,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2766,7 +2768,7 @@ describe('Progress notification support for tasks', () => { params: { taskId, status: 'cancelled', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString(), statusMessage: 'User cancelled' @@ -2823,7 +2825,7 @@ describe('Progress notification support for tasks', () => { }); void protocol.request(request, resultSchema, { - task: { ttl: 60000 }, + task: { ttl: 60_000 }, onprogress: progressCallback }); @@ -2841,7 +2843,7 @@ describe('Progress notification support for tasks', () => { task: { taskId, status: 'working', - ttl: 60000, + ttl: 60_000, createdAt: new Date().toISOString() } } @@ -2895,7 +2897,7 @@ describe('Progress notification support for tasks', () => { void protocol.request(request, resultSchema, { task: { - ttl: 60000 + ttl: 60_000 }, onprogress: onProgressMock }); @@ -2920,7 +2922,7 @@ describe('Progress notification support for tasks', () => { void protocol.request(request, resultSchema, { task: { - ttl: 30000 + ttl: 30_000 }, onprogress: onProgressMock }); @@ -2970,7 +2972,7 @@ describe('Progress notification support for tasks', () => { void protocol.request(request, resultSchema, { task: { - ttl: 30000 + ttl: 30_000 }, onprogress: onProgressMock }); @@ -2987,7 +2989,7 @@ describe('Progress notification support for tasks', () => { task: { taskId: 'task-123', status: 'working', - ttl: 30000, + ttl: 30_000, createdAt: new Date().toISOString() } } @@ -3075,7 +3077,7 @@ describe('Message interception for task-related notifications', () => { await server.connect(transport); // Create a task first - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a notification with related task metadata await server.notification( @@ -3139,7 +3141,7 @@ describe('Message interception for task-related notifications', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Fill the queue to max capacity (100 messages) for (let i = 0; i < 100; i++) { @@ -3219,7 +3221,7 @@ describe('Message interception for task-related notifications', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send multiple notifications for (let i = 0; i < 5; i++) { @@ -3261,7 +3263,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task first - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata (don't await - we're testing queuing) const requestPromise = server.request( @@ -3352,7 +3354,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata const requestPromise = server.request( @@ -3405,7 +3407,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata const requestPromise = server.request( @@ -3475,7 +3477,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Send a request with related task metadata void server.request( @@ -3546,7 +3548,7 @@ describe('Message interception for task-related requests', () => { await server.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 'test-request-1', { method: 'tools/call', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 'test-request-1', { method: 'tools/call', params: {} }); // Fill the queue to max capacity (100 messages) const promises: Promise[] = []; @@ -4886,7 +4888,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue a response message without a corresponding resolver await taskMessageQueue.enqueue(task.taskId, { @@ -4931,7 +4933,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue a response with missing resolver, then a valid notification await taskMessageQueue.enqueue(task.taskId, { @@ -4972,7 +4974,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue a request without storing a resolver await taskMessageQueue.enqueue(task.taskId, { @@ -5002,7 +5004,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const requestId = 42; const resolverMock = vi.fn(); @@ -5042,7 +5044,7 @@ describe('Error handling for missing resolvers', () => { await protocol.connect(transport); // Create a task - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const testProtocol = protocol as unknown as TestProtocol; @@ -5146,7 +5148,7 @@ describe('Error handling for missing resolvers', () => { it('should not throw when processing response with missing resolver', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); await taskMessageQueue.enqueue(task.taskId, { type: 'response', @@ -5178,7 +5180,7 @@ describe('Error handling for missing resolvers', () => { it('should not throw during task cleanup with missing resolvers', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); await taskMessageQueue.enqueue(task.taskId, { type: 'request', @@ -5202,7 +5204,7 @@ describe('Error handling for missing resolvers', () => { it('should route error messages to resolvers correctly', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const requestId = 42; const resolverMock = vi.fn(); @@ -5255,7 +5257,7 @@ describe('Error handling for missing resolvers', () => { it('should log error for unknown request ID in error messages', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); // Enqueue an error message without a corresponding resolver await taskMessageQueue.enqueue(task.taskId, { @@ -5299,7 +5301,7 @@ describe('Error handling for missing resolvers', () => { it('should handle error messages with data field', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const requestId = 42; const resolverMock = vi.fn(); @@ -5348,7 +5350,7 @@ describe('Error handling for missing resolvers', () => { it('should not throw when processing error with missing resolver', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); await taskMessageQueue.enqueue(task.taskId, { type: 'error', @@ -5385,7 +5387,7 @@ describe('Error handling for missing resolvers', () => { it('should handle mixed response and error messages in queue', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const testProtocol = protocol as unknown as TestProtocol; // Set up resolvers for multiple requests @@ -5471,7 +5473,7 @@ describe('Error handling for missing resolvers', () => { it('should maintain FIFO order when processing responses and errors', async () => { await protocol.connect(transport); - const task = await taskStore.createTask({ ttl: 60000 }, 1, { method: 'test', params: {} }); + const task = await taskStore.createTask({ ttl: 60_000 }, 1, { method: 'test', params: {} }); const testProtocol = protocol as unknown as TestProtocol; const callOrder: number[] = []; @@ -5495,7 +5497,7 @@ describe('Error handling for missing resolvers', () => { message: { jsonrpc: '2.0', id: 2, - error: { code: -32600, message: 'Error' } + error: { code: -32_600, message: 'Error' } }, timestamp: 2000 });