@statedelta-axiom/realm-system
v0.1.0
Published
RealmSystem — composition root funcional. Cria engines, boota definitions, executa ticks via TickRunner
Readme
@statedelta-axiom/realm-system
Composition root do sistema statedelta-actions. Cria engines, boota definitions, executa ticks.
O RealmSystem (RS) é o System — o porteiro que virtualiza o Realm. Recebe physics (leis do mundo), cria toda a infraestrutura internamente, aceita definitions no boot, e executa ticks via TickRunner. O customer não toca em engines nem handlers — só em physics, definitions e a API pública.
Filosofia
RS = composition root. Não é engine, não é orquestrador. É quem monta tudo e opera. O customer diz o que quer (physics + definitions). O RS decide como fazer (engines, handlers, wiring).
Duas fases de construção (ADR-051). Physics define as leis do mundo (imutável). Boot define os recursos iniciais (states, actions, rules, events, views). São concerns separados — regras do jogo vs estado inicial do jogo.
States via Store (ADR-052). Todo state é criado via store.create() com factory registry. O RS nunca instancia states diretamente. O Store do apex-store coordena transações, snapshot/restore e lifecycle.
Handlers são built-in (ADR-044). O RS cria e registra 6 handlers internamente (dispatch, action, emit, halt, notify, request). O customer não passa handlers — são concern do RS que tem o contexto necessário pra montá-los.
O Realm não sabe onde está. Como o Neo na Matrix — opera com o que recebeu. Não sabe se está num RS, Sandbox, teste, ou é child de outra sandbox. O System processa e virtualiza.
Boot com erro não transiciona. Se o boot tiver erros, o RS emite error e permanece em "created". O realm não bootou.
Erro no step re-throw. Se o TickRunner lança exceção, o RS emite error e re-lança. O tick não completa — transition não capturada, step:complete não emitido.
Instalação
pnpm add @statedelta-axiom/realm-systemQuick Start
import { createRealmSystem } from "@statedelta-axiom/realm-system";
// 1. Construção — physics define as leis do mundo
const rs = createRealmSystem({
enableViews: true,
enableEvents: true,
temporal: { stepSize: 4, cycleSize: 10 },
});
// 2. Boot — definitions populam o realm, env injeta birth data
const bootResult = rs.boot({
states: [
{ id: "hp", type: "counter", data: { value: 100, min: 0, max: 100 } },
{ id: "score", type: "counter", data: { value: 0 } },
{ id: "status", type: "flags", data: { initial: ["alive"] } },
],
actions: [
{
id: "heal",
directives: [
{ type: "dispatch", target: "hp", op: "inc", value: 20 },
{ type: "notify", event: "healed", data: { amount: 20 } },
],
},
],
rules: [
{
id: "auto-heal",
priority: 100,
when: (ctx) => (ctx.get("hp") as number) < 30,
then: [{ type: "action", id: "heal" }],
},
{
id: "death-check",
priority: 500,
when: (ctx) => (ctx.get("hp") as number) <= 0,
then: [
{ type: "dispatch", target: "status", op: "remove", value: "alive" },
{ type: "halt" },
],
},
],
events: [
{
id: "on-damaged",
priority: 100,
on: "damaged",
then: [{ type: "dispatch", target: "score", op: "inc", value: 5 }],
},
],
}, { env: { previousRealm: "forest", difficulty: "hard" } });
// 3. Ticks — executa PPP (Preparar → Processar → Propagar)
const step1 = rs.step(); // tick 0
const step2 = rs.step(); // tick 1
// 4. Leitura
rs.get("hp"); // valor atual
rs.get("hp", "max"); // field específico
// 5. Operações imperativas (entre ticks)
rs.invoke("heal"); // invoca action diretamente
rs.dispatch("hp", "set", { value: 50 }); // muta estado diretamente
rs.emit("damaged", { amount: 10 }); // enfileira evento
rs.processEvents(); // drena buffer de eventos
// 6. Observação
rs.on("step:complete", ({ tick, result }) => { /* ... */ });
rs.on("notify", ({ event, data }) => { /* ... */ });
// 7. Consulta — sempre disponível, sem guards
rs.getPhase(); // "created" | "booted" | "running"
rs.getTick(); // número do tick atual (-1 antes do primeiro step)
rs.isIdle(); // true entre ticks
rs.hasState("hp");
rs.hasAction("heal");
rs.hasRule("auto-heal");
rs.getView("combat");Lifecycle
created ──boot()──→ booted ──step()──→ running ──step()──→ running ...
↑ ↑
boot com erro step com erro
permanece created emite error + re-throw| Transição | Quando | Validação |
|-----------|--------|-----------|
| created → booted | boot() com success | Boot sem erros |
| created → created | boot() com erro | Boot com erros — não transiciona |
| booted → running | Primeiro step() ou stepAsync() | Phase booted ou running |
| running → running | Steps subsequentes | Phase booted ou running |
Physics — Leis do Mundo
Imutável após construção. Qualquer mudança = criar outro realm.
interface RealmPhysics {
allowedDirectives?: DirectivePermissionEntry[]; // whitelist (exclusivo com blocked)
blockedDirectives?: DirectivePermissionEntry[]; // blacklist
limits?: Partial<FrameLimits>; // maxDepth, maxRules, maxDirectives
maxEventPasses?: number; // drain loop limit (default: 10)
async?: boolean; // pausa intra-tick (default: false)
enableViews?: boolean; // Pass 0 (default: false)
enableEvents?: boolean; // Pass 2+ (default: true)
temporal?: { stepSize?: number; cycleSize?: number }; // default: 1:1
schemaValidator?: ISchemaValidator; // validação de effects (opt-in, DI)
imperatives?: boolean; // habilita invoke/dispatch/emit/processEvents (default: true)
}System Config — Tuning
Tuning de processamento e injeção de comportamento.
interface RealmSystemConfig {
mode?: "interpret" | "jit" | "auto"; // compilação (default: "auto")
autoJitThreshold?: number; // auto-promote (default: 8)
analyzer?: boolean | AnalyzerConfig; // ActionAnalyzer (default: false)
debug?: boolean; // eventos internos (default: false)
handlers?: Record<string, HandlerDefinition<RealmCtx>>; // handlers customizados
}Handlers Customizados
Handlers injetados via systemConfig.handlers são registrados no ActionEngine
junto com os 6 built-in. O RS não sabe o que fazem — mesmo pattern de DI.
const rs = createRealmSystem({}, {
handlers: {
sandbox: mySandboxHandler,
analytics: myAnalyticsHandler,
},
});Actions podem usar diretivas dos handlers customizados normalmente:
{ id: "create-child", directives: [{ type: "sandbox", target: "child-1" }] }Boot Definitions
Todos os campos opcionais. Tipos definidos em @statedelta-axiom/contracts.
interface BootDefinitions<TCtx> {
states?: StateDefinition[];
actions?: ActionDefinition<TCtx>[];
rules?: RuleDefinition<TCtx>[];
events?: EventListenerDefinition<TCtx>[];
eventDefinitions?: EventDefinition[];
views?: ViewDefinition<TCtx>[];
}StateDefinition — 7 types
{ id: "config", type: "record", data: { volume: 80, theme: "dark" } }
{ id: "hp", type: "counter", data: { value: 100, min: 0, max: 100 } }
{ id: "inv", type: "collection", data: [{ id: "sword", damage: 10 }] }
{ id: "queue", type: "list", data: ["task-1", "task-2"] }
{ id: "board", type: "matrix", data: { rows: 3, cols: 3, defaultValue: null } }
{ id: "status", type: "flags", data: { initial: ["alive"] } }
{ id: "phase", type: "statemachine", data: { initial: "menu", states: ["menu", "playing"], transitions: [{ from: "menu", to: "playing" }] } }BootResult
result.success; // true se zero erros
result.registered.states; // IDs registrados
result.registered.actions;
result.registered.rules;
result.registered.events;
result.registered.views;
result.errors; // [{ resource, id, code, message }]Erros são isolados — falha em um recurso não impede os outros.
Boot Options
Segundo argumento opcional do boot(). Injeta dados persistentes no ctx.env().
rs.boot(definitions, {
env: { score: 500, previousRealm: "forest" }, // birth data
});Birth data é acessível via ctx.env("score") em qualquer tick. Persiste
durante toda a vida do realm.
Effects — Input Externo Passivo
Effects são o mecanismo passivo de input externo. A App deposita dados, rules reagem no próximo tick. Key-value, um effect por tipo por tick.
// App deposita entre ticks
rs.applyEffect("user-input", { key: "space" });
rs.applyEffect("mouse", { x: 10, y: 20 });
// Rules consultam durante o tick
ctx.run("effects") // → { "user-input": { key: "space" }, "mouse": { x: 10, y: 20 } }
// Effects morrem no fim do tick — per-runRequest — Realm Pede Input
A diretiva { type: "request", id: "..." } permite que o realm solicite
input externo. O tick completa normalmente. O lock entra em vigor pro
próximo step() — o System trava até que a App forneça o effect solicitado.
// Rule solicita input com JSON Schema opcional
{ type: "request", id: "user-choice", schema: {
type: "object",
properties: { option: { type: "string" } },
required: ["option"],
}}
// App escuta o evento e responde
rs.on("request", ({ id }) => {
showDialog().then(answer => rs.applyEffect(id, answer));
});
// Próximo step() trava se request não satisfeito
rs.step(); // → Error: System is locked. Pending requests: user-choiceapplyEffect() satisfaz o request e deslocka. O effect fica disponível
em ctx.run("effects") no tick seguinte.
Schema Validation (ADR-021)
Requests podem declarar um JSON Schema (Draft-07) que define o formato esperado do payload. A validação é opt-in — só acontece quando:
- O request declara
schema - O RS recebeu
schemaValidatorna physics (DI)
Se o payload falha na validação, applyEffect() lança erro e o lock é
mantido — o request continua pendente.
import type { ISchemaValidator } from "@statedelta-axiom/contracts";
// Server injeta o validator (ex: wrapper de ajv)
const rs = createRealmSystem({
schemaValidator: myAjvValidator, // implementa ISchemaValidator
});Sem schemaValidator, schemas são ignorados — zero overhead.
Sem schema no request, o validator não é chamado — aceita qualquer payload.
Async Mode (ADR-024)
O RS suporta execução async via physics.async: true. Em async mode,
o tick pode conter hooks async (directive hooks, rule hooks, event hooks)
que são aguardados via await.
// Construção com async habilitado
const rs = createRealmSystem({ async: true, enableViews: true });
rs.boot(definitions);
// stepAsync() em vez de step()
const result = await rs.stepAsync();Dual-method: step() vs stepAsync()
| Método | Quando usar | O que faz |
|--------|------------|-----------|
| step() | Realm sync (default) | Chama tickRunner.run() — sync |
| stepAsync() | Realm async (physics.async: true) | Chama await tickRunner.runAsync() — async |
step() em async mode lança erro:
"Cannot call step() in async mode. Use stepAsync() instead."stepAsync() funciona em ambos os modos — em realm sync, simplesmente
aguarda Promises que resolvem imediatamente.
Validação de coerência no boot
Se hooks async são detectados mas physics.async não está habilitado,
o boot lança erro:
"Async hooks detected but physics.async is not enabled."Isso previne misconfiguration silenciosa — hooks async em realm sync
causariam throw no evaluate() do RuleEngine/EventProcessor.
Na direção oposta, physics.async: true com hooks todos sync é válido —
o realm declara capacidade async sem ter hooks async no momento.
Propagação transitiva
ActionEngine (directiveHooks async?)
└── isAsync = true
├── RuleEngine herda
├── EventProcessor herda
└── ViewEvaluator herda
└── RS detecta no bootSnapshot / Restore
Captura e restaura estado completo do realm. Não inclui definitions — concern da Sandbox/DSL layer.
// Captura após ticks
rs.step();
rs.step();
const snap = rs.snapshot();
// Mais ticks...
rs.step();
rs.step();
// Restaura — volta pro ponto do snapshot
rs.restore(snap);
rs.step(); // continua do ponto restauradoO snapshot inclui: Store (todos os states + $transition), tick, phase, birth data (env), pending requests e pending effects.
StepResult
step() e stepAsync() retornam o mesmo tipo:
const result = rs.step(); // sync
const result = await rs.stepAsync(); // async — mesmo StepResult
result.tick; // número do tick executado (0, 1, 2, ...)
result.runResult; // resultado do PPP (ruleResult, eventPasses, etc)
result.halted; // true se halt ocorreu
result.thrown; // true se throw ocorreu
result.requests; // requests emitidos neste tick ([{ id, schema? }])RealmCtx — 4 Canais
O contexto que rules, views e ops recebem durante o tick.
ctx.get("hp") // estado via Router → Store
ctx.get("hp", "max") // field específico
ctx.get("combat:$pct") // field de view (split por :)
ctx.run("tick") // dados efêmeros do tick (tick, step, effects, etc)
ctx.env("score") // birth data (imutável, setado no boot)
ctx.getDelta("hp") // { previous: 100, changed: true }
// previous = início do tick anterior (pre-mutation)
// current = estado ao vivo (inclui mutations intra-tick)Paths com : são splitados automaticamente: "combat:$pct" →
router.get("combat", "$pct"). Isso permite cross-view deps e
acesso a fields de qualquer state type.
System Events
Canal de lifecycle events separado dos eventos internos do realm. EventEmitter type-safe próprio do RS.
// Lifecycle
rs.on("booted", ({ result }) => { /* boot completou */ });
rs.on("step:complete", ({ tick, result }) => { /* tick completou */ });
rs.on("notify", ({ event, data }) => { /* diretiva notify disparou */ });
rs.on("request", ({ id, schema }) => { /* realm pede input externo */ });
rs.on("error", ({ phase, error }) => { /* erro no boot ou step */ });
// Imperativas (ADR-043 — hooks pra registro/replay)
rs.on("imperative:invoke", ({ id, params, tick }) => { /* ... */ });
rs.on("imperative:dispatch", ({ stateId, op, params, tick }) => { /* ... */ });
rs.on("imperative:emit", ({ event, data, tick }) => { /* ... */ });
rs.on("imperative:processEvents", ({ eventsProcessed, tick }) => { /* ... */ });
// Unsubscribe
const unsub = rs.on("step:complete", listener);
unsub(); // para de ouvirOperações Imperativas (ADR-038)
Ações diretas executadas entre ticks, quando o System está idle.
Requerem: phase === "running", isIdle() === true, physics.imperatives !== false.
// Invoca action diretamente — eventos gerados ficam no buffer
const result = rs.invoke("heal", { target: "player" });
// Muta estado diretamente
rs.dispatch("hp", "set", { value: 50 });
// Enfileira evento — não drena imediatamente
rs.emit("damaged", { amount: 10 });
// Drena buffer de eventos (drain loop completo)
rs.processEvents();Variantes async: rs.invokeAsync(), rs.processEventsAsync().
Eventos gerados por invoke() ficam no buffer — drenam via processEvents() ou
no próximo step(). Cada operação emite system event (imperative:invoke, etc)
pra registro pela lib superior.
Desabilitação via physics.imperatives: false — todas as imperativas lançam erro.
Consulta (ADR-039)
Métodos de leitura direto no RS. Sempre disponíveis, qualquer phase, sem guards. Delegam pros engines internos — zero estado novo, zero overhead.
// Lifecycle
rs.getPhase() // "created" | "booted" | "running"
rs.getTick() // -1 antes do primeiro step
rs.isIdle() // true entre ticks
rs.isAsync() // true se hooks async detectados
rs.getCompilationMode() // "interpret" | "jit"
// States
rs.getStateIds() // ["hp", "config", ...]
rs.hasState("hp") // true
// Actions
rs.getActionIds() // ["heal", "damage", ...]
rs.hasAction("heal") // true
rs.getActionDefinition("heal") // ActionDefinition | undefined
// Rules
rs.getRuleCount() // 2
rs.hasRule("auto-heal") // true
// Events
rs.getEventDefinition("hit") // EventDefinition | undefined
// Views
rs.getViewCount() // 1
rs.hasView("combat") // true
rs.getView("combat") // IViewRecord (dados computados, locked após step)
rs.getViewStateDeps() // Set<string> — paths observadosRecords de views ficam locked (read-only) após cada step. O evaluator
faz unlock → compute → lock a cada tick. Tentativa de set() em view
locked lança erro — protege contra mutação externa entre ticks.
Temporal
Subdivisão temporal via physics. Default 1:1 (tick === step, sem cycle).
{ temporal: { stepSize: 4, cycleSize: 10 } }| Counter | Descrição | Acesso |
|---------|-----------|--------|
| tick | Global, sempre incrementa | ctx.run("tick") |
| step | Incrementa a cada stepSize ticks | ctx.run("step") |
| tickAt | Posição do tick no step (1..stepSize) | ctx.run("tickAt") |
| cycle | Incrementa a cada cycleSize steps | ctx.run("cycle") |
| stepAt | Posição do step no cycle (1..cycleSize) | ctx.run("stepAt") |
Factories — Uso em Testes
As duas fases de construção são funções puras exportáveis (ADR-053).
import {
createSystemComponents,
bootDefinitions,
} from "@statedelta-axiom/realm-system";
// Fase 1 — cria infraestrutura
const components = createSystemComponents({ enableViews: true });
// Fase 2 — popula realm
const result = bootDefinitions(components, {
states: [{ id: "hp", type: "counter", data: { value: 100 } }],
rules: [{ id: "r1", priority: 1, when: () => true, then: [...] }],
});
// Testar sem lifecycle, sem ticks
components.store.has("hp"); // true
components.ruleEngine.has("r1"); // true
components.actionEngine.invoke("heal", undefined, components.ctx);Exports
// RS
import { createRealmSystem } from "@statedelta-axiom/realm-system";
// Factory
import {
createSystemComponents,
bootDefinitions,
} from "@statedelta-axiom/realm-system";
import type { SystemComponents } from "@statedelta-axiom/realm-system";
// Módulos auxiliares
import {
createRealmCtx,
createTransitionStore,
createBuiltinHandlers,
createRequestBridge,
computeTemporalEnv,
buildRunEnv,
initObservedPaths,
captureObservedValues,
EventEmitter,
} from "@statedelta-axiom/realm-system";
// Types
import type {
RealmPhysics,
RealmSystemConfig,
SystemPhase,
StepResult,
RequestEntry,
RealmSnapshot,
RealmCtx,
DeltaInfo,
BootOptions,
SystemEvents,
IRealmSystem,
TransitionStore,
TemporalCounters,
IEventEmitter,
} from "@statedelta-axiom/realm-system";Arquitetura
Para internals técnicos, ver docs/ARCHITECTURE.md.
Para decisões arquiteturais (59 ADRs), ver docs/DECISIONS.md.
Licença
MIT
