mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor(server): centralize task graph readiness (#840)
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import type { Task, TaskStore } from "./types.js";
|
||||
import { buildChildrenMap, loadScopedTaskGraph, sortByPriorityThenCreated } from "./task-graph.js";
|
||||
import {
|
||||
buildChildrenMap,
|
||||
getTasksById,
|
||||
isTaskExecutableInOrder,
|
||||
loadScopedTaskGraph,
|
||||
sortByPriorityThenCreated,
|
||||
} from "./task-graph.js";
|
||||
|
||||
export interface ExecutionOrderResult {
|
||||
/** Tasks in execution order (done first, then pending) */
|
||||
@@ -24,24 +30,13 @@ export async function computeExecutionOrder(
|
||||
): Promise<ExecutionOrderResult> {
|
||||
const graph = await loadScopedTaskGraph(store, scopeId);
|
||||
|
||||
const simDone = new Set(graph.allTasks.filter((t) => t.status === "done").map((t) => t.id));
|
||||
const simDone = new Set(graph.doneTaskIds);
|
||||
const remaining = new Set(
|
||||
graph.candidates
|
||||
.filter((t) => t.status === "open" || t.status === "in_progress")
|
||||
.map((t) => t.id),
|
||||
);
|
||||
|
||||
const isReady = (taskId: string): boolean => {
|
||||
const task = graph.taskMap.get(taskId);
|
||||
if (!task) return false;
|
||||
const depsOk = task.deps.every((depId) => simDone.has(depId));
|
||||
const children = (graph.childrenMap.get(taskId) ?? []).filter((c) =>
|
||||
graph.candidateIds.has(c.id),
|
||||
);
|
||||
const childrenOk = children.every((c) => simDone.has(c.id));
|
||||
return depsOk && childrenOk;
|
||||
};
|
||||
|
||||
const timeline: Task[] = [];
|
||||
const orderMap = new Map<string, number>();
|
||||
let orderIdx = 0;
|
||||
@@ -56,9 +51,8 @@ export async function computeExecutionOrder(
|
||||
|
||||
// Then pending tasks in execution order
|
||||
while (remaining.size > 0) {
|
||||
const readyNow = [...remaining]
|
||||
.filter(isReady)
|
||||
.map((tid) => graph.taskMap.get(tid)!)
|
||||
const readyNow = getTasksById(graph, remaining)
|
||||
.filter((task) => isTaskExecutableInOrder(graph, task.id, simDone))
|
||||
.sort(sortByPriorityThenCreated);
|
||||
|
||||
if (readyNow.length === 0) break;
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface TaskGraph {
|
||||
taskMap: Map<string, Task>;
|
||||
childrenMap: Map<string, Task[]>;
|
||||
candidateIds: Set<string>;
|
||||
doneTaskIds: Set<string>;
|
||||
}
|
||||
|
||||
type TaskGraphStore = Pick<TaskStore, "list" | "get" | "getDescendants">;
|
||||
@@ -35,6 +36,47 @@ export function buildChildrenMap(tasks: Task[]): Map<string, Task[]> {
|
||||
return childrenMap;
|
||||
}
|
||||
|
||||
export function isReadyTask(graph: TaskGraph, task: Task): boolean {
|
||||
return (
|
||||
task.status === "open" &&
|
||||
areTaskDepsDone(graph, task, graph.doneTaskIds) &&
|
||||
areTaskChildrenDone(graph, task.id, graph.doneTaskIds)
|
||||
);
|
||||
}
|
||||
|
||||
export function isBlockedTask(graph: TaskGraph, task: Task): boolean {
|
||||
return (
|
||||
task.status !== "draft" &&
|
||||
task.status !== "done" &&
|
||||
task.deps.length > 0 &&
|
||||
!areTaskDepsDone(graph, task, graph.doneTaskIds)
|
||||
);
|
||||
}
|
||||
|
||||
export function isTaskExecutableInOrder(
|
||||
graph: TaskGraph,
|
||||
taskId: string,
|
||||
completedTaskIds: Set<string>,
|
||||
): boolean {
|
||||
const task = graph.taskMap.get(taskId);
|
||||
return (
|
||||
task !== undefined &&
|
||||
areTaskDepsDone(graph, task, completedTaskIds) &&
|
||||
areTaskChildrenDone(graph, task.id, completedTaskIds, { scoped: true })
|
||||
);
|
||||
}
|
||||
|
||||
export function getTasksById(graph: TaskGraph, taskIds: Iterable<string>): Task[] {
|
||||
const tasks: Task[] = [];
|
||||
for (const taskId of taskIds) {
|
||||
const task = graph.taskMap.get(taskId);
|
||||
if (task) {
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
export async function loadScopedTaskGraph(
|
||||
store: TaskGraphStore,
|
||||
scopeId?: string,
|
||||
@@ -48,9 +90,32 @@ export async function loadScopedTaskGraph(
|
||||
taskMap: buildTaskMap(allTasks),
|
||||
childrenMap: buildChildrenMap(allTasks),
|
||||
candidateIds: new Set(candidates.map((task) => task.id)),
|
||||
doneTaskIds: new Set(allTasks.filter((task) => task.status === "done").map((task) => task.id)),
|
||||
};
|
||||
}
|
||||
|
||||
function areTaskDepsDone(graph: TaskGraph, task: Task, completedTaskIds: Set<string>): boolean {
|
||||
return task.deps.every((depId) => {
|
||||
const dep = graph.taskMap.get(depId);
|
||||
return dep !== undefined && completedTaskIds.has(depId);
|
||||
});
|
||||
}
|
||||
|
||||
function areTaskChildrenDone(
|
||||
graph: TaskGraph,
|
||||
taskId: string,
|
||||
completedTaskIds: Set<string>,
|
||||
options?: { scoped: boolean },
|
||||
): boolean {
|
||||
const children = graph.childrenMap.get(taskId) ?? [];
|
||||
return children.every((child) => {
|
||||
if (options?.scoped === true && !graph.candidateIds.has(child.id)) {
|
||||
return true;
|
||||
}
|
||||
return completedTaskIds.has(child.id);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadScopedCandidates(
|
||||
store: TaskGraphStore,
|
||||
allTasks: Task[],
|
||||
|
||||
@@ -2,7 +2,12 @@ import { readdir, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { Task, TaskStore, CreateTaskOptions, TaskStatus } from "./types.js";
|
||||
import { loadScopedTaskGraph, sortByPriorityThenCreated } from "./task-graph.js";
|
||||
import {
|
||||
isBlockedTask,
|
||||
isReadyTask,
|
||||
loadScopedTaskGraph,
|
||||
sortByPriorityThenCreated,
|
||||
} from "./task-graph.js";
|
||||
|
||||
function generateId(): string {
|
||||
return randomBytes(4).toString("hex");
|
||||
@@ -253,34 +258,14 @@ export class FileTaskStore implements TaskStore {
|
||||
|
||||
async getReady(scopeId?: string): Promise<Task[]> {
|
||||
const graph = await loadScopedTaskGraph(this, scopeId);
|
||||
|
||||
const isReady = (task: Task): boolean => {
|
||||
if (task.status !== "open") return false;
|
||||
const depsReady = task.deps.every((depId) => {
|
||||
const dep = graph.taskMap.get(depId);
|
||||
return dep?.status === "done";
|
||||
});
|
||||
if (!depsReady) return false;
|
||||
const children = graph.childrenMap.get(task.id) ?? [];
|
||||
return children.every((c) => c.status === "done");
|
||||
};
|
||||
|
||||
return graph.candidates.filter(isReady).sort(sortByPriorityThenCreated);
|
||||
return graph.candidates
|
||||
.filter((task) => isReadyTask(graph, task))
|
||||
.sort(sortByPriorityThenCreated);
|
||||
}
|
||||
|
||||
async getBlocked(scopeId?: string): Promise<Task[]> {
|
||||
const graph = await loadScopedTaskGraph(this, scopeId);
|
||||
|
||||
const isBlocked = (task: Task): boolean => {
|
||||
if (task.status === "draft" || task.status === "done") return false;
|
||||
if (task.deps.length === 0) return false;
|
||||
return task.deps.some((depId) => {
|
||||
const dep = graph.taskMap.get(depId);
|
||||
return dep?.status !== "done";
|
||||
});
|
||||
};
|
||||
|
||||
return graph.candidates.filter(isBlocked);
|
||||
return graph.candidates.filter((task) => isBlockedTask(graph, task));
|
||||
}
|
||||
|
||||
async getClosed(scopeId?: string): Promise<Task[]> {
|
||||
|
||||
Reference in New Issue
Block a user