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/collection-state

v3.0.0

Published

ApexStore Collection primitive - typed ordered list state with item management

Readme

@statedelta-apex/collection-state

Collection primitive — lista ordenada de items tipados com identidade por ID único, transações aninhadas e imutabilidade por convenção.

Filosofia

State managers tradicionais (Redux, Zustand) armazenam objetos genéricos — o consumer escreve reducers pra mutar e selectors pra ler. A Collection inverte isso: é um store completo e autossuficiente que já sabe como armazenar, mutar, fazer transaction/rollback, notificar listeners e serializar seu próprio estado.

Diferente de um array genérico, cada item possui um campo de identidade único. Todas as operações CRUD são baseadas nesse ID. A Collection não é um wrapper de array — é um storage especializado onde a identidade dos items é first-class citizen.

import { createCollection } from "@statedelta-apex/collection-state";

const users = createCollection<User>("users", [
  { id: "1", name: "Anderson", role: "admin" },
  { id: "2", name: "Rosa", role: "user" },
]);

users.add({ id: "3", name: "Carlos", role: "user" });
users.update("1", { role: "superadmin" });
users.remove("2");

users.getById("1"); // { id: '1', name: 'Anderson', role: 'superadmin' }
users.size; // 2

Princípios

  • Determinístico — mesmas actions no mesmo estado produzem sempre o mesmo resultado.
  • Imutável por convenção — toda mutação gera uma nova referência via shallow clone. Referências anteriores permanecem intactas.
  • Autossuficiente — funciona standalone ou gerenciado por um Store. Transaction, subscribe, middleware, snapshot — tudo built-in.
  • Zero branches no hot path — o pipeline de mutação usa strategy swap. Quem não usa subscribe/middleware/hook não paga nada.
  • Identidade por ID — items são identificados por um campo configurável (default "id"). Toda operação CRUD é ID-based.

Instalação

pnpm add @statedelta-apex/collection-state

Requer @statedelta-apex/store como dependência (instalada automaticamente).

Quick Start

import { createCollection } from "@statedelta-apex/collection-state";

// Criar
const inventory = createCollection<Item>("inventory", [
  { id: "sword", name: "Sword", damage: 10 },
  { id: "shield", name: "Shield", defense: 5 },
]);

// Mutar
inventory.add({ id: "potion", name: "Potion", heal: 20 });
inventory.update("sword", { damage: 15 });
inventory.remove("shield");

// Ler
inventory.getById("sword"); // { id: 'sword', name: 'Sword', damage: 15 }
inventory.has("shield"); // false
inventory.size; // 2

// Reagir
const unsub = inventory.subscribe((items, prevItems) => {
  console.log(`Inventory: ${prevItems.length} → ${items.length} items`);
});

// Transacionar
inventory.transaction(() => {
  inventory.add({ id: "bow", name: "Bow", damage: 8 });
  inventory.remove("potion");
  // throw aqui → rollback ambos
});

// Simular (what-if)
const wouldOverflow = inventory.simulate(() => {
  inventory.add({ id: "armor", name: "Armor", defense: 20 });
  return inventory.size > MAX_ITEMS;
});
// inventory intacto

Criação

createCollection(id, initialItems, config?)

function createCollection<T extends Record<string, unknown>>(
  id: string,
  initialItems: T[],
  config?: CollectionConfig,
): CollectionState<T>;

| Parâmetro | Tipo | Descrição | | -------------- | ------------------ | -------------------------------------- | | id | string | Identificador único do primitivo | | initialItems | T[] | Items iniciais (clonados internamente) | | config | CollectionConfig | Configuração opcional |

// Default — idField é 'id'
const col = createCollection<User>("users", initialUsers);

// Custom idField
const cards = createCollection<Card>("cards", initialCards, {
  idField: "cardId",
});

// Strict mode
const strict = createCollection<Item>("items", data, { strict: true });

Também disponível via new:

import { CollectionState } from "@statedelta-apex/collection-state";

const col = new CollectionState<User>("users", initialUsers);

Actions (mutações)

Toda action produz uma nova referência do array e dispara o pipeline de mutação (hook → middleware → state → listeners). Uma action = um clone = um notify.

add(item)

Insere um item no final. Valida unicidade por ID.

add(item: T): void
users.add({ id: "3", name: "Carlos", role: "user" });
// users.add({ id: "1", ... })  // ← throw: already exists

addMany(items)

Batch insert — N items numa única mutação. Valida todos antes de aplicar (fail-fast no batch inteiro). Um clone, um notify.

addMany(items: T[]): void
users.addMany([
  { id: "4", name: "Maria", role: "user" },
  { id: "5", name: "Pedro", role: "admin" },
]);

upsert(item)

Add se não existe, shallow merge se existe. Operação single que evita o padrão has → add/update.

upsert(item: T): void
// Não existe → adiciona
users.upsert({ id: "6", name: "Lucas", role: "user" });

// Já existe → merge
users.upsert({ id: "6", name: "Lucas Silva", role: "admin" });

update(id, partial)

Busca por ID, faz shallow merge do partial. Protege o campo de identidade — não permite alterar o ID.

update(id: unknown, partial: Partial<T>): void
users.update("1", { role: "superadmin" });
// users.update("1", { id: "999" })  // ← throw: Cannot change id field

update(id, fn) — overload funcional

Functional update atômico. Recebe o item atual e retorna o novo.

update(id: unknown, fn: (item: T) => T): void
users.update("1", (user) => ({ ...user, loginCount: user.loginCount + 1 }));

updateBy(predicate, fn)

Update funcional em todos os items que satisfazem o predicado.

updateBy(predicate: (item: T) => boolean, fn: (item: T) => T): void
users.updateBy(
  (user) => user.role === "user",
  (user) => ({ ...user, role: "admin" }),
);

remove(id)

Remove item por ID. Noop se o ID não existe.

remove(id: unknown): void
users.remove("3");

removeBy(predicate)

Remove todos os items que satisfazem o predicado.

removeBy(predicate: (item: T) => boolean): void
users.removeBy((user) => user.role === "admin");

sort(comparator)

Reordena os items. Comparator genérico — o consumer decide a lógica.

sort(comparator: (a: T, b: T) => number): void
users.sort((a, b) => a.name.localeCompare(b.name));

clear()

Remove todos os items. Noop se já está vazio.

clear(): void

replace(items)

Substitui todos os items. Input é clonado internamente.

replace(items: T[]): void
users.replace([{ id: "1", name: "New", role: "admin" }]);

reset()

Restaura os items iniciais (passados na criação). Cada reset produz um clone independente.

reset(): void

Getters (leitura)

Getters são leituras puras. Sem side effects, sem mutações.

getById(id)

Lookup por ID. Retorna undefined se não encontrado.

getById(id: unknown): T | undefined
users.getById("1"); // { id: '1', name: 'Anderson', ... }
users.getById("999"); // undefined

has(id)

Verifica existência por ID.

has(id: unknown): boolean

at(index)

Acesso posicional. Suporta índices negativos (-1 = último).

at(index: number): T | undefined
users.at(0); // primeiro item
users.at(-1); // último item
users.at(999); // undefined

indexOf(id)

Retorna o índice de um item por ID. -1 se não encontrado.

indexOf(id: unknown): number

findBy(predicate)

Primeiro item que satisfaz o predicado.

findBy(predicate: (item: T) => boolean): T | undefined
const admin = users.findBy((u) => u.role === "admin");

filterBy(predicate)

Retorna novo array com items que satisfazem o predicado. Não muta estado.

filterBy(predicate: (item: T) => boolean): T[]
const admins = users.filterBy((u) => u.role === "admin");

every(predicate)

true se todos os items satisfazem o predicado.

every(predicate: (item: T) => boolean): boolean

some(predicate)

true se pelo menos um item satisfaz o predicado.

some(predicate: (item: T) => boolean): boolean

map(fn)

Projeta todos os items. Retorna novo array sem mutar estado.

map<R>(fn: (item: T) => R): R[]
const names = users.map((u) => u.name); // ['Anderson', 'Rosa']

Properties

size

Número de items. Getter.

readonly size: number

isEmpty

true se zero items. Getter.

readonly isEmpty: boolean

first

Primeiro item ou undefined. Getter.

readonly first: T | undefined

last

Último item ou undefined. Getter.

readonly last: T | undefined

items

Referência ao array de items (alias pra getState()). Imutável por convenção.

readonly items: T[]

idField

Campo de identidade configurado. Imutável.

readonly idField: string

Estado

getState()

Retorna o array de items completo. A referência é imutável por convenção — não modifique diretamente.

getState(): T[]
const items = users.getState(); // [{ id: '1', ... }, { id: '2', ... }]

// Cada mutação gera uma nova referência
users.add({ id: "3", name: "Carlos", role: "user" });
const newItems = users.getState();
items !== newItems; // true — referências diferentes
items.length; // 2 — original intacto

id / type

readonly id: string    // ID passado na criação
readonly type: string  // 'collection'

Transactions

A Collection suporta transações aninhadas via stack de snapshots. Cada beginTransaction() empilha o estado atual. rollback() restaura, commitTransaction() descarta o snapshot.

API básica

const col = createCollection<Item>("items", initialItems);

col.beginTransaction();
col.add({ id: "x", name: "X", value: 1 });
col.get("x"); // encontrado

col.rollback();
col.has("x"); // false — restaurado

transaction(fn)

Commit automático no sucesso, rollback automático no throw.

transaction<R>(fn: () => R): R
// Sucesso → commit
const result = col.transaction(() => {
  col.add({ id: "x", name: "X", value: 1 });
  col.add({ id: "y", name: "Y", value: 2 });
  return col.size;
});
// result === initialSize + 2

// Falha → rollback
try {
  col.transaction(() => {
    col.add({ id: "z", name: "Z", value: 3 });
    throw new Error("abort");
  });
} catch {}
col.has("z"); // false — rollback restaurou

simulate(fn)

Sempre rollback. Para cenários "what if?" sem mutar estado.

simulate<R>(fn: () => R): R
const wouldOverflow = inventory.simulate(() => {
  inventory.add({ id: "heavy", name: "Heavy", weight: 999 });
  return inventory.size > MAX_ITEMS;
});
// wouldOverflow calculado, inventory intacto

Nesting

Transações suportam nesting arbitrário.

col.transaction(() => {
  col.add({ id: "x", name: "X", value: 1 });

  const safe = col.simulate(() => {
    col.add({ id: "y", name: "Y", value: -1 });
    return col.every((i) => i.value >= 0);
  });
  // col.has('y') === false

  if (!safe) throw new Error("invalid state");
});

inTransaction / transactionDepth

readonly inTransaction: boolean   // true se algum nível está ativo
readonly transactionDepth: number // 0 = sem transaction

Subscribe (reatividade)

Registra um listener chamado a cada mutação. Recebe (items, prevItems).

subscribe(listener: (items: T[], prevItems: T[]) => void): () => void

Retorna uma função de unsubscribe.

const unsub = inventory.subscribe((items, prev) => {
  console.log(`Inventory: ${prev.length} → ${items.length} items`);
});

inventory.add({ id: "potion", name: "Potion", heal: 20 });
// log: "Inventory: 2 → 3 items"

unsub();
inventory.add({ id: "arrow", name: "Arrow", damage: 1 });
// sem log

Zero overhead sem subscribers

Quando nenhum listener está registrado, o pipeline de mutação usa a strategy sem listeners. Sem iteração, sem checagem.


Middleware

Interceptors de mutação. Podem observar, validar, bloquear ou transformar mutações.

use(middleware: (current: T[], next: T[], proceed: (override?: T[]) => void) => void): () => void

Retorna uma função de remoção.

| Chamada | Comportamento | | -------------------- | ----------------------------------------- | | proceed() | Aplica o valor original (next) | | proceed(override) | Substitui next pelo override e aplica | | sem chamar proceed | Bloqueia a mutação |

Logging

const remove = inventory.use((current, next, proceed) => {
  console.log(`${current.length} → ${next.length} items`);
  proceed();
});

Validação (bloquear mutação)

Não chamar proceed() bloqueia a mutação. O estado permanece inalterado e listeners não são notificados.

inventory.use((current, next, proceed) => {
  if (next.length > MAX_ITEMS) return; // bloqueia
  proceed();
});

inventory.add({ id: "overflow", name: "Overflow", value: 0 });
// bloqueado se excede MAX_ITEMS

Normalização — proceed(override)

proceed(override) substitui o valor antes de aplicar. Para normalização, ordenação automática, limitação de tamanho.

// Normalizar nomes para uppercase
inventory.use((_current, next, proceed) => {
  const normalized = next.map((item) => ({
    ...item,
    name: item.name.toUpperCase(),
  }));
  proceed(normalized);
});

inventory.add({ id: "sword", name: "sword", value: 10 });
inventory.getById("sword")!.name; // "SWORD"

// Limitar tamanho da collection
inventory.use((_current, next, proceed) => {
  if (next.length > MAX_ITEMS) {
    proceed(next.slice(0, MAX_ITEMS)); // trunca
  } else {
    proceed();
  }
});

// Auto-sort por valor após cada mutação
inventory.use((_current, next, proceed) => {
  proceed([...next].sort((a, b) => b.value - a.value));
});

O override flui pela chain — cada middleware recebe o valor já transformado pelo anterior.


Snapshot / Restore

Serialização e restauração de estado para persistência.

snapshot()

snapshot(): PrimitiveSnapshot
// { type: 'collection', id: string, data: T[] }
const snap = inventory.snapshot();
// { type: 'collection', id: 'inventory', data: [...items] }

localStorage.setItem("inventory", JSON.stringify(snap));

restore(snapshot)

restore(snapshot: PrimitiveSnapshot): void
const saved = JSON.parse(localStorage.getItem("inventory")!);
inventory.restore(saved);

Store Integration

A Collection funciona standalone por padrão. Quando registrada em um Store, o Store seta hooks internos para coordenar transações entre múltiplos primitivos.

// Standalone — sem Store
const inventory = createCollection<Item>("inventory", items);
inventory.add({ id: "sword", name: "Sword", damage: 10 });

// Com Store — coordenação entre primitivos
store.register(inventory);
store.transaction(() => {
  inventory.add({ id: "sword", name: "Sword", damage: 10 });
  hp.set("value", 50);
  // throw → rollback AMBOS
});

Extensão via Composição

A Collection é um primitivo — cuida de storage, transaction, snapshot, reactivity. Tipos derivados compõem com a Collection (via extends), adicionando actions/getters de domínio sem se preocupar com mecânica interna.

Exemplo: Deck

Um Deck é uma Collection de cartas com ações de domínio (shuffle, draw, peek).

import { CollectionState } from "@statedelta-apex/collection-state";

interface Card {
  id: string;
  suit: string;
  rank: string;
}

class Deck extends CollectionState<Card> {
  shuffle(): void {
    const shuffled = [...this.getState()];
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j]!, shuffled[i]!];
    }
    this.replace(shuffled);
  }

  draw(count: number = 1): Card[] {
    const items = this.getState();
    const drawn = items.slice(0, count);
    this.replace(items.slice(count));
    return drawn;
  }

  peek(count: number = 1): Card[] {
    return this.getState().slice(0, count);
  }
}

function createDeck(id: string, cards: Card[]): Deck {
  return new Deck(id, cards);
}
const deck = createDeck("main", allCards);

deck.shuffle();
const hand = deck.draw(5);
deck.size; // allCards.length - 5

// Transaction, subscribe, middleware — tudo funciona
deck.transaction(() => {
  const drawn = deck.draw(3);
  if (drawn.some((c) => c.rank === "Joker")) {
    throw new Error("no jokers allowed");
  }
});

deck.subscribe((cards, prev) => {
  console.log(`Deck: ${prev.length} → ${cards.length} cards`);
});

O Deck não sabe como transaction funciona, como snapshot serializa, como subscribe notifica. A Collection cuida de tudo.


Configuração

interface CollectionConfig {
  /** Campo de identidade dos items. Default: 'id' */
  idField?: string;

  /** Validação de serializabilidade a cada mutação. Default: false */
  strict?: boolean;

  /** Rastreamento de diffs (audit mode). Default: false */
  trackDeltas?: boolean;
}

| Config | Default | Descrição | | ------------- | ------- | --------------------------------------------- | | idField | "id" | Campo usado como identidade única dos items | | strict | false | Validação de serializabilidade a cada mutação | | trackDeltas | false | Rastreamento de diffs (audit mode) |


Tipos Exportados

import {
  createCollection,
  CollectionState,
} from "@statedelta-apex/collection-state";
import type { CollectionConfig } from "@statedelta-apex/collection-state";

| Export | Tipo | Descrição | | -------------------- | --------- | ----------------------------------------------------- | | createCollection | function | Factory principal | | CollectionState<T> | class | Classe do primitivo (usável com new ou via factory) | | CollectionConfig | interface | Configuração (extends PrimitiveConfig) |