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 ProjectResourceDependencyDepth = { resourceId: string; dependencyDepth: number; }; export type ProjectResourceGraphReadModel = { resourceIds: string[]; referenceEdges: ProjectResourceReferenceEdge[]; taskFlows: ProjectResourceTaskFlow[]; connectionIndex: ProjectResourceConnectionIndexDto[]; producerAssignments: ProjectResourceProducerAssignment[]; dependencyDepths: ProjectResourceDependencyDepth[]; unresolvedReferenceResourceIds: string[]; cyclicResourceIds: string[]; cyclicTaskIds: string[]; producerMappingTruncated: boolean; }; type ProjectResourceConnectionIndex = { upstreamReferenceResourceIds: ReadonlySet; downstreamReferenceResourceIds: ReadonlySet; referenceEdgeIds: ReadonlySet; taskFlowIds: ReadonlySet; }; export type ProjectResourceGraph = { resourceIds: ReadonlySet; referenceEdges: ProjectResourceReferenceEdge[]; referenceEdgeById: ReadonlyMap; taskFlows: ProjectResourceTaskFlow[]; taskFlowById: ReadonlyMap; connectionIndex: ReadonlyMap; producerTaskIdByResourceId: ReadonlyMap; dependencyDepthByResourceId: ReadonlyMap; unresolvedReferenceResourceIds: string[]; cyclicResourceIds: ReadonlySet; cyclicTaskIds: ReadonlySet; producerMappingTruncated: boolean; }; export type ProjectResourceGraphNeighbors = { upstreamResourceIds: ReadonlySet; downstreamResourceIds: ReadonlySet; connectedEdgeIds: ReadonlySet; }; const emptyStringSet: ReadonlySet = new Set(); const emptyStringMap: ReadonlyMap = new Map(); const emptyNumberMap: ReadonlyMap = new Map(); const emptyConnectionMap: ReadonlyMap = new Map(); const emptyTaskFlowMap: ReadonlyMap = new Map< string, ProjectResourceTaskFlow >(); const emptyReferenceEdgeMap: ReadonlyMap = new Map(); 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, dependencyDepthByResourceId: emptyNumberMap, unresolvedReferenceResourceIds: [], cyclicResourceIds: emptyStringSet, cyclicTaskIds: emptyStringSet, producerMappingTruncated: false, }; function uniqueSorted(values: Iterable) { return Array.from(new Set(values)).sort((left, right) => left < right ? -1 : left > right ? 1 : 0, ); } export function normalizeProjectResourceGraph( readModel: ProjectResourceGraphReadModel, ): ProjectResourceGraph { const producerMappingTruncated = Boolean(readModel.producerMappingTruncated); 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 = (producerMappingTruncated ? [] : 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(); 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(); const dependencyDepthByResourceId = new Map(); for (const assignment of producerMappingTruncated ? [] : readModel.producerAssignments) { if (!resourceIds.has(assignment.resourceId) || !assignment.taskId) { continue; } producerTaskIdByResourceId.set(assignment.resourceId, assignment.taskId); } for (const depth of readModel.dependencyDepths) { if ( resourceIds.has(depth.resourceId) && Number.isSafeInteger(depth.dependencyDepth) && depth.dependencyDepth >= 0 ) { dependencyDepthByResourceId.set( depth.resourceId, Math.max( dependencyDepthByResourceId.get(depth.resourceId) ?? 0, depth.dependencyDepth, ), ); } } return { resourceIds, referenceEdges, referenceEdgeById: new Map(referenceEdges.map((edge) => [edge.id, edge])), taskFlows, taskFlowById: new Map(taskFlows.map((flow) => [flow.id, flow])), connectionIndex, producerTaskIdByResourceId, dependencyDepthByResourceId, unresolvedReferenceResourceIds: uniqueSorted( readModel.unresolvedReferenceResourceIds, ), cyclicResourceIds: new Set( readModel.cyclicResourceIds.filter((resourceId) => resourceIds.has(resourceId), ), ), cyclicTaskIds: new Set( producerMappingTruncated ? [] : readModel.cyclicTaskIds, ), 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 }; }