Skip to content

Commit

Permalink
Use breadth first flattening order for stages
Browse files Browse the repository at this point in the history
  • Loading branch information
kalilsn committed Jan 23, 2025
1 parent 730195c commit a04646f
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 59 deletions.
24 changes: 10 additions & 14 deletions core/app/c/[communitySlug]/stages/components/StageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getStageActions } from "~/lib/db/queries";
import { getPubsWithRelatedValuesAndChildren } from "~/lib/server";
import { selectCommunityMembers } from "~/lib/server/member";
import { getStages } from "~/lib/server/stages";
import { getStageWorkflows } from "~/lib/stages";
import { getOrderedStages } from "~/lib/stages";
import { PubListSkeleton } from "../../pubs/PubList";
import { StagePubActions } from "./StagePubActions";

Expand All @@ -31,22 +31,18 @@ export async function StageList(props: Props) {
selectCommunityMembers({ communityId }).execute(),
]);

const stageWorkflows = getStageWorkflows(communityStages);
const stages = getOrderedStages(communityStages);

return (
<div>
{stageWorkflows.map((stages) => (
<div key={stages[0].id}>
{stages.map((stage) => (
<StageCard
userId={props.userId}
key={stage.id}
stage={stage}
members={communityMembers}
pageContext={props.pageContext}
/>
))}
</div>
{stages.map((stage) => (
<StageCard
userId={props.userId}
key={stage.id}
stage={stage}
members={communityMembers}
pageContext={props.pageContext}
/>
))}
</div>
);
Expand Down
72 changes: 27 additions & 45 deletions core/lib/stages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,6 @@ export type StagesById<T extends CommunityStage = CommunityStage> = {
[key: StagesId]: T;
};

/**
* Takes a stage, a map of stages to their IDs, and an optional list of stages
* that have been visited so far. Returns a list of stages that can be reached
* from the stage provided without mutating the visited stages.
* @param stage - The current stage
* @param stages - A map of stage IDs to stage objects
* @param visited - (Optional) An array of visited stages, defaults to an empty array
* @returns A new array of stages that have been visited
*/
function createStageList<T extends CommunityStage>(
stage: T,
stages: StagesById,
visited: Array<T> = []
): Array<T> {
if (!stage) {
return visited;
}

// If the stage has already been visited, return the current visited list
if (visited.includes(stage)) {
return visited;
}

// Add the current stage to the visited list (non-mutating)
const newVisited = [...visited, stage];

// Recursively process the stages reachable from this stage
return stage.moveConstraints.reduce((acc, constraint) => {
const nextStage = stages[constraint.id];
// If the next stage is undefined, just return the accumulator
// This would happen if the move constraint isn't in the stage list (because of user permissions)
if (!stage) {
return acc;
}
return createStageList<T>(nextStage as T, stages, acc);
}, newVisited);
}

/**
*
* @param stages
Expand All @@ -56,11 +18,12 @@ export const makeStagesById = <T extends { id: StagesId }>(stages: T[]): { [key:
};

/**
* this function takes a list of stages and recursively builds a topological sort of the stages
* This function takes a list of stages and returns them in the order of a breadth-first flattening
* of their graph. When the stages form multiple independent graphs
* @param stages
* @returns
*/
export function getStageWorkflows<T extends CommunityStage>(stages: T[]): Array<Array<T>> {
export function getOrderedStages<T extends CommunityStage>(stages: T[]): Array<T> {
const stagesById = makeStagesById(stages);
// find all stages with edges that only point to them
// also make sure to filter to only move constraints that there are stages for (permission restrictions may return more move constraint stages than a user can see)
Expand All @@ -70,11 +33,30 @@ export function getStageWorkflows<T extends CommunityStage>(stages: T[]): Array<
}
return !stage.moveConstraintSources.every((constraint) => stagesById[constraint.id]);
});
// for each stage, create a list of stages that can be reached from it
const stageWorkflows = stageRoots.map((stage) => {
return createStageList(stage, stagesById);
});
return stageWorkflows as T[][];

const orderedStages = new Set<T>();
const stagesQueue: T[] = stageRoots;
// Breadth-first traversal of the graph(s)
while (stagesQueue.length > 0) {
const stage = stagesQueue.shift();
if (!stage) {
// This should be unreachable because of the condition in the while, but Typescript
// doesn't know that
break;
}
orderedStages.add(stage);
stage.moveConstraints.forEach((destinationStage) =>
stagesQueue.push(stagesById[destinationStage.id])
);
}

// Because the algorithm above starts with "root" stages only, it will totally exclude graphs
// where every stage is part of a cycle (biconnected graphs). Since we don't know where those
// graphs should begin, we skip sorting them but make sure their stages are included in the
// output
stages.forEach((stage) => orderedStages.add(stage));

return [...orderedStages];
}

// this function takes a stage and a map of stages and their IDs and returns a list of stages that can be reached from the stage provided
Expand Down

0 comments on commit a04646f

Please sign in to comment.