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.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/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 | 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: false

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

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
}

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

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

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
}

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