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-axiom/tick-runner

v0.1.0

Published

PPP tick orchestration (Prepare → Process → Propagate) with drain loop

Readme

@statedelta-actions/tick-runner

Orquestrador de 1 ciclo PPP (Preparar → Processar → Propagar) sobre o ActionEngine.

O TickRunner integra views, rules e events numa execução sequencial por ciclo. Recebe todos os recursos via DI — não instancia nada. Método run(), não tick(): quem conta ticks é o RealmSystem (futuro).

import { createTickRunner, createCtx, createEventBridge } from "@statedelta-actions/tick-runner";

// Montar engines externamente, passar pro TR via DI
const runner = createTickRunner(
  { ctx, ruleEngine, eventProcessor, collectEvents },
  { maxEventPasses: 5 },
);

const result = runner.run({ tick: 1 });
// result.halted, result.thrown, result.eventPasses, result.ruleResult...

Filosofia

PPP é a mecânica universal

Todo ciclo segue a mesma sequência fixa:

  1. Preparar — ViewEvaluator computa sinais derivados (sweep linear + cache). Records locked.
  2. Processar — RuleEngine avalia rules contra estado + sinais. Rules emitem eventos.
  3. Propagar — EventProcessor drena eventos internos. Multi-pass até fila vazia ou limite.

Não existe outro modelo de orquestração. Variações de domínio (FPS, ETL, board game) são resolvidas por composição (rules, priorities, estado), não por pipelines diferentes.

Orquestrador puro

O TR conhece a mecânica de cada fase, mas não implementa nenhuma. Delega:

  • Views → IViewComputer.evaluate() / evaluateAsync()
  • Rules → IRuleEngine.evaluate() / evaluateAsync()
  • Events → IEventProcessor.processEvents() / processEventsAsync()

Se uma fase não tem componente (sem viewComputer, sem eventProcessor), é skippada silenciosamente. O mínimo funcional é um RuleEngine.

O TR oferece run() (sync) e runAsync() (async). Não detecta async — oferece ambos. Quem decide é o caller (RealmSystem) baseado em physics.async.

Halt = commit, Throw = rollback

Semântica fixa, sem configuração:

| | Rules param? | Eventos drenam? | Semântica | |---|---|---|---| | halt | Sim | Sim — normalmente | "Terminei rules, honre os eventos emitidos" | | throw | Sim | Não — descartados | "Inválido, cancela tudo" |

Estado vive fora — ctx é ponteiro

O TR é o único package deste monorepo que conhece estado (apex-store). Mas o ctx não carrega dados — delega pra Router → Store. O ctx é criado uma vez e reutilizado entre runs. Dados efêmeros (tick, effects) entram via run() e morrem quando o run acaba.

Opt-in progressivo

Sem viewComputer → Pass 0 não executa. Sem eventProcessor → Pass 2+ não executa. Sem run data → ctx.run() retorna undefined. O mínimo é um RuleEngine — o resto é opt-in. Componente não fornecido = zero overhead (nem instanciado).

Instalação

pnpm add @statedelta-actions/tick-runner

Requer: @statedelta-actions/actions, rules, events, view-state e apex-store.

Quick Start

O TR recebe tudo pronto via DI. O consumer (ou um composition root como o RS futuro) monta os engines e passa pro TR:

import { createTickRunner, createCtx, createEventBridge } from "@statedelta-actions/tick-runner";
import { createActionEngine } from "@statedelta-actions/actions";
import { createRuleEngine } from "@statedelta-actions/rules";
import { createEventProcessor } from "@statedelta-actions/events";
import { createStore } from "apex-store";
import { createRouter } from "apex-store/router";

// 1. Infra
const store = createStore();
const router = createRouter(store);

// 2. Event bridge — closure pair que conecta emit handler ↔ TR
const { emitHandler, collectEvents } = createEventBridge();

// 3. Engines
const actionEngine = createActionEngine({ handlers: { ...myHandlers, emit: emitHandler } });
const ruleEngine = createRuleEngine({ actionEngine });
const eventProcessor = createEventProcessor({ actionEngine });

// 4. Ctx — persistente, reutilizado entre runs
const ctx = createCtx(router);

// 5. TickRunner — orquestrador PPP
const runner = createTickRunner(
  { ctx, ruleEngine, eventProcessor, collectEvents },
  { maxEventPasses: 10 },
);

// Executar ciclos
const r1 = runner.run({ tick: 1 });
const r2 = runner.run({ tick: 2 });

Com Views (Pass 0)

Para habilitar views reativas, forneça viewComputer e get:

import { createViewEvaluator } from "@statedelta-actions/view-state";

const viewEvaluator = createViewEvaluator({
  actionEngine,
  createRecord: (viewId) => createViewRecord(viewId, store),
});

const runner = createTickRunner(
  {
    ctx,
    ruleEngine,
    collectEvents,
    viewComputer: viewEvaluator,            // habilita Pass 0
    eventProcessor,
    get: (path) => router.get(path),         // bound pro ViewEvaluator
  },
  { maxEventPasses: 10 },
);

Sem viewComputer, Pass 0 é skippado — zero overhead.

API

createTickRunner

function createTickRunner<TCtx>(deps: TickRunnerDeps<TCtx>, options?: TickRunnerOptions): ITickRunner;
interface TickRunnerDeps<TCtx> {
  /** Ctx persistente — get() → Router → Store, run() → dados efêmeros. */
  readonly ctx: TCtx & InternalCtx;

  /** Pass 1: RuleEngine (obrigatório — core do ciclo). */
  readonly ruleEngine: IRuleEngine<TCtx>;

  /** Função que coleta eventos enfileirados e limpa a fila. */
  readonly collectEvents: () => QueuedEvent[];

  /** Pass 0: ViewEvaluator (opcional — sem ele, skip Pass 0). */
  readonly viewComputer?: IViewComputer<TCtx>;

  /** Pass 2+: EventProcessor (opcional — sem ele, eventos coletados mas não drenados). */
  readonly eventProcessor?: IEventProcessor<TCtx>;

  /** Função get pro ViewEvaluator — router.get bound. Requerido se viewComputer presente. */
  readonly get?: (path: string) => unknown;
}

interface TickRunnerOptions {
  /** Limite de passadas de event drain por ciclo. Default: 10. */
  readonly maxEventPasses?: number;
}

ITickRunner

interface ITickRunner {
  run(env?: RunEnv): RunResult;
  runAsync(env?: RunEnv): Promise<RunResult>;
}

interface RunEnv {
  readonly tick?: number;
  readonly effects?: Record<string, unknown>;
  readonly [key: string]: unknown;
}

run() e runAsync() produzem o mesmo RunResult — a diferença é que runAsync() usa await nos métodos async dos sub-engines (evaluateAsync, processEventsAsync). Com handlers todos sync, ambos produzem resultado idêntico.

createCtx

Cria o ctx persistente com duas camadas: estado (Store) e run (per-run).

function createCtx(router: Router): TickContext & InternalCtx;

createEventBridge

Closure pair que conecta o handler emit do ActionEngine ao TR:

function createEventBridge<TCtx>(): EventBridge<TCtx>;

interface EventBridge<TCtx> {
  readonly emitHandler: HandlerDefinition<TCtx>;
  readonly collectEvents: () => QueuedEvent[];
}

O emitHandler é registrado no ActionEngine como handler "emit". Diretivas { type: "emit", event: "...", data: ... } enfileiram na closure. O TR chama collectEvents() após cada fase pra coletar e limpar a fila.

O handler usa createEmitAnalyzer() do events package — integra com o grafo do Analyzer (capability "emit", dependency para event definitions).

Ctx — Duas Camadas

O ctx expõe dois canais de leitura:

| Canal | Método | Lifetime | Conteúdo | |-------|--------|----------|----------| | Estado | ctx.get(stateId, field?) | Persiste entre runs | Todo estado via Router → Store | | Run | ctx.run(key) | Efêmero por run | tick, effects, dados injetados |

// Estado — persiste, acessível sempre
ctx.get("hp")                    // → Counter.value via Router → Store
ctx.get("combat", "$hpPercent")  // → View Record via Router → Store

// Run — efêmero, morre no fim do run
ctx.run("tick")                  // → 42 (se passado no run)
ctx.run("effects")               // → { playerInput: { key: "space" } }

O ctx é criado uma vez e reutilizado. O run data é substituído a cada run(env).

RunResult

Cada run() retorna resultado estruturado com histórico completo:

const r = runner.run({ tick: 1 });

// Overview
r.halted              // halt em rules? (eventos drenaram normalmente)
r.thrown              // throw? (tudo abortado)
r.thrownAt            // "throw:rule" | "throw:event:pass-1:damage" | undefined
r.totalPasses         // quantos passes de eventos
r.maxPassesExceeded   // ERRO: drain não convergiu?
r.pendingEvents       // eventos não processados

// Rule phase
r.ruleResult          // RuleEvaluationResult completo

// Event phase
r.eventPasses[0]                          // primeiro pass
r.eventPasses[0].events                   // eventos processados
r.eventPasses[0].result                   // EventProcessingResult
r.eventPasses[0].result.eventResults[0]   // per-event: SingleEventResult

// Todos os eventos (convenience)
r.allEvents           // flat de todos os eventos de todos os passes

Ciclo PPP — Mecânica

run(env) / runAsync(env)
  │
  ├── ctx._setRun(env ?? {})     ← injeta run data efêmero no ctx persistente
  │
  ├── FASE 0: Preparar (se viewComputer presente)
  │     viewComputer.evaluate(ctx, get)              // sync
  │     await viewComputer.evaluateAsync(ctx, get)   // async
  │
  ├── FASE 1: Processar (sempre)
  │     ruleResult = ruleEngine.evaluate(ctx)              // sync
  │     ruleResult = await ruleEngine.evaluateAsync(ctx)   // async
  │     throw? → return imediato, eventos descartados
  │
  ├── FASE 2+: Propagar (se eventProcessor presente)
  │     events = collectEvents()
  │     while (events.length > 0 && pass < maxEventPasses):
  │       eventProcessor.processEvents(events, ctx)              // sync
  │       await eventProcessor.processEventsAsync(events, ctx)   // async
  │       throw em evento? → break
  │       events = collectEvents()
  │
  └── return RunResult

Halt vs Throw — detalhado

Halt em rules:

Rule A (priority 900) → executa → emite "saved"
Rule B (priority 800) → halt
Rule C (priority 700) → NÃO executa

Eventos: ["saved"] → drenam normalmente na Fase 2+
halted: true, thrown: false

Throw em rules:

Rule A (priority 900) → executa → emite "saved"
Rule B (priority 800) → throw

Eventos: ["saved"] → DESCARTADOS
thrown: true, thrownAt: "throw:rule"

Throw em events:

Pass 1: "saved" → listener executa → throw

Drain para. Eventos pendentes reportados.
thrown: true, thrownAt: "throw:event:pass-1:saved"

Halt em events — scoped ao evento. Para listeners daquele evento, mas o próximo evento continua. Não para o drain.

Escopo e Limites

O que o TR faz:

  • Orquestra a execução sequencial das 3 fases PPP
  • Coleta eventos via closure bridge e drena via EventProcessor
  • Monta resultado estruturado com histórico de cada fase
  • Respeita halt/throw com semântica fixa

O que o TR não faz:

  • Não cria engines — recebe prontos via DI
  • Não conta ticks — método é run(), não tick()
  • Não gerencia effects — concern do RS
  • Não faz persistência/replay — concern do RS futuro
  • Não registra rules/listeners/views — feito externamente pelo consumer

Testes

77 testes em 6 suites.

| Suite | Testes | O que cobre | |-------|:------:|-------------| | runner.test.ts | 16 | Unit do ciclo PPP com mocks: run data injection, Pass 0/1/2+, halt/throw, multi-pass, reusabilidade | | async.test.ts | 10 | runAsync: operação básica, run data injection, Pass 0/1/2+ async, throw em rules/events, maxPasses, equivalência sync/async | | integration.test.ts | 17 | Ciclo completo via Factory: views→rules→events, recompute entre runs, cascata multi-pass, ping/pong infinite loop, halt/throw semântica, env, múltiplas views | | factory.test.ts | 18 | Wiring da Factory: componentes criados, enableViews opt-in, ctx delegation, run data lifecycle, PPP básico, halt/throw, view Records locked | | context.test.ts | 8 | Unit do ctx: get→Router delegation, run data injection/replacement, persistência | | event-bridge.test.ts | 8 | Unit do bridge: enqueue, drain, ordem, cópia, analyze capability |

Integração — ciclos testados

O integration.test.ts valida o ciclo PPP completo com engines reais:

  • Views → Rules → Events — view computa $isLowHp, rule lê e emite, listener drena
  • Recompute entre runs — hp muda, view reflete, rule reage diferente
  • State continuity — hp 100→50→0 ao longo de 3 runs, rule matcha só no 0
  • Cascata 3 níveis — rule emite chain-1 → listener emite chain-2 → listener emite chain-3
  • Loop infinito — ping/pong detectado por maxEventPasses
  • Halt honra eventos — 2 rules emitem antes do halt, ambos drenam
  • Throw descarta tudo — eventos pendentes reportados, zero passes
  • Múltiplas views — combat + magic com priorities diferentes, ambas acessíveis

Factory (proto-RS) — testes apenas

Uma Factory (createTickSystem) existe em tests/helpers/factory.ts como composition root temporário pra testes de integração. Instancia e conecta todos os packages (Store, Router, engines, ViewEvaluator) e entrega pro TR.

Não é exportada pelo package. Não faz parte da API pública. O RS futuro substituirá como composition root real. Ver tests/helpers/README.md.

Documentação

| Doc | Conteúdo | |-----|----------| | docs/ARCHITECTURE.md | Mecânica interna, módulos, wiring, dependências | | docs/DECISIONS.md | ADRs: ctx, scope, views opt-in, factory, locked records | | docs/IMPLEMENTATION-PROPOSAL.md | Proposta original de implementação | | docs/CHANGELOG.md | Histórico de mudanças |

Exports

// Runner
export { createTickRunner } from "./runner";

// Context
export { createCtx } from "./context";

// Event Bridge
export { createEventBridge } from "./event-bridge";
export type { EventBridge } from "./event-bridge";

// Types
export type {
  TickContext,
  InternalCtx,
  RunEnv,
  TickRunnerDeps,
  TickRunnerOptions,
  EventPassResult,
  RunResult,
  ITickRunner,
} from "./types";

Licença

MIT