Split the extractable parts out of the mail store #373

Closed
opened 2026-09-16 05:49:26 +00:00 by jcoffey-dev · 0 comments
Owner

Stacked on #372 — branched from it, so merge that first or this diff will look like it contains both.

store/mail.ts was 1,463 lines, the largest module in the app. It is now a directory, so @/store/mail resolves to index.ts and none of the 36 modules that import useMail changes a line.

File Lines Holds
mail/props.ts 72 MAILBOX_PROPS, LIST_PROPS, FULL_PROPS, BODY_PROPS
mail/types.ts 125 ListQuery, ListState, MailState, DEFAULT_SORT
mail/mailboxes.ts 28 mailboxIcon, ROLE_ORDER
mail/index.ts 1,266 the store, and everything bound to it

Everything exported before is still exported from index.ts. No behavior change, no call-site change.

What this does not do

index.ts is still 1,266 lines, because 947 of them are a single create<MailState>((set, get) => ({ … })). This PR removes 13% of the file, not 60%.

Cutting up the rest means Zustand slices — splitting the state object itself and recombining it. That is a change to how the store is built, not to where its text lives, in the part of the app every screen leans on. It deserves its own PR and its own argument rather than a quiet ride along with a file move. Say the word and I'll do it as a follow-up.

Three boundaries that look wrong until you check

Each of these is somewhere the obvious split fails:

  • listKey sits among the type declarations but is a function the store calls, not a type. Moving it to types.ts compiles and then fails at the call site.
  • ensureFolderPath, folderRefs, followFolders read like folder helpers and look like they belong beside mailboxIcon — but they close over useMail. Moving them makes mailboxes.ts import index.ts, which imports mailboxes.ts.
  • The sieve import is dynamicawait import("./sieve"), not a static from clause — so a path rewrite driven by from "…" silently misses it. It needed ../sieve after the move.

Translations: adds none. No user-visible strings.

Testing: npm run typecheck exit 0 across both workspaces, npm test exit 0 (135 web test files / 1,292 tests, 228 server tests), npm run build exit 0. Run locally, not left to CI.

Merged 2026-09-15 as coffey-labs/ihasmail@5cc31037c1

Rebuilt from: git history, session transcript.

**Stacked on #372** — branched from it, so merge that first or this diff will look like it contains both. `store/mail.ts` was 1,463 lines, the largest module in the app. It is now a directory, so `@/store/mail` resolves to `index.ts` and **none of the 36 modules that import `useMail` changes a line.** | File | Lines | Holds | | --- | --- | --- | | `mail/props.ts` | 72 | `MAILBOX_PROPS`, `LIST_PROPS`, `FULL_PROPS`, `BODY_PROPS` | | `mail/types.ts` | 125 | `ListQuery`, `ListState`, `MailState`, `DEFAULT_SORT` | | `mail/mailboxes.ts` | 28 | `mailboxIcon`, `ROLE_ORDER` | | `mail/index.ts` | 1,266 | the store, and everything bound to it | Everything exported before is still exported from `index.ts`. No behavior change, no call-site change. ## What this does not do **`index.ts` is still 1,266 lines**, because 947 of them are a single `create<MailState>((set, get) => ({ … }))`. This PR removes 13% of the file, not 60%. Cutting up the rest means Zustand slices — splitting the state object itself and recombining it. That is a change to *how the store is built*, not to where its text lives, in the part of the app every screen leans on. It deserves its own PR and its own argument rather than a quiet ride along with a file move. Say the word and I'll do it as a follow-up. ## Three boundaries that look wrong until you check Each of these is somewhere the obvious split fails: - **`listKey`** sits among the type declarations but is a function the store calls, not a type. Moving it to `types.ts` compiles and then fails at the call site. - **`ensureFolderPath`, `folderRefs`, `followFolders`** read like folder helpers and look like they belong beside `mailboxIcon` — but they close over `useMail`. Moving them makes `mailboxes.ts` import `index.ts`, which imports `mailboxes.ts`. - **The sieve import is dynamic** — `await import("./sieve")`, not a static `from` clause — so a path rewrite driven by `from "…"` silently misses it. It needed `../sieve` after the move. **Translations:** adds none. No user-visible strings. **Testing:** `npm run typecheck` exit 0 across both workspaces, `npm test` exit 0 (135 web test files / 1,292 tests, 228 server tests), `npm run build` exit 0. Run locally, not left to CI. **Merged** 2026-09-15 as coffey-labs/ihasmail@5cc31037c178 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.