Skip to main content

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 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.

The six state layers

LayerPrimary toolWhere it lives
Server cacheApiService + 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)RxJSServices and interceptors
Local component stateSignals (preferred) or component fieldsInside each component
Persisted statelocalStoragecache.service.ts, dark-mode.service.ts
URL stateAngular Router params and query paramsapp.routes.ts

1. Server cache

All HTTP calls go through ApiService (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.
@Injectable({ providedIn: 'root' })
export class CacheService {
  // User session
  isLoggedIn = signal(false);
  isValidatingToken = signal(false);
  dataCache: WritableSignal<DataCache> = signal(
    (() => {
      const raw = localStorage.getItem('data');
      if (!raw) return {};
      try { return JSON.parse(raw); } catch { return {}; }
    })()
  );

  // Current result context
  currentResultId: WritableSignal<string | number> = signal(0);
  currentMetadata: WritableSignal<GetMetadata> = signal({});
  greenChecks = signal<GreenChecks>({});

  // Computed signals derived from other signals
  allGreenChecksAreTrue = computed(() =>
    Object.values(this.greenChecks()).every(check => check)
  );
  isMyResult = computed(() =>
    Number(this.currentMetadata().created_by) === Number(this.dataCache().user.sec_user_id)
  );
  getCurrentNumericResultId = computed(() =>
    this.extractNumericId(this.currentResultId())
  );

  // Layout state
  isSidebarCollapsed = signal(localStorage.getItem('isSidebarCollapsed') !== 'false');
  windowWidth = signal(window.innerWidth);
  windowHeight = signal(window.innerHeight);
  hasSmallScreen = computed(() => this.windowHeight() < 768);
}
Key patterns visible here:
  • 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.
  • localStorage read 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 (via HttpClient 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

Prefer signal() over plain class fields for state that drives template rendering. This gives Angular fine-grained change detection without requiring ChangeDetectorRef calls.
// Preferred
export class ResultFormComponent {
  isSaving = signal(false);
  validationErrors = signal<string[]>([]);
}

5. Persisted state — localStorage

Tokens and user session data are serialized into localStorage 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.
Tokens are never logged, never sent to analytics SDKs, and never stored in sessionStorage (which is cleared on tab close). The JWT interceptor reads tokens from CacheService.dataCache() — access the signal, never reach into localStorage directly from a component.

6. URL state — Angular Router

Route params (for example resultId 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 in src/app/shared/interfaces/cache.interface.ts:
export class DataCache {
  access_token = '';
  refresh_token = '';
  user: UserCache = {} as UserCache;
  exp = 0;
}

export interface UserCache {
  is_active: boolean;
  sec_user_id: number;
  first_name: string;
  last_name: string;
  roleName: string;
  email: string;
  status_id: number;
  user_role_list: Userrolelist[];
}

The interceptor chain

Three functional HTTP interceptors are registered in order in app.config.ts. Order matters — they are applied left to right on the outbound request and right to left on the response.
provideHttpClient(withInterceptors([
  jWtInterceptor,
  httpErrorInterceptor,
  resultInterceptor
]))

1. jWtInterceptor

Located at src/app/shared/interceptors/jwt.interceptor.ts.
  • Reads the current access_token from CacheService.dataCache().
  • Checks token expiry proactively before each request via ActionsService.isTokenExpired().
  • If expired, calls POST /authorization/refresh-token, updates localStorage and 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 and access-token: <token> for Text-Mining and File Manager calls.
  • Requests with the no-auth-interceptor header 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 PostError object (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 by jWtInterceptor), and refresh-token responses.

3. resultInterceptor

Located at src/app/shared/interceptors/result.interceptor.ts.
  • Reads the X-Use-Year request header to determine whether the request should include result-versioning query parameters.
  • When present, reads the ?version= query param from the current URL and injects reportYear and reportingPlatforms query parameters into the outgoing request URL.
  • Handles the X-Platform override 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: true flag on the request options.

Hard rules

These three rules apply to every change inside src/:
  1. No NgRx. Service-per-domain + signals is the established pattern. Do not add @ngrx/* as a dependency.
  2. No two-way binding for cross-cutting state. Use signals with explicit .set() / .update() calls so state mutations are traceable.
  3. Always unwrap MainResponse<T>. Never pass a raw MainResponse<T> to a template or downstream function. Check successfulRequest first, then consume data. Pass failures through to ActionsService so the user sees a toast.

Build docs developers (and LLMs) love