@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
740 lines (611 loc) • 27.9 kB
Markdown
# Commander Architecture
> Documentação técnica da arquitetura do `@darksnow-ui/commander`
## Visão Geral
O **Commander** é um sistema de gerenciamento de comandos enterprise-grade para aplicações React. Implementa o padrão Command com funcionalidades avançadas como validação de input, sistema de eventos, busca inteligente e histórico de execução.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ @darksnow-ui/commander │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ React │ │ Commander │ │ Builder │ │ Errors │ │
│ │ Hooks │───▶│ Core │◀───│ Pattern │ │ System │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Types & Interfaces │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Estrutura de Arquivos
```
src/
├── commander.ts # Classe principal Commander
├── builder.ts # CommandBuilder e templates
├── errors.ts # Classes de erro customizadas
├── types.ts # Tipos e interfaces TypeScript
├── utils.ts # Funções utilitárias
├── index.ts # Exports públicos
│
├── hooks/
│ ├── index.ts # Re-exports dos hooks
│ ├── context.tsx # CommanderProvider e useCommander
│ ├── useCommand.ts # Hook principal para executar comandos
│ ├── useCustomCommand.ts # Hook para registrar comandos temporários
│ └── useInvoker.ts # Hook simplificado para invocação
│
└── __tests__/
├── commander.test.ts
├── builder.test.ts
├── validation.test.ts
├── utils.test.ts
└── ...hooks tests
```
## Core: Commander Class
### Estruturas de Dados
```typescript
class Commander {
// Armazenamento principal - Map para O(1) lookup
protected _commands: Map<CommandKey, Command> = new Map();
// Sistema de eventos Pub/Sub
protected listeners: Map<string, EventListener[]> = new Map();
// Histórico de execuções (buffer circular)
protected executionHistory: CommandExecutionContext[] = [];
// Comandos recentes (LRU cache)
protected recentCommands: CommandKey[] = [];
// Configurações
public maxHistorySize = 100;
public maxRecentSize = 10;
}
```
### Decisões de Design
| Estrutura | Escolha | Justificativa |
|-----------|---------|---------------|
| Commands | `Map<CommandKey, Command>` | O(1) lookup por chave |
| Listeners | `Map<string, EventListener[]>` | Múltiplos listeners por evento |
| History | `Array` (circular) | Memória limitada, FIFO natural |
| Recent | `Array` (LRU) | Ordem de acesso importa |
## Pipeline de Execução
### Fluxo do `invoke()`
```
invoke(key, input, source)
│
├── 1. COMMAND LOOKUP ─────────────────────────────────── O(1)
│ └── Map.get(key)
│ └── Throw CommandNotFoundError if missing
│
├── 2. AVAILABILITY CHECK ─────────────────────────────── O(1) ou async
│ └── command.when() → boolean
│ └── Throw CommandUnavailableError if false
│
├── 3. INPUT VALIDATION ───────────────────────────────── O(1) ou async
│ └── command.inputValidator(input) → true | errors[]
│ └── Throw InputValidationError if errors
│ └── Emit "command:validation-error" event
│
├── 4. BUILD CONTEXT ──────────────────────────────────── O(1)
│ └── { command, input, startTime, source }
│
├── 5. EMIT "command:executing" ───────────────────────── O(n listeners)
│
├── 6. EXECUTE WITH TIMEOUT ───────────────────────────── O(handler)
│ └── Promise.race([handler, timeout])
│ └── Throw CommandTimeoutError if exceeded
│
├── 7. TRACK EXECUTION ────────────────────────────────── O(1)
│ └── Add to history (circular buffer)
│ └── Update recent commands (LRU)
│
├── 8. EMIT "command:completed" ───────────────────────── O(n listeners)
│
└── 9. RETURN RESULT ──────────────────────────────────── O(1)
```
### Diagrama de Sequência
```
┌──────────┐ ┌───────────┐ ┌─────────┐ ┌─────────┐
│ Client │ │ Commander │ │ Command │ │ Handler │
└────┬─────┘ └─────┬─────┘ └────┬────┘ └────┬────┘
│ │ │ │
│ invoke(key,in) │ │ │
│────────────────▶│ │ │
│ │ │ │
│ │ get(key) │ │
│ │───────────────▶│ │
│ │ │ │
│ │ when() │ │
│ │───────────────▶│ │
│ │◀───────────────│ │
│ │ │ │
│ │ inputValidator() │
│ │───────────────▶│ │
│ │◀───────────────│ │
│ │ │ │
│ │ emit("executing") │
│ │─ ─ ─ ─ ─ ─ ─ ─▶│ │
│ │ │ │
│ │ handle(input) │ │
│ │───────────────────────────────▶│
│ │ │ │
│ │◀───────────────────────────────│
│ │ │ result │
│ │ │ │
│ │ trackExecution() │
│ │─ ─ ─ ─ ─ ─ ─ ─▶│ │
│ │ │ │
│ │ emit("completed") │
│ │─ ─ ─ ─ ─ ─ ─ ─▶│ │
│ │ │ │
│◀────────────────│ │ │
│ result │ │ │
│ │ │ │
```
## Sistema de Tipos
### Hierarquia de Tipos
```typescript
// Tipos base (branded types para type safety)
type CommandKey = string & { __brand: "CommandKey" };
type CommandCategory = "system" | "file" | "edit" | "view" | "debug" | "tools" | "custom";
// Interface principal do comando
interface Command<TInput = any, TOutput = any> {
// Identificação (required)
key: CommandKey;
label: string;
handle: (input?: TInput) => Promise<TOutput>;
// Metadados (optional)
description?: string;
category?: CommandCategory;
owner?: string;
tags?: string[];
icon?: string;
shortcut?: string;
searchKeywords?: string[];
priority?: number;
timeout?: number;
// Comportamento (optional)
when?: () => boolean | Promise<boolean>;
inputValidator?: InputValidator<TInput>;
}
// Validação de input
interface ValidationErrorDetail {
path: string; // "email", "user.name", "items[0].id"
message: string; // "Email is required"
code?: string; // "required", "type", "format"
expected?: unknown;
received?: unknown;
}
type ValidationResult = true | ValidationErrorDetail[];
type InputValidator<T> = (input: T | undefined) => ValidationResult | Promise<ValidationResult>;
```
### Diagrama de Tipos
```
┌─────────────────┐
│ Command │
│ <TInput,TOut> │
└────────┬────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CommandKey │ │ InputValidator │ │ CommandCategory│
│ (branded) │ │ <TInput> │ │ (union) │
└─────────────────┘ └────────┬────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ValidationResult │
│ true | errors[] │
└────────┬────────┘
│
▼
┌─────────────────┐
│ValidationError │
│ Detail │
└─────────────────┘
```
## Sistema de Validação
### Fluxo de Validação
```
Input → inputValidator() → ValidationResult
│
┌──────────┴──────────┐
│ │
▼ ▼
true ValidationErrorDetail[]
│ │
▼ ▼
Continue to Throw InputValidationError
handler │
▼
Emit "command:validation-error"
```
### InputValidationError
```typescript
class InputValidationError extends CommandError {
errors: ValidationErrorDetail[];
input?: unknown;
// Helper methods
getFieldErrors(path: string): ValidationErrorDetail[];
getRequiredErrors(): ValidationErrorDetail[];
getMissingFields(): string[];
hasFieldError(path: string): boolean;
toJSON(): object;
}
```
### Integração com Bibliotecas
```typescript
// Zod
inputValidator: (input) => {
const result = schema.safeParse(input);
if (result.success) return true;
return result.error.issues.map(i => ({
path: i.path.join('.'),
message: i.message,
code: i.code
}));
}
// Yup
inputValidator: async (input) => {
try {
await schema.validate(input, { abortEarly: false });
return true;
} catch (err) {
return err.inner.map(e => ({
path: e.path,
message: e.message,
code: e.type
}));
}
}
// AJV (JSON Schema)
inputValidator: (input) => {
const valid = ajv.validate(schema, input);
if (valid) return true;
return ajv.errors.map(e => ({
path: e.instancePath.slice(1).replace(/\//g, '.'),
message: e.message,
code: e.keyword
}));
}
```
## Sistema de Eventos
### Eventos Disponíveis
| Evento | Payload | Quando |
|--------|---------|--------|
| `command:added` | `Command` | Comando registrado |
| `command:removed` | `Command` | Comando removido |
| `command:executing` | `CommandExecutionContext` | Antes da execução |
| `command:completed` | `CommandExecutionContext, result` | Execução bem-sucedida |
| `command:failed` | `CommandExecutionContext, error` | Execução falhou |
| `command:error` | `CommandKey, error` | Erro geral (not found, unavailable) |
| `command:validation-error` | `CommandKey, InputValidationError` | Validação falhou |
| `history:cleared` | - | Histórico limpo |
### Arquitetura Pub/Sub
```
┌─────────────────────────────────────────────────────────────┐
│ Event System │
├─────────────────────────────────────────────────────────────┤
│ │
│ listeners: Map<string, EventListener[]> │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ "command:added" │ │"command:executing"│ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ [listener1] │ │ [listener1] │ │
│ │ [listener2] │ │ [listener2] │ │
│ │ [listener3] │ │ [listener3] │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ emit(event, ...args) │
│ └── Promise.allSettled(listeners.map(l => l.callback())) │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Opções de Listener
```typescript
// Listener permanente
const listener = commander.listen("command:completed", (ctx, result) => {
analytics.track("command_executed", { key: ctx.command.key });
});
// Listener único (auto-remove após primeira execução)
commander.listen("command:added", handler, { once: true });
// Remover listener
commander.removeListener(listener);
```
## Sistema de Busca
### Algoritmo de Scoring
```
Para cada comando que passa nos filtros:
│
├── Label exact match ──────────────── +100 pontos
├── Label starts with ─────────────── +80 pontos
├── Label contains ────────────────── +60 pontos
├── Description match ─────────────── +40 pontos
├── Tags match ────────────────────── +30 pontos
├── SearchKeywords match ──────────── +50 pontos
├── Fuzzy match (Levenshtein) ─────── +1-30 pontos
│
└── Sort by: score (desc) → priority (desc)
```
### Complexidade
| Operação | Complexidade | Notas |
|----------|--------------|-------|
| `search(query)` | O(n × m) | n = comandos, m = termos |
| `getAllAvailable()` | O(n) | Com availability check |
| `getCommandsByCategory()` | O(n) | Agrupa por categoria |
## React Hooks
### Hierarquia de Hooks
```
┌─────────────────────┐
│ CommanderProvider │
│ (Context Provider) │
└──────────┬──────────┘
│
┌──────────┴──────────┐
│ useCommander() │
│ (Context Consumer) │
└──────────┬──────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ useCommand │ │ useCustomCommand│ │ useInvoker │
│ (Execution) │ │ (Registration) │ │ (Simplified) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### useCommand - API Completa
```typescript
interface CommandInvoker<TInput, TOutput> {
// Estado
exists: boolean;
isAvailable: boolean;
isLoading: boolean;
lastResult: TOutput | null;
lastError: Error | null;
lastInput: TInput | null;
lastExecution: Date | null;
executionCount: number;
command: Command | null;
// Métodos de execução
invoke(input?, options?): Promise<TOutput>;
attempt(input?, options?): Promise<ExecutionResult<TOutput>>;
execute(input?, callbacks?): Promise<TOutput | null>;
// Verificações
canInvoke(input?): Promise<boolean>;
validateInput(input?): boolean | string;
// Utilitários
reset(): void;
clearError(): void;
refresh(): void;
}
```
### Opções do useCommand
```typescript
interface UseCommandOptions<TInput, TOutput> {
// Execução
source?: "palette" | "shortcut" | "api";
throwOnError?: boolean;
timeout?: number;
// Retry & Rate Limiting
retry?: number;
retryDelay?: number | ((attempt: number) => number);
debounce?: number;
throttle?: number;
// Input handling
defaultInput?: Partial<TInput>;
validateInput?: (input?: TInput) => boolean | string;
transformInput?: (input?: TInput) => TInput;
// Output handling
transformOutput?: (output: TOutput) => TOutput;
// Callbacks
onSuccess?: (result: TOutput, input?: TInput) => void;
onError?: (error: Error, input?: TInput) => void;
onFinally?: (input?: TInput) => void;
// Estado
resetOnKeyChange?: boolean;
}
```
## Builder Pattern
### CommandBuilder API
```typescript
CommandBuilder.create<TInput, TOutput>("command:key")
.label("Command Label") // Required
.description("Description") // Optional
.category("tools") // Optional
.owner("my-module") // Optional
.tags("tag1", "tag2") // Optional
.icon("icon-name") // Optional
.shortcut("ctrl+shift+p") // Optional
.searchKeywords("keyword1", "kw2") // Optional
.priority(10) // Optional
.timeout(5000) // Optional
.when(() => isFeatureEnabled) // Optional
.inputValidator(validator) // Optional
.handle(async (input) => result) // Required
.build();
```
### Templates
```typescript
// Comandos pré-configurados por categoria
CommandTemplate.system() // category: "system", owner: "system", priority: 10
CommandTemplate.file() // category: "file", tags: ["file"], priority: 5
CommandTemplate.debug() // category: "debug", when: () => isDev, priority: 15
CommandTemplate.view() // category: "view", tags: ["view", "ui"], priority: 3
CommandTemplate.tools() // category: "tools", tags: ["tools", "utility"], priority: 8
CommandTemplate.custom() // category: "custom", owner: "temporary", priority: 1
```
## Hierarquia de Erros
```
Error
└── CommandError
├── CommandNotFoundError # Comando não existe
├── CommandUnavailableError # when() retornou false
├── CommandTimeoutError # Timeout excedido
├── CommandExecutionError # Erro durante handle()
└── InputValidationError # Validação de input falhou
```
### Factory de Erros
```typescript
createCommandError(type, command, details?)
// Tipos:
// "not-found" → CommandNotFoundError
// "unavailable" → CommandUnavailableError
// "timeout" → CommandTimeoutError(command, details.timeout)
// "execution" → CommandExecutionError(command, details.error)
// "validation" → InputValidationError(command, details.errors, details.input)
```
### Type Guards
```typescript
isInputValidationError(error) // error is InputValidationError
isCommandError(error) // error is CommandError
```
## Performance
### Análise de Complexidade
| Operação | Time | Space | Notas |
|----------|------|-------|-------|
| `add(command)` | O(1) | O(1) | Map.set |
| `remove(key)` | O(1) | O(1) | Map.delete |
| `has(key)` | O(1) | O(1) | Map.has |
| `getCommand(key)` | O(1) | O(1) | Map.get |
| `invoke(key)` | O(1) + handler | O(1) | Lookup + execute |
| `search(query)` | O(n × m) | O(n) | n=commands, m=terms |
| `getAvailableCommands()` | O(n) | O(n) | Async availability |
| `getCommandsByCategory()` | O(n) | O(n) | Grouping |
### Otimizações
1. **Lazy Evaluation**
- Availability checks só quando necessário
- Event emission só se há listeners
- History tracking com buffer limitado
2. **Memory Management**
- History circular (max 100 entries)
- Recent commands LRU (max 10 entries)
- Listeners cleanup automático para `once`
3. **Search Optimizations**
- Early filtering antes do scoring
- Limit results para parar busca
- Normalização de termos com cache
## Boas Práticas
### Nomenclatura de Commands
```typescript
// Padrão: <domain>:<action> ou <domain>:<entity>:<action>
"file:save"
"file:open"
"user:create"
"user:profile:update"
"admin:users:delete"
```
### Organização por Owner
```typescript
// Componente registra seus comandos
useCustomCommand({
key: "editor:format",
label: "Format Document",
owner: "editor-component", // ← identificação
handle: async () => formatDocument()
});
// Cleanup automático quando componente desmonta
// ou manual:
commander.removeByOwner("editor-component");
```
### Validação de Input
```typescript
// Sempre valide inputs críticos
commander.add({
key: "payment:process",
label: "Process Payment",
inputValidator: (input) => {
const errors = [];
if (!input?.amount || input.amount <= 0) {
errors.push({ path: "amount", message: "Invalid amount", code: "invalid" });
}
if (!input?.cardToken) {
errors.push({ path: "cardToken", message: "Card required", code: "required" });
}
return errors.length ? errors : true;
},
handle: async (input) => processPayment(input)
});
```
### Error Handling
```typescript
// Usando attempt() para não lançar exceção
const result = await commander.attempt("risky:command", input);
if (!result.success) {
if (isInputValidationError(result.error)) {
showValidationErrors(result.error.errors);
} else {
showGenericError(result.error);
}
}
// Ou com try/catch
try {
await commander.invoke("risky:command", input);
} catch (error) {
if (isInputValidationError(error)) {
// Campos faltando - pode redirecionar para coletar
const missing = error.getMissingFields();
await collectMissingFields(missing);
}
}
```
## Roadmap: Middleware System
> Em desenvolvimento - veja `PROPOSAL-COMMANDER-INTERCEPTOR.md`
### Arquitetura Planejada
```
invoke(key, input, source)
│
├── 1. Command Lookup
├── 2. Build Context
│
├── 3. MIDDLEWARE PIPELINE ─────────────────────────── NOVO
│ ├── Global middlewares
│ ├── Category middlewares
│ ├── Pattern middlewares ("file:*")
│ ├── Specific middlewares ("file:save")
│ └── Custom filter middlewares
│
├── 4. Availability Check
├── 5. Input Validation
├── 6. Execute Handler
└── 7. Return Result
```
### API Planejada
```typescript
// Global
commander.use(loggingMiddleware);
// Por categoria
commander.use("admin", authMiddleware);
// Por pattern
commander.use("file:*", auditMiddleware);
// Por comando
commander.use("user:delete", confirmMiddleware);
// Por filtro
commander.use(cmd => cmd.tags?.includes("dangerous"), confirmMiddleware);
// Arrays
commander.use(["user:delete", "data:purge"], [authMiddleware, confirmMiddleware]);
```
## Referências
- [README.md](../README.md) - Documentação de uso
- [useInvoker-guide.md](./useInvoker-guide.md) - Guia detalhado de hooks
- [Middleware Proposal](./proposals/PROPOSAL-COMMANDER-INTERCEPTOR.md) - Proposta de middleware system
*Última atualização: 2025-12-03*
*Versão: 1.0.5*