Skip to content

ADR-084 — Decompose the data.ts mega-client into $lib/data/ submodules

Status · Accepted Date · 2026-08-05 Gates · architectural-review R6 (data.ts accreting business logic vs. TA.md §constraints "no hidden business logic in the data client") Builds on · ADR-006 (data client), ADR-017 (locale-overlay merge)

Gating sentence: the 2,554-line src/lib/data.ts mega-client is decomposed incrementally into $lib/data/ submodules over a shared $lib/data/core.ts fetch+cache+i18n-overlay keystone; data.ts stays as a re-export barrel so all 65 consumers (from '$lib/data') keep working with ZERO import changes. Phase 1 (this ADR) extracts the keystone (core.ts) and the flagged business logic — the hero-override gallery builders (galleries.ts). Phases 2+ peel domain loaders (universe, science, stations, provenance) as separate, independently-shippable passes.

Context

data.ts is 2,554 lines / 147 exports — the single data client every route imports. The architectural review (R6, MED) flagged it as eroding the TA.md §constraints rule "no hidden business logic in the data client": it mixes pure fetch+cache+overlay loaders with business logic — the gallery-URL builders that orchestrate hero-override reordering (getMissionGallery/getFleetGallery/getCategoryGallery + 11 more, each doing loadHeroOverrides + applyHeroOverride + URL synthesis). It has also become an implicit god-module: a change anywhere risks the whole client.

Two facts make a safe split tractable (verified in code):

  1. Consumers import barrel-style — 65 files do from '$lib/data', none reach into internal symbols. A re-export barrel is transparent to them.
  2. The gallery builders depend only on get() + assetOrigin + $lib/image-hero (and each other) — NOT on the domain loaders. So they extract without a circular dependency.

Decision

  1. A shared keystone, not a barrel of copies. $lib/data/core.ts owns the fetch+cache+i18n-overlay primitives — get<T>(), the cache + i18nBundles maps, loadI18nBundle, the FetchLike type, and resetCoreCache(). Every current and future submodule imports get from here. This is the single source of the caching contract (ADR-006/017); it must not be duplicated.

  2. Extract the flagged business logic first. $lib/data/galleries.ts owns the hero-override gallery builders + their helpers (GALLERY_ID_ALIASES, gallerySiteIdVariants, the internal getCategoryGallery). This is the concrete R6 fix — the "business logic in the client" moves out, importing get from core and loadHeroOverrides/applyHeroOverride from $lib/image-hero.

  3. data.ts becomes a thin re-export barrel + the remaining domain loaders. It imports get/FetchLike from core, re-exports everything from galleries, and keeps __resetCache (which calls resetCoreCache() then clears its own provenance vars). Consumers see no change.

  4. Phases 2+ are separate PRs, one domain at a time. Universe (getNamedStars, getExoplanetSystems, getDeepSkyObjects, getMilkyWaySchematic, getLocalGroup, getBlackHoles, getCultureDoors), science (getScienceSection/getScienceTab/getProgram/getEssay), stations (ISS/Tiangong), and provenance (image/audio/logos/text/badges) each peel into $lib/data/<domain>.ts importing core, re-exported by data.ts. Each phase is independently green-testable and low-risk because the barrel keeps imports stable. The full end-state is data.ts = a barrel; the loaders live in domain files.

Consequences

  • Positive: the caching contract has one home; the constraint-violating gallery logic is out of the client; the god-module shrinks per phase without a big-bang rewrite; each phase is a small, reviewable diff with zero consumer churn.
  • Cost: a re-export barrel adds one indirection hop (negligible; tree-shaking handles it). During Phase 1 the barrel and submodules must agree on the get() signature — enforced by the type system + the existing data.test.ts suite.
  • Risk: circular imports if a submodule imported data.ts — avoided by the rule "submodules import core, never data.ts; data.ts imports submodules." Galleries reference each other within galleries.ts (fine).

Alternatives considered

  • Big-bang full decomposition — split all ~10 domains at once. Rejected: a 2.5k-line file touched by 65 consumers in one PR is high-risk and unreviewable; the phased barrel gives the same end-state safely.
  • Leave it, just extract galleries — addresses R6 literally but leaves the keystone entangled, so Phase 2 would have to extract core anyway. Doing core in Phase 1 unblocks all later phases for the same cost.
  • A $lib/data/index.ts barrel replacing data.ts — would require rewriting 65 import paths ($lib/data already resolves to data.ts; making it a dir needs an index). Keeping data.ts as the barrel is zero-churn.

Orrery — architecture documentation · MIT · No tracking