npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@statedelta-actions/actions

v0.13.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.

Modo interactive opcional. Actions podem pausar entre diretivas e aguardar input externo via generators (sync ou async). Implementado de forma ortogonal ao modo async — a granularidade é per-action transitiva via mini-graph interno. Ver Modo Interactive abaixo.

Instalação

pnpm add @statedelta-actions/actions

Iní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 → 2

Handlers

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 | Floor — processa a diretiva e retorna resultado | | executeAsync | Runtime | Variante opcional — usada em actions transitivamente async (ver Handler Multi-Variant) | | executeInteractive | Runtime | Variante opcional — usada em actions transitivamente interactive | | analyze | — | Consumido pelo ActionAnalyzer externo, não pelo engine | | async (flag) | Construct | Marca handler como assíncrono (opt-in explícito) | | interactive (flag) | Construct | Marca handler como interactive (opt-in explícito) | | subDirectives | Register | Declara campos com sub-arrays que entram no grafo |

Sub-Directives — handlers com filhos no grafo

Handlers customizados podem ter sub-arrays de diretivas que participam do grafo de análise (deps, async/interactive transitivo, denied-scan, capabilities). Em vez de o engine hardcodear nomes como then/else/catch, o handler declara explicitamente quais campos da sua diretiva são sub-blocos:

const handlers = {
  simulate: {
    subDirectives: {
      directives: { required: true },
    },
    execute(d, frame, engine) {
      beginTransaction(frame.ctx);
      const r = engine.runDirectives(d.directives, frame);
      if (!r.success) rollback(frame.ctx);
      else commit(frame.ctx);
      return { ok: r.success, data: r.data };
    },
  },

  try_: {
    subDirectives: {
      body:    { required: true },
      catch:   { required: false },
      finally: { required: false },
    },
    execute(d, frame, engine) {
      const r = engine.runDirectives(d.body, frame);
      if (!r.success && d.catch) {
        frame.scope.$exception = r.errors[0]?.message;
        engine.runDirectives(d.catch, frame);
      }
      if (d.finally) engine.runDirectives(d.finally, frame);
      return { ok: r.success, data: r.data };
    },
  },
};

Uso na action:

engine.register([{
  id: "checkout",
  directives: [
    {
      type: "simulate",
      directives: [                                  // ← entra no grafo
        { type: "action", id: "charge" },            // edge: checkout → charge
        { type: "fetchUserAsync", id: 1 },           // checkout vira async transitivo
      ],
    },
  ],
}]);

Comportamento estrutural:

| Campo | Default | Efeito | |-------|---------|--------| | required: true | — | Campo ausente ou não-array → throw em register-time | | required: false | ✓ | Campo ausente é tratado como [] silenciosamente | | graph: true | ✓ | Sub-array participa de mini-graph e walks do analyzer | | graph: false | — | Sub-array é validado estruturalmente mas NÃO conta como path de execução (ex: campos de metadata/preview) |

Importante:

  • O descritor é puramente estrutural — não interfere na execução. O handler execute decide se, quando e como rodar os sub-blocos via engine.runDirectives / runDirectivesAsync.
  • if e catch permanecem hardcoded — primitivas do engine com otimizações específicas. O novo descritor é aditivo.
  • Sub-directives são propagadas transitivamente: action que usa simulate cujo sub-bloco invoca handler async automaticamente vira async — engine.isActionAsync("checkout") retorna true.

Inspection metadata

Tanto o HandlerDefinition quanto cada SubDirectiveFieldConfig aceitam campos de inspeção opcionais — zero custo runtime, expostos via engine.handlerDefinitions accessor pra tooling, DSL JSON externo, IDE hover docs, documentação auto-gerada:

const try_: HandlerDefinition = {
  description: "Executa body; em falha executa catch; finally roda sempre.",
  tags: ["control-flow", "error-handling"],
  since: "0.5.0",
  // deprecated: "use 'tryAsync' since v0.6",
  // aliasOf: "tryAsync",

  subDirectives: {
    body: {
      required: true,
      purpose: "body",
      description: "Diretivas tentadas. Falha dispara catch.",
      examples: [[{ type: "action", id: "charge" }]],
      tags: ["execution"],
    },
    catch: {
      required: false,
      purpose: "catch",
      description: "Diretivas executadas em falha. scope.$exception disponível.",
    },
    finally: {
      required: false,
      purpose: "finalizer",
    },
  },

  execute(d, frame, engine) { /* ... */ },
};

Campos suportados:

| Campo | Nível | Tipo | Uso típico | |-------|-------|------|-----------| | description | handler + sub-campo | string | Hover docs em IDE, descoberta de esquema por DSL | | tags | handler + sub-campo | string[] | Categorização cruzada, filtros | | purpose | sub-campo | string | Label semântico ("body", "catch", "branch", "finalizer", "metadata", custom) | | examples | sub-campo | Directive[][] | DSL/IDE autocomplete, docs auto-geradas | | deprecated | handler | boolean \| string | Marca obsolescência, mensagem de migração | | since | handler | string | Versionamento (semver ou livre) | | aliasOf | handler | string | Declara alias semântico de outro handler |

Handlers Async

Handlers podem retornar Promise<ApplyResult>. O engine detecta async de duas formas:

const handlers = {
  // 1. Auto-detect via `async function` — pega quando execute é declarado async.
  fetchUser: {
    async execute(directive, frame) {
      const user = await api.get(`/users/${directive.id}`);
      return { ok: true, data: user };
    },
  },

  // 2. Flag explícita — para wrappers que retornam Promise sem ser `async function`.
  delegateAsync: {
    async: true,
    execute: (d, frame, engine) => engine.invokeAsync("inner", undefined, frame),
  },
};

Engine híbrido. O engine pode misturar handlers sync e async no mesmo registry. Se qualquer handler for async (ou qualquer hook for async), o engine inteiro vira async — invoke() lança e você passa a usar invokeAsync(). Mas o JIT per-action decide sync/async por action: actions que só usam handlers sync compilam wrapper sync, sem await. FPS/games com 100% handlers sync pagam zero overhead de async; ETL/business com handlers async awaita só onde precisa.

const engine = createActionEngine({
  handlers: {
    update:  { execute: (d, f) => { /* sync */ return { ok: true }; } },
    fetchDB: { async execute(d, f) { return { ok: true, data: await db.query(...) }; } },
  },
});

engine.isAsync;          // true (fetchDB é async)
engine.invoke("anything"); // throws — use invokeAsync

// JIT compila:
//   action que usa só `update`  → wrapper sync, zero await
//   action que usa `fetchDB`    → wrapper async, await no handler

Handler Multi-Variant

Para wrapper handlers (simulate, try_, transaction) que delegam execução do sub-bloco, o modo correto depende do conteúdo concreto do sub-bloco, não de algo intrínseco ao handler. Em JS, uma função tem shape fixo na definição (function / async function / function* / async function*), então um único execute não consegue ser polimórfico.

Solução: o handler declara 3 variantes opcionais. O engine escolhe a certa em compile-time da action via mini-graph:

const simulate = {
  subDirectives: { directives: { required: true } },

  // Floor — usado em actions sync transitivas
  execute(d, frame, engine) {
    beginTx(frame.ctx);
    const r = engine.runDirectives(d.directives, frame);
    if (!r.success) rollback(frame.ctx); else commit(frame.ctx);
    return { ok: r.success, data: r.data };
  },

  // Opcional — usado em actions async transitivas
  async executeAsync(d, frame, engine) {
    beginTx(frame.ctx);
    const r = await engine.runDirectivesAsync(d.directives, frame);
    if (!r.success) rollback(frame.ctx); else commit(frame.ctx);
    return { ok: r.success, data: r.data };
  },

  // Opcional — usado em actions interactive transitivas (propaga pauses)
  async *executeInteractive(d, frame, engine) {
    beginTx(frame.ctx);
    const session = engine.runDirectivesInteractive(d.directives, frame);
    const r = yield* session;
    if (!r.success) rollback(frame.ctx); else commit(frame.ctx);
    return { ok: r.success, data: r.data };
  },
};

Regras de seleção (compile-time da action, via mini-graph):

| Action transitivamente | Variante chamada | |------------------------|------------------| | sync | execute (floor) | | async | executeAsync se presente, senão execute (engine awaita — await em sync é no-op) | | interactive | executeInteractive se presente, senão execute (deve ser generator ou flag interactive: true) |

Custo runtime: zero. A escolha de variante acontece 1x no compile da action. JIT consome $.h[type] direto — código gerado é idêntico ao single-mode. Benchmark confirma: multi-variant vs single-mode = +1.23% (dentro do ruído).

Convenção: executeInteractive é sempre async function* (async generator) — cobre os 2 modos interactive (sync+interactive e async+interactive) com 1 implementação. await em valor sync é no-op (V8 otimiza).

Capability vs Mode. Variantes opcionais (executeAsync, executeInteractive) são capabilities — declaram "o handler pode ser chamado nesse modo se necessário". NÃO entram no asyncHandlerSet/interactiveHandlerSet do mini-graph. O modo do handler em si é determinado pelo floor execute + flags. Isso preserva o princípio "actions sync não pagam overhead async/interactive".

Validação register-time. Se uma action é transitivamente interactive (sub-bloco contém pause, handler interactive, ou call pra action interactive) e o wrapper handler usado não tem suporte interactive (executeInteractive ausente + execute não é generator + flag interactive: true não set), o register lança erro MISSING_INTERACTIVE_VARIANT e remove a action do registry (rollback). Leaf handlers (sem subDirectives) não precisam de variante interactive — JIT chama execute regular mesmo em action interactive.

Quem precisa de variantes? Wrapper handlers que delegam o sub-bloco. Leaf handlers (state, emit, log, fetchUser) continuam single-mode normal.

Inversão do "function coloring problem". Em JS, uma função declarada async contamina toda a cadeia de callers. Aqui o engine resolve a contaminação automaticamente — o customer escreve simulate 1x com variantes, e o engine decide qual chamar baseado em quem usa. Mesmo nome, múltiplos modos.

Ver ADR-030 para detalhes da decisão.

Handler action canônico — createActionHandler()

O tipo { type: "action", id } (composição de actions) não tem handler built-in — o engine não hardcoda significado de handler (ADR-028). Mas o handler action correto não é engine.invoke(...) simples: ele precisa ramificar pelo modo transitivo do target (isActionInteractive → yield* / isActionAsync → invokeAsync / else invoke — ADR-024). Reimplementar isso à mão é repetitivo e fácil de errar (a parte interactive quebra silenciosamente).

O actions exporta a forma canônica desse protocolo — opt-in, multi-variant, zero overhead:

import { createActionEngine, createActionHandler } from "@statedelta-actions/actions";

const engine = createActionEngine({
  handlers: {
    action: createActionHandler(),   // composição canônica
    state, emit, log,
  },
});

Cobre toda a matriz container × target (sync/async/interactive) — o resolver per-action (ADR-030) escolhe a variante pelo modo transitivo do container; o executeInteractive delega via engine.invokeInteractive, que resolve o modo do target. Campos configuráveis:

createActionHandler({ idField: "ref", paramsField: "args" }); // default: "id"/"params"

Peça completa, incluindo analyze. Um handler é execute (+ variantes) + analyze. O factory entrega o analyze canônico por default{ capabilities: ["invoke"], dependencies: [<id alvo>] } — porque isso é identidade do handler, não concern do consumer (entregar só execute faria o consumer recablear analyze, com erro silencioso = buraco no grafo do Analyzer). Custo zero quando o analyzer está off (property inerte). Override: createActionHandler({ analyze: customFn }) ou { analyze: false } (invisível ao grafo).

Precedente: @statedelta-actions/rules exporta HALT_HANDLER pelo mesmo princípio (a lib dona do protocolo fornece a peça canônica completa). Ver ADR-034.

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 e ramificação:

{ type: "return", value: "done" }                     // success: true, data: "done"
{ type: "throw", message: "saldo insuficiente" }      // success: false
{ type: "if", cond: (ctx, scope) => scope.hp > 0,
  then: [...], else: [...] }                          // ramificação inline

Handler — 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, if, pause são tipos reservados — o engine rejeita handlers com esses nomes.

if (then/else)

cond aceita função (ctx, scope) => boolean ou boolean literal. Branches executam no mesmo scope da actionconst/let em branch ficam visíveis depois. return/throw/halt em branch saem da action inteira (semântica esperada). Aninhamento livre.

{ type: "if",
  cond: (_ctx, scope) => scope.hp > 0,
  then: [{ type: "state", target: "status", value: "alive" }],
  else: [{ type: "throw", message: "dead" }],
}

cond lança → erro coletado em errors[], nenhum branch executa, prossegue. JIT emite if/else JS nativo unrolled (zero call overhead).

Diretivas suportam:

  • as — captura result.data no scope: scope["prev"] = result.data
  • catch — em caso de falha, executa sub-diretivas com scope.$exception
  • halt — handler retorna { ok: true, halt: true } para saída antecipada
  • resolve — campos dinâmicos mesclados antes da chamada: resolve(ctx, scope) → campos mesclados

Registro

const result = engine.register(actions);

O pipeline de registro:

  1. Validate — valida estrutura via handler.validate() (rejeita diretivas malformadas). Diretivas reservadas (const, let, return, throw) são reconhecidas pelo type e não precisam de handler.
  2. Store — armazena no registry
  3. Compile — compila executor (interpret/JIT)
  4. Emit — emite evento register via lifecycle events

Retorna RegisterResult:

{
  registered: string[];        // IDs registrados com sucesso
  errors: RegisterError[];     // Falhas de validação
  warnings: RegisterWarning[]; // NO_ANALYZE, ANALYZE_ERROR
}

// RegisterError — ADR-038: register-time é sempre dado coletado
// (boot-as-data). `code` é o contrato estável que o consumer
// discrimina; `error` (message) é cosmético; `data` carrega o detalhe.
interface RegisterError {
  actionId: string;
  error: string;        // message — não-contratual
  code: string;         // obrigatório — REGISTER_ERROR_CODES.* ou code opaco do consumer
  data?: unknown;       // detalhe acionável (mantém os codes coarse)
}

Lei de disposição (ADR-038). Register-time nunca lança por erro estrutural — emite sempre RegisterError coletado. Handler ausente, sub-directive required/não-array, target não-resolvido (UNRESOLVED_TARGET), shape inválido — tudo é dado coletado, a action não registra, as demais seguem. O único throw em register é o fatal opt-in do consumer (seam de validador → DirectivePolicyError estrutural). Taxonomy de origem do engine exportada em REGISTER_ERROR_CODES (INVALID_SHAPE, MISSING_ID, UNKNOWN_HANDLER, UNRESOLVED_TARGET, MISSING_INTERACTIVE_VARIANT, PAUSE_REQUIRES_INTERACTIVE, DIRECTIVE_VALIDATOR_ISSUE).

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" emitido

Suporta 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 ou handler 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 em falha de handler ou condição de domínio. Exceções de handlers e { throw }/validação de negócio são dado — coletados em result.errors, o consumer reage (ADR-005).

Lança em erro de programa (carve-out ADR-005, estendida pelo ADR-036): invoke sem ctx; invoke quando isAsync; e target estrutural não-resolvido{ type:"action", id } com id fora do registry é undefinedFunction(), não no-op soft. Ids estáticos falham já no register/endBatch (RegisterError UNRESOLVED_TARGET, antes do tick zero); id dinâmico fora do registry lança ActionNotFoundError em runtime — tagueado estrutural, não-catchável pelo catch da diretiva (resolução de referência ≠ falha de execução). O consumer distingue via isStructuralError(e) / e instanceof ActionNotFoundError (exportados) e mapeia pra sua política. Ver ADR-036.

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 raiz

Lifecycle 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")

// Map `type → readonly fieldNames[]` dos campos de sub-directives no grafo
// (HandlerDefinition.subDirectives com graph !== false; "catch" filtrado)
engine.subDirectiveFieldsForGraph;  // ReadonlyMap<string, readonly string[]>

// 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) |

Decisão sync/async é per-action. No JIT per-action, cada action é compilada sync ou async independentemente, baseado nos handlers que ela usa. Em um engine híbrido, actions 100% sync ficam com wrapper sync e zero await; actions com pelo menos um handler async ficam com wrapper async e await em todos os handlers da action. Isso garante que uso fully-sync (FPS, game loops) não pague o custo de async functions só porque outro handler do mesmo engine é async.

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 algum hook OU handler é async
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(). Handlers async (via flag async: true ou async function) também tornam o engine async — ver Handlers Async.

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 beforeAction skipar, afterAction nã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:

  • allowedDirectives e blockedDirectives sã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");
// → false

Validadores de Diretiva

Validação estrita registrada de fora. O consumer injeta predicados opacos rodados por diretiva no register (FASE 1). Útil quando outra lib monta actions em runtime (sandbox/eval) e precisa barrar diretiva proibida — ou uso suspeito de diretiva permitida — antes da action existir. A política é 100% do consumer; o engine não conhece a semântica (ex.: o que é "estado privado").

createActionEngine({
  handlers,
  directiveValidators: [
    (directive, path, actionId) =>
      directive.type === "dispatch" && directive.state === "realm"
        ? {
            message: "mutação de estado privado do sistema",
            code: "RESERVED_TARGET_WRITE", // opaco ao engine — política do consumer
            data: { target: "realm" },
            fatal: true,
          }
        : null,
  ],
});

Contrato:

  • (directive, path, actionId) => DirectiveIssue | null | undefined. null/undefined = ok.
  • DirectiveIssue = string (sugar) ou { message, code?, data?, fatal? }.
  • code/data são opacos ao engine — política 100% do consumer; o engine só transporta (não interpreta a semântica de "realm"). message é cosmético, não-contratual.
  • Issue não-fatalRegisterError coletado; a action não registra. code/data propagam pro RegisterError. Sem code (inclusive a forma string) → default DIRECTIVE_VALIDATOR_ISSUEnunca code-less (ADR-038): o consumer discrimina por code, não por regex-em-message.
  • Issue fatal → lança DirectivePolicyError no register — erro estrutural, não-catchável, isStructuralError(e) true. Carrega issue.code/issue.data (default code: "DIRECTIVE_POLICY_VIOLATION") + actionId/path. Simetria: a mesma policy é discriminável pelo mesmo code nas duas disposições (não-fatal coletado e fatal throw). Superfície estável pro consumer mapear (ex.: TICK_EXCEPTION).
  • Lista: roda todos; coleta todas as issues não-fatais por action; uma fatal curto-circuita a action.
  • Custo zero se ausente — nada é gerado nem chamado.
  • toda diretiva alcançável (recursivo em catch, if.then/else, sub-directives declaradas); path é o caminho do nó (ex.: directive[0].then[1]).

Ver ADR-037 (mecanismo do seam) e ADR-038 (contrato code/data, lei de disposição).

Referê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
  directiveValidators?: DirectiveValidator[];   // Validadores estritos no register (FASE 1)
}

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,
  SubDirectiveFieldConfig,
  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";

// Directive Validators + Structural Errors
import type {
  DirectiveValidator,
  DirectiveIssue,
} from "@statedelta-actions/actions";

import {
  ActionNotFoundError,
  DirectivePolicyError,
  isStructuralError,
  REGISTER_ERROR_CODES,      // taxonomy estável de code de register (ADR-038)
  DIRECTIVE_POLICY_VIOLATION, // default do code fatal
} from "@statedelta-actions/actions";

// Action Hooks
import type {
  ActionHooks,
  ActionInterceptResult,
} from "@statedelta-actions/actions";

// Interactive
import type {
  InteractiveConfig,
  PauseEvent,
  InteractiveSession,
  AsyncInteractiveSession,
  InteractiveApplyResult,
  Responder,
} from "@statedelta-actions/actions";

import {
  drainSync,
  drainAsync,
  replayResponder,
} 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 (per-action)
import { buildActionExecutor } from "@statedelta-actions/actions";
import type { GeneratedActionExecutor } from "@statedelta-actions/actions";

Modo Interactive

Pausa execução de uma action e aguarda input externo do customer. Implementado via generators (sync ou async), ortogonal ao modo async.

Use cases:

  • UX interativa em produção (wizards, prompts, confirmações)
  • Debug entre diretivas (futuro — modo separado)

Mecânica:

  • Handler interactive declarado com interactive: true (espelha async: true) — handler é generator function
  • Diretiva reservada type: "pause" — engine emite yield direto pra confirmação/breakpoint declarativo
  • Mini-graph interno propaga _interactiveActions transitivamente (ADR-026)
  • API: engine.invokeInteractive(id, params, ctx) retorna Iterator | AsyncIterator

Habilitação

const engine = createActionEngine({
  handlers,
  interactive: {},   // habilita modo interactive
});

Sem interactive configurado:

  • Handler com interactive: true → erro no constructor (fail-fast)
  • Diretiva type: "pause" → erro no register

Handler interactive

Handlers são primitivas Lego — genéricas, sem semântica de domínio (ADR-028). Vocabulário canonical pequeno (state, emit, action, log, halt) + handler interactive primitivo proposto: input (yield + schema + retry).

⚠️ Não use nomes domain-specific (askUser, confirmDelete, validateEmail). Cria explosão combinatória, polui vocabulário canonical e viola o princípio Lego. Customer compõe domínio via payload das diretivas, não criando handlers.

// Pattern recomendado: primitiva `input` genérica com schema + retry interno
const handlers = {
  input: {
    interactive: true,
    *execute(directive) {
      let lastError: string | null = null;
      let attempt = 1;
      const max = directive.maxAttempts ?? Infinity;
      while (attempt <= max) {
        const answer = yield {
          kind: "input",
          payload: directive.payload,
          schema: directive.schema,
          attempt,
          lastError,
        };
        if (!directive.schema) return { ok: true, data: answer };
        const r = directive.schema.parse(answer);
        if (r.ok) return { ok: true, data: r.data };
        lastError = r.error;
        attempt++;
      }
      return { ok: false, error: `validation failed after ${max} attempts` };
    },
  },
};

Customer compõe actions com payload domain-specific sobre a primitiva:

{ id: "register", directives: [
  { type: "input", payload: { kind: "text", label: "Nome" }, as: "name", schema: nameSchema },
  { type: "input", payload: { kind: "number", label: "Idade" }, as: "age", schema: ageSchema },
  { type: "input", payload: { kind: "confirm", message: "Confirma?" }, as: "ok" },
  { type: "state", target: "users", op: "push", value: { name: ..., age: ... } },
]}

Consumer interpreta payload.kind — renderiza UI/CLI/voice. Framework não opina.

Schema agnóstico — interface mínima { parse(raw): { ok: true, data } | { ok: false, error } }. Customer adapta zod/valibot/json-schema/custom em 1 linha.

input (intra-tick) ≠ request (cross-tick). input pausa dentro de uma invocação (yield/next imediato). request (RealmSystem futuro, fora deste package) anota dependência declarativa, lock cross-tick. São conceitos distintos com nomes propositalmente diferentes.

Auto-detect via isGeneratorFunction(execute) cobre function* e async function*. Use a flag interactive: true pra wrappers que retornam iterator sem ser generator function.

Diretiva type: "pause" (engine-level)

{ type: "pause", message: "Confirmação destrutiva?", as: "ack" }

Engine yield direto:

session.next();              // → { source: "pause", payload: { message: "..." }, ... }
session.next("ok");          // → continua, captura "ok" em scope.ack
session.next(false);         // → aborta (abortedBy: "pause")
session.next("cancel");      // → aborta
session.next("abort");       // → aborta

Drenagem do iterator

// Customer dirigindo manualmente
const session = engine.invokeInteractive("wizard", undefined, ctx);
const r1 = session.next();           // PauseEvent ou payload custom
const r2 = session.next("Anderson"); // entrega resposta
// quando r.done === true, r.value é o DirectiveResult final

Helpers opcionais pra "play mode":

import { drainSync, drainAsync, replayResponder } from "@statedelta-actions/actions";

// Drena com responder programático
const result = drainSync(session, (event) => {
  // event pode ser PauseEvent (type:"pause") ou payload custom (handler)
  if ("prompt" in event) return responses[event.prompt];
  if ((event as PauseEvent).source === "pause") return "ok";
  return undefined;
});

// Replay determinístico (testes)
const result = drainSync(session, replayResponder(["Anderson", 42, "yes"]));

Sub-actions interativas (yield* propaga pausas)

Quando action root invoca child interactive, pausas do child fluem pro consumer no nível raiz via yield*. Handler action (consumer-defined) decide:

const handlers = {
  action: {
    execute: (d, f, e) => {
      if (e.isActionInteractive(d.id)) {
        // Target é interactive → propaga via iterator
        return { ok: true, iterator: e.invokeInteractive(d.id, d.params, f) };
      }
      // Target sync → invoke regular
      const r = e.invoke(d.id, d.params, f);
      return { ok: r.success, data: r.data };
    },
  },
};

Engine consulta mini-graph e marca actions transitivamente interativas. JIT detecta _result.iterator em runtime e emite yield* automaticamente.

Matriz async × interactive

| Engine async? | Action interactive? | Compilação | |---------------|---------------------|------------| | não | não | function () | | não | sim | function* () | | sim | não | async function () | | sim | sim | async function* () |

Granularidade per-action via mini-graph (ADR-021/025/026). FPS 100% sync com 1 action interactive isolada não paga overhead de generator nas outras.

invoke() per-action transitivo

invoke() lança per-action (não global):

engine.invoke("syncAction");           // OK — sub-árvore inteira sync
engine.invoke("asyncAction");          // throw "use invokeAsync"
engine.invoke("interactiveAction");    // throw "use invokeInteractive"

Engine híbrido (handler async + actions sync isoladas) permite invoke() regular nas sync. ADR-026.

Action hooks fora do generator

beforeAction / afterAction (ADR-019) executam antes e depois do generator. Skip via beforeAction retorna memoResult imediatamente. afterAction substitui o resultado final.

Catch atômico

Diretivas dentro de catch executam atomicamente (ADR-023) via engine.runDirectives — sem yield. Caminho de erro não pausa. Diretivas type: "pause" ou handler interactive dentro de catch geram warning em register-time.

Auto-step directives (debug mode)

Pra debuggers, profilers, time-travel UIs ou dry-run que precisam pause-point universal sem instrumentar definitions, habilite autoStepDirectives no config interactive:

const engine = createActionEngine({
  handlers,
  interactive: { autoStepDirectives: true },
});

O interpreter generator passa a emitir PauseEvent { source: "step" } antes de cada directive durante uma invocação interactive. Consumer drena via session.next(cmd?) e pode:

| next(cmd) | Efeito | |-------------|--------| | omitido | continua — executa a directive normalmente | | { skip: true } | pula a directive (incrementa skippedCount) | | { replaceWith: directive } | substitui antes de executar | | { abort: true } | aborta com abortedBy: "step" |

Coexiste com pause directive e handlers interactive — em uma action que mistura tudo, a ordem de yields é: step (engine antes de processar) → yield natural (pause payload, ou handler yield*).

const session = engine.invokeInteractive("checkout", undefined, ctx);
let r = session.next();
while (!r.done) {
  const ev = r.value as PauseEvent;
  if (ev.source === "step") {
    console.log("next:", ev.directive.type, "at", ev.frame.path);
    r = session.next();          // continua
  } else if (ev.source === "pause") {
    r = session.next("ok");       // responde pause
  } else {
    r = session.next(responses[ev.payload?.prompt]);  // handler
  }
}

Interpret-only. JIT permanece atômico por design — pause-points em código compilado violariam o propósito do JIT. Engine em mode: "jit" com autoStepDirectives: true faz fallback pro interpreter generator durante invocações interactive das actions transitivamente marcadas. Invocações regulares (invoke/invokeAsync) e actions não-interactive continuam usando JIT normalmente — autoStep só desvia o caminho interactive.

Propagação transitiva grátis. Step yields propagam automaticamente via yield* em sub-actions interactive (sem código adicional) e em sub-blocos de wrapper handlers que delegam via engine.runDirectivesInteractive. Catch permanece atômico — engine.runDirectives (interpreter regular, não-generator) não emite step yields, preservando ADR-023.

replayResponder e step events. Em play mode, replayResponder ignora step events sem consumir a queue de respostas — pré-programados continuam alinhados aos yields semânticos (pause / handler).

Custos. Sem a flag: zero overhead. Com a flag: 1 yield + 1 PauseEvent alloc por directive executada (proporcional ao tamanho da action — desprezível em debug humano). Fallback JIT→interpreter custa ~10× mais lento que JIT compilado (aceitável — debug é interação humana, não hot path).


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