Files
fusion/packages/dashboard/app/utils/taskStuck.ts
2026-04-12 07:00:49 -07:00

62 lines
2.1 KiB
TypeScript

import type { Task } from "@fusion/core";
const NON_STUCK_STATUSES = new Set(["failed", "stuck-killed"]);
/**
* Check if a task is stuck based on the project's stuck timeout setting.
*
* A task is considered stuck when:
* - It is in the "in-progress" column
* - A positive `taskStuckTimeoutMs` value is provided (stuck detection enabled)
* - Its `updatedAt` timestamp is older than `taskStuckTimeoutMs` milliseconds ago
* compared to `dataAsOfMs` (or `Date.now()` if `dataAsOfMs` is not provided)
*
* When `taskStuckTimeoutMs` is undefined, null, or 0, stuck detection is
* disabled and this function always returns false.
*
* The optional `dataAsOfMs` parameter represents when the task data was last
* confirmed fresh by the server. When provided, it is used instead of `Date.now()`
* for the comparison. This prevents false positives when the tab has been in
* the background and the task data is stale.
*/
export function isTaskStuck(task: Task, taskStuckTimeoutMs: number | undefined, dataAsOfMs?: number): boolean {
if (task.column !== "in-progress") {
return false;
}
if (task.status && NON_STUCK_STATUSES.has(task.status)) {
return false;
}
if (!taskStuckTimeoutMs || taskStuckTimeoutMs <= 0) {
return false;
}
const updatedAt = new Date(task.updatedAt).getTime();
// Use dataAsOfMs if provided, otherwise fall back to current time
const now = dataAsOfMs ?? Date.now();
return now - updatedAt > taskStuckTimeoutMs;
}
/**
* Derive the stuck task count from a list of tasks using the given threshold.
*
* Returns 0 when stuck detection is disabled (undefined/0 threshold).
*
* The optional `dataAsOfMs` parameter is passed through to `isTaskStuck()` for
* freshness-aware stuck detection.
*/
export function countStuckTasks(tasks: Task[], taskStuckTimeoutMs: number | undefined, dataAsOfMs?: number): number {
if (!taskStuckTimeoutMs || taskStuckTimeoutMs <= 0) {
return 0;
}
let count = 0;
for (const task of tasks) {
if (isTaskStuck(task, taskStuckTimeoutMs, dataAsOfMs)) {
count++;
}
}
return count;
}