Work / Aakalan / Wiki / Concepts
data-seam
Conceptverified 2026-10-30
data-seam
src/data/ is split so a change to how state is HELD cannot reach how it is COMPUTED.
| File | Job |
|---|---|
reducer.ts |
The pure half: types, EMPTY, reducer. No React, no I/O. Every state test drives it directly. |
actions.ts |
Every write, over dispatch + getState. Applies locally FIRST, queues the server write behind it. |
store.ts |
The Zustand store, derived balances, and effects (auth, hydration, persistence, the live subscription). |
hooks.ts |
The seam. What screens read. |
- Screens never import
@/data/storeor@/services/firebase. Money is derived in the hooks from expenses and settlements, never stored, so an action updates every screen at once and no two views can disagree. - State lives outside React, which is why Zustand: sync,
RecurringRunnerandReceiptUploaderall read and write the ledger and none is a screen.actionsis bound once at module scope, so a background runner or deep link can fire one with no component in scope, andgetState()always reads the current ledger. - Derived balances are computed once per dispatch, not per render.
groupBalanceandgroupBalancesare rebuilt with them on purpose: they are closures over the ledger andhooks.tslists them in memo dependencies. A module-stable function would leave a group's rows stale the moment an expense was added. src/lib/is pure, with no I/O and no React, which is why it is the part under test.
Related: sync-gates, local-first-ledger