@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
945 lines (769 loc) • 26.6 kB
Markdown
# Proposal: Commander Middleware System
> Sistema de middleware/interceptor para `-ui/commander` com suporte a filtros granulares.
## Visão Geral
Adicionar sistema de middleware inspirado em Express/Koa/Fastify que permite:
1. **Interceptar** comandos antes/depois da execução
2. **Cancelar** execução baseado em condições
3. **Modificar** input antes de executar
4. **Compor** middlewares reutilizáveis (logging, auth, debug, etc.)
5. **Filtrar** por comando, categoria, pattern ou função customizada
---
## API Proposta
### Registro de Middlewares
```typescript
// ============================================================================
// GLOBAL - roda em TODOS os comandos
// ============================================================================
commander.use(loggingMiddleware)
commander.use([analyticsMiddleware, errorTrackingMiddleware])
// ============================================================================
// POR CATEGORIA
// ============================================================================
commander.use("file", authMiddleware)
commander.use("admin", [authMiddleware, roleMiddleware("admin")])
// ============================================================================
// POR COMANDO ESPECÍFICO
// ============================================================================
commander.use("user:delete", confirmMiddleware)
commander.use("user:delete", [authMiddleware, confirmMiddleware, auditMiddleware])
// ============================================================================
// POR PATTERN (glob-like)
// ============================================================================
commander.use("file:*", auditMiddleware) // file:save, file:delete, etc
commander.use("admin:*", adminOnlyMiddleware) // admin:users, admin:settings
commander.use("api:v2:*", rateLimitMiddleware) // api:v2:users, api:v2:products
// ============================================================================
// POR FILTRO CUSTOMIZADO
// ============================================================================
commander.use(
(cmd) => cmd.tags?.includes("dangerous"),
confirmMiddleware
)
commander.use(
(cmd) => cmd.category === "file" && cmd.owner === "editor",
[validateMiddleware, auditMiddleware]
)
// ============================================================================
// MÚLTIPLOS MATCHERS
// ============================================================================
commander.use(["file:*", "edit:*"], loggingMiddleware)
commander.use(
["user:delete", "data:purge", "system:reset"],
[authMiddleware, confirmMiddleware]
)
// ============================================================================
// REMOÇÃO
// ============================================================================
commander.unuse(loggingMiddleware) // remove de todos os registros
commander.unuse("file:*", auditMiddleware) // remove só desse matcher
commander.clearMiddlewares() // remove todos
commander.clearMiddlewares("admin:*") // remove todos de um matcher
```
---
## Tipos
```typescript
// ============================================================================
// MIDDLEWARE CONTEXT
// ============================================================================
export interface MiddlewareContext<TInput = any, TOutput = any> {
/** O comando sendo executado */
readonly command: Command<TInput, TOutput>
/** Input original (imutável) */
readonly input: TInput
/** Input modificado (pode ser alterado pelo middleware) */
modifiedInput?: TInput
/** Fonte da execução */
readonly source: "palette" | "shortcut" | "api"
/** Timestamp do início */
readonly startTime: Date
/** Metadados extensíveis (middlewares podem adicionar dados) */
meta: Record<string, any>
/** Referência ao Commander */
readonly commander: Commander
}
// ============================================================================
// MIDDLEWARE FUNCTION
// ============================================================================
export type NextFunction<TOutput = any> = () => Promise<TOutput>
export type Middleware<TInput = any, TOutput = any> = (
ctx: MiddlewareContext<TInput, TOutput>,
next: NextFunction<TOutput>
) => Promise<TOutput | MiddlewareCancelResult>
// ============================================================================
// CANCEL RESULT
// ============================================================================
export interface MiddlewareCancelResult {
readonly cancelled: true
readonly reason?: string
readonly data?: any
}
export function isCancelled(result: any): result is MiddlewareCancelResult {
return result && typeof result === "object" && result.cancelled === true
}
// ============================================================================
// MATCHER TYPES
// ============================================================================
export type MiddlewareMatcher =
| "*" // global
| CommandCategory // "file", "edit", "admin"
| CommandKey // "file:save"
| `${string}:*` // pattern "file:*"
| ((command: Command) => boolean) // função customizada
export type MatcherInput = MiddlewareMatcher | MiddlewareMatcher[]
export type MiddlewareInput = Middleware | Middleware[]
// ============================================================================
// INTERNAL ENTRY
// ============================================================================
interface MiddlewareEntry {
id: string
matcher: MiddlewareMatcher
middleware: Middleware
priority: number // para ordenação
}
```
---
## Ordem de Execução
Quando `invoke("file:save", input)` é chamado:
```
┌─────────────────────────────────────────────────────────────────┐
│ MIDDLEWARE PIPELINE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. GLOBAL middlewares (ordem de registro) │
│ └── loggingMiddleware │
│ └── analyticsMiddleware │
│ │
│ 2. CATEGORY middlewares ("file") │
│ └── authMiddleware │
│ │
│ 3. PATTERN middlewares ("file:*") │
│ └── auditMiddleware │
│ │
│ 4. SPECIFIC middlewares ("file:save") │
│ └── validateMiddleware │
│ │
│ 5. CUSTOM FILTER middlewares (que matcham) │
│ └── (middlewares cujo filtro retorna true) │
│ │
│ 6. COMMAND HANDLER │
│ └── command.handle(finalInput) │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Prioridade de matching (do mais genérico ao mais específico):**
1. `"*"` (global)
2. `CommandCategory` ("file", "admin")
3. `Pattern` ("file:*")
4. `CommandKey` ("file:save")
5. `Function` (filtros customizados)
Dentro de cada grupo, executa na **ordem de registro**.
---
## Implementação
### Commander Class (modificações)
```typescript
export default class Commander {
// ... propriedades existentes ...
/** Middleware entries */
private middlewareEntries: MiddlewareEntry[] = []
private middlewareIdCounter = 0
// ==========================================================================
// MIDDLEWARE REGISTRATION
// ==========================================================================
/**
* Registra middleware(s) com matcher opcional
*/
use(middleware: MiddlewareInput): this
use(matcher: MatcherInput, middleware: MiddlewareInput): this
use(
matcherOrMiddleware: MatcherInput | MiddlewareInput,
middleware?: MiddlewareInput
): this {
const matchers = middleware
? this.toArray(matcherOrMiddleware as MatcherInput)
: ["*" as const]
const middlewares = middleware
? this.toArray(middleware)
: this.toArray(matcherOrMiddleware as MiddlewareInput)
for (const matcher of matchers) {
for (const mw of middlewares) {
this.middlewareEntries.push({
id: `mw_${++this.middlewareIdCounter}`,
matcher,
middleware: mw,
priority: this.getMatcherPriority(matcher),
})
}
}
this.emit("middleware:added", { matchers, middlewares })
return this
}
/**
* Remove middleware(s)
*/
unuse(middleware: Middleware): this
unuse(matcher: MatcherInput, middleware: Middleware): this
unuse(
matcherOrMiddleware: MatcherInput | Middleware,
middleware?: Middleware
): this {
if (middleware) {
// Remove de matchers específicos
const matchers = this.toArray(matcherOrMiddleware as MatcherInput)
this.middlewareEntries = this.middlewareEntries.filter(
(entry) =>
entry.middleware !== middleware ||
!matchers.some((m) => this.matchersEqual(m, entry.matcher))
)
} else {
// Remove de todos os registros
const mw = matcherOrMiddleware as Middleware
this.middlewareEntries = this.middlewareEntries.filter(
(entry) => entry.middleware !== mw
)
}
return this
}
/**
* Remove todos os middlewares (opcionalmente de um matcher específico)
*/
clearMiddlewares(matcher?: MatcherInput): this {
if (matcher) {
const matchers = this.toArray(matcher)
this.middlewareEntries = this.middlewareEntries.filter(
(entry) => !matchers.some((m) => this.matchersEqual(m, entry.matcher))
)
} else {
this.middlewareEntries = []
}
return this
}
/**
* Retorna middlewares registrados
*/
getMiddlewares(matcher?: MiddlewareMatcher): MiddlewareEntry[] {
if (matcher) {
return this.middlewareEntries.filter((e) =>
this.matchersEqual(matcher, e.matcher)
)
}
return [...this.middlewareEntries]
}
// ==========================================================================
// MIDDLEWARE MATCHING
// ==========================================================================
/**
* Retorna prioridade do matcher (menor = executa primeiro)
*/
private getMatcherPriority(matcher: MiddlewareMatcher): number {
if (matcher === "*") return 0
if (typeof matcher === "function") return 50
if (matcher.endsWith(":*")) return 20
if (["system", "file", "edit", "view", "debug", "tools", "custom"].includes(matcher)) return 10
return 30 // comando específico
}
/**
* Verifica se comando casa com matcher
*/
private matchesCommand(matcher: MiddlewareMatcher, command: Command): boolean {
if (matcher === "*") return true
if (typeof matcher === "function") {
try {
return matcher(command)
} catch {
return false
}
}
// Pattern: "file:*"
if (matcher.endsWith(":*")) {
const prefix = matcher.slice(0, -1) // "file:"
return command.key.startsWith(prefix)
}
// Category match
if (command.category === matcher) return true
// Exact key match
return command.key === matcher
}
/**
* Compara dois matchers
*/
private matchersEqual(a: MiddlewareMatcher, b: MiddlewareMatcher): boolean {
if (typeof a === "function" || typeof b === "function") {
return a === b // referência
}
return a === b
}
/**
* Coleta middlewares que se aplicam ao comando
*/
private getMiddlewaresForCommand(command: Command): Middleware[] {
return this.middlewareEntries
.filter((entry) => this.matchesCommand(entry.matcher, command))
.sort((a, b) => a.priority - b.priority)
.map((entry) => entry.middleware)
}
// ==========================================================================
// EXECUTION WITH MIDDLEWARE
// ==========================================================================
async invoke<T = any>(
key: CommandKey,
input?: any,
source: "palette" | "shortcut" | "api" = "api"
): Promise<T> {
const command = this._commands.get(key)
if (!command) {
const error = new CommandNotFoundError(key)
this.emit("command:error", key, error)
throw error
}
// Build context
const ctx: MiddlewareContext = {
command,
input,
source,
startTime: new Date(),
meta: {},
commander: this,
}
this.emit("command:executing", ctx)
try {
// Get applicable middlewares
const middlewares = this.getMiddlewaresForCommand(command)
// Execute through pipeline
const result = await this.executePipeline<T>(ctx, middlewares)
// Handle cancellation
if (isCancelled(result)) {
this.emit("command:cancelled", ctx, result)
return result as T
}
// Track successful execution
this.trackExecution(ctx, result)
this.addToRecent(key)
this.emit("command:completed", ctx, result)
return result
} catch (error) {
const executionError =
error instanceof Error
? new CommandExecutionError(key, error)
: new CommandExecutionError(key, new Error(String(error)))
this.emit("command:failed", ctx, executionError)
throw executionError
}
}
/**
* Executa o pipeline de middlewares (Koa-style compose)
*/
private async executePipeline<T>(
ctx: MiddlewareContext,
middlewares: Middleware[]
): Promise<T> {
let index = 0
const dispatch = async (): Promise<T> => {
if (index < middlewares.length) {
const middleware = middlewares[index++]
return middleware(ctx, dispatch) as Promise<T>
}
// End of pipeline: execute command
return this.executeCommand<T>(ctx)
}
return dispatch()
}
/**
* Execução real do comando (após middlewares)
*/
private async executeCommand<T>(ctx: MiddlewareContext): Promise<T> {
const { command } = ctx
// Availability check
if (!(await this.isCommandAvailable(command))) {
throw new CommandUnavailableError(command.key)
}
// Use modified input if available
const finalInput = ctx.modifiedInput ?? ctx.input
// Execute with timeout
return this.executeWithTimeout<T>(command, finalInput)
}
// ==========================================================================
// HELPERS
// ==========================================================================
private toArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value]
}
}
```
---
## Helpers de Middleware
```typescript
// src/middleware/helpers.ts
import type { Middleware, MiddlewareContext, MiddlewareCancelResult } from "../types"
/**
* Cria middleware que executa ANTES do comando
*/
export function before<TInput = any>(
fn: (ctx: MiddlewareContext<TInput>) => Promise<void | {
input?: TInput
cancel?: boolean
reason?: string
}>
): Middleware<TInput> {
return async (ctx, next) => {
const result = await fn(ctx)
if (result?.cancel) {
return { cancelled: true, reason: result.reason } as MiddlewareCancelResult
}
if (result?.input !== undefined) {
ctx.modifiedInput = result.input
}
return next()
}
}
/**
* Cria middleware que executa DEPOIS do comando
*/
export function after<TInput = any, TOutput = any>(
fn: (ctx: MiddlewareContext<TInput, TOutput>, result: TOutput) => Promise<TOutput | void>
): Middleware<TInput, TOutput> {
return async (ctx, next) => {
const result = await next()
const modified = await fn(ctx, result)
return modified ?? result
}
}
/**
* Cria middleware condicional
*/
export function when(
condition: (ctx: MiddlewareContext) => boolean | Promise<boolean>,
middleware: Middleware
): Middleware {
return async (ctx, next) => {
if (await condition(ctx)) {
return middleware(ctx, next)
}
return next()
}
}
/**
* Compõe múltiplos middlewares em um
*/
export function compose(...middlewares: Middleware[]): Middleware {
return async (ctx, next) => {
let index = 0
const dispatch = async (): Promise<any> => {
if (index < middlewares.length) {
const mw = middlewares[index++]
return mw(ctx, dispatch)
}
return next()
}
return dispatch()
}
}
/**
* Middleware de logging
*/
export function logging(
logger: (msg: string, data?: any) => void = console.log
): Middleware {
return async (ctx, next) => {
const start = Date.now()
logger(`[Commander] → ${ctx.command.key}`, { input: ctx.input, source: ctx.source })
try {
const result = await next()
logger(`[Commander] ✓ ${ctx.command.key} (${Date.now() - start}ms)`)
return result
} catch (error) {
logger(`[Commander] ✗ ${ctx.command.key} (${Date.now() - start}ms)`, { error })
throw error
}
}
}
/**
* Middleware de analytics
*/
export function analytics(
tracker: (event: string, data: Record<string, any>) => void
): Middleware {
return async (ctx, next) => {
const start = Date.now()
try {
const result = await next()
tracker("command:success", {
key: ctx.command.key,
category: ctx.command.category,
duration: Date.now() - start,
source: ctx.source,
})
return result
} catch (error) {
tracker("command:error", {
key: ctx.command.key,
category: ctx.command.category,
duration: Date.now() - start,
source: ctx.source,
error: error instanceof Error ? error.message : String(error),
})
throw error
}
}
}
/**
* Middleware de rate limiting
*/
export function rateLimit(options: {
maxPerSecond?: number
maxPerMinute?: number
keyFn?: (ctx: MiddlewareContext) => string
}): Middleware {
const { maxPerSecond = 10, maxPerMinute = 100, keyFn = (ctx) => ctx.command.key } = options
const counters = new Map<string, { second: number[]; minute: number[] }>()
return async (ctx, next) => {
const key = keyFn(ctx)
const now = Date.now()
if (!counters.has(key)) {
counters.set(key, { second: [], minute: [] })
}
const counter = counters.get(key)!
// Clean old entries
counter.second = counter.second.filter((t) => now - t < 1000)
counter.minute = counter.minute.filter((t) => now - t < 60000)
// Check limits
if (counter.second.length >= maxPerSecond) {
return { cancelled: true, reason: "rate_limit_second" } as MiddlewareCancelResult
}
if (counter.minute.length >= maxPerMinute) {
return { cancelled: true, reason: "rate_limit_minute" } as MiddlewareCancelResult
}
// Track
counter.second.push(now)
counter.minute.push(now)
return next()
}
}
/**
* Middleware de confirmação
*/
export function confirm(
promptFn: (ctx: MiddlewareContext) => Promise<boolean>
): Middleware {
return async (ctx, next) => {
const confirmed = await promptFn(ctx)
if (!confirmed) {
return { cancelled: true, reason: "user_cancelled" } as MiddlewareCancelResult
}
return next()
}
}
/**
* Middleware de autenticação
*/
export function auth(
checkFn: () => boolean | Promise<boolean>,
onUnauthorized?: (ctx: MiddlewareContext) => void
): Middleware {
return async (ctx, next) => {
const isAuthenticated = await checkFn()
if (!isAuthenticated) {
onUnauthorized?.(ctx)
return { cancelled: true, reason: "unauthorized" } as MiddlewareCancelResult
}
return next()
}
}
/**
* Middleware de role/permission
*/
export function requireRole(
role: string | string[],
getRoles: () => string[] | Promise<string[]>
): Middleware {
const requiredRoles = Array.isArray(role) ? role : [role]
return async (ctx, next) => {
const userRoles = await getRoles()
const hasRole = requiredRoles.some((r) => userRoles.includes(r))
if (!hasRole) {
return { cancelled: true, reason: "forbidden", data: { requiredRoles } } as MiddlewareCancelResult
}
return next()
}
}
```
---
## Exemplos de Uso
### 1. Setup Básico
```typescript
import { Commander } from "@darksnow-ui/commander"
import { logging, analytics, auth, confirm } from "@darksnow-ui/commander/middleware"
const commander = new Commander()
// Global: logging em tudo
commander.use(logging())
// Global: analytics
commander.use(analytics((event, data) => {
mixpanel.track(event, data)
}))
```
### 2. Autenticação por Categoria
```typescript
// Comandos de file precisam de auth
commander.use("file", auth(
() => !!currentUser,
() => router.push("/login")
))
// Comandos admin precisam de role
commander.use("admin:*", [
auth(() => !!currentUser),
requireRole("admin", () => currentUser?.roles ?? [])
])
```
### 3. Confirmação para Ações Perigosas
```typescript
commander.use(
["user:delete", "data:purge", "system:reset"],
confirm(async (ctx) => {
return await showConfirmDialog({
title: "Confirmar ação",
message: `Deseja executar "${ctx.command.label}"?`,
confirmText: "Sim, executar",
cancelText: "Cancelar"
})
})
)
```
### 4. Debug Mode
```typescript
let debugEnabled = false
const debugMiddleware: Middleware = async (ctx, next) => {
if (!debugEnabled) return next()
if (ctx.command.key === "debug:toggle") return next()
const action = await showDebugModal({
command: ctx.command,
input: ctx.input,
})
if (action.cancel) {
return { cancelled: true, reason: "debug_cancelled" }
}
if (action.modifiedInput !== undefined) {
ctx.modifiedInput = action.modifiedInput
}
return next()
}
commander.use(debugMiddleware)
// Comando para toggle
commander.add({
key: "debug:toggle",
label: "Toggle Debug Mode",
shortcut: "alt+shift+d",
handle: async () => {
debugEnabled = !debugEnabled
return { enabled: debugEnabled }
}
})
```
### 5. Rate Limiting em APIs
```typescript
commander.use(
"api:*",
rateLimit({
maxPerSecond: 5,
maxPerMinute: 100,
})
)
```
### 6. Middleware Condicional
```typescript
import { when } from "@darksnow-ui/commander/middleware"
// Só loga comandos em dev
commander.use(
when(
() => process.env.NODE_ENV === "development",
logging()
)
)
// Confirma só comandos marcados como dangerous
commander.use(
(cmd) => cmd.tags?.includes("dangerous"),
confirm(async (ctx) => {
return await showDangerConfirm(ctx.command.label)
})
)
```
---
## React Hook: useMiddleware
```typescript
// Hook para adicionar middleware temporário (cleanup no unmount)
export function useMiddleware(
matcher: MatcherInput | MiddlewareInput,
middleware?: MiddlewareInput
) {
const { commander } = useCommander()
useEffect(() => {
if (middleware) {
commander.use(matcher as MatcherInput, middleware)
} else {
commander.use(matcher as MiddlewareInput)
}
return () => {
if (middleware) {
const middlewares = Array.isArray(middleware) ? middleware : [middleware]
middlewares.forEach((mw) => commander.unuse(matcher as MatcherInput, mw))
} else {
const middlewares = Array.isArray(matcher) ? matcher : [matcher as Middleware]
middlewares.forEach((mw) => commander.unuse(mw))
}
}
}, [commander]) // Sem deps para evitar re-registro
}
// Uso
function ProtectedArea() {
useMiddleware("admin:*", authMiddleware)
return <AdminPanel />
}
```
---
## Breaking Changes
**Nenhum!** A API existente continua funcionando:
- `invoke()` mantém mesma assinatura
- Eventos continuam sendo emitidos
- `attempt()` funciona igual
- Sem middlewares registrados = comportamento idêntico ao atual
---
## Checklist de Implementação
- [ ] Adicionar tipos de middleware em `types.ts`
- [ ] Adicionar `MiddlewareEntry` e propriedades no Commander
- [ ] Implementar `use()` com overloads
- [ ] Implementar `unuse()` com overloads
- [ ] Implementar `clearMiddlewares()`
- [ ] Implementar `getMiddlewares()`
- [ ] Implementar `getMatcherPriority()`
- [ ] Implementar `matchesCommand()`
- [ ] Implementar `getMiddlewaresForCommand()`
- [ ] Refatorar `invoke()` para usar pipeline
- [ ] Implementar `executePipeline()`
- [ ] Criar `src/middleware/helpers.ts`
- [ ] Criar `src/middleware/index.ts` (exports)
- [ ] Adicionar evento `command:cancelled`
- [ ] Testes unitários para middleware system
- [ ] Testes de integração
- [ ] Hook `useMiddleware`
- [ ] Documentação
- [ ] Exemplos
---
## Estrutura de Arquivos
```
src/
├── commander.ts # Modificado com middleware system
├── types.ts # Novos tipos de middleware
├── middleware/
│ ├── index.ts # Exports públicos
│ ├── helpers.ts # before, after, when, compose
│ ├── logging.ts # logging middleware
│ ├── analytics.ts # analytics middleware
│ ├── auth.ts # auth, requireRole
│ ├── rateLimit.ts # rate limiting
│ └── confirm.ts # confirmation dialogs
├── hooks/
│ ├── ...existing...
│ └── useMiddleware.ts # Novo hook
└── index.ts # Atualizar exports
```
---
*Atualizado em: 2025-12-03*
*Autor: Anderson / Claude*