apex-store
v0.3.0
Published
ApexStore — Typed state management with specialized primitives, derived types, transactions and registry pattern
Maintainers
Readme
apex-store
Typed state management com primitivos especializados, tipos derivados, transações aninhadas e registry pattern.
Um install, um import — zero peer dependency headaches.
import {
createStore,
createRecord,
createCollection,
createList,
createMatrix,
createCounter,
createFlags,
createStateMachine,
} from "apex-store";Instalação
pnpm add apex-storePor que apex-store?
State managers tradicionais (Redux, Zustand) armazenam objetos genéricos — o consumer escreve reducers pra mutar e selectors pra ler. O apex-store inverte isso: cada state é um store completo e autossuficiente que já sabe como armazenar, mutar, transacionar, notificar listeners e serializar.
O consumer só chama actions tipadas e getters tipados. Toda mecânica interna (shallow clone, strategy swap, middleware chain, transaction stack) é invisível.
Princípios
- Quem não usa, não paga — features inativas têm custo zero. Sem subscriber? Zero overhead. Sem middleware? Zero overhead. Sem transaction? Zero overhead.
- States são autossuficientes — funcionam standalone ou gerenciados por um Store. Transaction, subscribe, middleware, snapshot — tudo built-in.
- Store orquestra, não implementa — delega tudo pros states via interface
State<T>. Coordena transações cross-state, persistence global e lifecycle hooks. - Composição sobre herança — tipos derivados (Counter, Flags, StateMachine) compõem com um primitivo, expondo apenas actions de domínio. Mecânica interna 100% encapsulada.
- Imutabilidade por convenção — toda mutação gera nova referência via shallow clone. Referências anteriores permanecem intactas.
- Zero branches no hot path — o pipeline de mutação usa strategy swap (8 funções pré-definidas). A cada mutação é uma chamada direta — sem
if/else.
Arquitetura
┌──────────────────────────────────────────────────────┐
│ Store (orquestrador) │
│ registry · transactions · persistence · lifecycle │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Record │ │ Collection │ │ Matrix │ │
│ │ key/val │ │ ID-based │ │ 2D grid │ │
│ └──────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────┐ │
│ │ List │ 4 Primitivos │
│ │ positional│ │
│ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Counter │ │ Flags │ │ StateMachine │ │
│ │ num+bounds│ │ string set │ │ FSM + graph │ │
│ └──────────┘ └──────────────┘ └──────────────────┘ │
│ 3 Derivados │
└──────────────────────────────────────────────────────┘Quick Start
import {
createStore,
createCounter,
createCollection,
createStateMachine,
} from "apex-store";
// States — funcionam standalone, Store é opt-in
const hp = createCounter("hp", { value: 100, min: 0, max: 100 });
const inventory = createCollection<Item>("inventory", []);
const phase = createStateMachine("phase", {
initial: "menu",
states: ["menu", "playing", "paused", "gameover"],
transitions: [
{ from: "menu", to: "playing" },
{ from: "playing", to: ["paused", "gameover"] },
{ from: "paused", to: ["playing", "menu"] },
],
});
// Store — orquestra N states
const store = createStore();
store.register(hp);
store.register(inventory);
store.register(phase);
// Transaction atômica cross-state
store.transaction(() => {
phase.transition("playing");
hp.decrement(30);
inventory.add({ id: "sword", name: "Sword", damage: 10 });
// throw em qualquer ponto → rollback de TODOS os states tocados
});
// Simulação — sempre rollback, retorna resultado
const wouldDie = store.simulate(() => {
hp.decrement(999);
return hp.isMin;
});
// wouldDie === true, hp.value ainda 100
// Reatividade global — qualquer state muta, listener dispara
const unsub = store.subscribe((id, state, prev) => {
console.log(`${id} mudou`);
});
// Persistence — snapshot e restore de todos os states
const snap = store.snapshot();
// ... tempo passa ...
store.restore(snap);Store
O Store é o orquestrador central. Coordena registry, transações entre states, persistence global, factory registry e lifecycle hooks.
Registry
const store = createStore();
store.register(hp);
store.register(inventory);
store.register(board);
store.get<CounterState>("hp"); // busca tipada por ID
store.getByType("record"); // O(1) via index
store.has("hp"); // true
store.size; // 3
store.all(); // todos os states
store.cleanup((s) => s.getState().value === 0); // remove por condição
store.clear(); // remove todosFactory Registry
Cria states via Store sem conhecer implementações concretas — inversion of control.
store.defineType("counter", (id, data) => createCounter(id, data));
store.defineType("collection", (id, data) => createCollection(id, data));
// Criação via Store — auto-register
const hp = store.create<CounterState>("counter", "hp", {
value: 100,
min: 0,
max: 100,
});
store.has("hp"); // trueTransactions Cross-State
Lazy snapshot — states não tocados não pagam nada. Nesting arbitrário com adoption strategy.
// Atômico — commit no sucesso, rollback no throw
store.transaction(() => {
hp.decrement(50);
mp.decrement(30);
phase.transition("gameover");
// throw em qualquer ponto → rollback de TODOS os states tocados
});
// Simulação — SEMPRE rollback
const result = store.simulate(() => {
hp.decrement(999);
return hp.isMin; // true
});
// hp.value ainda intacto
// Nesting — simulate dentro de transaction
store.transaction(() => {
hp.decrement(20);
const wouldDie = store.simulate(() => {
hp.decrement(999);
return hp.isMin;
});
// hp.value ainda com -20 do nível pai
if (wouldDie) phase.transition("gameover");
});Reatividade Global
// Disparado quando QUALQUER state registrado muta
const unsub = store.subscribe((id, state, prevState) => {
console.log(`${id} mudou`);
});
// Estado combinado de todos os states (referências diretas, zero clone)
store.getState();
// { hp: { value: 70, ... }, mp: { value: 20, ... }, ... }Lifecycle Hooks
store.onRegister((state) => console.log(`Registered: ${state.id}`));
store.onUnregister((state) => console.log(`Removing: ${state.id}`));Persistence Global
const snap = store.snapshot();
localStorage.setItem("game", JSON.stringify(snap));
// Depois...
const saved = JSON.parse(localStorage.getItem("game")!);
store.restore(saved);Record
Armazenamento key-value tipado com acesso deep path, transações aninhadas e imutabilidade por convenção. O primitivo mais versátil — cobre configurações, entidades, formulários, estados de UI.
const user = createRecord("user", {
name: "Anderson",
age: 30,
role: "admin",
settings: { theme: "dark", lang: "pt-BR" },
});Actions
// Set — type-safe (key é keyof T, value é T[K])
user.set("name", "Rosa");
user.set("age", 31);
// Merge — batch, uma mutação, um notify
user.merge({ role: "superadmin", age: 32 });
// Update — functional, atômico
user.update("age", (v) => v + 1);
// Deep path — sem dot-notation parsing
user.setPath(["settings", "theme"], "light");
user.updatePath(["settings", "lang"], (v) => (v as string).toUpperCase());
// Delete
user.delete("role");
// Replace / Reset
user.replace({ name: "New", age: 0, settings: { theme: "dark", lang: "en" } });
user.reset(); // volta ao estado da criaçãoGetters
user.get("name"); // "Anderson" — type-safe
user.getPath(["settings", "theme"]); // "dark"
user.getOr("nickname", "N/A"); // fallback se undefined
user.pick("name", "age"); // { name: "Anderson", age: 30 }
user.omit("settings"); // { name: "Anderson", age: 30, role: "admin" }
user.has("name"); // true
user.hasPath(["settings", "x"]); // false
user.keys(); // ["name", "age", "role", "settings"]
user.values(); // ["Anderson", 30, "admin", { ... }]
user.entries(); // [["name", "Anderson"], ...]
user.size; // 4
user.isEmpty; // falseTransactions
// Commit no sucesso, rollback no throw
user.transaction(() => {
user.set("name", "temp");
user.set("age", 0);
throw new Error("abort");
});
// name ainda "Anderson", age ainda 30
// Simulate — sempre rollback
const preview = user.simulate(() => {
user.set("age", 99);
return user.get("age");
});
// preview === 99, user.get("age") ainda 30Subscribe & Middleware
// Reatividade
const unsub = user.subscribe((state, prev) => {
console.log(`name: ${prev.name} → ${state.name}`);
});
// Middleware — validação, logging, blocking
user.use((current, next, proceed) => {
if (next.age < 0) return; // bloqueia mutações inválidas
proceed();
});Collection
Lista ordenada de items tipados com identidade por ID único. Toda operação CRUD é ID-based. Duplicatas são proibidas. Cobre users, products, inventory, decks de cartas — qualquer lista de entidades.
const inventory = createCollection<Item>("inventory", [
{ id: "sword", name: "Sword", damage: 10 },
{ id: "shield", name: "Shield", defense: 5 },
]);Actions
// Add — valida unicidade por ID
inventory.add({ id: "potion", name: "Potion", heal: 20 });
// Batch add — N items, uma mutação, um notify
inventory.addMany([
{ id: "bow", name: "Bow", damage: 8 },
{ id: "arrow", name: "Arrow", damage: 1 },
]);
// Upsert — add se não existe, merge se existe
inventory.upsert({ id: "sword", name: "Sword +1", damage: 15 });
// Update — partial merge ou functional
inventory.update("sword", { damage: 20 });
inventory.update("sword", (item) => ({ ...item, damage: item.damage + 5 }));
// Update em batch por predicado
inventory.updateBy(
(item) => item.damage > 10,
(item) => ({ ...item, enchanted: true }),
);
// Remove — por ID ou predicado
inventory.remove("arrow");
inventory.removeBy((item) => item.damage < 5);
// Sort, replace, reset, clear
inventory.sort((a, b) => b.damage - a.damage);
inventory.replace(newItems);
inventory.reset(); // volta aos items da criação
inventory.clear(); // remove todosGetters
inventory.getById("sword"); // { id: "sword", ... } | undefined
inventory.has("sword"); // true
inventory.at(0); // primeiro item
inventory.at(-1); // último item
inventory.indexOf("sword"); // 0
inventory.findBy((i) => i.damage > 10); // primeiro match
inventory.filterBy((i) => i.damage > 10); // todos os matches
inventory.every((i) => i.damage > 0); // true
inventory.some((i) => i.heal); // true
inventory.map((i) => i.name); // ["Sword", "Shield", ...]
inventory.size; // 5
inventory.isEmpty; // false
inventory.first; // primeiro item
inventory.last; // último item
inventory.items; // referência ao array (alias de getState())
inventory.idField; // "id"Custom ID field
const cards = createCollection<Card>("cards", initialCards, {
idField: "cardId", // usa "cardId" em vez de "id"
});List
Lista ordenada tipada sem semântica de identidade. Qualquer tipo T, duplicatas válidas, acesso dual-end, capacidade opcional. A ordem é o que importa, não quem é o item. Base natural pra Queue (FIFO), Stack (LIFO), buffers, logs, histórico.
const tasks = createList<string>("tasks", ["process", "validate"]);
const scores = createList<number>("scores", [100, 85, 92]);
const buffer = createList<Message>("buffer", [], { maxSize: 100 });Actions
// Dual-end — append/prepend
tasks.append("deploy");
tasks.prepend("urgent-fix");
tasks.appendMany(["test", "release"]);
tasks.prependMany(["lint", "format"]);
// Dual-end — remove e retorna
const next = tasks.removeFirst(); // "urgent-fix"
const last = tasks.removeLast(); // "release"
// Replace, reset, clear
tasks.replace(["a", "b", "c"]);
tasks.reset(); // volta ao estado da criação
tasks.clear(); // remove todosGetters
tasks.peekFirst(); // olha o início sem remover
tasks.peekLast(); // olha o final sem remover
tasks.at(0); // acesso posicional
tasks.at(-1); // último
tasks.find((t) => t.startsWith("urgent"));
tasks.findIndex((t) => t === "deploy");
tasks.includes("process");
tasks.indexOf("process");
tasks.every((t) => t.length > 0);
tasks.some((t) => t.startsWith("urgent"));
tasks.toArray(); // cópia segura pra mutar
tasks.items; // referência direta
tasks.size; // count
tasks.isEmpty; // boolean
tasks.isFull; // true se size >= maxSize
tasks.maxSize; // Infinity ou o limite configuradoCapacidade
const buffer = createList<string>("buffer", [], { maxSize: 5 });
buffer.append("a");
buffer.append("b");
buffer.append("c");
buffer.append("d");
buffer.append("e");
buffer.isFull; // true
buffer.append("f"); // RangeError: would exceed maxSizeValidado em toda operação que aumenta o tamanho. Operações que reduzem nunca falham por capacidade.
Queue / Stack via composição
O ListState é o mecanismo — o consumer decide o padrão de acesso:
// FIFO — usa append + removeFirst
tasks.append("task-A");
tasks.append("task-B");
const next = tasks.removeFirst(); // "task-A"
// LIFO — usa append + removeLast
stack.append("frame-1");
stack.append("frame-2");
const top = stack.removeLast(); // "frame-2"Matrix
Grid 2D tipado de dimensões fixas com structural sharing, neighbors, diagonals e ranges. Cobre tabuleiros de jogos, mapas, grids de layout, matrizes numéricas, puzzles, cellular automata.
const board = createMatrix<string | null>("board", 3, 3, null);
const grid = createMatrix<number>("heatmap", 100, 100, 0);Dimensões e defaultValue são imutáveis — definidos na criação.
Actions
// Célula individual
board.setCell(0, 0, "X");
board.updateCell(1, 1, (v) => v ?? "O");
// Row inteira
grid.fillRow(0, 1); // primeira row = 1
grid.updateRow(2, (v, col) => v + col); // transform por coluna
// Coluna inteira
grid.fillCol(0, 5);
// Range retangular (from inclusivo, to exclusivo)
grid.fillRange(1, 3, 1, 3, 9);
// [0, 0, 0, 0]
// [0, 9, 9, 0]
// [0, 9, 9, 0]
// [0, 0, 0, 0]
// Fill total
board.fill(null); // limpa tudo
// Swap — troca duas células
board.setCell(0, 0, "X");
board.setCell(2, 2, "O");
board.swap(0, 0, 2, 2);
board.getCell(0, 0); // "O"
board.getCell(2, 2); // "X"
// Move — move célula, origem vira defaultValue
board.move(0, 0, 1, 1);
board.getCell(0, 0); // null
// Replace — substitui grid inteiro (valida dimensões)
board.replace([
["X", "O", "X"],
["O", "X", "O"],
["X", "O", "X"],
]);
// Reset — volta ao defaultValue
board.reset();Getters
board.getCell(0, 0); // valor da célula
board.getRow(0); // cópia da row
board.getCol(0); // coluna como array
board.getRange(0, 2, 0, 2); // sub-grid T[][]
// Neighbors — cardinal (4) ou all (8)
board.getNeighbors(1, 1); // 4 vizinhos N/E/S/W
board.getNeighbors(1, 1, "all"); // 8 vizinhos (com diagonais)
board.getNeighbors(0, 0); // 2 vizinhos (canto)
// Diagonais
board.getDiagonal(); // principal
board.getDiagonal("anti"); // anti-diagonal
// Busca
board.find((v) => v === "X"); // primeira célula { row, col, value }
board.findAll((v) => v === "X"); // todas as células
board.count((v) => v !== null); // contagem
// Checks
board.isCellEmpty(0, 0); // true se === defaultValue
board.every((v) => v !== null); // todas preenchidas?
board.some((v) => v === "X"); // algum X?
board.rows; // número de linhas (imutável)
board.cols; // número de colunas (imutável)
board.defaultValue; // valor default (imutável)
board.cells; // T[][] (alias de getState())
board.size; // rows × colsStructural Sharing
Mutações clonam apenas as rows afetadas. Se uma row não é tocada, a referência permanece a mesma:
const before = grid.getState();
grid.setCell(0, 0, 99);
const after = grid.getState();
before !== after; // true — grid mudou
before[0] !== after[0]; // true — row 0 mudou
before[1] === after[1]; // true — row 1 intacta (mesma referência)Bounds Checking
board.setCell(5, 0, "X");
// RangeError: Position [5, 0] out of bounds for 3×3 matrix
board.fillRange(2, 1, 0, 3, "X");
// RangeError: Invalid range: from must be less than toCounter
Valor numérico com bounds, step e clamping automático. Derived type sobre RecordState via DerivedState. Cobre HP/MP, ammo, cooldowns, XP, scores, progress bars, budgets, turn counters.
const hp = createCounter("hp", { value: 100, min: 0, max: 100 });
const xp = createCounter("xp", { value: 0, min: 0 }); // sem max
const cd = createCounter("cooldown", { value: 5, min: 0, max: 5 });Actions
hp.increment(); // +step (default 1)
hp.increment(30); // +30
hp.decrement(); // -step
hp.decrement(50); // -50 (clampa a min)
hp.set(75); // direto (clampa aos bounds)
hp.fill(); // value = max
hp.empty(); // value = min
hp.reset(); // volta ao valor da criação
// Runtime bounds
hp.setMax(150); // level up
hp.setMin(10); // debuff
hp.setStep(5); // mudança de fire rateGetters
hp.value; // 70
hp.initial; // 100
hp.min; // 0
hp.max; // 100
hp.step; // 1
hp.percentage; // 0.7 (0–1)
hp.isMin; // false
hp.isMax; // false
hp.canIncrement; // true
hp.canDecrement; // trueCenários
// Game — HP com level up
const hp = createCounter("hp", { value: 100, min: 0, max: 100 });
hp.decrement(30); // take damage
hp.fill(); // potion
hp.setMax(150); // level up
// Ammo — shoot and reload
const ammo = createCounter("ammo", { value: 30, min: 0, max: 30 });
ammo.decrement(); // shoot
ammo.fill(); // reload
// Progress bar
const progress = createCounter("progress", { value: 0, min: 0, max: 10000 });
progress.increment(500);
progress.percentage; // 0.05Flags
Set de flags (strings) com whitelist opcional, toggle e batch atômico. Derived type sobre RecordState. Cada flag é uma key — O(1) para has/add/remove. Cobre status effects, feature flags, permissions, pipeline stages, checkbox groups.
const status = createFlags("status", {
initial: ["alive"],
allowed: ["alive", "poisoned", "stunned", "shielded", "burning"],
});
const features = createFlags("features"); // sem whitelist — qualquer flagActions
status.add("poisoned"); // add (no-op se já existe)
status.remove("poisoned"); // remove (no-op se não existe)
status.toggle("shielded"); // inverte presença
// Batch atômico — uma mutação, um notify
status.addMany("strength", "haste", "shield");
status.removeMany("strength", "haste");
status.set(["alive", "shielded"]); // replace all
status.clear(); // remove tudo
status.reset(); // volta ao initialGetters
status.has("poisoned"); // true — O(1)
status.hasAll("alive", "poisoned"); // true
status.hasAny("stunned", "frozen"); // false
status.hasNone("stunned", "frozen"); // true
status.all; // ["alive", "poisoned"] (novo array)
status.size; // 2
status.isEmpty; // false
status.allowed; // readonly ["alive", "poisoned", ...] | nullCenários
// RPG — status effects com whitelist
const canAct =
status.has("alive") && status.hasNone("stunned", "frozen", "sleeping");
// Feature flags — sem whitelist
const features = createFlags("features");
features.toggle("dark-mode");
features.toggle("beta-enabled");
// Permissions
const roles = createFlags("roles", {
allowed: ["admin", "editor", "viewer"],
initial: ["viewer"],
});
roles.add("editor");
roles.hasAny("admin", "editor"); // true
// ETL pipeline stages
const pipeline = createFlags("pipeline");
pipeline.add("extracted");
pipeline.add("validated");
pipeline.add("transformed");
pipeline.add("loaded");
pipeline.hasAll("extracted", "validated", "transformed", "loaded"); // trueStateMachine
Máquina de estados finita (FSM) com grafo de transições pre-compilado. Derived type sobre RecordState. O grafo é compilado uma vez no constructor em Map<string, Set<string>> — toda checagem de transição é O(1). Suporta wildcards, arrays e bypass.
const phase = createStateMachine("phase", {
initial: "menu",
states: ["menu", "playing", "paused", "gameover"],
transitions: [
{ from: "menu", to: "playing" },
{ from: "playing", to: ["paused", "gameover"] },
{ from: "paused", to: ["playing", "menu"] },
],
});TransitionRule
// Arrays — múltiplos destinos ou origens
{ from: "playing", to: ["paused", "gameover"] }
{ from: ["a", "b"], to: "c" }
// Wildcards — qualquer estado
{ from: "*", to: "error" } // qualquer → error
{ from: "admin", to: "*" } // admin → qualquer
{ from: "*", to: "*" } // sem restriçõesActions
phase.transition("playing"); // validada pelo grafo
phase.transition("gameover"); // Error: transição não permitida
phase.force("gameover"); // bypass — ignora grafo (admin/debug)
phase.force("unknown"); // RangeError: estado não existe
phase.reset(); // volta ao initialGetters
phase.current; // "playing"
phase.initial; // "menu"
phase.is("playing"); // true
phase.can("paused"); // true
phase.can("menu"); // false
phase.available; // ["paused", "gameover"]
phase.states; // readonly ["menu", "playing", "paused", "gameover"]Cenários
// Order — cancel de qualquer ponto
const order = createStateMachine("order", {
initial: "pending",
states: ["pending", "paid", "shipped", "delivered", "cancelled"],
transitions: [
{ from: "pending", to: "paid" },
{ from: "paid", to: "shipped" },
{ from: "shipped", to: "delivered" },
{ from: "*", to: "cancelled" }, // wildcard
],
});
// AI — behavior
const ai = createStateMachine("ai", {
initial: "idle",
states: ["idle", "patrol", "chase", "attack", "flee"],
transitions: [
{ from: "idle", to: "patrol" },
{ from: "patrol", to: ["chase", "idle"] },
{ from: "chase", to: ["attack", "flee"] },
{ from: "attack", to: ["chase", "flee"] },
{ from: "flee", to: "idle" },
],
});
// Wizard — steps com back
const wizard = createStateMachine("wizard", {
initial: "step1",
states: ["step1", "step2", "step3", "complete"],
transitions: [
{ from: "step1", to: "step2" },
{ from: "step2", to: ["step1", "step3"] },
{ from: "step3", to: ["step2", "complete"] },
],
});
// Connection — error recovery
const conn = createStateMachine("conn", {
initial: "disconnected",
states: ["disconnected", "connecting", "connected", "error"],
transitions: [
{ from: "disconnected", to: "connecting" },
{ from: "connecting", to: ["connected", "error"] },
{ from: "connected", to: "disconnected" },
{ from: "error", to: ["connecting", "disconnected"] },
],
});Shared Features
Todo state (primitivo ou derivado) implementa a interface State<T> e compartilha a mesma base:
Transaction
// Commit no sucesso, rollback no throw
state.transaction(() => {
state.doSomething();
state.doSomethingElse();
// throw → rollback automático
});
// Simulate — SEMPRE rollback
const result = state.simulate(() => {
state.doSomething();
return state.getSomething();
});
// state intacto, result capturado
// Nesting arbitrário
state.transaction(() => {
state.doA();
const safe = state.simulate(() => {
state.doB();
return state.isValid();
});
if (!safe) throw new Error("invalid");
});
// API manual (se necessário)
state.beginTransaction();
state.commitTransaction();
state.rollback();
state.inTransaction; // boolean
state.transactionDepth; // numberSubscribe
const unsub = state.subscribe((current, prev) => {
console.log("mudou:", prev, "→", current);
});
state.doSomething(); // listener disparado
unsub(); // para de ouvirprevState vem de graça — a referência anterior já existe porque toda mutação é shallow clone. Zero custo extra.
Middleware
// Intercepta toda mutação
const remove = state.use((current, next, proceed) => {
console.log("before:", current);
proceed(); // permite mutação
console.log("after:", state.getState());
});
// Validação — bloqueia não chamando proceed()
state.use((current, next, proceed) => {
if (!isValid(next)) return; // mutação bloqueada, listeners não notificados
proceed();
});
// Normalização — proceed(override) substitui valor antes de aplicar
state.use((_current, next, proceed) => {
if (next.value > next.max) {
proceed({ ...next, value: next.max }); // clamp
} else {
proceed();
}
});
remove(); // remove middlewareMeta
Dados auxiliares persistidos junto ao state, mas separados do _state. Canal de reatividade próprio.
// Set / Get
state.meta("observedPaths", ["foo.bar", "hp.value"]);
const paths = state.meta<string[]>("observedPaths");
// Functional update
state.meta((cur) => ({ ...cur, tick: (cur.tick as number) + 1 }));
// Get all
state.meta(); // { observedPaths: [...], tick: 2 }
// Observe mudanças
const unsub = state.subscribeMeta((meta, prev) => {
console.log("meta mudou:", meta);
});Meta é coberto por transactions (rollback restaura meta) e persistence (snapshot inclui meta).
Snapshot / Restore
const snap = state.snapshot();
// { type: "record", id: "settings", data: { ... }, meta: { ... } }
localStorage.setItem("state", JSON.stringify(snap));
const saved = JSON.parse(localStorage.getItem("state")!);
state.restore(saved);Standalone
Todo state funciona sem Store. O Store é opt-in — adiciona coordenação cross-state quando necessário.
// Funciona perfeitamente sozinho
const config = createRecord("config", { theme: "dark" });
config.set("theme", "light");
config.subscribe((s) => console.log(s));
config.transaction(() => {
/* ... */
});
config.snapshot();Encapsulamento dos Derivados
Tipos derivados (Counter, Flags, StateMachine) compõem com um primitivo internamente, mas não expõem os métodos do primitivo. Só actions de domínio são acessíveis:
const hp = createCounter("hp", { value: 100, min: 0, max: 100 });
hp.increment(); // ✓ domain action
hp.value; // ✓ domain getter
hp.subscribe(fn); // ✓ delegado via DerivedState
hp.transaction(fn); // ✓ delegado via DerivedState
store.register(hp); // ✓ Counter IS State
hp.set("value", 0); // ✗ não existe (RecordState.set)
hp.merge({}); // ✗ não existe
hp.delete("value"); // ✗ não existeTodos os Exports
// Store (core)
import {
createStateCore,
createStore,
DerivedState,
assertSerializable,
} from "apex-store";
// Primitivos
import { createRecord, RecordState } from "apex-store";
import { createCollection, CollectionState } from "apex-store";
import { createList, ListState } from "apex-store";
import { createMatrix, MatrixState } from "apex-store";
// Derivados
import { createCounter, CounterState } from "apex-store";
import { createFlags, FlagsState } from "apex-store";
import { createStateMachine, StateMachineState } from "apex-store";
// Tipos
import type {
State,
StateCore,
StateConfig,
StateSnapshot,
StateDefinition,
StateFactory,
IStore,
StoreConfig,
StoreSnapshot,
StoreListener,
StoreLifecycleHook,
Listener,
Middleware,
RecordConfig,
CollectionConfig,
ListConfig,
IListState,
MatrixConfig,
IMatrixState,
CellPosition,
CellPredicate,
CounterData,
CounterConfig,
FlagsData,
FlagsConfig,
StateMachineData,
StateMachineConfig,
TransitionRule,
} from "apex-store";Documentação Detalhada
| Doc | Descrição |
| -------------------------------------- | ------------------------------------------------------------------------------------ |
| Store | Core — createStateCore, createStore, DerivedState, assertSerializable, tipos |
| Record | Primitivo key-value tipado com deep path |
| Collection | Primitivo lista com identidade por ID |
| List | Primitivo lista posicional com capacidade |
| Matrix | Primitivo grid 2D com structural sharing |
| Counter | Derivado numérico com bounds/step |
| Flags | Derivado set de strings com whitelist |
| StateMachine | Derivado FSM com grafo pre-compilado |
License
MIT
