Files
EasyFlow/easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/joinMode.ts

164 lines
4.5 KiB
TypeScript

import type { Edge, Node } from '@xyflow/svelte';
export const JOIN_MODE_ANY = 'any';
export const JOIN_MODE_ALL = 'all';
export type JoinMode = typeof JOIN_MODE_ANY | typeof JOIN_MODE_ALL;
export type JoinModeAnalysis = {
incomingCount: number;
mode: JoinMode | null;
invalidMode: boolean;
allAllowed: boolean;
allDisabledReason: string;
};
const text = (value: unknown) => (value == null ? '' : String(value).trim());
export function parseJoinMode(value: unknown): JoinMode | null {
if (value === undefined) {
return JOIN_MODE_ANY;
}
const normalized = text(value).toLowerCase();
if (normalized === JOIN_MODE_ANY) {
return JOIN_MODE_ANY;
}
if (normalized === JOIN_MODE_ALL) {
return JOIN_MODE_ALL;
}
return null;
}
export function getJoinModeBadge(value: unknown) {
return parseJoinMode(value) === JOIN_MODE_ALL ? '等待全部' : '';
}
export function analyzeJoinMode(
nodes: Node[],
edges: Edge[],
nodeId: string,
): JoinModeAnalysis {
const targetNode = nodes.find((node) => node.id === nodeId);
const directInward = edges.filter((edge) => edge.target === nodeId);
const hasJoinMode = targetNode?.data
? Object.prototype.hasOwnProperty.call(targetNode.data, 'joinMode')
: false;
const mode = parseJoinMode(targetNode?.data?.joinMode);
const invalidMode = hasJoinMode && mode === null;
if (!targetNode) {
return {
incomingCount: directInward.length,
mode,
invalidMode,
allAllowed: false,
allDisabledReason: '节点不存在,请刷新画布后重试。',
};
}
if (text(targetNode.parentId)) {
return {
incomingCount: directInward.length,
mode,
invalidMode,
allAllowed: false,
allDisabledReason: '显式循环子图暂不支持等待全部上游,请使用“任一上游完成”。',
};
}
if (directInward.length <= 1) {
return {
incomingCount: directInward.length,
mode,
invalidMode,
allAllowed: true,
allDisabledReason: '',
};
}
const guaranteedNodes = findGuaranteedNodes(nodes, edges);
const allAllowed = directInward.every(
(edge) => !hasEdgeCondition(edge) && guaranteedNodes.has(edge.source),
);
if (allAllowed) {
return {
incomingCount: directInward.length,
mode,
invalidMode,
allAllowed: true,
allDisabledReason: '',
};
}
let reason = '存在条件、互斥或无法证明必达的上游路径,可能永久等待。请调整连线或使用“任一上游完成”。';
if (directInward.some(hasEdgeCondition)) {
reason = '存在带条件的直接入边,部分入边可能不会到达。请调整连线或使用“任一上游完成”。';
} else if (directInward.some((edge) => {
const source = nodes.find((node) => node.id === edge.source);
return hasAdvancedCondition(source);
})) {
reason = '上游节点包含高级执行条件,无法保证每条入边都会到达。请调整条件或使用“任一上游完成”。';
}
return {
incomingCount: directInward.length,
mode,
invalidMode,
allAllowed: false,
allDisabledReason: reason,
};
}
function findGuaranteedNodes(nodes: Node[], edges: Edge[]) {
const guaranteed = new Set(
nodes
.filter((node) => !text(node.parentId) && node.type === 'startNode')
.map((node) => node.id),
);
const inwardByTarget = new Map<string, Edge[]>();
edges.forEach((edge) => {
const inward = inwardByTarget.get(edge.target) || [];
inward.push(edge);
inwardByTarget.set(edge.target, inward);
});
let changed = true;
while (changed) {
changed = false;
nodes.forEach((node) => {
if (
text(node.parentId)
|| guaranteed.has(node.id)
|| hasAdvancedCondition(node)
) {
return;
}
const mode = parseJoinMode(node.data?.joinMode);
if (!mode) {
return;
}
const inward = inwardByTarget.get(node.id) || [];
const isGuaranteed = mode === JOIN_MODE_ALL
? inward.length > 0 && inward.every(
(edge) => !hasEdgeCondition(edge) && guaranteed.has(edge.source),
)
: inward.some(
(edge) => !hasEdgeCondition(edge) && guaranteed.has(edge.source),
);
if (isGuaranteed) {
guaranteed.add(node.id);
changed = true;
}
});
}
return guaranteed;
}
function hasAdvancedCondition(node: Node | undefined) {
return Boolean(text(node?.data?.condition));
}
function hasEdgeCondition(edge: Edge) {
return Boolean(text(edge.data?.condition));
}