/** * Operation Dispatcher * * Generic dispatcher pattern to extract operation routing logic from adapters. * Reduces boilerplate by centralizing switch/case logic into a reusable component. * * **Why this exists:** * Every adapter has similar `switch (operation.type)` logic that routes to handler functions. * This dispatcher extracts that pattern into a reusable, type-safe component. * * **Benefits:** * - Reduces adapter boilerplate by ~30-50 lines per adapter * - Consistent error handling for unsupported operations * - Easier to test handlers in isolation * - Self-documenting (getSupportedOperations() lists what's available) * - Type-safe operation routing with generics * * @example Basic usage in an adapter * ```typescript * import { OperationDispatcher } from "@wavespec/kit/dispatch"; * import type { TestSpec, TestContext, TestResult } from "@wavespec/types"; * * type MyOperation = "action_a" | "action_b" | "list_actions"; * * class MyAdapter implements Adapter { * private dispatcher: OperationDispatcher; * * constructor() { * this.dispatcher = new OperationDispatcher(); * this.registerOperations(); * } * * private registerOperations(): void { * this.dispatcher.register("action_a", (params, ctx) => this.handleActionA(params)); * this.dispatcher.register("action_b", (params, ctx) => this.handleActionB(params)); * this.dispatcher.register("list_actions", (params, ctx) => this.handleListActions(params)); * } * * async run(spec: TestSpec, ctx: TestContext): Promise { * return this.dispatcher.dispatch(spec.operation as MyOperation, spec.params, ctx); * } * * // Handler methods stay clean and focused * private async handleActionA(params: any): Promise { * // Implementation... * } * } * ``` * * @example With type-safe params * ```typescript * interface ActionAParams { * name: string; * value: number; * } * * this.dispatcher.register("action_a", async (params, ctx) => { * // params is typed as ActionAParams * console.log(params.name, params.value); * return await this.handleActionA(params); * }); * ``` * * @example Error handling * ```typescript * try { * return await this.dispatcher.dispatch("unknown_op", {}, ctx); * } catch (error) { * // Throws CoachingError with UNSUPPORTED_OPERATION code * // Includes helpful hints about available operations * } * ``` */ import type { TestContext } from "@wavespec/types"; import { CoachingError } from "@wavespec/types"; /** * Operation handler function type * * Handlers receive operation params and test context, and return a result. * This is the signature that all registered operations must match. * * @template TParams - Type of params for this operation * @template TResult - Type of result returned by this operation * @param params - Operation-specific parameters from TestSpec * @param context - Test execution context (mode, timeout, cache, etc.) * @returns Promise resolving to operation result */ export type OperationHandler = ( params: TParams, context: TestContext, ) => Promise; /** * Operation Dispatcher * * Generic dispatcher for routing operation types to handler functions. * Eliminates repetitive switch/case logic in adapters. * * @template TOperation - Union type of operation strings (e.g., "tool_call" | "list_tools") * @template TResult - Type returned by handlers (typically TestResult or AdapterResponse) * * @example Creating a dispatcher * ```typescript * type MCPOperation = "tool_call" | "prompt_get" | "tools_list"; * const dispatcher = new OperationDispatcher(); * ``` * * @example Registering handlers * ```typescript * dispatcher.register("tool_call", async (params, ctx) => { * const result = await client.callTool(params.name, params.arguments); * return formatResult(result); * }); * ``` * * @example Dispatching operations * ```typescript * const result = await dispatcher.dispatch( * spec.operation as MCPOperation, * spec.params, * ctx * ); * ``` */ export class OperationDispatcher { /** * Map of operation names to handler functions * * Handlers are registered via register() and invoked via dispatch(). */ private readonly handlers = new Map>(); /** * Register an operation handler * * Associates an operation name with a handler function. * Multiple calls with the same operation name will overwrite previous registrations. * * @template TParams - Type of params for this specific operation * @param operation - Operation name to register * @param handler - Handler function for this operation * * @example Register a handler * ```typescript * dispatcher.register("tool_call", async (params, ctx) => { * return await executeToolCall(params.name, params.arguments); * }); * ``` * * @example Register with typed params * ```typescript * interface ToolCallParams { * name: string; * arguments: Record; * } * * dispatcher.register("tool_call", async (params, ctx) => { * // params is typed as ToolCallParams * return await client.callTool(params.name, params.arguments); * }); * ``` */ register( operation: TOperation, handler: OperationHandler, ): void { this.handlers.set(operation, handler as OperationHandler); } /** * Dispatch an operation to its handler * * Routes the operation to the registered handler function. * Throws CoachingError if operation is not registered. * * @param operation - Operation name to dispatch * @param params - Operation parameters from TestSpec * @param context - Test execution context * @returns Promise resolving to operation result * @throws {CoachingError} UNSUPPORTED_OPERATION if operation not registered * * @example Dispatch an operation * ```typescript * try { * const result = await dispatcher.dispatch( * spec.operation as MyOperation, * spec.params, * ctx * ); * return result; * } catch (error) { * // Handle unsupported operation error * } * ``` */ async dispatch( operation: TOperation, params: any, context: TestContext, ): Promise { const handler = this.handlers.get(operation); if (!handler) { const supportedOps = [...this.handlers.keys()]; throw new CoachingError({ code: "UNSUPPORTED_OPERATION", message: `Operation '${operation}' is not supported by this adapter`, hint: supportedOps.length > 0 ? `Supported operations: ${supportedOps.join(", ")}` : "This adapter has no operations registered", fix: supportedOps.length > 0 ? `Use one of the supported operations: ${supportedOps.slice(0, 3).join(", ")}${supportedOps.length > 3 ? "..." : ""}` : "Check adapter documentation for available operations", severity: "error", context: { operation, supportedOperations: supportedOps, }, }); } return handler(params, context); } /** * Get list of supported operations * * Returns all operation names that have been registered. * Useful for introspection and documentation. * * @returns Array of operation names * * @example List available operations * ```typescript * const operations = dispatcher.getSupportedOperations(); * console.log("Available operations:", operations); * // Output: ["tool_call", "prompt_get", "tools_list", ...] * ``` * * @example Validate operation exists * ```typescript * const ops = dispatcher.getSupportedOperations(); * if (!ops.includes(requestedOp)) { * console.error(`Operation ${requestedOp} not available`); * } * ``` */ getSupportedOperations(): TOperation[] { return [...this.handlers.keys()]; } /** * Check if an operation is registered * * Returns true if the operation has a handler registered. * * @param operation - Operation name to check * @returns True if operation is registered * * @example Check before dispatching * ```typescript * if (dispatcher.hasOperation("tool_call")) { * await dispatcher.dispatch("tool_call", params, ctx); * } * ``` */ hasOperation(operation: TOperation): boolean { return this.handlers.has(operation); } /** * Clear all registered handlers * * Removes all operation handlers. Useful for testing or reset scenarios. * * @example Reset dispatcher * ```typescript * dispatcher.clear(); * dispatcher.register("new_operation", handler); * ``` */ clear(): void { this.handlers.clear(); } /** * Get the number of registered operations * * @returns Count of registered operations * * @example Check registration count * ```typescript * console.log(`Dispatcher has ${dispatcher.size()} operations`); * ``` */ size(): number { return this.handlers.size; } }