import type { Command, CommandSchema } from './types'; import type {CommandResult, CommandRunner, CommandRunnerContext, CommandRunnerEvents, PromptEvent} from './command-runner'; import { parseCommand } from './command-parse'; import { applyCommandSchema } from './command-validate'; import { parseCommandSchema } from './schema-parse'; import {AsyncQueue} from "@/utils/async-queue"; type CanRunParsed = { runParsed(command: Command): Promise>, } type CmdFunc = (ctx: TContext, ...args: any[]) => Promise; export class CommandRegistry extends Map>{ register = CmdFunc>(schema: CommandSchema | string, run: TFunc) { const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema; registerCommand(this, { schema: parsedSchema, async run(this: CommandRunnerContext, command: Command){ const params = command.params; return await run.call(this.context, this.context, ...params); }, }); type TParams = TFunc extends (ctx: TContext, ...args: infer X) => Promise ? X : null; type TResult = TFunc extends (ctx: TContext, ...args: any[]) => Promise ? X : null; return function(ctx: TContext & CanRunParsed, ...args: TParams){ return ctx.runParsed({ options: {}, params: args, flags: {}, name: parsedSchema.name, }) as Promise>; } } } export function createCommandRegistry(): CommandRegistry { return new CommandRegistry(); } export function registerCommand( registry: CommandRegistry, runner: CommandRunner ): void { registry.set(runner.schema.name, runner as CommandRunner); } export function unregisterCommand( registry: CommandRegistry, name: string ): void { registry.delete(name); } export function hasCommand( registry: CommandRegistry, name: string ): boolean { return registry.has(name); } export function getCommand( registry: CommandRegistry, name: string ): CommandRunner | undefined { return registry.get(name); } type PromptListener = (e: PromptEvent) => void; type PromptEndListener = () => void; export type CommandRunnerContextExport = CommandRunnerContext & { registry: CommandRegistry; promptQueue: AsyncQueue; _activePrompt: PromptEvent | null; _tryCommit: (commandOrInput: Command | string) => string | null; _cancel: (reason?: string) => void; _pendingInput: string | null; }; export function createCommandRunnerContext( registry: CommandRegistry, context: TContext ): CommandRunnerContextExport { const promptListeners = new Set(); const promptEndListeners = new Set(); const emitPromptEnd = () => { for (const listener of promptEndListeners) { listener(); } }; const on = (_event: T, listener: (e: CommandRunnerEvents[T]) => void) => { if (_event === 'prompt') { promptListeners.add(listener as PromptListener); } else { promptEndListeners.add(listener as PromptEndListener); } }; const off = (_event: T, listener: (e: CommandRunnerEvents[T]) => void) => { if (_event === 'prompt') { promptListeners.delete(listener as PromptListener); } else { promptEndListeners.delete(listener as PromptEndListener); } }; let activePrompt: PromptEvent | null = null; const tryCommit = (commandOrInput: Command | string) => { if (activePrompt) { const result = activePrompt.tryCommit(commandOrInput); if (result === null) { activePrompt = null; emitPromptEnd(); } return result; } return 'No active prompt'; }; const cancel = (reason?: string) => { if (activePrompt) { activePrompt.cancel(reason); activePrompt = null; emitPromptEnd(); } }; const prompt = ( schema: CommandSchema | string, validator?: (command: Command) => string | null, currentPlayer?: string | null ): Promise => { const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema; return new Promise((resolve, reject) => { const tryCommit = (commandOrInput: Command | string) => { const command = typeof commandOrInput === 'string' ? parseCommand(commandOrInput) : commandOrInput; const schemaResult = applyCommandSchema(command, resolvedSchema); if (!schemaResult.valid) { return schemaResult.errors.join('; '); } const error = validator?.(schemaResult.command); if (error) return error; resolve(schemaResult.command); return null; }; const cancel = (reason?: string) => { activePrompt = null; emitPromptEnd(); reject(new Error(reason ?? 'Cancelled')); }; activePrompt = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel }; const event: PromptEvent = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel }; for (const listener of promptListeners) { listener(event); } }); }; const runnerCtx: CommandRunnerContextExport = { registry, context, run: (input: string) => runCommandWithContext(runnerCtx, input) as Promise>, runParsed: (command: Command) => runCommandParsedWithContext(runnerCtx, command) as Promise>, prompt, on, off, _activePrompt: null, _tryCommit: tryCommit, _cancel: cancel, _pendingInput: null, promptQueue: null! }; Object.defineProperty(runnerCtx, '_activePrompt', { get: () => activePrompt, }); let promptQueue: AsyncQueue; Object.defineProperty(runnerCtx, 'promptQueue', { get(){ if (!promptQueue) { promptQueue = new AsyncQueue(); promptListeners.add(async (event) => { promptQueue.push(event); }); } return promptQueue; } }); return runnerCtx; } async function executeWithRunnerContext( runnerCtx: CommandRunnerContextExport, runner: CommandRunner, command: Command ): Promise { try { const result = await runner.run.call(runnerCtx, command); return { success: true, result }; } catch (e) { const error = e as Error; return { success: false, error: error.message }; } } export async function runCommand( registry: CommandRegistry, context: TContext, input: string ): Promise { const runnerCtx = createCommandRunnerContext(registry, context); return await runCommandWithContext(runnerCtx, input); } async function runCommandWithContext( runnerCtx: CommandRunnerContextExport, input: string ): Promise<{ success: true; result: unknown } | { success: false; error: string }> { const command = parseCommand(input); return await runCommandParsedWithContext(runnerCtx, command); } export async function runCommandParsed( registry: CommandRegistry, context: TContext, command: Command ): Promise { const runnerCtx = createCommandRunnerContext(registry, context); return await runCommandParsedWithContext(runnerCtx, command); } async function runCommandParsedWithContext( runnerCtx: CommandRunnerContextExport, command: Command ): Promise { const runner = runnerCtx.registry.get(command.name); if (!runner) { return { success: false, error: `Unknown command: ${command.name}` }; } const validationResult = applyCommandSchema(command, runner.schema); if (!validationResult.valid) { return { success: false, error: validationResult.errors.join('; ') }; } return await executeWithRunnerContext(runnerCtx, runner, validationResult.command); }