| name | core/collections |
| description | Normalized entity storage via Collection<T, K>: createCollection plus addItem, addItems, addItemAt, upsertItem, updateItem, replaceItem, removeItem, filterCollection, getItem, getItems, findItem, filterItems, and reference counting (increaseRefsCount, decreaseRefsCount, getRefsCount, addItemAndCountRef). All operations are immutable and return a new Collection (or the same reference on a no-op). Public API: @augmentcode/themis/utils/collections/collection-utils; docs: docs/COLLECTIONS.md. |
| type | sub-skill |
| library | themis |
| requires | ["core","core/reducers"] |
| sources | ["augmentcode/themis:docs/COLLECTIONS.md","@augmentcode/themis/utils/collections/collection-utils"] |
| triggers | ["create collection","addItem collection","upsert item","reference counting"] |
Collections — Collection<T, K>
Operational guidance for normalized entity state. Full API reference and examples: docs/COLLECTIONS.md. Public API: @augmentcode/themis/utils/collections/collection-utils.
Use when
- State contains entities with a unique string id and needs O(1) lookup plus stable ordering.
- Reducers need immutable add/update/remove/upsert operations.
- Multiple features reference shared entities and need reference-count cleanup.
Shape and imports
- A collection is
{ idField, ids, map, refsCount }; ids preserves order and map stores id → item.
- Import helpers from
@augmentcode/themis/utils/collections/collection-utils.
- Use selectors, not components, to read collection contents.
Do
- Initialize with
createCollection<T, K>(idField, items?).
- Use helpers (
addItem, addItems, upsertItem, updateItem, replaceItem, removeItem, filterCollection) instead of mutating .ids, .map, or .refsCount.
- Preserve parent reducer reference equality when a helper returns the same collection reference.
- Use
getItem for O(1) reads and getItems for ordered materialization.
- Use
store.createSelector(...) plus getItem/getItems for selector-facing reads.
Reference counting cues
- Use
addItemAndCountRef / upsertItemAndCountRef when adding shared entities and incrementing ownership together.
- Use
increaseRefsCount and decreaseRefsCount for manual ownership changes.
decreaseRefsCount removes the item from ids, map, and refsCount when the count reaches <= 0.
getRefsCount returns 1 for an existing item with no explicit count and 0 for a missing item.
Don't
- Do not store entities as
Item[] when id lookup is needed.
- Do not store
Map/Set in Redux state; use collections or plain records.
- Do not add derived flags to item records when a separate relationship map or selector can represent the view.
- Do not call
getItems(collection).find(...) for id lookup; use getItem(collection, id).
- Do not import collection utilities directly into components; expose app reads through selectors.
Examples
Serializable initial collection state
import { type Collection, createCollection } from "@augmentcode/themis/utils/collections/collection-utils";
type Todo = { id: string; title: string; completed: boolean };
type TodosState = { todos: Collection<Todo, "id"> };
export const initialState: TodosState = {
todos: createCollection<Todo, "id">("id"),
};
Add, update, upsert, and remove in reducers
import { addItem, createCollection, removeItem, updateItem, upsertItem } from "@augmentcode/themis/utils/collections/collection-utils";
type Todo = { id: string; title: string; completed: boolean };
const state = { todos: createCollection<Todo, "id">("id") };
const todos = addItem(state.todos, { id: "todo-1", title: "Draft", completed: false });
const renamed = updateItem(todos, { id: "todo-1", title: "Ship docs" });
const synced = upsertItem(renamed, { id: "todo-2", title: "Review", completed: false });
const withoutDraft = removeItem(synced, "todo-1");
const nextState = withoutDraft === state.todos ? state : { ...state, todos: withoutDraft };
Reference-count shared entities
import { addItemAndCountRef, createCollection, decreaseRefsCount, getRefsCount } from "@augmentcode/themis/utils/collections/collection-utils";
type Todo = { id: string; title: string; completed: boolean };
const state = { todos: createCollection<Todo, "id">("id") };
const withOwner = addItemAndCountRef(state.todos, { id: "todo-1", title: "Draft", completed: false });
const ownerCount = getRefsCount(withOwner, "todo-1");
const afterRelease = decreaseRefsCount(withOwner, "todo-1");
ownerCount satisfies number;
afterRelease.ids satisfies string[];
Collection selectors expose item and ordered-list reads
import { Store } from "<selected Store family package>";
import { type Collection, createCollection, getItem, getItems } from "@augmentcode/themis/utils/collections/collection-utils";
import { createReducer } from "@augmentcode/themis/utils/store/create-reducer";
type Todo = { id: string; title: string; completed: boolean };
type TodosState = { todos: Collection<Todo, "id"> };
const initialState: TodosState = { todos: createCollection<Todo, "id">("id") };
const todosReducer = createReducer(initialState);
const store = new Store({ todos: todosReducer });
export const selectTodo = store.createSelector((state, id: string) => getItem(state.todos.todos, id));
export const selectTodos = store.createSelector((state) => getItems(state.todos.todos));
Plain arrays are fine for tiny ordered facts without id lookup
type BreadcrumbState = {
labels: string[];
};
const breadcrumbs: BreadcrumbState = {
labels: ["Projects", "Settings"],
};
❌ Bad: mutating internals corrupts normalized identity
import { type Collection, createCollection } from "@augmentcode/themis/utils/collections/collection-utils";
import { createAction } from "@augmentcode/themis/utils/store/create-action";
import { createReducer } from "@augmentcode/themis/utils/store/create-reducer";
type Todo = { id: string; title: string; completed: boolean };
type TodosState = { todos: Collection<Todo, "id"> };
const initialState: TodosState = { todos: createCollection<Todo, "id">("id") };
const renameTodo = createAction("todos/rename", (id: string, title: string) => ({ id, title }));
export const todosReducer = createReducer(initialState).with(renameTodo, (state, { payload }) => {
state.todos.map[payload.id] = { ...state.todos.map[payload.id], title: payload.title };
state.todos.ids.push(payload.id);
return state;
});
Verification cues
- Reducer tests cover add/update/remove/upsert branches and no-op same-reference behavior.
- Selector tests cover item lookup and list materialization using
.select(mockState, ...).
- Manual review checks that detailed helper examples remain in
docs/COLLECTIONS.md; this skill keeps only implementation cues.
See also
docs/COLLECTIONS.md — human reference for helper signatures and examples.
core/reducers/SKILL.md — pure immutable reducer updates.
- Selected Store family selector skill — collection selector helpers.
core/state-serialization/SKILL.md — collections as serializable alternatives to Map/Set.