refactor(apps/app): decouple lib/axios.ts from stores via callback seam #3
Reference in New Issue
Block a user
Delete Branch "refactor/axios-store-coupling"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Decouples
apps/app/src/lib/axios.tsfrom thestores/layer through atyped callback seam (
registerInterceptors(client, deps)), backed by anew bindings plugin (
plugins/3.axios-bindings.ts). Pays down debtdeferred during WS-3 sessie 1c.
Background
WS-3 sessie 1c introduced
eslint-plugin-boundarieswith a strictlayered-architecture matrix where
lib → storesis disallowed. At thetime, four
lib/axios.ts → stores/imports (2 static, 2 dynamic) weremarked with per-line
eslint-disable-next-linecomments referencingTECH-AXIOS-STORE-COUPLING. This PR removes those four exceptionsstructurally — not by suppression.
Architecture
lib/axios.tsis now pure HTTP infrastructure:apiClient(axios instance, no interceptors attached by default)registerInterceptors(client, deps)— the seam functionAxiosBindingsDepsinterface with five typed callbacksplugins/3.axios-bindings.tsprovides the runtime wiring:3.runs after2.pinia.tsso Pinia is active@core/utils/plugins.tsglob — nomain.tschangeCommits
5eac201—docs(refactor): audit axios↔store coupling for decoupling workPhase A read-only audit →
dev-docs/TECH-AXIOS-STORE-COUPLING-AUDIT.md.53f6a7b—refactor(apps/app): extract axios interceptors to registerInterceptors seamlib/axios.tsrewrite; the foureslint-disablemarkers removed structurally.26a92b3—feat(apps/app): add plugins/3.axios-bindings.ts to wire stores into axiosNew plugin; suplies the four runtime closures.
4197df2—docs: close TECH-AXIOS-STORE-COUPLING and add TECH-AXIOS-INTERCEPTOR-TESTSBACKLOG.md + ARCH-CONSOLIDATION-2026-04.md updated; new follow-up entry
for the four scenarios (X-Org-Id header, 401/403 logout, impersonation
revocation, error toasts) that remain untested today.
853939e—refactor(apps/app): decouple axios from impersonation sessionStorage contractFifth callback
getImpersonationTargetUserIdadded toAxiosBindingsDeps;lib/axios.tsno longer readssessionStoragedirectly nor knows the'crewli_impersonation'key or{ targetUserId }shape. The store isthe canonical source (verified:
useImpersonationStore.targetUserIdisa public computed hydrated from sessionStorage at store-init).
de07cca—chore(apps/app): drop unnecessary async on synchronous error handlersTwo
async error => { throw error }→error => { throw error }. Mechanical.Architectural decisions
Phase B sign-off (Bert):
AxiosBindingsDepslives inlib/axios.tsitself, exported.TECH-AXIOS-INTERCEPTOR-TESTSbacklog entry.3.axios-bindings.tsflat .ts file (matches2.pinia.tsconvention).Post-Phase-C improvements (during PR review):
legacy direct sessionStorage reads inside the request interceptor.
Audit showed
useImpersonationStore.targetUserIdis an existing publiccomputed; Option A (delegate via callback) was the clean path.
asynckeywords from synchronous errorhandlers. Behavior-neutral.
Acceptance
pnpm lint0 problems inapps/app/(pre-existing boundaries-v6 deprecation warnings tracked separately
as
TECH-BOUNDARIES-V6-SELECTOR-MIGRATION)pnpm typecheckcleanpnpm test49/49 passed (no regression)pnpm buildsucceedsuseAuthStoreanduseImpersonationStoreno longer dynamically imported bylib/axios.ts)eslint-disable-next-line boundaries/element-typesmarkersin
lib/axios.tsare gone (structurally, not suppressed)lib/axios.tshas zero references to@/stores/*orsessionStorageCo-Authored-By: Claudeon all commitsapps/portal/,api/,packages/,docs/modifiedFollow-up backlog item
TECH-AXIOS-INTERCEPTOR-TESTS(medium prio) — added toBACKLOG.md.Covers the four acceptance scenarios that remain untested:
Deferred deliberately: mixing refactor and test-creation in the same
session destroys the ability to verify whether tests spec pre- or
post-refactor behavior. Tests will be added in a focused session
against the new architecture from zero.
Merge
--no-ff(web UI: kies "Create merge commit") per CLAUDE.md commit hygiene.Supplies the runtime closures that the registerInterceptors seam needs. The plugin imports the four stores (`useOrganisationStore`, `useNotificationStore`, `useAuthStore`, `useImpersonationStore`) — allowed by the boundaries matrix (`plugins → stores`) — and passes them as lazy callbacks so the store factories only resolve when an HTTP call actually fires. Numeric prefix `3.` runs after `2.pinia.ts` (auto-loaded by `@core/utils/plugins.ts` in alphabetical-path order), so Pinia is guaranteed active before the bindings register. No change to `main.ts` is required — the file is picked up by the existing `import.meta.glob('./plugins/*.{ts,js}')` glob. Two redirects previously inside axios.ts now live where they belong: - `window.location.href = '/platform'` on impersonation revocation, in the `onImpersonationRevoked` closure. - `handleUnauthorized()` (which itself redirects to `/login`) on 401, gated by `isInitialized` inside the `onAuthFail` closure — preserves the race-condition fix from sessie 1b-iii. With this commit the two Vite mixed-import warnings (useAuthStore + useImpersonationStore being both statically and dynamically imported) disappear from `pnpm build`. Lint stays at 0 problems, typecheck clean, 49/49 tests pass. Refs TECH-AXIOS-STORE-COUPLING. Co-Authored-By: Claude <noreply@anthropic.com>Chose Option A from the follow-up brief: useImpersonationStore already holds an `ImpersonationState` ref hydrated from sessionStorage at store-init and exposes the active impersonation target user as a public `targetUserId` computed. The store is the canonical source; sessionStorage is just its persistence sidecar. Adds a fifth callback `getImpersonationTargetUserId: () => string | null` to AxiosBindingsDeps and replaces the sessionStorage.getItem('crewli_impersonation') + JSON.parse block in the request interceptor with a single `deps.getImpersonationTargetUserId()` call. The bindings plugin wires it to `useImpersonationStore().targetUserId`. After this commit lib/axios.ts has zero references to sessionStorage and zero magic strings about impersonation persistence — the only persistence-mechanism knowledge left is in useImpersonationStore (where it belongs) and in plugins/3.axios-bindings.ts (allowed to know about stores). The HTTP module is now unambiguously pure infrastructure. Behavior preserved 1:1: the store hydrates from sessionStorage synchronously inside the defineStore factory, so the very first HTTP request after page load sees the same target user id as before. Co-Authored-By: Claude <noreply@anthropic.com>Pull request closed