place

@place/data

Data primitives over @place/reactivity. v0.1 ships exactly one helper — collection<T>() — the keyed-CRUD shape that every entity store hand-rolls. The position is deliberate: most app-level "data" problems collapse to a typed array in a State<T[]> plus the existing capability, persistence, and cache primitives. A typed-query layer lands only if a real workload demands one the reactivity primitives can't satisfy.

Operates on a State, not an opaque store
collection() takes a State<T[]> and returns CRUD helpers over it. The state stays exposed — compose it with persistedState, history(), crossTabAdapter unchanged. Domain logic (id generation, timestamps, validation) lives in the consumer, not the primitive.

collection<T>(state, options?)

Builds a keyed-CRUD interface over a State<T[]>. all() and get(key) are reactive reads; add / update / remove are writes. add throws on a duplicate key; update throws if the patch would change the key (use remove + add for a rename) — loud failures over silent corruption.

ts
// collection<T>(state, options?) — keyed CRUD over a State<T[]>. // The collection operates on a reactive array; the State stays // exposed so you can compose it with persistedState, history, etc. import { state } from '@place/reactivity' import { collection } from '@place/data' interface Note { id: string; title: string; tags: string[] } const notes = state<Note[]>([]) const c = collection<Note>(notes, { // sort comparator for all(); omit for insertion order. sortBy: (a, b) => a.title.localeCompare(b.title), }) c.add({ id: 'a', title: 'first', tags: [] }) c.get('a') // → { id: 'a', … } | null (reactive) c.update('a', { title: 'edit' }) // merge patch into item 'a' c.remove('a') c.all() // → readonly Note[], sorted (reactive)

Custom & composite keys

The key extractor defaults to (item) => item.id. Pass id for a differently-named property or a composite key.

ts
// The key defaults to (item) => item.id. Pass `id` for a // differently-named or composite key. const users = collection<User>(state<User[]>([]), { id: (u) => u.uuid, }) const items = collection<Item>(state<Item[]>([]), { id: (it) => `${it.org}:${it.slug}`, // composite key })

Composing with persistence

Because the collection wraps a plain State<T[]>, wrapping that state with persistedState makes every mutation durable — no special integration.

ts
// The underlying State<T[]> stays exposed — wrap it with // persistedState so the whole collection survives reloads. import { state } from '@place/reactivity' import { collection } from '@place/data' import { persistedState, localStorageAdapter } from '@place/persistence' const { state: noteState } = persistedState( localStorageAdapter<Note[]>('notes', []), ) const notes = collection<Note>(noteState) // Every add / update / remove now persists automatically — the // collection mutates the state, persistedState's watch saves it.

See also