@statedelta-apex/matrix-state
v3.0.0
Published
ApexStore Matrix primitive - typed 2D grid state with cell management
Downloads
230
Maintainers
Readme
@statedelta-apex/matrix-state
Matrix primitive — grid 2D tipado de dimensões fixas, com 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 Matrix 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 T[][] genérico, a Matrix possui dimensões fixas, valor default e operações otimizadas de grid (neighbors, diagonals, ranges). A Matrix não é um wrapper de array bidimensional — é um storage especializado onde posições 2D e structural sharing são first-class citizens.
import { createMatrix } from "@statedelta-apex/matrix-state";
const board = createMatrix<string | null>("board", 3, 3, null);
board.setCell(0, 0, "X");
board.setCell(1, 1, "O");
board.swap(0, 0, 2, 2);
board.getCell(2, 2); // "X"
board.getNeighbors(1, 1); // 4 vizinhos cardinais
board.isCellEmpty(0, 0); // true (voltou a null após swap)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.
- Dimensões fixas —
rows,colsedefaultValuesão imutáveis. O grid é denso — toda célula tem um valor. - Structural sharing — mutações clonam apenas as rows afetadas. Rows não tocadas mantêm a mesma referência.
Instalação
pnpm add @statedelta-apex/matrix-stateRequer @statedelta-apex/store como dependência (instalada automaticamente).
Quick Start
import { createMatrix } from "@statedelta-apex/matrix-state";
// Criar — 8×8 grid de números, default 0
const grid = createMatrix<number>("grid", 8, 8, 0);
// Mutar
grid.setCell(3, 4, 99);
grid.fillRow(0, 1);
grid.swap(3, 4, 7, 7);
// Ler
grid.getCell(7, 7); // 99
grid.getRow(0); // [1, 1, 1, 1, 1, 1, 1, 1]
grid.getNeighbors(4, 4, "all"); // 8 vizinhos
grid.find((v) => v === 99); // { row: 7, col: 7, value: 99 }
// Reagir
const unsub = grid.subscribe((cells, prevCells) => {
console.log("Grid changed");
});
// Transacionar
grid.transaction(() => {
grid.setCell(0, 0, 42);
grid.setCell(0, 1, 43);
// throw aqui → rollback ambos
});
// Simular (what-if)
const result = grid.simulate(() => {
grid.fill(0);
return grid.every((v) => v === 0);
});
// result === true, grid intactoCriação
createMatrix(id, rows, cols, defaultValue, config?)
function createMatrix<T>(
id: string,
rows: number,
cols: number,
defaultValue: T,
config?: MatrixConfig,
): MatrixState<T>;| Parâmetro | Tipo | Descrição |
| -------------- | -------------- | -------------------------------------------- |
| id | string | Identificador único do primitivo |
| rows | number | Número de linhas (>= 1, imutável) |
| cols | number | Número de colunas (>= 1, imutável) |
| defaultValue | T | Valor inicial de todas as células (imutável) |
| config | MatrixConfig | Configuração opcional |
// Tabuleiro 3×3
const board = createMatrix<string | null>("board", 3, 3, null);
// Grid numérico
const grid = createMatrix<number>("grid", 100, 100, 0);
// Strict mode
const strict = createMatrix("safe", 4, 4, 0, { strict: true });Também disponível via new:
import { MatrixState } from "@statedelta-apex/matrix-state";
const board = new MatrixState<string | null>("board", 3, 3, null);Dimensões devem ser >= 1. createMatrix("x", 0, 3, null) lança RangeError.
Actions (mutações)
Toda action produz uma nova referência do grid e dispara o pipeline de mutação (hook → middleware → state → listeners). Apenas as rows afetadas são clonadas — structural sharing garante que rows intocadas mantêm a mesma referência.
setCell(row, col, value)
Seta o valor de uma célula.
setCell(row: number, col: number, value: T): voidboard.setCell(0, 0, "X");
board.setCell(2, 2, "O");updateCell(row, col, fn)
Atualiza uma célula via função transformadora. Recebe o valor atual, retorna o novo.
updateCell(row: number, col: number, fn: (value: T) => T): voidgrid.updateCell(0, 0, (v) => v + 1);
grid.updateCell(1, 1, (v) => v * 2);updateRow(row, fn)
Transforma todos os valores de uma row. A função recebe o valor e o índice da coluna.
updateRow(row: number, fn: (value: T, col: number) => T): voidgrid.updateRow(0, (v, col) => v + col); // [0,1,2,3,4,...]
grid.updateRow(2, () => 99); // toda a row vira 99fill(value)
Preenche toda a matrix com um valor.
fill(value: T): voidboard.fill(null); // limpa o tabuleiro
grid.fill(0); // zera o gridfillRow(row, value)
Preenche uma linha inteira.
fillRow(row: number, value: T): voidgrid.fillRow(0, 1); // primeira linha toda = 1fillCol(col, value)
Preenche uma coluna inteira.
fillCol(col: number, value: T): voidgrid.fillCol(0, 5); // primeira coluna toda = 5fillRange(fromRow, toRow, fromCol, toCol, value)
Preenche um retângulo. from é inclusivo, to é exclusivo.
fillRange(fromRow: number, toRow: number, fromCol: number, toCol: number, value: T): void// Preenche o quadrante central de um 4×4
grid.fillRange(1, 3, 1, 3, 9);
// [0, 0, 0, 0]
// [0, 9, 9, 0]
// [0, 9, 9, 0]
// [0, 0, 0, 0]swap(fromRow, fromCol, toRow, toCol)
Troca os valores de duas células. Noop se as posições são iguais.
swap(fromRow: number, fromCol: number, toRow: number, toCol: number): voidboard.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(fromRow, fromCol, toRow, toCol)
Move o valor de uma célula para outra posição. A origem vira defaultValue. Noop se as posições são iguais.
move(fromRow: number, fromCol: number, toRow: number, toCol: number): voidboard.setCell(0, 0, "X");
board.move(0, 0, 2, 2);
board.getCell(0, 0); // null (defaultValue)
board.getCell(2, 2); // "X"replace(data)
Substitui a matrix inteira por um T[][]. Deve ter as mesmas dimensões. Input é clonado internamente.
replace(data: T[][]): voidboard.replace([
["X", "O", "X"],
["O", "X", "O"],
["X", "O", "X"],
]);Lança RangeError se as dimensões não batem.
reset()
Volta todas as células ao defaultValue.
reset(): voidGetters (leitura)
Getters são leituras puras. Sem side effects, sem mutações.
getCell(row, col)
Valor de uma célula.
getCell(row: number, col: number): Tboard.getCell(0, 0); // "X"
board.getCell(1, 1); // nullgetRow(row)
Linha inteira. Retorna uma cópia — modificar o array retornado não afeta o state.
getRow(row: number): T[]const row = board.getRow(0); // ["X", null, null]
row[0] = "mutated";
board.getCell(0, 0); // "X" — original intactogetCol(col)
Coluna inteira. Retorna um novo array construído a partir do grid.
getCol(col: number): T[]board.getCol(0); // ["X", null, null] (coluna 0 de cada row)getRange(fromRow, toRow, fromCol, toCol)
Sub-grid retangular. from inclusivo, to exclusivo. Retorna um novo T[][].
getRange(fromRow: number, toRow: number, fromCol: number, toCol: number): T[][]grid.getRange(0, 2, 0, 2); // [[v00, v01], [v10, v11]]getNeighbors(row, col, mode?)
Células vizinhas de uma posição. Retorna array de CellPosition<T>.
getNeighbors(row: number, col: number, mode?: "cardinal" | "all"): CellPosition<T>[]| Modo | Vizinhos | Direções |
| ---------------------- | -------- | -------------------------- |
| "cardinal" (default) | Até 4 | N, E, S, W |
| "all" | Até 8 | N, E, S, W, NE, SE, SW, NW |
Células fora dos limites são excluídas automaticamente (cantos retornam 2-3 vizinhos).
board.getNeighbors(1, 1); // 4 vizinhos (centro de 3×3)
board.getNeighbors(0, 0); // 2 vizinhos (canto)
board.getNeighbors(1, 1, "all"); // 8 vizinhos (centro, com diagonais)
board.getNeighbors(0, 0, "all"); // 3 vizinhos (canto, com diagonais)Cada CellPosition contém:
interface CellPosition<T> {
row: number;
col: number;
value: T;
}getDiagonal(direction?)
Diagonal principal ou anti-diagonal. Para matrizes não-quadradas, percorre min(rows, cols) elementos.
getDiagonal(direction?: "main" | "anti"): T[]// 3×3 com diagonal principal: A, E, I
// A B C
// D E F
// G H I
board.getDiagonal(); // ["A", "E", "I"]
board.getDiagonal("anti"); // ["C", "E", "G"]find(predicate)
Primeira célula que satisfaz o predicate. Percorre em row-major order (esquerda→direita, cima→baixo). Short-circuit no primeiro match.
find(predicate: (value: T, row: number, col: number) => boolean): CellPosition<T> | undefinedboard.find((v) => v === "X");
// { row: 0, col: 0, value: "X" }
board.find((v) => v === "Z");
// undefined
board.find((v, row, col) => row === col && v !== null);
// primeira célula na diagonal que não é nullfindAll(predicate)
Todas as células que satisfazem o predicate. Row-major order.
findAll(predicate: (value: T, row: number, col: number) => boolean): CellPosition<T>[]board.findAll((v) => v === "X");
// [{ row: 0, col: 0, value: "X" }, { row: 2, col: 2, value: "X" }]
board.findAll((v) => v === "Z");
// []count(predicate)
Conta células que satisfazem o predicate.
count(predicate: (value: T, row: number, col: number) => boolean): numberboard.count((v) => v !== null); // células preenchidas
board.count((v) => v === null); // células vaziasisCellEmpty(row, col)
Verifica se uma célula contém o defaultValue.
isCellEmpty(row: number, col: number): booleanboard.isCellEmpty(0, 0); // true (se defaultValue é null e célula é null)
board.setCell(0, 0, "X");
board.isCellEmpty(0, 0); // falseevery(predicate)
true se todas as células satisfazem o predicate. Short-circuit no primeiro false.
every(predicate: (value: T, row: number, col: number) => boolean): booleanconst allEmpty = board.every((v) => v === null);
const allPositive = grid.every((v) => v > 0);
const topRowFilled = board.every((v, row) => row !== 0 || v !== null);some(predicate)
true se alguma célula satisfaz o predicate. Short-circuit no primeiro true.
some(predicate: (value: T, row: number, col: number) => boolean): booleanconst hasX = board.some((v) => v === "X");
const hasNegative = grid.some((v) => v < 0);Properties
rows
Número de linhas. Imutável.
readonly rows: numbercols
Número de colunas. Imutável.
readonly cols: numberdefaultValue
Valor default das células. Imutável.
readonly defaultValue: Tcells
O grid como T[][]. Alias para getState(). Imutável por convenção.
readonly cells: T[][]size
Número total de células (rows × cols). Getter.
readonly size: numberEstado
getState()
Retorna o grid completo T[][]. A referência é imutável por convenção — não modifique diretamente.
getState(): T[][]const cells = board.getState();
// [[null, null, null], [null, null, null], [null, null, null]]
// Cada mutação gera uma nova referência
board.setCell(0, 0, "X");
const newCells = board.getState();
cells !== newCells; // true — referências diferentes
cells[0]![0]; // null — original intactoid / type
readonly id: string // ID passado na criação
readonly type: string // "matrix"Transactions
A Matrix suporta transações aninhadas via stack de snapshots. Cada beginTransaction() empilha o estado atual. rollback() restaura, commitTransaction() descarta o snapshot.
API básica
board.beginTransaction();
board.setCell(0, 0, "X");
board.setCell(1, 1, "O");
board.rollback();
board.getCell(0, 0); // null — restauradotransaction(fn)
Commit automático no sucesso, rollback automático no throw.
transaction<R>(fn: () => R): R// Sucesso → commit
board.transaction(() => {
board.setCell(0, 0, "X");
board.setCell(1, 1, "O");
});
// Falha → rollback
try {
board.transaction(() => {
board.setCell(0, 0, "X");
throw new Error("abort");
});
} catch {}
board.getCell(0, 0); // null — rollback restaurousimulate(fn)
Sempre rollback. Para cenários "what if?" sem mutar estado.
simulate<R>(fn: () => R): Rconst isDraw = board.simulate(() => {
board.setCell(2, 2, "X");
return board.every((v) => v !== null);
});
// isDraw calculado, board intactoNesting
Transações suportam nesting arbitrário.
board.transaction(() => {
board.setCell(0, 0, "X");
const wouldWin = board.simulate(() => {
board.setCell(1, 1, "X");
board.setCell(2, 2, "X");
return checkDiagonalWin(board);
});
// board tem só [0,0]="X"
if (!wouldWin) {
board.setCell(0, 1, "X"); // estratégia diferente
}
});inTransaction / transactionDepth
readonly inTransaction: boolean // true se algum nível está ativo
readonly transactionDepth: number // 0 = sem transactionSubscribe (reatividade)
Registra um listener chamado a cada mutação. Recebe (cells, prevCells).
subscribe(listener: (cells: T[][], prevCells: T[][]) => void): () => voidRetorna uma função de unsubscribe.
const unsub = board.subscribe((cells, prev) => {
console.log("Board changed");
});
board.setCell(0, 0, "X");
// log: "Board changed"
unsub();
board.setCell(1, 1, "O");
// sem logZero 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 ou bloquear mutações.
use(middleware: (current: T[][], next: T[][], proceed: (override?: T[][]) => void) => void): () => voidRetorna uma função de remoção.
Logging
const remove = board.use((current, next, proceed) => {
console.log("Board mutation");
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.
board.use((current, next, proceed) => {
// Impede que a célula central seja sobrescrita
if (current[1]![1] !== next[1]![1]) return;
proceed();
});Snapshot / Restore
Serialização e restauração de estado para persistência.
snapshot()
snapshot(): StateSnapshot
// { type: "matrix", id: string, data: T[][] }const snap = board.snapshot();
// { type: "matrix", id: "board", data: [[null,"X",null], ...] }
localStorage.setItem("board", JSON.stringify(snap));restore(snapshot)
restore(snapshot: StateSnapshot): voidconst saved = JSON.parse(localStorage.getItem("board")!);
board.restore(saved);O restore valida que o snapshot tem as mesmas dimensões (rows × cols). Lança RangeError se incompatível.
Bounds Checking
Toda operação que aceita coordenadas valida limites antes de operar. Lança RangeError com mensagem descritiva.
board.setCell(5, 0, "X");
// RangeError: Position [5, 0] out of bounds for 3×3 matrix
board.getRow(-1);
// RangeError: Row -1 out of bounds (0–2)
board.fillRange(0, 4, 0, 3, "X");
// RangeError: Range [0:4, 0:3] out of bounds for 3×3 matrix
board.fillRange(2, 1, 0, 3, "X");
// RangeError: Invalid range: from must be less than to (got [2:1, 0:3])Convenção de ranges: from inclusivo, to exclusivo (mesmo padrão de Array.prototype.slice()).
Store Integration
A Matrix funciona standalone por padrão. Quando registrada em um Store, o Store seta hooks internos para coordenar transações entre múltiplos primitivos.
import { createStore } from "@statedelta-apex/store";
import { createMatrix } from "@statedelta-apex/matrix-state";
// Standalone — sem Store
const board = createMatrix<string | null>("board", 3, 3, null);
board.setCell(0, 0, "X");
// Com Store — coordenação entre primitivos
const store = createStore();
store.register(board);
store.transaction(() => {
board.setCell(0, 0, "X");
board.setCell(1, 1, "O");
// throw → rollback AMBOS
});Extensão via Composição
A Matrix é um primitivo — cuida de storage, transaction, snapshot, reactivity. Tipos derivados compõem com a Matrix (via extends), adicionando actions/getters de domínio sem se preocupar com mecânica interna.
Exemplo: Board
Um Board é uma Matrix com semântica de tabuleiro de jogo (placement rules, win conditions).
import { MatrixState } from "@statedelta-apex/matrix-state";
class Board extends MatrixState<string | null> {
place(row: number, col: number, piece: string): void {
if (!this.isCellEmpty(row, col)) {
throw new Error(`Cell [${row}, ${col}] is occupied`);
}
this.setCell(row, col, piece);
}
isFull(): boolean {
return this.every((v) => v !== null);
}
getEmptyCells() {
return this.findAll((v) => v === null);
}
checkLine(positions: [number, number][]): boolean {
if (positions.length === 0) return false;
const first = this.getCell(positions[0]![0], positions[0]![1]);
if (first === null) return false;
return positions.every(([r, c]) => this.getCell(r, c) === first);
}
}
function createBoard(id: string, size: number): Board {
return new Board(id, size, size, null);
}const board = createBoard("tic-tac-toe", 3);
board.place(0, 0, "X");
board.place(1, 1, "O");
board.place(2, 2, "X");
board.isFull(); // false
board.getEmptyCells().length; // 6
board.checkLine([
[0, 0],
[1, 1],
[2, 2],
]); // false ("X", "O", "X")
// Transaction, subscribe, middleware — tudo funciona
board.transaction(() => {
board.place(0, 1, "X");
board.place(0, 2, "X");
});
board.subscribe((cells, prev) => {
console.log("Board updated");
});O Board não sabe como transaction funciona, como snapshot serializa, como subscribe notifica. A Matrix cuida de tudo.
Configuração
type MatrixConfig = StateConfig;interface StateConfig {
/** 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 |
| ------------- | ------- | --------------------------------------------- |
| strict | false | Validação de serializabilidade a cada mutação |
| trackDeltas | false | Rastreamento de diffs (audit mode) |
Tipos Exportados
import { createMatrix, MatrixState } from "@statedelta-apex/matrix-state";
import type {
MatrixConfig,
IMatrixState,
CellPosition,
CellPredicate,
} from "@statedelta-apex/matrix-state";| Export | Tipo | Descrição |
| ------------------ | --------- | ------------------------------------------------------ |
| createMatrix | function | Factory principal |
| MatrixState<T> | class | Classe do primitivo (usável com new ou via factory) |
| MatrixConfig | type | Configuração (alias de StateConfig) |
| IMatrixState<T> | interface | Interface pública completa |
| CellPosition<T> | interface | Posição + valor de uma célula { row, col, value } |
| CellPredicate<T> | type | Predicate para iteração (value, row, col) => boolean |
