Files
Stirling-PDF/frontend/editor/src/proprietary/components/policies/policyRunStore.ts
T
James BruntonandGitHub 9a883be697 Cleanup of SaaS code (#6669)
# Description of Changes
De-AI comments and fix ridiculously indented code
2026-06-16 11:49:13 +01:00

162 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* External store for real backend policy runs (auto-run on upload).
*
* The auto-run controller fires a backend run for each enabled policy × each
* newly-uploaded file and records it here; the detail view's activity feed reads
* from it. `dispatched` keys (`categoryId:fileId`) ensure a given file is only
* ever run once per policy, surviving remounts via localStorage.
*
* Read with {@code useSyncExternalStore}; mutated by the controller.
*/
import { useSyncExternalStore } from "react";
import type { PolicyRunStatus } from "@app/services/policyPipeline";
export interface PolicyRunRecord {
runId: string;
categoryId: string;
fileId: string;
fileName: string;
fileSize: number;
status: PolicyRunStatus;
/** Output files (downloadable via /api/v1/general/files/{id}) once done. */
outputs: { fileId: string; fileName: string }[];
/** True once ALL outputs have been imported into the workspace. */
imported?: boolean;
/** Output fileIds already imported — tracked per-file so a partial failure
* retries only the missing ones and never re-adds the ones that succeeded. */
importedFileIds?: string[];
/** Workspace fileIds of the imported output files (the versioned child for
* "new version", or the added file for "new file"). Drives the policy badge,
* which marks the policy's OUTPUT — not the input it ran on. */
outputFileIds?: string[];
error: string | null;
/** Stable backend failure code (e.g. an entitlement sentinel) when FAILED; null otherwise. */
errorCode?: string | null;
/** Epoch ms when the run was dispatched. */
startedAt: number;
}
interface RunState {
runs: PolicyRunRecord[];
dispatched: string[];
}
const STORAGE_KEY = "stirling-policy-runs";
/** Cap stored runs so the activity log can't grow without bound. */
const MAX_RUNS = 50;
function read(): RunState {
try {
const raw =
typeof localStorage !== "undefined"
? localStorage.getItem(STORAGE_KEY)
: null;
if (raw) {
const parsed = JSON.parse(raw) as Partial<RunState>;
return {
// Normalise older persisted records (which predate the `outputs` field)
// so consumers can always rely on `outputs` being an array.
runs: Array.isArray(parsed.runs)
? parsed.runs.map((r) => ({
...r,
outputs: Array.isArray(r.outputs) ? r.outputs : [],
importedFileIds: Array.isArray(r.importedFileIds)
? r.importedFileIds
: [],
}))
: [],
dispatched: Array.isArray(parsed.dispatched) ? parsed.dispatched : [],
};
}
} catch {
// Corrupt/unavailable storage — start empty.
}
return { runs: [], dispatched: [] };
}
let state: RunState = read();
const listeners = new Set<() => void>();
function emit() {
try {
if (typeof localStorage !== "undefined") {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
} catch {
// Best-effort persistence.
}
for (const l of listeners) l();
}
function subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function getSnapshot(): RunState {
return state;
}
const SERVER_SNAPSHOT: RunState = { runs: [], dispatched: [] };
function getServerSnapshot(): RunState {
return SERVER_SNAPSHOT;
}
/** Key identifying a single (policy, file) run attempt. */
export function dispatchKey(categoryId: string, fileId: string): string {
return `${categoryId}:${fileId}`;
}
/** Whether this (policy, file) pair has already been dispatched. */
export function isDispatched(categoryId: string, fileId: string): boolean {
return state.dispatched.includes(dispatchKey(categoryId, fileId));
}
/** Record a newly-dispatched run (marks it dispatched + adds the record). */
export function recordRunStart(record: PolicyRunRecord) {
const key = dispatchKey(record.categoryId, record.fileId);
state = {
runs: [record, ...state.runs].slice(0, MAX_RUNS),
dispatched: state.dispatched.includes(key)
? state.dispatched
: [...state.dispatched, key],
};
emit();
}
/** Mark a (policy, file) pair dispatched without a run (e.g. dispatch failed). */
export function markDispatched(categoryId: string, fileId: string) {
const key = dispatchKey(categoryId, fileId);
if (state.dispatched.includes(key)) return;
state = { ...state, dispatched: [...state.dispatched, key] };
emit();
}
/** Patch an in-flight run's status/outputs/error as it progresses. */
export function updateRun(runId: string, patch: Partial<PolicyRunRecord>) {
let changed = false;
const runs = state.runs.map((r) => {
if (r.runId !== runId) return r;
changed = true;
return { ...r, ...patch };
});
if (!changed) return;
state = { ...state, runs };
emit();
}
/** Reset the store — used by tests to isolate it. */
export function resetPolicyRuns() {
state = { runs: [], dispatched: [] };
emit();
}
export function usePolicyRuns(): PolicyRunRecord[] {
return useSyncExternalStore(
subscribe,
() => getSnapshot().runs,
() => getServerSnapshot().runs,
);
}