@statedelta-actions/actions
v0.2.0
Published
Directive execution engine with JIT compilation and BailHook interception
Readme
@statedelta-actions/actions
Motor de execução de diretivas com validação em register-time e compilação JIT.
Defina ações como objetos JS declarativos. Registre. O engine valida estrutura, compila executores otimizados. Em runtime, invoke é O(1) lookup + execução — zero validação, zero análise.
Filosofia
Engine = V8. Runtime puro.
O engine é um executor. Não analisa dependências, não computa grafos, não valida contratos. Registro valida estrutura e compila. Invocação executa. Análise estática (grafo, capabilities, ciclos, composition control) é responsabilidade do @statedelta-actions/analyzer — uma camada externa opcional.
Ações são declarativas. Uma action é um ID mais uma lista ordenada de diretivas — objetos JS declarativos despachados por um campo de tipo (type por padrão). O engine não sabe o que "state", "emit" ou "action" significam. Handlers dão significado às diretivas. O engine orquestra a execução.
Handlers têm fases. Um handler não é só um executor. Ele pode validar estrutura da diretiva em register-time, contribuir código JIT, e executar em runtime. Cada fase é opcional — uma função simples funciona como handler (compat V1). Uma definição completa desbloqueia todo o pipeline. A fase analyze existe na interface mas é consumida pelo ActionAnalyzer, não pelo engine.
Sem sugar no runtime. Diretivas devem ser passadas em forma canônica — toda diretiva tem campo type. Sugar forms (shorthands de autoria) são concern do compilador JSON DSL, que normaliza antes de entregar pro engine. O engine não interpreta nem converte sugar.
Instalação
pnpm add @statedelta-actions/actionsInício Rápido
import { createActionEngine } from "@statedelta-actions/actions";
// 1. Defina handlers — dão significado aos tipos de diretiva
const handlers = {
state: {
execute: (directive, frame) => {
const { target, value } = directive;
frame.ctx.state[target] = value;
return { ok: true, data: value };
},
},
emit: {
execute: (directive, frame) => {
frame.ctx.events.push(directive.event);
return { ok: true };
},
},
action: {
execute: (directive, frame, engine) => {
const result = engine.invoke(directive.id, directive.params, frame);
return { ok: result.success, data: result.data };
},
},
};
// 2. Crie o engine
const engine = createActionEngine({ handlers });
// 3. Registre ações
const result = engine.register([
{
id: "heal",
directives: [
{ type: "state", target: "hp", value: 100 },
{ type: "emit", event: "healed" },
],
},
{
id: "combat",
directives: [
{ type: "action", id: "heal" },
{ type: "emit", event: "combat:done" },
],
},
]);
// result.registered → ["heal", "combat"]
// result.errors → []
// result.warnings → []
// 4. Invoque com contexto
const ctx = { state: {}, events: [] };
const r = engine.invoke("heal", undefined, ctx);
// r.success → true
// r.appliedCount → 2Handlers
Um handler processa diretivas de um tipo específico. Dois formatos:
V1 — Função simples
const handlers = {
log: (directive, frame, engine) => {
console.log(directive.message);
return { ok: true };
},
};Compatível com versões anteriores. Sem análise em register-time. Emite warning NO_ANALYZE.
V2 — Definição completa
const handlers = {
state: {
// Register-time: valida estrutura da diretiva
validate(directive) {
if (!directive.target) return { valid: false, error: "missing target" };
},
// Runtime: executa a diretiva
execute(directive, frame, engine) {
frame.ctx.state[directive.target] = directive.value;
return { ok: true, data: directive.value };
},
// Usado pelo ActionAnalyzer (não pelo engine):
// analyze(directive) {
// return { capabilities: ["write"], dependencies: [] };
// },
},
};| Fase | Quando | Propósito |
|------|--------|-----------|
| validate | Register | Validação estrutural (rejeita diretivas malformadas) |
| execute | Runtime | Processa a diretiva e retorna resultado |
| analyze | — | Consumido pelo ActionAnalyzer externo, não pelo engine |
Ações
Uma ação é um ID mais diretivas:
engine.register([{
id: "checkout",
directives: [
{ type: "validate", schema: "cart" },
{ type: "state", target: "status", value: "processing" },
{ type: "action", id: "checkout/charge" },
{ type: "emit", event: "checkout:complete" },
],
}]);Categorias de Diretiva
Três categorias. Toda diretiva tem campo type (forma canônica). O engine opera exclusivamente sobre forma canônica.
Binding — escrita no scope de execução:
{ type: "const", name: "tax", value: 0.1 }
{ type: "let", name: "total", value: "$", resolve: (ctx, scope) => ({ value: scope.subtotal * 1.1 }) }Control — saída antecipada:
{ type: "return", value: "done" } // success: true, data: "done"
{ type: "throw", message: "saldo insuficiente" } // success: falseHandler — dispatch pro handler registrado:
{ type: "state", target: "hp", value: 100, as: "prev" }
{ type: "action", id: "heal", catch: [{ type: "emit", event: "heal:failed" }] }O interpreter e JIT operam sobre formato uniforme — um único dispatch via type field, sem branching de categorias.
Os nomes const, let, return, throw são tipos reservados — o engine rejeita handlers com esses nomes.
Diretivas suportam:
as— capturaresult.datano scope:scope["prev"] = result.datacatch— em caso de falha, executa sub-diretivas comscope.$exceptionhalt— handler retorna{ ok: true, halt: true }para saída antecipadaresolve— campos dinâmicos mesclados antes da chamada:resolve(ctx, scope) → campos mesclados
Registro
const result = engine.register(actions);O pipeline de registro:
- Validate — valida estrutura via
handler.validate()(rejeita diretivas malformadas). Diretivas reservadas (const,let,return,throw) são reconhecidas pelotypee não precisam de handler. - Store — armazena no registry
- Compile — compila executor (interpret/JIT)
- Emit — emite evento
registervia lifecycle events
Retorna RegisterResult:
{
registered: string[]; // IDs registrados com sucesso
errors: RegisterError[]; // Falhas de validação
warnings: RegisterWarning[]; // NO_ANALYZE, ANALYZE_ERROR
}Desregistrar
engine.unregister("checkout");
// Também remove filhas: "checkout/charge", "checkout/validate", etc.Modo Batch
Batch agrupa múltiplos registros e adia emissão de eventos pro endBatch():
engine.beginBatch();
engine.register([actionA]);
engine.register([actionB]);
const result = engine.endBatch(); // Único evento "register" emitidoSuporta aninhamento — endBatch() interno é no-op. Processamento acontece na profundidade 0.
Invocação
// Passe ctx direto na invocação (recomendado)
const result = engine.invoke("heal", undefined, ctx);
const result = engine.invoke("heal", { amount: 50 }, ctx);
// Ou defina ctx global e invoque sem passar ctx
engine.setContext(ctx);
const result = engine.invoke("heal");
// Ou ctx temporário com escopo (múltiplas invocações)
engine.context(ctx, () => {
engine.invoke("heal");
engine.invoke("combat");
});
// Async (obrigatório se algum hook for async)
const result = await engine.invokeAsync("heal", undefined, ctx);Três formas de fornecer ctx, em ordem de preferência:
| Forma | Quando usar |
|-------|-------------|
| invoke(id, params, ctx) | Invocação única, ctx por chamada — mais direto |
| context(ctx, fn) | Múltiplas invocações com mesmo ctx temporário |
| setContext(ctx) | Ctx global estável entre múltiplas invocações |
Se ctx for passado no invoke/invokeAsync, tem precedência sobre setContext/context. Se omitido, fallback pro ctx global (erro se nenhum definido).
Retorna DirectiveResult:
{
success: boolean;
aborted: boolean;
abortedBy?: string; // "halt" | "throw" | "maxDepth"
appliedCount: number;
skippedCount: number;
errors: DirectiveError[];
data?: unknown; // De return directive ou halt
counters: FrameCounters;
}Nunca lança exceções. Erros são coletados em result.errors. Exceções de handlers são capturadas e reportadas.
Sub-Actions (Visibilidade)
Ações com / no ID são privadas:
engine.register([
{ id: "checkout/validate", directives: [...] }, // privada
{ id: "checkout", directives: [
{ type: "action", id: "checkout/validate" }, // ok — escopo do pai
]},
]);
engine.invoke("checkout/validate"); // erro — não acessível da raizLifecycle Events
O engine emite eventos de ciclo de vida via on(). Retorna função de unsubscribe idempotente:
const unsub = engine.on("register", (event) => {
console.log("Registrados:", event.registered);
console.log("Resultado:", event.result);
});
engine.on("unregister", (event) => {
console.log("Removido:", event.id);
console.log("Cascata:", event.cascaded);
});
// Cancelar subscription
unsub();| Evento | Payload | Quando |
|--------|---------|--------|
| register | { actions, result, registered } | Após register() ou endBatch() |
| unregister | { id, cascaded } | Após unregister() |
Em batch mode, o evento register é emitido uma única vez no endBatch() com todas as actions do batch.
Read-Only Accessors
Acessores de leitura para introspection por camadas externas (e.g. analyzer):
// Map de handler definitions V2 registradas
engine.handlerDefinitions; // ReadonlyMap<string, HandlerDefinition>
// Set de IDs de todas as actions no registry
engine.registeredIds; // ReadonlySet<string>
// Lê uma action definition pelo ID
engine.getActionDefinition("heal"); // ActionDefinition | undefined
// Campo de tipo pra dispatch de diretivas
engine.typeField; // string (default: "type")
// Mapa de permissões de diretivas (computado no boot, imutável)
engine.directivePermissions; // ReadonlyMap<string, DirectivePermission>
// Slots de hooks preenchidos (nomes dos hooks registrados)
engine.directiveHookSlots; // ReadonlySet<string>
// Slots async (nomes dos hooks que são async)
engine.asyncSlots; // ReadonlySet<string>Modos de Compilação
createActionEngine({ handlers, mode: "interpret" }); // Interpretador loop-based (dev)
createActionEngine({ handlers, mode: "jit" }); // Compila tudo no register (prod)
createActionEngine({ handlers, mode: "auto" }); // Interpreta primeiro, promove após N invocações (padrão)| Modo | Register | Runtime | Ideal para |
|------|----------|---------|------------|
| interpret | Rápido | Loop interpretado | Desenvolvimento, debug |
| jit | Mais lento (compila) | new Function compilado | Produção, ações estáveis |
| auto | Rápido | Promove per-action após threshold | Uso geral (padrão) |
Threshold de auto-promote (padrão: 8):
createActionEngine({ handlers, mode: "auto", autoJitThreshold: 4 });Forçar compilação de todas as ações registradas:
engine.compile();Informações de compilação:
engine.isAsync; // true se hooks async detectados
engine.compilationMode; // "interpret" | "jit" (modo atual, não o requestado)Hooks
Três pontos de hook em nível de diretiva:
createActionEngine({
handlers,
directiveHooks: {
beforeDirective(directive, frame) {
// Retorne "skip" pra pular, "abort" pra abortar
// Retorne { directive } pra substituir, { ctx } pra sobrescrever contexto
},
afterDirective(directive, result, frame) {
// Retorne "abort" pra parar execução
},
onDirectivesComplete(result) {
// Fire-and-forget — observa resultado final
},
},
});Hooks podem ser sync ou async. Hooks async tornam o engine async (engine.isAsync === true), exigindo invokeAsync().
Custo zero quando ausente. A compilação JIT não emite código de hook para hooks não registrados.
Action Hooks
Hooks no nível de action — interceptam antes e depois de executar todas as diretivas de uma action. Diferente de directiveHooks que operam por diretiva individual.
createActionEngine({
handlers,
actionHooks: {
beforeAction(id, params, frame) {
// Antes de executar qualquer diretiva da action.
// Retorne void pra prosseguir normalmente.
// Retorne { skip: true, data? } pra skip total (zero diretivas processadas).
},
afterAction(id, params, result, frame) {
// Após execução completa. NÃO dispara se beforeAction skipou.
// Retorne void pra manter resultado original.
// Retorne DirectiveResult pra substituir.
},
},
});Use cases: memoização, profiling, auditoria, mock/dry-run, governance.
Memoização via beforeAction
O engine não sabe o que é memo. O consumer implementa a lógica via closure (ADR-017):
const cache = new Map<string, { data: unknown }>();
const engine = createActionEngine({
handlers,
actionHooks: {
beforeAction(id) {
const cached = cache.get(id);
if (cached) return { skip: true, data: cached.data };
},
afterAction(id, _params, result) {
cache.set(id, { data: result.data });
},
},
});Comportamento
- Frame: O hook recebe o
childFrame(scope filho, depth incrementado), não o frame do caller. - Skip e auto-promote: Skip não incrementa
invokeCount— não conta pro threshold de auto-promote. - Skip e afterAction: Se
beforeActionskipar,afterActionnão dispara. - Sub-actions: Hooks disparam pra cada action na cadeia de invocação (parent e child).
- Sem hooks: Custo zero — um null check por invoke.
- JIT: Zero impacto. Hooks vivem no caller (
_invokeInternal), não no código gerado.
Handler Permissions
Controle declarativo de quais diretivas uma instância do engine pode usar. Útil pra governança (sandbox parent→child) e arquitetura (query actions não devem mutar estado).
// Whitelist: só estes tipos permitidos
createActionEngine({
handlers,
allowedDirectives: ["state:query", "emit", "action"],
});
// Blacklist: estes tipos bloqueados
createActionEngine({
handlers,
blockedDirectives: ["state:dispatch", "emit"],
});
// Blacklist com motivações
createActionEngine({
handlers,
blockedDirectives: [
{ pattern: "state:dispatch", reason: "sandbox read-only", source: "parent:root" },
{ pattern: "emit", reason: "events disabled", source: "config" },
],
});Regras:
allowedDirectiveseblockedDirectivessão mutuamente exclusivos. Se ambos fornecidos: erro no constructor.- Se nenhum fornecido: tudo permitido (default, zero overhead).
- Diretivas estáticas (
const,let,return,throw) são sempre permitidas — não podem ser bloqueadas. - Patterns com wildcard
*são suportados:"state:*","*:dispatch","*".
O engine não bloqueia execução. Permissions são config, não enforcement. Actions com handler denied registram e executam normalmente. Quem valida violations é o ActionAnalyzer. Quem decide a política (rejeitar boot, warning, ignorar) é o consumer.
// Consultar permissões
const perms = engine.directivePermissions;
perms.get("emit");
// → { status: "available" }
perms.get("state:dispatch");
// → { status: "denied", reason: "sandbox read-only", source: "parent:root" }
// Handler inexistente: não está no mapa (unavailable é derivado)
perms.has("notify");
// → falseReferência de Configuração
interface IActionEngineConfig<TCtx> {
handlers: HandlerInputMap<TCtx>; // Obrigatório — handlers de diretivas
typeField?: string; // Campo de dispatch (padrão: "type")
directiveHooks?: DirectiveHooks<TCtx>; // Hooks de diretiva
limits?: Partial<FrameLimits>; // maxDepth (10), maxRules, maxDirectives
mode?: "interpret" | "jit" | "auto"; // Modo de compilação (padrão: "auto")
autoJitThreshold?: number; // Auto-promote após N invocações (padrão: 8)
allowedDirectives?: DirectivePermissionEntry[]; // Whitelist com pattern matching
blockedDirectives?: DirectivePermissionEntry[]; // Blacklist com pattern matching
}Exports
// Factory
import { createActionEngine } from "@statedelta-actions/actions";
// Tipos (V1)
import type {
DirectiveHandler,
DirectiveHandlerMap,
DirectiveHooks,
DirectiveExecutorFn,
DirectiveRunnerFn,
} from "@statedelta-actions/actions";
// Tipos (V2)
import type {
HandlerDefinition,
HandlerAnalysis,
ValidationResult,
HandlerInput,
HandlerInputMap,
ActionDefinition,
RegisterResult,
RegisterError,
RegisterWarning,
IActionEngineConfig,
IActionEngine,
} from "@statedelta-actions/actions";
// Lifecycle Events
import type {
RegisterEvent,
UnregisterEvent,
EngineEventMap,
EngineEventName,
} from "@statedelta-actions/actions";
// Directive Permissions
import type {
DirectivePermissionStatus,
DirectivePermission,
DirectivePermissionConfig,
DirectivePermissionEntry,
} from "@statedelta-actions/actions";
// Action Hooks
import type {
ActionHooks,
ActionInterceptResult,
} from "@statedelta-actions/actions";
// Register Pipeline
import { RESERVED_TYPES } from "@statedelta-actions/actions";
// Emitter
import { SimpleEmitter } from "@statedelta-actions/actions";
import type { Listener } from "@statedelta-actions/actions";
// Interpreter
import { createDirectiveInterpreter } from "@statedelta-actions/actions";
// JIT (generic)
import { buildDirectiveExecutor } from "@statedelta-actions/actions";
import type { GeneratedDirectiveExecutor } from "@statedelta-actions/actions";
// JIT (per-action)
import { buildActionExecutor } from "@statedelta-actions/actions";
import type { GeneratedActionExecutor } from "@statedelta-actions/actions";Analyzer
Funcionalidades de análise estática foram extraídas para o pacote @statedelta-actions/analyzer:
- Grafo de dependências — capabilities, leaf, maxDepth, ciclos
- Declarations — trust boundaries, conflitos de contrato
- Propagators — propriedades computadas e propagadas pelo grafo
- Composition Control — manifest declarativo, tags, matchers, transitividade
O analyzer consome os read-only accessors e lifecycle events do engine. Consulte a documentação do @statedelta-actions/analyzer para detalhes.
Licença
MIT
