import { Injectable, computed, inject, signal } from '@angular/core'; import { RemoteData } from '@shared/application/remote-data'; import { runSubmit } from '@shared/application/submit'; import { Result, ok, err } from '@shared/kernel/fp'; import { FeatureFlag } from '@shared/domain/feature-flag'; import { FeatureFlagsAdapter, parseFlags } from '@shared/infrastructure/feature-flags.adapter'; type Err = Error | undefined; const SET_FAILED = $localize`:@@flags.set.failed:De functievlag kon niet worden opgeslagen.`; /** * Runtime feature-flag state (WP-47) — one root singleton, mirroring `AccessStore`. Loads the * resolved flag set once from `GET /flags`; `enabled(key)` gates a feature (deny-by-default: * false until loaded / unknown key). `set()` is the admin toggle (PUT + reload). The catalog is * server-owned; the FE only mirrors + renders it. */ @Injectable({ providedIn: 'root' }) export class FeatureFlagStore { private adapter = inject(FeatureFlagsAdapter); private state = signal>({ tag: 'Loading' }); readonly flags = this.state.asReadonly(); /** The resolved list (empty until loaded) — for the admin toggle UI. */ readonly all = computed(() => { const rd = this.state(); return rd.tag === 'Success' ? rd.value : []; }); constructor() { void this.load(); } async load() { if (this.state().tag !== 'Success') this.state.set({ tag: 'Loading' }); try { const parsed = parseFlags(await this.adapter.list()); this.state.set( parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) }, ); } catch (e) { this.state.set({ tag: 'Failure', error: e as Error }); } } /** Deny-by-default: false while loading/failed or for an unknown key. Reactive (reads the signal). */ enabled(key: string): boolean { const rd = this.state(); return rd.tag === 'Success' && (rd.value.find((f) => f.key === key)?.enabled ?? false); } /** * Admin toggle: persist, then reload so the state reflects the server either way. * * Returns the failure rather than throwing. The previous `try/finally` had no `catch`, * so a rejected PUT escaped into the `void store.set(...)` call site as an unhandled * rejection: the reload then snapped the toggle back to its old value and the admin saw * a control that silently refused to move, with no error anywhere. */ async set(key: string, enabled: boolean): Promise> { const r = await runSubmit(() => this.adapter.set(key, enabled), SET_FAILED); await this.load(); return r.ok ? ok(undefined) : err(r.error); } }