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-apex/store

v0.4.0

Published

ApexStore - Typed state management with specialized state primitives and registry pattern

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) e ManagedState<T> (interna).
  • Composição sobre herança — primitivos extendem PrimitiveState, tipos derivados extendem DerivedState.
  • 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/store

Funciona 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ória

Features

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ífico

proceed() 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; // 101

Lock é 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 todos

Erros:

store.register(hp);
store.register(hp);  // Error: State "hp" already registered

Factory 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"); // true

Erros:

store.create("unknown", "x", {});  // Error: Type "unknown" not defined. Use store.defineType() first.
store.create("record", "hp", {});  // Error: State "hp" already registered

Transaction — 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") === 70

Reactivity

| 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/provisionalregister/committed | | register em tx + rollback | register/provisionalregister/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 + meta restaurados, 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).
  • isLocked não é capturado — é runtime-only.

Interação com transactions:

  • cp criado 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 em transaction({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") === 50

restore() 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, transactionDepth
  • snapshot(), 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 existe

assertSerializable(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 number
assertSerializable({ a: 1, b: "ok" }); // ok
assertSerializable({ handler: () => {} }); // throws
assertSerializable({ nested: { date: new Date() } }); // throws

Strict 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 _freeze escolhido 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) e meta.

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 createStateCore

Store-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> }