← Files NGS Analysis WorkbenchARCHIVED FILE

src/hooks/useNgsWorkbench.ts

12.5 KB · Sep 30, 2026 · 23:20 UTC

↓ Download file

import { useCallback, useEffect, useRef, useState } from "react";

import type {
  ComputeTarget,
  DisplayMode,
  Pipeline,
  RunDetail,
  RunObservation,
  RunReport,
  RunSummary,
} from "../model";
import type { ReportArtifact } from "../report";
import { applyHostContext, type NgsWorkbenchClient } from "../mcp/client";
import {
  getRunDetail,
  getRunReport,
  listComputeTargets,
  listPipelines,
  listRunLineages,
  listRuns,
  observeRun,
} from "../mcp/ngs";
import { isActiveRunStatus, isTerminalRunStatus, latestActiveRunFilters } from "../runStatus";
import { isCurrentObservation, mergeRunIntoHistory } from "../runRefresh";

const POLL_INTERVAL_MS = 2_500;

export function useNgsWorkbench(client: NgsWorkbenchClient) {
  const [connected, setConnected] = useState(false);
  const [catalogLoading, setCatalogLoading] = useState(true);
  const [pipelines, setPipelines] = useState<Pipeline[]>([]);
  const [computeTargets, setComputeTargets] = useState<ComputeTarget[]>();
  const [targetsLoading, setTargetsLoading] = useState(false);
  const [targetsRequested, setTargetsRequested] = useState(false);
  const [targetsError, setTargetsError] = useState<string>();
  const [monitoredRun, setMonitoredRun] = useState<RunSummary>();
  const [analysisRun, setAnalysisRun] = useState<RunDetail>();
  const [analysisObservation, setAnalysisObservation] = useState<RunObservation>();
  const [analysisReport, setAnalysisReport] = useState<RunReport>();
  const [runHistory, setRunHistory] = useState<RunSummary[]>([]);
  const [historyLoading, setHistoryLoading] = useState(false);
  const [detailLoading, setDetailLoading] = useState(false);
  const [observationLoading, setObservationLoading] = useState(false);
  const [reportLoading, setReportLoading] = useState(false);
  const [observationError, setObservationError] = useState<{
    registryRunId: string;
    message: string;
  }>();
  const [reportError, setReportError] = useState<string>();
  const [operation, setOperation] = useState<"refreshing">();
  const [error, setError] = useState<string>();
  const [displayMode, setDisplayMode] = useState<DisplayMode>("inline");
  const openAnalysisRequestId = useRef(0);
  const reportRequestId = useRef(0);
  const observationRequests = useRef(new Map<string, Promise<RunObservation>>());
  const analysisIdentity = useRef<{ registryRunId: string; revision: number } | undefined>(
    undefined,
  );
  const analysisDetail = useRef<RunDetail | undefined>(undefined);

  const loadRunLineage = useCallback((run: RunSummary) => listRuns(client, {
    first_run_id: run.first_run_id,
    limit: 200,
  }), [client]);

  const loadRunHistory = useCallback(async () => {
    setHistoryLoading(true);
    try {
      const histories = await listRunLineages(client);
      setRunHistory(histories);
      return histories;
    } finally {
      setHistoryLoading(false);
    }
  }, [client]);

  const loadComputeTargets = useCallback(async () => {
    setTargetsRequested(true);
    setTargetsLoading(true);
    try {
      setComputeTargets(await listComputeTargets(client));
      setTargetsError(undefined);
    } catch (targetError) {
      setTargetsError(errorMessage(targetError, "Could not load compute targets."));
    } finally {
      setTargetsLoading(false);
    }
  }, [client]);

  const refreshRunHistory = useCallback(async () => {
    try {
      await loadRunHistory();
      setError(undefined);
    } catch (historyError) {
      setError(errorMessage(historyError, "Could not load durable run history."));
    }
  }, [loadRunHistory]);

  const loadCatalog = useCallback(async () => {
    setCatalogLoading(true);
    try {
      const availablePipelines = await listPipelines(client);
      setPipelines(availablePipelines);
      setError(undefined);
      try {
        const [, activeRuns] = await Promise.all([
          loadRunHistory(),
          listRuns(client, latestActiveRunFilters()),
        ]);
        setMonitoredRun((current) => current ?? activeRuns[0]);
      } catch (historyError) {
        setError(errorMessage(historyError, "Could not load durable run history."));
      }
    } catch (catalogError) {
      setError(errorMessage(catalogError, "Could not load the workflow catalog."));
    } finally {
      setCatalogLoading(false);
    }
  }, [client, loadRunHistory]);

  useEffect(() => {
    let disposed = false;
    const updateHostContext = () => {
      const context = client.getHostContext();
      applyHostContext(context);
      if (!context || disposed) return;
      setDisplayMode(context.displayMode === "fullscreen" ? "fullscreen" : "inline");
    };
    const removeHostListener = client.onHostContextChanged(updateHostContext);

    void client.connect().then(() => {
      if (disposed) return;
      setConnected(true);
      updateHostContext();
      void loadCatalog();
    }).catch((connectionError) => {
      if (disposed) return;
      setCatalogLoading(false);
      setError(errorMessage(connectionError, "Waiting for the MCP host."));
    });

    return () => {
      disposed = true;
      removeHostListener();
    };
  }, [client, loadCatalog]);

  const loadReport = useCallback(async (run: RunDetail) => {
    if (run.status !== "completed" || run.target?.target_id !== "local") return;
    const requestId = ++reportRequestId.current;
    setReportLoading(true);
    try {
      const response = await getRunReport(client, run.registry_run_id);
      const current = analysisIdentity.current;
      if (
        current?.registryRunId !== response.registry_run_id
        || response.revision < current.revision
      ) return;
      setAnalysisReport(response);
      setReportError(undefined);
    } catch (reportFailure) {
      if (requestId === reportRequestId.current) {
        setReportError(errorMessage(reportFailure, "Could not load the completed report."));
      }
    } finally {
      if (requestId === reportRequestId.current) setReportLoading(false);
    }
  }, [client]);

  const observeOnce = useCallback((registryRunId: string) => {
    const pending = observationRequests.current.get(registryRunId);
    if (pending) return pending;
    const request = observeRun(client, registryRunId).finally(() => {
      observationRequests.current.delete(registryRunId);
    });
    observationRequests.current.set(registryRunId, request);
    return request;
  }, [client]);

  const applyObservation = useCallback(async (observation: RunObservation) => {
    setMonitoredRun((current) => (
      current?.registry_run_id === observation.registry_run_id
        && observation.revision >= current.revision
        ? {
            ...current,
            status: observation.status,
            revision: observation.revision,
            pid: observation.pid ?? null,
            returncode: observation.returncode ?? null,
          }
        : current
    ));
    const identity = analysisIdentity.current;
    if (isCurrentObservation(identity, observation)) {
      setAnalysisObservation((current) => (
        !current || observation.revision >= current.revision ? observation : current
      ));
      analysisIdentity.current = {
        registryRunId: observation.registry_run_id,
        revision: observation.revision,
      };
      const detail = analysisDetail.current;
      if (detail && isTerminalRunStatus(observation.status)) {
        await loadReport({ ...detail, status: observation.status });
      }
    }
    setRunHistory((current) => mergeRunIntoHistory(current, observation));
    if (isTerminalRunStatus(observation.status)) await loadRunHistory();
  }, [loadReport, loadRunHistory]);

  const refreshStatus = useCallback(async () => {
    if (!analysisRun) return;
    setOperation((current) => current ?? "refreshing");
    setObservationLoading(true);
    try {
      await applyObservation(await observeOnce(analysisRun.registry_run_id));
      setObservationError((current) => (
        current?.registryRunId === analysisRun.registry_run_id ? undefined : current
      ));
    } catch (statusError) {
      setObservationError({
        registryRunId: analysisRun.registry_run_id,
        message: errorMessage(statusError, "Could not refresh the run status."),
      });
    } finally {
      setObservationLoading(false);
      setOperation((current) => current === "refreshing" ? undefined : current);
    }
  }, [analysisRun, applyObservation, observeOnce]);

  const refreshMonitoredStatus = useCallback(async () => {
    if (!monitoredRun) return;
    try {
      await applyObservation(await observeOnce(monitoredRun.registry_run_id));
      setObservationError((current) => (
        current?.registryRunId === monitoredRun.registry_run_id ? undefined : current
      ));
    } catch (statusError) {
      setObservationError({
        registryRunId: monitoredRun.registry_run_id,
        message: errorMessage(statusError, "Could not refresh the active run status."),
      });
    }
  }, [applyObservation, monitoredRun, observeOnce]);

  useEffect(() => {
    if (!monitoredRun || !isActiveRunStatus(monitoredRun.status)) return;
    const refreshWhenVisible = () => {
      if (document.visibilityState === "visible") void refreshMonitoredStatus();
    };
    const interval = window.setInterval(refreshWhenVisible, POLL_INTERVAL_MS);
    window.addEventListener("focus", refreshWhenVisible);
    document.addEventListener("visibilitychange", refreshWhenVisible);
    return () => {
      window.clearInterval(interval);
      window.removeEventListener("focus", refreshWhenVisible);
      document.removeEventListener("visibilitychange", refreshWhenVisible);
    };
  }, [monitoredRun, refreshMonitoredStatus]);

  const loadRunDetail = useCallback(
    (run: RunSummary) => getRunDetail(client, run.registry_run_id, pipelines),
    [client, pipelines],
  );
  const loadRunObservation = useCallback(
    (run: RunSummary) => observeOnce(run.registry_run_id),
    [observeOnce],
  );

  const openAnalysis = useCallback(async (run: RunSummary) => {
    const requestId = ++openAnalysisRequestId.current;
    reportRequestId.current += 1;
    setReportLoading(false);
    setDetailLoading(true);
    setError(undefined);
    try {
      const detail = await loadRunDetail(run);
      if (requestId !== openAnalysisRequestId.current) return;
      analysisIdentity.current = {
        registryRunId: detail.registry_run_id,
        revision: detail.revision,
      };
      analysisDetail.current = detail;
      setAnalysisRun(detail);
      setAnalysisObservation(undefined);
      setAnalysisReport(undefined);
      setObservationError(undefined);
      setReportError(undefined);
      setObservationLoading(true);
      void observeOnce(detail.registry_run_id).then((observation) => {
        if (requestId === openAnalysisRequestId.current) void applyObservation(observation);
      }).catch((observationFailure) => {
        if (requestId === openAnalysisRequestId.current) {
          setObservationError({
            registryRunId: detail.registry_run_id,
            message: errorMessage(observationFailure, "Could not observe this run."),
          });
        }
      }).finally(() => {
        if (requestId === openAnalysisRequestId.current) setObservationLoading(false);
      });
      void loadReport(detail);
    } catch (runError) {
      if (requestId !== openAnalysisRequestId.current) return;
      setError(errorMessage(runError, "Could not open the registered run."));
    } finally {
      if (requestId === openAnalysisRequestId.current) setDetailLoading(false);
    }
  }, [applyObservation, loadReport, loadRunDetail, observeOnce]);

  const openArtifact = useCallback(async (artifact: ReportArtifact) => {
    if (!artifact.openUrl) return;
    try {
      const result = await client.openLink(artifact.openUrl);
      if (result.isError) throw new Error("The host declined to open this report.");
      setError(undefined);
    } catch (openError) {
      setError(errorMessage(openError, "Could not open the generated report."));
    }
  }, [client]);

  return {
    analysisObservation,
    analysisReport,
    analysisRun,
    catalogLoading,
    computeTargets,
    connected,
    detailLoading,
    dismissError: () => setError(undefined),
    displayMode,
    error,
    historyLoading,
    loadCatalog,
    loadComputeTargets,
    loadRunDetail,
    loadRunLineage,
    loadRunObservation,
    observationError,
    observationLoading,
    operation,
    openArtifact,
    openAnalysis,
    pipelines,
    refreshRunHistory,
    refreshStatus,
    reportError,
    reportLoading,
    runHistory,
    targetsError,
    targetsLoading,
    targetsRequested,
  };
}

function errorMessage(error: unknown, fallback: string) {
  return error instanceof Error ? error.message : fallback;
}

SHA-256: 4af74063e9260f1e736ab1a0ae9c04b937f9d8c7c3718c8c1ca6b4c035a8ed9c