@statedelta-apex/store
v0.4.0
Published
ApexStore - Typed state management with specialized state primitives and registry pattern
Maintainers
Readme
@statedelta-apex/store
Núcleo do ApexStore — state container determinístico com primitivos especializados, transações aninhadas, registry transacional, checkpoints e canais de observação tipados.
Esta lib é o centro do monorepo: provê createStore() (orquestrador), createStateCore() (factory de mecânica compartilhada), as classes base PrimitiveState e DerivedState, e todos os contratos (State<T>, ManagedState<T>, StateCore<T>). Os packages record-state, collection-state, matrix-state, list-state e derived-states extendem essa fundação.
Filosofia
State container determinístico com primitivos especializados. Cada state é um store completo e autossuficiente — sabe armazenar, fazer snapshot, rollback e notificar mudanças. O Store orquestra — coordena transações, snapshots e ciclo de vida entre N states.
- Quem não usa, não paga — features inativas têm custo zero. Strategy swap pré-compila a função de mutate certa pro cenário.
- States são autossuficientes — funcionam standalone ou gerenciados por um Store.
- Store orquestra, não implementa — delega tudo pros states via interface
State<T>(pública) eManagedState<T>(interna). - Composição sobre herança — primitivos extendem
PrimitiveState, tipos derivados extendemDerivedState. - Imutabilidade por convenção — cada mutação cria nova referência via shallow clone. Snapshots são pointer copies, não deep clones. A convenção é enforçada em dev via auto-freeze (modelo Immer); em prod é convenção pura, zero custo.
- Nunca throw no hot path — lock e transações são feitos via strategy swap, não branches.
Instalação
pnpm add @statedelta-apex/storeFunciona standalone. Geralmente usado junto com pelo menos um package de primitivo (record/collection/matrix/list) ou via o meta-package apex-store.
Quick Start
import {
createStore,
PrimitiveState,
type StateConfig,
} from "@statedelta-apex/store";
// 1. Definir um primitivo extendendo PrimitiveState
class Counter extends PrimitiveState<{ value: number }> {
constructor(id: string, initial = 0, config?: StateConfig) {
super({
id,
type: "counter",
initialState: { value: initial },
serialize: (s) => ({ ...s }),
deserialize: (d) => d as { value: number },
config,
});
}
increment(): void {
const s = this._core.getState();
this._core.mutate({ value: s.value + 1 });
}
get value(): number {
return this._core.getState().value;
}
}
// 2. Criar Store + registrar
const store = createStore();
const hp = new Counter("hp", 100);
const mp = new Counter("mp", 50);
store.register(hp);
store.register(mp);
// 3. Observar
const unsub = hp.subscribe((s, prev) => {
console.log(`HP: ${prev.value} → ${s.value}`);
});
// 4. Mutar
hp.increment(); // "HP: 100 → 101"
// 5. Transação cross-state — rollback atômico
store.transaction(() => {
hp.increment();
mp.increment();
// throw aqui → ambos voltam
});
// 6. Simulação — sempre rollback, observers silenciados por default
const wouldOverflow = store.simulate(() => {
for (let i = 0; i < 1000; i++) hp.increment();
return hp.value > 1000;
});
// hp continua em 101 — simulação descartada, zero notificações
// 7. Checkpoint — save point
const cp = store.checkpoint();
hp.increment();
hp.increment();
cp.revert(); // hp volta pro ponto do cp, observers notificados
cp.discard(); // libera memóriaFeatures
Strategy swap — zero branches no hot path
O createStateCore() mantém 8 variantes pré-compiladas de mutate() selecionadas pela combinação de hook/middleware/listeners. Quando o cenário muda (subscribe novo, middleware adicionado, lock ativado, observable desligado), _updateStrategy() troca o ponteiro. No hot path, é uma chamada de função direta.
Transações aninhadas + lazy snapshot
Transações suportam nesting arbitrário com adoption strategy entre níveis. Lazy snapshot — states que não mutam durante a transação não pagam nada (sem snapshot, sem cleanup).
Registry transacional
register() e unregister() dentro de transações são provisórios. Rollback restaura. Soft-delete via hidden flag preserva instâncias pra ressurreição.
Checkpoints
store.checkpoint() retorna um handle independente do escopo de tx. revert() restaura state + meta + registry. Eager capture O(N) — diferente de transactions, múltiplos checkpoints coexistem em paralelo.
Dual subscriber channels
subscribe(fn, { internal: true }) marca um listener como comportamento de domínio (chain logic, derived states). Internal listeners sobrevivem ao silenciamento por events:false em transactions — usados pra reações que precisam acontecer mesmo em simulações throwaway.
Events flag — perf em hot loops
simulate(fn) defaulta events:false (throwaway é silencioso por design). transaction(fn, { events: false }) permite bulk import sem UI redraw. Suprime apenas observers externos; middleware e internal subscribers continuam rodando.
Lock — bloqueio de mutações
state.lock() faz strategy swap pra variante que joga erro em qualquer mutação. unlock() restaura. Zero overhead — sem branches no hot path.
Strict mode
Validação opcional de serializabilidade a cada mutação (bloqueia functions, symbols, NaN, etc.). Walk recursivo O(n) — só paga quem ativa.
Auto-freeze (dev) — enforcement da imutabilidade
getState() / meta() entregam a referência interna viva (structural sharing + estabilidade referencial — requisito de useSyncExternalStore e do memo por-item). Isso significa que um consumer pode mutar o estado vivo e corromper o realm silenciosamente, quebrando determinismo/replay — sem erro.
Modelo Immer: prod = convenção pura, zero custo. Dev = o estado committed é deep-frozen, então arr.push() / obj.x = … num estado entregue estoura TypeError na hora.
import { setAutoFreeze } from "@statedelta-apex/store";
// Default: ligado fora de produção (NODE_ENV !== "production"),
// desligado em produção. Override global (modelo Immer):
setAutoFreeze(false);Gate global (não per-state como strict) — imutabilidade é invariante do realm inteiro. Decidido uma vez na criação do state: zero branch no hot path; deepFreeze é in-place + short-circuit em Object.isFrozen, então structural sharing continua barato (O(nós novos)).
Persistence
snapshot() / restore() em qualquer state. store.snapshot() agrega todos. Meta é persistido junto.
Documentação
- README.md (este arquivo) — overview + API reference completa
ARCHITECTURE.md— internals: strategy swap, transaction algorithm, adoption strategy, checkpoint lifecycle, auto-freeze (dev)PROPOSAL-TX2.md— design spec do Transaction System v2 (registry transacional + events + internal subscribers + checkpoints)
Tipos Exportados
import {
// Factories
createStateCore,
createStore,
// Classes base
PrimitiveState,
DerivedState,
// Utilitário
assertSerializable,
// Auto-freeze (dev)
deepFreeze,
setAutoFreeze,
getAutoFreeze,
} from "@statedelta-apex/store";
import type {
Listener,
ManagedState,
MetaAccessor,
MetaListener,
Middleware,
State,
StateConfig,
StateSnapshot,
StateDefinition,
StateCore,
StateFactory,
IStore,
StoreConfig,
StoreSnapshot,
StoreListener,
StoreLifecycleHook,
// Transaction v2
TransactionOptions,
SubscribeOptions,
Checkpoint,
LifecycleEvent,
LifecyclePhase,
LifecycleListener,
} from "@statedelta-apex/store";Referência Completa
State<TState> — Interface pública
Contrato base que todo state implementa. API pública que consumers veem. O Store usa internamente uma variante (ManagedState<TState>) que adiciona hooks de coordenação — não exposta na API pública.
Identity
| Membro | Tipo | Descrição |
| ------ | ----------------- | ------------------------------------------------------------ |
| id | readonly string | ID único do state |
| type | readonly string | Tipo do state ('record', 'collection', 'matrix', etc.) |
State
| Método | Retorno | Descrição |
| ------------ | -------- | --------------------------------------------------------------------- |
| getState() | TState | Estado atual — referência imutável por convenção (deep-frozen em dev) |
Transaction
Transações aninhadas com rollback automático.
| Método | Retorno | Descrição |
| --------------------- | ------------------ | --------------------------------------------------------------- |
| beginTransaction() | void | Empilha snapshot do estado atual |
| commitTransaction() | void | Desempilha snapshot, mantém mudanças. Noop sem transaction |
| rollback() | boolean | Restaura estado anterior. false se sem transaction |
| transaction(fn) | T | Commit no sucesso, rollback no throw. Retorna resultado de fn |
| simulate(fn) | T | Sempre rollback. Retorna resultado de fn |
| inTransaction | readonly boolean | Transaction ativa? |
| transactionDepth | readonly number | Profundidade de nesting (0 = sem transaction) |
// transaction — atômico
hp.transaction(() => {
hp.set("value", 50);
hp.set("min", 10);
// throw aqui → rollback automático
});
// simulate — "what if?" sem mutar
const wouldDie = hp.simulate(() => {
hp.set("value", 0);
return hp.get("value") <= hp.get("min");
});
// wouldDie === true, hp intacto
// nesting natural
hp.transaction(() => {
hp.set("value", 80);
const safe = hp.simulate(() => {
hp.set("value", 0);
return hp.get("value") > hp.get("min");
});
// hp.get("value") ainda 80
if (!safe) throw new Error("would go below min");
});Persistence
| Método | Retorno | Descrição |
| ------------------- | --------------- | ----------------------------------------------------- |
| snapshot() | StateSnapshot | Serializa estado e meta ({ type, id, data, meta? }) |
| restore(snapshot) | void | Deserializa e aplica estado + meta |
Meta
Dados auxiliares persistidos junto ao state, mas fora do _state. Canal de reatividade separado — subscribe não reage a meta, subscribeMeta não reage a state.
| Membro | Retorno | Descrição |
| ------------------------- | ------------------------- | ---------------------------------------------------------- |
| meta() | Record<string, unknown> | Retorna o meta completo |
| meta<T>(key) | T | Retorna valor tipado de uma key |
| meta(key, value) | void | Seta valor de uma key |
| meta(fn) | void | Functional update — recebe current meta, retorna novo meta |
| subscribeMeta(listener) | () => void | Registra listener de meta. Retorna unsubscribe |
// Set
hp.meta("observedPaths", ["foo.bar", "hp.value"]);
// Get tipado
const paths = hp.meta<string[]>("observedPaths");
// Functional update — merge, delete, replace
hp.meta((cur) => ({ ...cur, newKey: "val" }));
hp.meta((cur) => {
const { removeMe, ...rest } = cur;
return rest;
});
// Get all
hp.meta(); // { observedPaths: ["foo.bar", "hp.value"], newKey: "val" }
// Observe mudanças
const unsub = hp.subscribeMeta((meta, prevMeta) => {
console.log("meta mudou:", meta);
});Meta é coberto por transactions — rollback restaura meta junto com state.
Reactivity
| Método | Retorno | Descrição |
| ---------------------------- | ------------ | -------------------------------------- |
| subscribe(listener, opts?) | () => void | Registra listener. Retorna unsubscribe |
opts.internal: true marca o listener como interno — sobrevive ao
silenciamento por events:false em transactions. Usar pra chain logic,
derived states, ou qualquer side effect que faz parte do domínio do realm.
// External (default) — silenciado durante simulate({events:false})
const unsub = hp.subscribe((state, prev) => {
console.log(`HP: ${prev.value} → ${state.value}`);
});
// Internal — sempre dispara, mesmo em simulate silencioso
hp.subscribe((state) => hpBar.set("value", state.value), { internal: true });
hp.set("value", 70); // ambos disparam
store.simulate(() => {
hp.set("value", 50); // só o internal dispara
});A mesma flag internal está disponível em subscribeMeta, store.subscribe,
store.onLifecycle, store.onRegister, store.onUnregister.
Middleware
| Método | Retorno | Descrição |
| ----------------- | ------------ | ---------------------------------------------- |
| use(middleware) | () => void | Registra middleware. Retorna função de remoção |
Middleware intercepta mutações. Não chamar proceed() bloqueia a mutação e impede notificação de listeners. proceed(override) substitui o valor antes de aplicar — para normalização, clamping, transformações.
// Logging
const remove = hp.use((current, next, proceed) => {
console.log("antes:", current);
proceed();
console.log("depois:", hp.getState());
});
// Validação — bloqueia mutações inválidas
hp.use((current, next, proceed) => {
if (next.value < 0) return; // bloqueia
proceed();
});
// Normalização — proceed(override) substitui valor
hp.use((current, next, proceed) => {
if (next.value > next.max) {
proceed({ ...next, value: next.max }); // clamp
} else {
proceed();
}
});
// Chain de transformações (FIFO)
hp.use((_cur, next, proceed) => {
proceed({ ...next, value: Math.round(next.value) }); // round
});
hp.use((_cur, next, proceed) => {
proceed({ ...next, value: Math.max(0, next.value) }); // floor em 0
});
hp.set("value", -3.7);
// mw1 recebe -3.7, passa -4 → mw2 recebe -4, passa 0 → aplica 0
// Diff capture — trabalha pós-proceed
hp.use((current, next, proceed) => {
proceed();
saveDelta(current, hp.getState()); // captura diff entre estado anterior e novo
});
// Múltiplos middleware executam em cadeia (FIFO)
hp.use((c, n, proceed) => {
console.log("mw1");
proceed();
});
hp.use((c, n, proceed) => {
console.log("mw2");
proceed();
});
hp.set("value", 50);
// "mw1" → "mw2" → mutação aplicada
remove(); // remove middleware específicoproceed() vs proceed(override):
| Chamada | Comportamento |
| -------------------- | ----------------------------------------- |
| proceed() | Aplica o valor original (next) |
| proceed(override) | Substitui next pelo override e aplica |
| sem chamar proceed | Bloqueia a mutação |
O override flui pela chain — cada middleware na sequência recebe o valor já transformado pelo anterior.
Lock
Trava/destrava mutações no state. Quando locked, mutate() lança Error. Tudo mais continua funcionando (meta, subscribe, middleware registration, snapshot, restore).
| Membro | Tipo | Descrição |
| ---------- | ------------------ | ----------------------------- |
| lock() | void | Trava o state. Idempotente |
| unlock() | void | Destrava o state. Idempotente |
| isLocked | readonly boolean | State está travado? |
const hp = new Counter("hp", { value: 100, min: 0, max: 100 });
hp.lock();
hp.increment(); // Error: State "hp" is locked
hp.value; // 100 — getters funcionam
hp.meta("key", "v"); // ok — meta não é bloqueado
hp.subscribe(fn); // ok — subscribe não é bloqueado
hp.snapshot(); // ok — persistence não é bloqueada
hp.unlock();
hp.increment(); // ok — volta a funcionar
hp.value; // 101Lock é runtime-only — não é persistido no snapshot. Implementado via strategy swap: zero branches no hot path, tanto locked quanto unlocked.
createStateCore<TState>(definition): StateCore<TState>
Factory que cria o core de um state. Cada tipo de state (Record, Collection, Matrix, List) usa esta factory e wrapa o resultado com suas actions/getters específicas.
Parâmetros
interface StateDefinition<TState> {
id: string; // ID único
type: string; // Tipo ('record' | 'collection' | 'matrix' | string)
initialState: TState; // Estado inicial
serialize: (state: TState) => unknown; // Serializa para persistência
deserialize: (data: unknown) => TState; // Deserializa de persistência
config?: StateConfig; // Configuração opcional
}
interface StateConfig {
strict?: boolean; // Validação de serializabilidade a cada mutação. Default: false
trackDeltas?: boolean; // Reservado para delta tracking futuro. Default: false
}Retorno — StateCore<TState>
Implementa State<TState> + expõe mutate().
interface StateCore<TState> extends State<TState> {
mutate: (newState: TState) => void;
}mutate() é chamado pelas actions de cada tipo de state. O caller é responsável por criar o novo estado via shallow clone. O pipeline executa na ordem: strict validation (se ativo) → hook → middleware (pode transformar via proceed(override)) → atribuição → listeners.
Exemplo — criando um tipo de state
class RecordState<T extends Record<string, unknown>> {
private readonly _core: StateCore<T>;
constructor(id: string, data: T, config?: StateConfig) {
this._core = createStateCore<T>({
id,
type: "record",
initialState: { ...data },
serialize: (state) => ({ ...state }),
deserialize: (d) => d as T,
config,
});
}
set<K extends keyof T>(key: K, value: T[K]): void {
this._core.mutate({ ...this._core.getState(), [key]: value });
}
get<K extends keyof T>(key: K): T[K] {
return this._core.getState()[key];
}
// Delega toda a interface State<T> pro core
get id() {
return this._core.id;
}
get type() {
return this._core.type;
}
getState() {
return this._core.getState();
}
subscribe(fn: Listener<T>) {
return this._core.subscribe(fn);
}
use(mw: Middleware<T>) {
return this._core.use(mw);
}
beginTransaction() {
this._core.beginTransaction();
}
commitTransaction() {
this._core.commitTransaction();
}
rollback() {
return this._core.rollback();
}
transaction<R>(fn: () => R) {
return this._core.transaction(fn);
}
simulate<R>(fn: () => R) {
return this._core.simulate(fn);
}
snapshot() {
return this._core.snapshot();
}
restore(snap: StateSnapshot) {
this._core.restore(snap);
}
// ... store integration
}createStore(config?): IStore
Cria o Store — orquestrador de states.
const store = createStore();Registry
| Método | Retorno | Descrição |
| -------------------- | ----------------- | -------------------------------------------------------- |
| register(state) | void | Registra state. Throws se ID duplicado |
| unregister(id) | boolean | Remove state. false se inexistente |
| get<T>(id) | T \| undefined | Busca por ID com generic para cast tipado |
| has(id) | boolean | Verifica existência |
| all() | State[] | Lista todos |
| getByType(type) | State[] | Busca por tipo. O(1) via index |
| size | readonly number | Contagem |
| clear() | void | Remove todos. Dispara onUnregister. Limpa transactions |
| cleanup(predicate) | number | Remove por condição. Retorna count de removidos |
store.register(hp);
store.register(inventory);
store.register(board);
store.get<RecordState<HP>>("hp")?.set("value", 70);
store.getByType("record"); // [hp, ...]
store.has("hp"); // true
store.size; // 3
store.cleanup((s) => s.getState().value === 0); // remove zerados
store.clear(); // remove todosErros:
store.register(hp);
store.register(hp); // Error: State "hp" already registeredFactory Registry
Cria states via Store sem conhecer implementações concretas.
| Método | Retorno | Descrição |
| ------------------------------------ | ------- | ----------------------------------------------------------------- |
| defineType(type, factory) | void | Registra factory por tipo. Sobrescreve se já existe |
| create<T>(type, id, data, config?) | T | Cria via factory + auto-register. Throws se tipo não definido |
// Registro de factories
store.defineType("record", (id, data, config) =>
createRecord(id, data, config),
);
store.defineType("collection", (id, data) => createCollection(id, data));
// Criação via Store — auto-register
const hp = store.create<RecordState<HP>>("record", "hp", { value: 100 });
hp.set("value", 70); // TypeScript conhece RecordState<HP>
hp.subscribe(() => {}); // herdado do core
store.has("hp"); // trueErros:
store.create("unknown", "x", {}); // Error: Type "unknown" not defined. Use store.defineType() first.
store.create("record", "hp", {}); // Error: State "hp" already registeredTransaction — Orquestração entre states
Coordena transações entre N states. Lazy snapshot — states não tocados não pagam nada.
Registry transacional: register/unregister dentro de tx são provisórios — rollback restaura.
| Método | Retorno | Descrição |
| ------------------------- | ------------------ | ------------------------------------------------- |
| beginTransaction(opts?) | void | Abre novo nível. opts.events controla observers |
| commitTransaction() | void | Commita nível atual. Noop sem transaction |
| rollback() | boolean | Rollback do nível atual. false sem transaction |
| transaction(fn, opts?) | T | Commit no sucesso, rollback no throw |
| simulate(fn, opts?) | T | Sempre rollback. Retorna resultado |
| inTransaction | readonly boolean | Transaction ativa? |
| transactionDepth | readonly number | Profundidade de nesting |
TransactionOptions.events
Controla notificação a observers externos durante a transação.
| API | Default | Para que serve |
| ------------------ | -------------- | --------------------------------------------------- |
| simulate | events:false | throwaway é silencioso (zero overhead em hot loops) |
| transaction | events:true | mutações são audíveis |
| beginTransaction | events:true | idem |
// Simulação massiva — observers não pagam
for (let i = 0; i < 10000; i++) {
store.simulate(() => {
hp.set("value", i); // subscribers NÃO disparam
board.setCell(0, 0, "X");
});
}
// Forçar observers a verem o throwaway (debug)
store.simulate(() => { ... }, { events: true });
// Bulk import sem UI redraw
store.transaction(() => importBatch(), { events: false });Suprimido quando events:false:
state.subscribe(sem{ internal: true })state.subscribeMeta(sem{ internal: true })store.subscribe(sem{ internal: true })store.onLifecycle/onRegister/onUnregister(sem{ internal: true })
NÃO suprimido:
- Middleware (parte do pipeline de mutação)
- Internal subscribers (registrados com
{ internal: true }) _onBeforeMutate(coordenação interna do Store)
Herança em nested: filho herda do parent salvo override explícito.
// Atômico — múltiplos states
store.transaction(() => {
hp.set("value", 50);
mp.set("value", 30);
board.setCell(1, 1, "O");
// throw → rollback de todos os states tocados
});
// Simulação — sempre reverte
const result = store.simulate(() => {
hp.set("value", 0);
return hp.get("value"); // 0
});
// hp.get("value") ainda 100
// Nesting
store.transaction(() => {
hp.set("value", 70);
const wouldDie = store.simulate(() => {
hp.set("value", 0);
return hp.get("value") === 0;
});
// hp.get("value") ainda 70
});
// hp.get("value") === 70Reactivity
| Método | Retorno | Descrição |
| --------------------- | ------------------------- | --------------------------------------------------------- |
| subscribe(listener) | () => void | Listener global. Disparado quando qualquer state muta |
| getState() | Record<string, unknown> | Estado combinado. Referências diretas, zero serialização |
const unsub = store.subscribe((id, state, prevState) => {
console.log(`${id} mudou`);
});
hp.set("value", 70); // "hp mudou"
mp.set("value", 30); // "mp mudou"
unsub();
store.getState();
// { hp: { value: 70 }, mp: { value: 30 } }Lazy — subscriptions internas nos states só existem enquanto há store-level listeners. States registrados depois de subscribe() são observados automaticamente.
Lifecycle Hooks
Três canais. onLifecycle é o canal completo; onRegister/onUnregister são atalhos
que filtram apenas eventos confirmados (phase: "committed").
| Método | Retorno | Quando dispara |
| ------------------------------ | ------------ | -------------------------------------------------------------------------- |
| onLifecycle(listener, opts?) | () => void | Em todo register/unregister, com fase provisional/committed/reverted |
| onRegister(hook, opts?) | () => void | Em register confirmado (commit root ou fora de tx) |
| onUnregister(hook, opts?) | () => void | Em unregister confirmado |
// Atalho — disparado em commits confirmados
store.onRegister((state) => initExternal(state));
store.onUnregister((state) => cleanupExternal(state));
// Canal completo — telemetria, UI optimistic, debug
store.onLifecycle((event) => {
switch (event.type) {
case "register":
if (event.phase === "provisional")
log.debug("Provisional:", event.primitive.id);
if (event.phase === "committed")
log.info("Confirmed:", event.primitive.id);
if (event.phase === "reverted") log.warn("Reverted:", event.primitive.id);
break;
case "unregister":
// ...
}
});Fluxos típicos
| Cenário | Eventos emitidos |
| ----------------------------------------- | ------------------------------------------------------------------------- |
| register fora de tx | register/committed |
| register em tx + commit | register/provisional → register/committed |
| register em tx + rollback | register/provisional → register/reverted |
| register + unregister mesma tx + commit | 2× provisional, sem committed (par cancelado) |
| unregister então register mesma tx | provisional unregister, provisional register, sem committed (resurreição) |
| Nested commit que não é root | (eventos provisional já emitidos) — committed só no commit root |
Opções { internal: true } em listeners
Marca listener como interno — sobrevive ao silenciamento por events:false.
Usado pra chain logic, derived states, ou efeitos colaterais essenciais do realm.
// Listener interno — dispara mesmo durante simulate({events:false})
store.onRegister(syncToRealm, { internal: true });
// Listener externo (default) — silenciado durante simulate
store.onRegister(updateUI);Checkpoint
Captura o estado do store num ponto e permite reverter depois. Diferente de transaction: não é "in-flight", múltiplos coexistem em paralelo, eventos sempre disparam normalmente.
| Método | Retorno | Descrição |
| -------------------- | ------------------ | ------------------------------------------------------------ |
| store.checkpoint() | Checkpoint | Captura eager O(N) do estado atual |
| cp.revert() | void | Restaura store ao estado capturado. Pode ser chamado N vezes |
| cp.discard() | void | Libera memória. Idempotente. Após discard, revert throws |
| cp.active | readonly boolean | false após discard |
| cp.id | readonly number | ID monotônico (debug) |
| cp.createdAt | readonly number | Timestamp (Date.now()) |
const cp = store.checkpoint(); // captura hp=100, mp=50, etc.
hp.set("value", 50); // observers normais
store.register(newState);
cp.revert(); // hp=100, newState unregistered
// observers veem as mudanças como mutações regulares
cp.revert(); // pode reverter de novo
cp.discard(); // libera memória
cp.revert(); // throws — "Checkpoint #X has been discarded"Semântica do revert:
- States existentes ao tempo do cp:
data+metarestaurados, observers notificados. - States registrados após o cp:
unregister(com lifecycle event normal). - States unregistered após o cp: re-registrados (com lifecycle event normal).
isLockednão é capturado — é runtime-only.
Interação com transactions:
cpcriado dentro de tx sobrevive ao rollback da tx — é bookmark no tempo, não tem escopo de tx.cp.revert()é uma mutação como qualquer outra: pode ser wrappado emtransaction({events:false})pra revert silencioso.
Quando NÃO usar: simulações throwaway de alta frequência. Pra esse caso, simulate({events:false}) é mais barato (não tem custo O(N) de captura).
Persistence
| Método | Retorno | Descrição |
| ------------------- | --------------- | --------------------------- |
| snapshot() | StoreSnapshot | Serializa todos os states |
| restore(snapshot) | void | Restaura states registrados |
const snap = store.snapshot();
// {
// primitives: {
// hp: { type: "record", id: "hp", data: { value: 100 } },
// mp: { type: "record", id: "mp", data: { value: 50 } },
// }
// }
hp.set("value", 0);
mp.set("value", 0);
store.restore(snap);
// hp.get("value") === 100, mp.get("value") === 50restore() ignora IDs do snapshot que não estão registrados. States não presentes no snapshot ficam intactos.
DerivedState<TState, TPrimitive> — Classe abstrata
Base para tipos derivados via composição. Delega toda a interface State<TState> para o state interno. O derivado adiciona apenas actions/getters de domínio.
Como criar um tipo derivado
type CounterData = { value: number; min: number; max: number; step: number };
class Counter extends DerivedState<CounterData, RecordState<CounterData>> {
constructor(id: string, config?: Partial<CounterData>) {
super(
new RecordState<CounterData>(id, {
value: config?.value ?? 0,
min: config?.min ?? -Infinity,
max: config?.max ?? Infinity,
step: config?.step ?? 1,
}),
);
}
// Domain actions — usa this._primitive (protected)
increment(amount?: number): void {
const { value, step, max } = this._primitive.getState();
this._primitive.set("value", Math.min(value + (amount ?? step), max));
}
decrement(amount?: number): void {
const { value, step, min } = this._primitive.getState();
this._primitive.set("value", Math.max(value - (amount ?? step), min));
}
// Domain getters
get value(): number {
return this._primitive.get("value");
}
get isMax(): boolean {
return this.value >= this._primitive.get("max");
}
}Delegação automática
Toda a interface State<TState> é delegada:
id,type,getState()beginTransaction(),commitTransaction(),rollback(),transaction(),simulate(),inTransaction,transactionDepthsnapshot(),restore()meta,subscribeMeta()subscribe(),use()lock(),unlock(),isLocked
Encapsulamento
Métodos do state interno não existem na API pública. Só acessíveis via this._primitive (protected).
const hp = new Counter("hp", { value: 100, min: 0, max: 100 });
hp.increment(); // domain action
hp.value; // domain getter
hp.subscribe(fn); // delegado
hp.transaction(fn); // delegado
store.register(hp); // Counter IS State
hp.set("value", 0); // TypeError — não existe
hp.merge({}); // TypeError — não existeassertSerializable(value, path?): void
Valida recursivamente se um valor é JSON-serializável.
Aceitos: null, undefined, string, boolean, número finito, plain object, array.
Bloqueados: function, symbol, bigint, NaN, Infinity, -Infinity, Date, RegExp, Map, Set, WeakMap, WeakSet, Promise, Error, class instances.
Throw: TypeError com path exato.
TypeError: Non-serializable value at 'root.config.db.handler': function
TypeError: Non-serializable value at 'root.items[2].timestamp': [object Date] is not a plain object
TypeError: Non-serializable value at 'root.score': NaN is not a finite numberassertSerializable({ a: 1, b: "ok" }); // ok
assertSerializable({ handler: () => {} }); // throws
assertSerializable({ nested: { date: new Date() } }); // throwsStrict Mode
Opt-in via config: { strict: true }. Valida serializabilidade a cada mutação.
| Modo | Overhead | Quando usar |
| ------------------------- | ------------------------------- | ------------------------------ |
| strict: false (default) | Zero | Performance, validação externa |
| strict: true | Walk recursivo O(n) por mutação | Dev, debug, input externo |
- Na criação:
initialStateé validado (fail-fast) - Em cada mutação: validação roda antes de middleware e listeners — se falha, zero side effects
- Imutável — não pode ser ligado/desligado em runtime
Auto-freeze (dev)
Enforcement só-dev da invariante "imutável por convenção", modelo Immer (setAutoFreeze). Resolve o caso em que o estado vivo sai pra um consumer externo (ex.: app React via SDK) que não respeita a convenção e muta a referência entregue — corrompendo structural sharing, determinismo e replay sem nenhum erro.
| Função | Retorno | Descrição |
| ---------------------- | --------- | -------------------------------------------------------------------- |
| setAutoFreeze(value) | void | Liga/desliga o gate global. Afeta states criados após a chamada. |
| getAutoFreeze() | boolean | Estado atual do gate. |
| deepFreeze(value) | T | Congela recursivamente in-place, devolve a mesma ref. |
| Modo | Overhead | Comportamento |
| --------------- | -------------------- | -------------------------------------------------------------- |
| prod (gate off) | Zero | Convenção pura — _freeze é identidade, JIT inlina pra nada |
| dev (gate on) | O(nós novos)/mutação | Estado committed deep-frozen — mutação acidental → TypeError |
import { createRecord } from "@statedelta-apex/record-state";
const hp = createRecord("hp", { value: 100, stats: { str: 10 } });
const s = hp.getState();
s.value = 0; // dev → TypeError: Cannot assign to read only property
s.stats.str = 0; // dev → TypeError (deep, não shallow)
hp.set("value", 70); // ok — mutação pela API cria nova ref (frozen em dev)Gate. Global, não per-state (diferente de strict): imutabilidade é invariante do realm inteiro, não opt-in por state. Default por NODE_ENV (!== "production" → ligado), override global via setAutoFreeze(). process.env.NODE_ENV é dead-stripped pelos bundlers em prod.
Invariantes preservadas:
- Zero custo em prod. O fork dev/prod mora num ponteiro
_freezeescolhido uma vez na criação do state (mesma filosofia do strategy-swap) — nenhum branch/walk no hot path quando desligado. - Estabilidade referencial intacta.
Object.freezeé in-place — reads continuam devolvendo a mesma ref. Congela uma vez na produção do estado, nunca por read. - Structural sharing barato mesmo em dev. Short-circuit em
Object.isFrozen— subárvore herdada de um estado anterior não é re-percorrida. Custo O(nós novos), não O(estado total). - Cobre o valor committed. Freeza o resultado final do pipeline (pós-
proceed(override)), em todos os pontos de produção:mutate,initialState,restore,_replaceState(checkpoint revert) emeta.
Transações compõem de graça: stacks são pointer-based, refs já vêm frozen da produção — rollback restaura ref válida, sem re-freeze, sem custo duplo.
Tipos — Referência
State-level
type Listener<TState> = (state: TState, prevState: TState) => void;
type MetaListener = (meta: Record<string, unknown>, prevMeta: Record<string, unknown>) => void;
type Middleware<TState> = (
currentState: TState,
nextState: TState,
next: (override?: TState) => void,
) => void;
interface MetaAccessor // Callable: () | (key) | (key, value) | (fn)
interface State<TState> // Contrato base
interface StateCore<TState> // State + mutate()
interface StateConfig // { strict?, trackDeltas? }
interface StateSnapshot // { type, id, data, meta? }
interface StateDefinition<T> // Input do createStateCoreStore-level
type StateFactory = (id: string, data: unknown, config?: unknown) => State;
type StoreListener = (primitiveId: string, state: unknown, prevState: unknown) => void;
type StoreLifecycleHook = (state: State) => void;
interface IStore // Interface do orquestrador
interface StoreConfig // Configuração global
interface StoreSnapshot // { primitives: Record<string, StateSnapshot> }