The Alliance Research Indicators client does not use NgRx or any other centralized state library. State is managed through a layered model: Angular Signals hold cross-cutting client state, RxJS handles asynchronous streams, and domain services own their own server-cache slices. This approach keeps each layer at the right abstraction level without the ceremony of a full flux store.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AllianceBioversityCIAT/alliance-research-indicators-client/llms.txt
Use this file to discover all available pages before exploring further.
The six state layers
| Layer | Primary tool | Where it lives |
|---|---|---|
| Server cache | ApiService + per-domain services + MainResponse<T> | src/app/shared/services/ |
| Client cache (cross-cutting) | Angular Signals (signal, computed, WritableSignal<T>) | shared/services/cache/cache.service.ts |
| Reactive streams (HTTP, WebSocket) | RxJS | Services and interceptors |
| Local component state | Signals (preferred) or component fields | Inside each component |
| Persisted state | localStorage | cache.service.ts, dark-mode.service.ts |
| URL state | Angular Router params and query params | app.routes.ts |
1. Server cache
All HTTP calls go throughApiService (or domain services that delegate to it). Every response is typed as MainResponse<T> — you always unwrap the envelope before using the data. The server is the source of truth; client-side caches of server data are intentionally short-lived and invalidated on navigation.
2. Client cache — Angular Signals
CacheService is the single authoritative store for cross-cutting UI state. It is provided in root and injected wherever the current user, current result, or layout signals are needed.
WritableSignal<T>for state that services mutate via.set()or.update().computed()for values derived from one or more other signals — these are automatically invalidated and recomputed when their dependencies change.localStorageread in the signal initializer so hydration is synchronous on startup.
3. Reactive streams — RxJS
RxJS is used for anything inherently asynchronous and time-ordered: HTTP responses (viaHttpClient inside ToPromiseService), WebSocket events, and any stream that requires operators such as switchMap, catchError, merge, or timer.
The interceptors (see the interceptor chain) are written as HttpInterceptorFn functional interceptors that return RxJS Observable<HttpEvent<T>>.
4. Local component state
Prefersignal() over plain class fields for state that drives template rendering. This gives Angular fine-grained change detection without requiring ChangeDetectorRef calls.
5. Persisted state — localStorage
Tokens and user session data are serialized intolocalStorage under the key 'data' and read back into CacheService.dataCache at startup. Layout preferences (sidebar collapse state, metadata panel visibility) are also stored in localStorage as individual string keys.
6. URL state — Angular Router
Route params (for exampleresultId in /result/:id) and query params (for example ?version=2) are the canonical source of truth for which resource is being viewed. CacheService.currentResultId is set by the route resolver or component init from the router — it is not an independent source of truth.
The DataCache interface
The shape of the persisted session object is defined insrc/app/shared/interfaces/cache.interface.ts:
The interceptor chain
Three functional HTTP interceptors are registered in order inapp.config.ts. Order matters — they are applied left to right on the outbound request and right to left on the response.
1. jWtInterceptor
Located at src/app/shared/interceptors/jwt.interceptor.ts.
- Reads the current
access_tokenfromCacheService.dataCache(). - Checks token expiry proactively before each request via
ActionsService.isTokenExpired(). - If expired, calls
POST /authorization/refresh-token, updateslocalStorageand the cache signal, then retries the original request with the new token. - On a 401 response from the upstream server, attempts a single refresh-and-retry. If the refresh itself fails, calls
actionsService.logOut()and clears tokens. - Applies
Authorization: Bearer <token>for Main API calls andaccess-token: <token>for Text-Mining and File Manager calls. - Requests with the
no-auth-interceptorheader bypass the interceptor entirely (used for unauthenticated endpoints).
2. httpErrorInterceptor
Located at src/app/shared/interceptors/http-error.interceptor.ts.
- Wraps every request in a 5-second timeout observable. If no response arrives within 5 seconds, a “pending” error object is posted to the error-tracking endpoint.
- On any HTTP error, constructs a structured
PostErrorobject (including the user’s session context) and sends it to the error-tracking endpoint. - Dispatches a user-visible error toast via
ActionsService.showToast()for all errors except 409 (conflict — handled by the link-to-existing flow), 401 (handled byjWtInterceptor), andrefresh-tokenresponses.
3. resultInterceptor
Located at src/app/shared/interceptors/result.interceptor.ts.
- Reads the
X-Use-Yearrequest header to determine whether the request should include result-versioning query parameters. - When present, reads the
?version=query param from the current URL and injectsreportYearandreportingPlatformsquery parameters into the outgoing request URL. - Handles the
X-Platformoverride header for cases where the platform code cannot be inferred from the URL. - This keeps versioning logic out of individual service methods — a service opts in by setting the
useResultInterceptor: trueflag on the request options.
Hard rules
These three rules apply to every change insidesrc/:
- No NgRx. Service-per-domain + signals is the established pattern. Do not add
@ngrx/*as a dependency. - No two-way binding for cross-cutting state. Use signals with explicit
.set()/.update()calls so state mutations are traceable. - Always unwrap
MainResponse<T>. Never pass a rawMainResponse<T>to a template or downstream function. ChecksuccessfulRequestfirst, then consumedata. Pass failures through toActionsServiceso the user sees a toast.