import { readEnginePreferences } from '../../app/engine-preferences'; import { beginActivity } from '../../platform/activity-store'; import type { RegulatoryFramework, RegulatoryMappingResponse } from '../regulatory-mapping'; import { statusPollApi } from './status-poll-client'; const tracked = new Set(); const listeners = new Set<(key: string, response: RegulatoryMappingResponse) => void>(); const isActive = (response: RegulatoryMappingResponse) => response.state === 'queued' || response.state === 'running'; const wait = (milliseconds: number) => new Promise(resolve => setTimeout(resolve, milliseconds)); // ISO keys keep their earlier form; a BSI mapping of the same finding is tracked separately. export const mappingRunKey = (projectId: string, resultProjectionId: string, framework: RegulatoryFramework = 'iso27001') => `${projectId}\u0000${resultProjectionId}${framework === 'iso27001' ? '' : `\u0000${framework}`}`; /** Views showing a mapping receive the tracker's status updates instead of polling themselves. */ export function subscribeRegulatoryMappingRuns(listener: (key: string, response: RegulatoryMappingResponse) => void): () => void { listeners.add(listener); return () => { listeners.delete(listener); }; } /** * Keeps one `Mapping ISO controls` activity for a queued or running mapping until the backend ends it, * independent of the open window, the selected finding or the active view. Switching the Compliance * Engine off stops observing; the backend work itself is never cancelled. */ export function trackRegulatoryMappingRun(projectId: string, resultProjectionId: string, response: RegulatoryMappingResponse): void { const framework = response.framework ?? 'iso27001'; const key = mappingRunKey(projectId, resultProjectionId, framework); if (!isActive(response) || tracked.has(key)) return; tracked.add(key); const label = framework === 'bsi-kernel' ? 'Mapping BSI requirements' : 'Mapping ISO controls'; const activity = beginActivity('Compliance Engine', label, `compliance-mapping:${key}`); if (response.state === 'queued') activity.queue(); void (async () => { let current = response; try { while (isActive(current)) { await wait(current.pollAfterMs ?? 1_500); if (!readEnginePreferences().compliance) { activity.update(`${label} continues in the backend`); activity.complete(); return; } current = await statusPollApi.getRegulatoryMappings(projectId, resultProjectionId, framework); if (current.state === 'running') activity.run(); listeners.forEach(listener => listener(key, current)); } if (current.state === 'failed') activity.fail(); else activity.complete(); } catch { activity.update(`${label}: status could not be checked`); activity.fail(); } finally { tracked.delete(key); } })(); }