@statedelta-apex/router
v3.0.0
Published
ApexStore Router - generic adapter between untyped DSL/engine and typed states via plugin system
Maintainers
Readme
@statedelta-apex/router
Adaptador genérico entre o mundo untyped (JSON DSL, engine tick-based) e o mundo typed (states). Algoritmo puro de roteamento — não conhece nenhum state type concreto. Todo conhecimento especialista vem via plugins declarativos.
Filosofia
Engines tick-based operam com strings: "hp", "set", { value: 30 }. Os states operam com tipos: CounterState.set(30), MatrixState.getCell(0, 1). O Router faz a ponte — recebe operações untyped, resolve o plugin correto via manifest, chama o método do state.
// Engine manda string → Router resolve → State executa
router.dispatch("hp", "dec", { value: 30 });
// internamente: state.decrement(30) via MethodMap ["decrement", "value"]Princípios
- States não sabem que o Router existe — zero acoplamento inverso.
- Store não depende do Router — o Router depende do Store.
- Plugins são declarativos — manifest declara o que existe,
MethodMapmapeia para métodos do state. - Se o state não faz, o plugin não faz — plugin é mapa de rotas, não extensão de funcionalidade.
- Router absorve DispatchResult — plugins executam a mutação, router monta
{ success, changed, value }. - Nunca throw no runtime — reads retornam
undefined, writes retornam{ success: false }. O tick nunca quebra. - Zero parsing no runtime — tudo pré-parseado pelo compiler DSL. Map lookup + handler call.
Instalação
pnpm add @statedelta-apex/routerRequer @statedelta-apex/store como peer dependency.
Quick Start
import { createStore } from "@statedelta-apex/store";
import { createRouter } from "@statedelta-apex/router";
import { createCounter } from "@statedelta-apex/derived-states";
import { counterPlugin } from "@statedelta-apex/derived-states";
// 1. Criar Store e Router
const store = createStore();
const router = createRouter(store);
// 2. Registrar plugins
router.register(counterPlugin);
// 3. Criar e registrar states
const hp = createCounter("hp", { value: 100, min: 0, max: 100 });
store.register(hp);
// 4. Operar via Router
router.get("hp"); // 100 (canonical)
router.get("hp", "percent"); // 1.0 (derived)
router.dispatch("hp", "dec", { value: 30 });
// { success: true, changed: true, value: 70 }
router.get("hp", "value"); // 70
router.get("hp", "percent"); // 0.7Como Funciona
Plugin = Manifest + MethodMap
Cada state type registra um plugin. O plugin declara o que existe (manifest) e qual método do state chamar (handlers via MethodMap):
// MethodMap: string mapeia property/method, array mapeia method + params
type MethodMap = string | [string, ...string[]];
// Exemplo: statemachinePlugin (100% declarativo)
handlers: {
canonical: "current", // → state.current
derived: { states: "states", is: ["is", "state"] },
queries: { can: ["can", "state"], available: "available" },
actions: {
transition: ["transition", "to"], // → state.transition(params.to)
forceState: ["force", "to"], // → state.force(params.to)
reset: "reset", // → state.reset()
},
}Function handlers existem apenas para exceções que o MethodMap não cobre (path resolution, spread args, predicates, conditional logic).
Três Tiers de Leitura
| Tier | API | Custo | Descrição |
| ---- | ---------------------------------- | ----- | -------------------------------- |
| 1 | router.get(id, field) | O(1) | Campo direto do getState() |
| 2 | router.get(id, field, ...args) | O(1) | Derivação computada pelo handler |
| 3 | router.query(id, method, params) | O(n+) | Busca, filtro, enumeração |
Tier 3 (query) valida params contra o schema do manifest e absorve throws — mesma garantia "nunca throw" do dispatch.
// Tier 1 — campo direto (só Counter e StateMachine)
router.get("hp", "value"); // state.getState().value
// Tier 2 — derived com handler
router.get("hp", "percent"); // handler computa
router.get("phase", "is", "playing"); // handler recebe arg
// Tier 3 — query O(n+)
router.query("inv", "filter", { where: (i) => i.damage > 10 });
router.query("phase", "available");Escrita
const result = router.dispatch("hp", "dec", { value: 30 });
// { success: true, changed: true, value: 70 }
// Params validados contra o schema do manifest
const bad = router.dispatch("hp", "set", { value: "text" });
// { success: false, changed: false, error: "expected number, got string" }Null como valor explícito
ParamDef.nullable: true permite passar null como valor legítimo (não tratado como "ausente"):
// matrix com defaultValue: null — limpar célula
router.dispatch("board", "set", { row: 0, col: 0, value: null });
// { success: true, changed: true, value: [[null, ...], ...] }
// Sem nullable: true, null seria rejeitado
// { success: false, error: "param 'value' cannot be null" }Plugins oficiais marcam nullable: true onde faz sentido: matrix (set/fill/etc.), record (set), list (push/unshift). Counter/flags/statemachine não — seus params são scalars estritos.
O dispatch sempre retorna DispatchResult. Nunca throw. O value é sempre o canonical do state após a action. O changed é determinado pelo router via ref comparison.
API
createRouter(store, options?): Router
const router = createRouter(store, {
onError: (err) => console.warn(err), // opcional
});router.register(plugin)
Registra um plugin de state type.
router.get(stateId, field?, ...args): unknown
Leitura Tier 1+2.
router.get("hp"); // canonical: 100
router.get("hp", "value"); // field: 100
router.get("hp", "percent"); // derived: 0.7
router.get("config", "volume"); // fallback: 80router.query(stateId, method, params?): unknown
Leitura Tier 3.
router.query("inv", "filter", { where: (i) => i.damage > 10 });
router.query("board", "find", { value: "X" });
router.query("phase", "can", { state: "playing" });router.dispatch(stateId, action, params?): DispatchResult
Escrita com validação de params. value é sempre o canonical do state após a action.
router.dispatch("hp", "dec", { value: 30 });
router.dispatch("board", "set", { row: 0, col: 0, value: "X" });
router.dispatch("phase", "transition", { to: "playing" });router.validate(stateId, kind, name, params?): ValidationResult
Validação sem execução — checa existência e tipos de params.
router.validate("hp", "get", "percent"); // { valid: true }
router.validate("hp", "dispatch", "fly"); // { valid: false, error: "..." }router.batch(commands): DispatchResult[]
N dispatches em sequência. Cada um independente.
router.batch([
{ state: "hp", action: "dec", params: { value: 30 } },
{ state: "board", action: "set", params: { row: 0, col: 0, value: "X" } },
]);router.transaction(fn) / router.simulate(fn)
Delega pro Store. Transaction = commit no sucesso, rollback no throw. Simulate = sempre rollback.
router.transaction(() => {
router.dispatch("hp", "dec", { value: 30 });
router.dispatch("board", "set", { row: 0, col: 0, value: "X" });
// throw → rollback de TODOS os states
});
const wouldDie = router.simulate(() => {
router.dispatch("hp", "dec", { value: 999 });
return router.get("hp", "value") === 0;
});
// wouldDie === true, hp intactoIntrospection
router.getManifest("counter"); // StateManifest | undefined
router.listTypes(); // ["counter", "record", ...]
router.hasType("counter"); // trueTratamento de Erros
O Router opera em runtime de engine — o tick nunca quebra. Erros são dados.
| Método | Sucesso | Falha | Throw? |
| ------------ | ----------------------------------- | --------------------------- | ------ |
| get() | valor | undefined | nunca |
| query() | valor | undefined + lastError | nunca |
| dispatch() | { success: true, changed, value } | { success: false, error } | nunca |
| validate() | { valid: true } | { valid: false, error } | nunca |
query() valida params igual dispatch() e absorve throws do handler — predicates que explodem viram undefined + lastError, não exceptions.
lastError — Distinguir undefined legítimo vs erro
const value = router.get("hp", "percent");
if (router.lastError) {
// undefined por erro
console.warn(router.lastError.detail);
} else {
// undefined legítimo (valor real do state)
}onError callback
createRouter(store, {
onError: (err) => engineLog.warn(err),
});RouterError
interface RouterError {
type:
| "state_not_found"
| "unknown_accessor"
| "unknown_query"
| "unknown_action"
| "invalid_params";
stateId: string;
detail: string;
}Plugins Disponíveis
Cada state package exporta seu plugin. O consumer registra os que precisa.
| Plugin | Pacote | Type | Canonical |
| -------------------- | ----------------------------------- | ---------------- | --------------- |
| counterPlugin | @statedelta-apex/derived-states | "counter" | valor numérico |
| flagsPlugin | @statedelta-apex/derived-states | "flags" | flags ativas |
| statemachinePlugin | @statedelta-apex/derived-states | "statemachine" | estado atual |
| recordPlugin | @statedelta-apex/record-state | "record" | objeto completo |
| collectionPlugin | @statedelta-apex/collection-state | "collection" | array de items |
| matrixPlugin | @statedelta-apex/matrix-state | "matrix" | grid T[][] |
| listPlugin | @statedelta-apex/list-state | "list" | array de items |
import {
counterPlugin,
flagsPlugin,
statemachinePlugin,
} from "@statedelta-apex/derived-states";
import { recordPlugin } from "@statedelta-apex/record-state";
import { collectionPlugin } from "@statedelta-apex/collection-state";
import { matrixPlugin } from "@statedelta-apex/matrix-state";
import { listPlugin } from "@statedelta-apex/list-state";
router.register(counterPlugin);
router.register(recordPlugin);
router.register(collectionPlugin);
router.register(matrixPlugin);
router.register(listPlugin);
router.register(flagsPlugin);
router.register(statemachinePlugin);Tipos Exportados
import { createRouter } from "@statedelta-apex/router";
import type {
Router,
RouterOptions,
RouterError,
StateManifest,
StatePlugin,
StateHandlers,
MethodMap,
DerivedHandler,
QueryHandler,
ActionHandler,
FallbackHandler,
FieldEntry,
DerivedEntry,
QueryEntry,
ActionEntry,
ParamDef,
ReturnDef,
ScalarType,
DispatchResult,
ValidationResult,
BatchCommand,
} from "@statedelta-apex/router";Referência Completa
Para documentação detalhada de cada operação por state type (com todos os exemplos de get/query/dispatch), consulte o API.md.
Para decisões de arquitetura e design interno, consulte o ARCHITECTURE.md.
