29dce59b35
新增只读资源依赖图模型,完成过滤、去重、环检测和任务流聚合 前端SVG仅负责几何渲染、搜索联动、选中高亮和拖动预览 补充资源图、覆盖层和工作台回归测试 同步更新工作台PRD、技术方案与项目记忆
243 lines
7.7 KiB
TypeScript
243 lines
7.7 KiB
TypeScript
export type ProjectResourceGraphNodeInput = {
|
|
resourceId: string;
|
|
manifestAssetId: string | null;
|
|
producerTaskId: string | null;
|
|
};
|
|
|
|
export type ProjectResourceReferenceEdge = {
|
|
id: string;
|
|
kind: 'asset-reference';
|
|
sourceResourceId: string;
|
|
targetResourceId: string;
|
|
cyclic: boolean;
|
|
};
|
|
|
|
export type ProjectResourceTaskFlow = {
|
|
id: string;
|
|
kind: 'task-flow';
|
|
sourceTaskId: string;
|
|
targetTaskId: string;
|
|
sourceResourceIds: string[];
|
|
targetResourceIds: string[];
|
|
cyclic: boolean;
|
|
};
|
|
|
|
export type ProjectResourceConnectionIndexDto = {
|
|
resourceId: string;
|
|
upstreamReferenceResourceIds: string[];
|
|
downstreamReferenceResourceIds: string[];
|
|
referenceEdgeIds: string[];
|
|
taskFlowIds: string[];
|
|
};
|
|
|
|
export type ProjectResourceProducerAssignment = {
|
|
resourceId: string;
|
|
taskId: string;
|
|
};
|
|
|
|
export type ProjectResourceGraphReadModel = {
|
|
resourceIds: string[];
|
|
referenceEdges: ProjectResourceReferenceEdge[];
|
|
taskFlows: ProjectResourceTaskFlow[];
|
|
connectionIndex: ProjectResourceConnectionIndexDto[];
|
|
producerAssignments: ProjectResourceProducerAssignment[];
|
|
unresolvedReferenceResourceIds: string[];
|
|
cyclicResourceIds: string[];
|
|
cyclicTaskIds: string[];
|
|
producerMappingTruncated: boolean;
|
|
};
|
|
|
|
type ProjectResourceConnectionIndex = {
|
|
upstreamReferenceResourceIds: ReadonlySet<string>;
|
|
downstreamReferenceResourceIds: ReadonlySet<string>;
|
|
referenceEdgeIds: ReadonlySet<string>;
|
|
taskFlowIds: ReadonlySet<string>;
|
|
};
|
|
|
|
export type ProjectResourceGraph = {
|
|
resourceIds: ReadonlySet<string>;
|
|
referenceEdges: ProjectResourceReferenceEdge[];
|
|
referenceEdgeById: ReadonlyMap<string, ProjectResourceReferenceEdge>;
|
|
taskFlows: ProjectResourceTaskFlow[];
|
|
taskFlowById: ReadonlyMap<string, ProjectResourceTaskFlow>;
|
|
connectionIndex: ReadonlyMap<string, ProjectResourceConnectionIndex>;
|
|
producerTaskIdByResourceId: ReadonlyMap<string, string>;
|
|
unresolvedReferenceResourceIds: string[];
|
|
cyclicResourceIds: ReadonlySet<string>;
|
|
cyclicTaskIds: ReadonlySet<string>;
|
|
producerMappingTruncated: boolean;
|
|
};
|
|
|
|
export type ProjectResourceGraphNeighbors = {
|
|
upstreamResourceIds: ReadonlySet<string>;
|
|
downstreamResourceIds: ReadonlySet<string>;
|
|
connectedEdgeIds: ReadonlySet<string>;
|
|
};
|
|
|
|
const emptyStringSet: ReadonlySet<string> = new Set<string>();
|
|
const emptyStringMap: ReadonlyMap<string, string> = new Map<string, string>();
|
|
const emptyConnectionMap: ReadonlyMap<string, ProjectResourceConnectionIndex> =
|
|
new Map<string, ProjectResourceConnectionIndex>();
|
|
const emptyTaskFlowMap: ReadonlyMap<string, ProjectResourceTaskFlow> = new Map<
|
|
string,
|
|
ProjectResourceTaskFlow
|
|
>();
|
|
const emptyReferenceEdgeMap: ReadonlyMap<
|
|
string,
|
|
ProjectResourceReferenceEdge
|
|
> = new Map<string, ProjectResourceReferenceEdge>();
|
|
|
|
export const EMPTY_PROJECT_RESOURCE_GRAPH_NEIGHBORS: ProjectResourceGraphNeighbors =
|
|
{
|
|
upstreamResourceIds: emptyStringSet,
|
|
downstreamResourceIds: emptyStringSet,
|
|
connectedEdgeIds: emptyStringSet,
|
|
};
|
|
|
|
export const EMPTY_PROJECT_RESOURCE_GRAPH: ProjectResourceGraph = {
|
|
resourceIds: emptyStringSet,
|
|
referenceEdges: [],
|
|
referenceEdgeById: emptyReferenceEdgeMap,
|
|
taskFlows: [],
|
|
taskFlowById: emptyTaskFlowMap,
|
|
connectionIndex: emptyConnectionMap,
|
|
producerTaskIdByResourceId: emptyStringMap,
|
|
unresolvedReferenceResourceIds: [],
|
|
cyclicResourceIds: emptyStringSet,
|
|
cyclicTaskIds: emptyStringSet,
|
|
producerMappingTruncated: false,
|
|
};
|
|
|
|
function uniqueSorted(values: Iterable<string>) {
|
|
return Array.from(new Set(values)).sort((left, right) =>
|
|
left < right ? -1 : left > right ? 1 : 0,
|
|
);
|
|
}
|
|
|
|
export function normalizeProjectResourceGraph(
|
|
readModel: ProjectResourceGraphReadModel,
|
|
): ProjectResourceGraph {
|
|
const resourceIds = new Set(uniqueSorted(readModel.resourceIds));
|
|
const referenceEdges = readModel.referenceEdges
|
|
.filter(
|
|
(edge) =>
|
|
edge.kind === 'asset-reference' &&
|
|
resourceIds.has(edge.sourceResourceId) &&
|
|
resourceIds.has(edge.targetResourceId),
|
|
)
|
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
const referenceEdgeIds = new Set(referenceEdges.map((edge) => edge.id));
|
|
const taskFlows = readModel.taskFlows
|
|
.flatMap((flow) => {
|
|
if (flow.kind !== 'task-flow') {
|
|
return [];
|
|
}
|
|
const sourceResourceIds = uniqueSorted(
|
|
flow.sourceResourceIds.filter((resourceId) =>
|
|
resourceIds.has(resourceId),
|
|
),
|
|
);
|
|
const targetResourceIds = uniqueSorted(
|
|
flow.targetResourceIds.filter((resourceId) =>
|
|
resourceIds.has(resourceId),
|
|
),
|
|
);
|
|
return sourceResourceIds.length > 0 && targetResourceIds.length > 0
|
|
? [{ ...flow, sourceResourceIds, targetResourceIds }]
|
|
: [];
|
|
})
|
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
const taskFlowIds = new Set(taskFlows.map((flow) => flow.id));
|
|
const connectionIndex = new Map<string, ProjectResourceConnectionIndex>();
|
|
for (const index of readModel.connectionIndex) {
|
|
if (!resourceIds.has(index.resourceId)) {
|
|
continue;
|
|
}
|
|
connectionIndex.set(index.resourceId, {
|
|
upstreamReferenceResourceIds: new Set(
|
|
index.upstreamReferenceResourceIds.filter((resourceId) =>
|
|
resourceIds.has(resourceId),
|
|
),
|
|
),
|
|
downstreamReferenceResourceIds: new Set(
|
|
index.downstreamReferenceResourceIds.filter((resourceId) =>
|
|
resourceIds.has(resourceId),
|
|
),
|
|
),
|
|
referenceEdgeIds: new Set(
|
|
index.referenceEdgeIds.filter((edgeId) =>
|
|
referenceEdgeIds.has(edgeId),
|
|
),
|
|
),
|
|
taskFlowIds: new Set(
|
|
index.taskFlowIds.filter((flowId) => taskFlowIds.has(flowId)),
|
|
),
|
|
});
|
|
}
|
|
const producerTaskIdByResourceId = new Map(
|
|
readModel.producerAssignments.flatMap((assignment) =>
|
|
resourceIds.has(assignment.resourceId) && assignment.taskId
|
|
? [[assignment.resourceId, assignment.taskId] as const]
|
|
: [],
|
|
),
|
|
);
|
|
|
|
return {
|
|
resourceIds,
|
|
referenceEdges,
|
|
referenceEdgeById: new Map(referenceEdges.map((edge) => [edge.id, edge])),
|
|
taskFlows,
|
|
taskFlowById: new Map(taskFlows.map((flow) => [flow.id, flow])),
|
|
connectionIndex,
|
|
producerTaskIdByResourceId,
|
|
unresolvedReferenceResourceIds: uniqueSorted(
|
|
readModel.unresolvedReferenceResourceIds,
|
|
),
|
|
cyclicResourceIds: new Set(
|
|
readModel.cyclicResourceIds.filter((resourceId) =>
|
|
resourceIds.has(resourceId),
|
|
),
|
|
),
|
|
cyclicTaskIds: new Set(readModel.cyclicTaskIds),
|
|
producerMappingTruncated: Boolean(readModel.producerMappingTruncated),
|
|
};
|
|
}
|
|
|
|
export function projectResourceGraphNeighbors(
|
|
graph: ProjectResourceGraph,
|
|
resourceId: string | null,
|
|
): ProjectResourceGraphNeighbors {
|
|
if (!resourceId || !graph.resourceIds.has(resourceId)) {
|
|
return EMPTY_PROJECT_RESOURCE_GRAPH_NEIGHBORS;
|
|
}
|
|
const index = graph.connectionIndex.get(resourceId);
|
|
if (!index) {
|
|
return EMPTY_PROJECT_RESOURCE_GRAPH_NEIGHBORS;
|
|
}
|
|
const upstreamResourceIds = new Set(
|
|
index.upstreamReferenceResourceIds,
|
|
);
|
|
const downstreamResourceIds = new Set(
|
|
index.downstreamReferenceResourceIds,
|
|
);
|
|
const connectedEdgeIds = new Set(index.referenceEdgeIds);
|
|
for (const flowId of index.taskFlowIds) {
|
|
const flow = graph.taskFlowById.get(flowId);
|
|
if (!flow) {
|
|
continue;
|
|
}
|
|
if (flow.targetResourceIds.includes(resourceId)) {
|
|
flow.sourceResourceIds.forEach((id) => upstreamResourceIds.add(id));
|
|
connectedEdgeIds.add(flow.id);
|
|
}
|
|
if (flow.sourceResourceIds.includes(resourceId)) {
|
|
flow.targetResourceIds.forEach((id) => downstreamResourceIds.add(id));
|
|
connectedEdgeIds.add(flow.id);
|
|
}
|
|
}
|
|
|
|
upstreamResourceIds.delete(resourceId);
|
|
downstreamResourceIds.delete(resourceId);
|
|
return { upstreamResourceIds, downstreamResourceIds, connectedEdgeIds };
|
|
}
|