Files
next-tjwater-frontend/src/features/workbench/hooks/use-workbench-runtime-data.ts
T
jiang d7eb01652a
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m7s
fix: deduplicate workbench requests and remove unused map paths
2026-11-09 18:49:32 +08:00

266 lines
10 KiB
TypeScript

import { toLocalDateKey } from "@/shared/utils/date-time";
import { useEffect, useState } from "react";
import { getWorkbenchProject, type WorkbenchAccess } from "../api/request-context";
import { env } from "@/shared/config/env";
import {
type WorkbenchRuntimeSnapshot
} from "../api/workbench-runtime-client";
import { fetchRealtimePipeFlows } from "../api/realtime-flow-client";
import {
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS,
createOperationalScheduledConditions
} from "../data/scheduled-conditions";
import { WORKBENCH_SCENARIOS } from "../data/workbench-session";
import type { ScheduledConditionItem } from "../types";
import type { WorkbenchSyncState } from "./use-workbench-sync";
const HEADER_DATA_TIME_STEP_MINUTES = 5;
const INITIAL_HEADER_DATA_TIME = "同步中";
const BACKEND_RUNTIME_REFRESH_INTERVAL_MS = 15_000;
export function resolveWorkbenchOperationalDate(
backendSnapshot: WorkbenchRuntimeSnapshot | null,
options: {
now?: Date;
runtimeMode?: "auto" | "demo" | "history" | "backend";
} = {}
) {
const now = options.now ?? new Date();
if (
!["demo", "history"].includes(options.runtimeMode ?? "auto")
|| !backendSnapshot
) return now;
const timestamps = backendSnapshot.workOrders
.map((workOrder) => Date.parse(workOrder.scheduledAt))
.filter(Number.isFinite);
const historicalTimestamps = timestamps.filter((timestamp) => timestamp <= now.getTime());
const latestTimestamp = historicalTimestamps.length > 0
? Math.max(...historicalTimestamps)
: timestamps.length > 0
? Math.min(...timestamps)
: null;
return latestTimestamp === null ? now : new Date(latestTimestamp);
}
export function formatHeaderDataTime(date: Date, snapToStep = true) {
const minutes = snapToStep
? Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES
: date.getMinutes();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const formattedMinutes = String(minutes).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${formattedMinutes}`;
}
export function getMsUntilNextHeaderDataTimeTick(date: Date) {
const nextTick = new Date(date);
const nextMinutes =
Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES +
HEADER_DATA_TIME_STEP_MINUTES;
nextTick.setMinutes(nextMinutes, 0, 0);
return Math.max(nextTick.getTime() - date.getTime(), 1_000);
}
export function resolveWorkbenchRuntimeData(
mockConditions: ScheduledConditionItem[],
backendSnapshot: WorkbenchRuntimeSnapshot | null,
options: {
operationalDate?: Date;
runtimeMode?: "auto" | "demo" | "history" | "backend";
} = {}
) {
const operationalDate = options.operationalDate ?? new Date();
const fallbackConditions = mockConditions.filter(
(condition) => condition.kind === "condition"
&& isSameLocalOperationalDate(condition.scheduledAt, operationalDate)
);
const backendWorkOrders = (backendSnapshot?.workOrders ?? []).filter(
(workOrder) => isSameLocalOperationalDate(workOrder.scheduledAt, operationalDate)
);
const backendWorkOrdersConfigured = Boolean(
backendSnapshot && backendSnapshot.workOrderAvailability !== "not_configured"
);
const resolvedWorkOrders = backendWorkOrdersConfigured ? backendWorkOrders : [];
const backendAlertsConfigured = Boolean(
backendSnapshot
&& backendSnapshot.alertAvailability !== "not_configured"
);
const useBackendScenarios = Boolean(
backendSnapshot
&& backendSnapshot.scenarioAvailability !== "not_configured"
&& backendSnapshot.scenarios.length > 0
);
const useBackendArtifact = Boolean(
backendSnapshot
&& backendSnapshot.artifactAvailability !== "not_configured"
&& backendSnapshot.latestAnalysisArtifact
);
return {
scheduledConditions: [...fallbackConditions, ...resolvedWorkOrders],
workbenchAlerts: backendAlertsConfigured
? backendSnapshot!.alerts
: [],
alertDataSource: backendAlertsConfigured ? "backend" as const : "none" as const,
runtimeDataSource: "mock" as const,
scenarios: useBackendScenarios ? backendSnapshot!.scenarios : WORKBENCH_SCENARIOS,
scenarioDataSource: useBackendScenarios ? "analysis_runs" as const : "mock" as const,
latestAnalysisArtifact: useBackendArtifact
? backendSnapshot!.latestAnalysisArtifact
: null,
simulationOverlay: useBackendArtifact ? backendSnapshot!.simulationOverlay : null,
artifactDataSource: useBackendArtifact ? "analysis_runs" as const : "mock" as const,
workOrderDataSource: backendWorkOrdersConfigured ? "backend" as const : "none" as const
};
}
function isSameLocalOperationalDate(value: string, operationalDate: Date) {
const candidate = new Date(value);
if (Number.isNaN(candidate.getTime()) || Number.isNaN(operationalDate.getTime())) return false;
return candidate.getFullYear() === operationalDate.getFullYear()
&& candidate.getMonth() === operationalDate.getMonth()
&& candidate.getDate() === operationalDate.getDate();
}
export function useWorkbenchRuntimeData(
getAccessToken: WorkbenchAccess | undefined,
sync: WorkbenchSyncState
) {
const scopedProject = getWorkbenchProject(getAccessToken);
const allowDemoScenarios = !scopedProject || scopedProject.gs_workspace === "tjwater_next";
const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME);
const [mockConditions, setMockConditions] = useState<ScheduledConditionItem[]>([]);
const backendSnapshot: WorkbenchRuntimeSnapshot | null = sync.snapshot?.runtime ?? null;
const [realtimePipeFlows, setRealtimePipeFlows] = useState<Awaited<
ReturnType<typeof fetchRealtimePipeFlows>
> | null>(null);
const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true);
useEffect(() => {
let timeoutId: number;
const refreshHeaderDataTime = () => {
const now = new Date();
setHeaderDataTime(formatHeaderDataTime(now));
timeoutId = window.setTimeout(refreshHeaderDataTime, getMsUntilNextHeaderDataTimeTick(now));
};
refreshHeaderDataTime();
return () => window.clearTimeout(timeoutId);
}, []);
useEffect(() => {
const refreshScheduledConditions = () => {
const operationalDate = resolveWorkbenchOperationalDate(backendSnapshot, {
runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE
});
setMockConditions(createOperationalScheduledConditions(operationalDate));
setScheduledConditionsLoading(false);
};
refreshScheduledConditions();
const intervalId = window.setInterval(
refreshScheduledConditions,
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS
);
return () => window.clearInterval(intervalId);
}, [backendSnapshot]);
useEffect(() => {
const controller = new AbortController();
let timeoutId: number | undefined;
let refreshing = false;
const refreshRealtimePipeFlows = async () => {
if (controller.signal.aborted || refreshing) return;
if (document.visibilityState === "hidden") {
timeoutId = window.setTimeout(refreshRealtimePipeFlows, BACKEND_RUNTIME_REFRESH_INTERVAL_MS);
return;
}
try {
refreshing = true;
const snapshot = await fetchRealtimePipeFlows(getAccessToken, controller.signal);
if (!controller.signal.aborted) {
setRealtimePipeFlows(snapshot.records.length > 0 ? snapshot : null);
}
} catch {
if (!controller.signal.aborted) setRealtimePipeFlows(null);
} finally {
refreshing = false;
if (!controller.signal.aborted) {
timeoutId = window.setTimeout(
refreshRealtimePipeFlows,
BACKEND_RUNTIME_REFRESH_INTERVAL_MS
);
}
}
};
const handleVisibilityChange = () => {
if (document.visibilityState !== "visible") return;
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
void refreshRealtimePipeFlows();
};
void refreshRealtimePipeFlows();
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
controller.abort();
document.removeEventListener("visibilitychange", handleVisibilityChange);
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
};
}, [getAccessToken]);
const operationalDate = resolveWorkbenchOperationalDate(backendSnapshot, {
runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE
});
const resolvedRuntimeData = resolveWorkbenchRuntimeData(mockConditions, backendSnapshot, {
operationalDate,
runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE
});
const backendDataTime = realtimePipeFlows?.observedAt;
const backendRuntimeDataTime = backendSnapshot?.generatedAt;
const demoMode = env.TJWATER_WORKBENCH_RUNTIME_MODE === "demo";
const historyMode = env.TJWATER_WORKBENCH_RUNTIME_MODE === "history";
const backendRuntimeActive = resolvedRuntimeData.runtimeDataSource !== "mock";
const backendWorkOrdersActive = resolvedRuntimeData.workOrderDataSource === "backend";
const backendHistoryActive = historyMode && (backendRuntimeActive || backendWorkOrdersActive);
return {
headerDataTime: backendHistoryActive
? `历史 ${formatHeaderDataTime(operationalDate, false)}`
: demoMode || historyMode
? `演示 ${formatHeaderDataTime(operationalDate, false)}`
: backendDataTime
? formatHeaderDataTime(new Date(backendDataTime), false)
: backendRuntimeActive && backendRuntimeDataTime
? formatHeaderDataTime(new Date(backendRuntimeDataTime), false)
: headerDataTime === INITIAL_HEADER_DATA_TIME
? headerDataTime
: `演示 ${headerDataTime}`,
headerDataSource: backendHistoryActive
? "history" as const
: demoMode || historyMode
? "demo" as const
: backendDataTime
? "realtime" as const
: backendRuntimeActive
? "backend" as const
: "mock" as const,
operationalDate: toLocalDateKey(operationalDate),
operationalCursorTime: operationalDate.toISOString(),
scheduledConditionsLoading,
realtimePipeFlows,
flowDataSource: realtimePipeFlows ? "realtime" as const : "mock" as const,
...resolvedRuntimeData,
scenarios: !allowDemoScenarios && resolvedRuntimeData.scenarioDataSource === "mock" ? [] : resolvedRuntimeData.scenarios
};
}