@statedelta-actions/events
v0.3.0
Published
Event processing engine with listener dispatch and JIT optimization
Readme
EventProcessor — Eventos como Actions Deferidas
Módulo:
events/Escopo: Dispatcher de listeners sobre o ActionEngine. Listeners reagem a eventos nomeados, executando diretivas via hidden actions. Dependências:core/(tipos, slot analysis),actions/(IActionEngine). Zero dependência derules/.Para arquitetura interna, pipelines e fluxos, ver
docs/ARCHITECTURE.md. Para decisões arquiteturais, verdocs/DECISIONS.md.
O que é
O EventProcessor é um dispatcher de listeners sobre o ActionEngine. Quando o consumer emite { event: "save" }, o processor resolve quais listeners escutam "save", monta params, e delega a execução pro ActionEngine via invoke().
Evento = action deferida. A única diferença entre action e evento é quando executa. A infraestrutura é idêntica — registro, params, directives, invoke. O EventProcessor não executa diretivas ele mesmo.
Listeners viram hidden actions. Cada listener registrado cria uma action event:{id} no ActionEngine. Isso significa que listeners coexistem no mesmo grafo com actions diretas e rules (rule:{id}). O analyzer valida tudo junto — ciclos, capabilities, composition control.
Sem condição, sem cascade. Listeners não têm when() (match por nome, não por expressão) nem sub-rules (dispatch direto, sem recursão). Chegou evento, matched, executa.
Halt é scoped ao evento. Halt em um listener para apenas os listeners daquele evento. O próximo evento na fila continua normalmente.
EventListenerDefinition — O que o consumer fornece
interface EventListenerDefinition<TCtx> {
readonly id: string;
readonly priority: number;
readonly on: string | string[];
readonly then: readonly Directive<TCtx>[];
readonly tags?: readonly string[];
readonly declarations?: Record<string, unknown>;
readonly metadata?: Record<string, unknown>;
}| Campo | Semântica |
|-------|-----------|
| id | Identificador único do listener |
| priority | Ordem de execução (desc — maior executa primeiro) |
| on | Evento(s) que o listener escuta. String ou array |
| then | Diretivas executadas quando matched |
| tags | Tags extras (além de "event" que é injetado automaticamente) |
| declarations | Contratos públicos passados pro grafo (trust boundaries) |
| metadata | Dados opacos armazenados na definition |
API
Construction
import { createEventProcessor } from "@statedelta-actions/events";
const ep = createEventProcessor({
actionEngine, // IActionEngine — obrigatório
eventHooks: { ... }, // EventHooks — opcional
middleware: [ ... ], // EventMiddleware[] — opcional
mode: "auto", // "interpret" | "jit" | "auto" (default: "auto")
autoJitThreshold: 8, // Chamadas antes de auto-promote (default: 8)
});Registration
// Registrar listeners
const result = ep.register([
{
id: "on-save",
priority: 100,
on: "save",
then: [{ type: "state", target: "hp", value: 10 }],
},
{
id: "on-damage",
priority: 200,
on: ["damage", "combat"], // multi-event
then: [{ type: "state", target: "shield", value: -1 }],
},
]);
// result: { registered: string[], errors: EventRegisterError[], warnings: RegisterWarning[] }
// Remover listener
ep.unregister("on-save"); // true se existia, false se nãoProcessing
// Sync (throws se isAsync = true)
const result = ep.processEvents(
[{ event: "save" }, { event: "damage", data: { amount: 10 } }],
ctx,
);
// Async (sempre funciona)
const result = await ep.processEventsAsync(
[{ event: "save" }],
ctx,
);Batch
ep.beginBatch();
ep.register(listeners1);
ep.register(listeners2);
const result = ep.endBatch();
// Suporta nesting — inner endBatch é no-op, outer processa tudoCompilation
// Força promoção pra JIT (no-op se já em JIT ou mode = "interpret")
ep.compile();
// Consultar modo atual
ep.compilationMode; // "interpret" | "jit"
ep.isAsync; // true se hooks ou actionEngine são asyncHooks — EventHooks
interface EventHooks<TCtx> {
beforeEvent?: (listener, evalCtx) => "skip" | "abort" | void;
afterEvent?: (listener, result, evalCtx) => "abort" | void;
onEventsComplete?: (result) => void;
}| Hook | Quando | Retornos |
|------|--------|----------|
| beforeEvent | Antes de cada listener | "skip" pula listener, "abort" para evento |
| afterEvent | Após invoke de cada listener | "abort" para evento |
| onEventsComplete | Após processar todos os eventos | fire-and-forget |
Hooks ausentes não têm custo — nem no interpreter nem no JIT.
Exemplo: disable via hook
const disabled = new Set<string>();
const ep = createEventProcessor({
actionEngine,
eventHooks: {
beforeEvent: (listener) => {
if (disabled.has(listener.id)) return "skip";
},
},
});
disabled.add("on-save"); // "disable"
disabled.delete("on-save"); // "enable"Middleware
type EventMiddleware<TCtx> = (
listener: EventListenerDefinition<TCtx>,
ctx: TCtx,
params: Record<string, unknown>,
) => Record<string, unknown>;Middleware é pipeline de enriquecimento de dados. Não afeta fluxo de controle (sem "skip", sem "abort"). Cada middleware recebe e retorna params.
const ep = createEventProcessor({
actionEngine,
middleware: [
(listener, ctx, params) => ({
...params,
timestamp: Date.now(),
source: listener.id,
}),
],
});Sem middleware, $event chega direto no scope da action. Com middleware, $event entra como base e o middleware enriquece.
$event Injection
Todo listener recebe automaticamente no scope:
{ $event: { type: "save", data: eventData } }Acessível dentro de handlers via frame.scope.$event.
Results
interface EventProcessingResult {
readonly eventResults: readonly SingleEventResult[];
readonly unprocessedEvents: readonly string[]; // eventos sem listeners
readonly errors: readonly RuleError[];
readonly counters: FrameCounters;
readonly totalEvents: number;
readonly processedEvents: number;
}
interface SingleEventResult {
readonly event: string;
readonly matched: readonly number[];
readonly skipped: readonly number[];
readonly notMatched: readonly number[];
readonly errors: readonly RuleError[];
readonly halted: boolean;
readonly haltedBy?: string;
readonly counters: FrameCounters;
}
interface EventRegisterResult {
readonly registered: readonly string[];
readonly errors: readonly EventRegisterError[];
readonly warnings: readonly RegisterWarning[];
}Compilation — 3 Modos
| Modo | Comportamento |
|------|--------------|
| "interpret" | Interpreter sempre. Sem promoção |
| "jit" | JIT imediato no constructor |
| "auto" (default) | Interpreter inicial. Promove pra JIT após autoJitThreshold chamadas |
O JIT gera código equivalente ao interpreter via new Function(), eliminando branches de hooks/middleware ausentes em compile-time. O consumer não precisa se preocupar — "auto" é o default e promove transparentemente.
