Files
kandev/apps/web/lib/tasks/launch-queue-view-model.ts
T
2026-09-17 15:59:28 +01:00

74 lines
2.4 KiB
TypeScript

import type { TaskStatusSummaryLaunchQueue } from "@/lib/types/task-status-summary";
export type LaunchQueueCapacityFreshness = "current" | "stale" | "unavailable";
export type LaunchQueueViewModel = {
destinationId: string | null;
queuedAt: string;
reason: TaskStatusSummaryLaunchQueue["reason"];
retrying: boolean;
capacityFreshness: LaunchQueueCapacityFreshness;
capacity: {
inUse: number;
limit: number;
observedAt: string;
} | null;
};
const LAUNCH_QUEUE_REASONS = new Set<TaskStatusSummaryLaunchQueue["reason"]>([
"session_capacity",
"ownership_unavailable",
"replay_error",
]);
export const LAUNCH_QUEUE_CAPACITY_STALE_AFTER_MS = 40_000;
function validCapacity(
capacity: TaskStatusSummaryLaunchQueue["capacity"],
): LaunchQueueViewModel["capacity"] {
if (!capacity || !capacity.observed_at) return null;
if (
!Number.isInteger(capacity.in_use) ||
!Number.isInteger(capacity.limit) ||
capacity.in_use < 0 ||
capacity.limit < 0
) {
return null;
}
if (!Number.isFinite(Date.parse(capacity.observed_at))) return null;
return {
inUse: capacity.in_use,
limit: capacity.limit,
observedAt: capacity.observed_at,
};
}
/** Convert the wire summary into the small, presentation-independent queue model. */
export function buildLaunchQueueViewModel(
queue: TaskStatusSummaryLaunchQueue | null | undefined,
options: { now?: number; isConnected?: boolean } = {},
): LaunchQueueViewModel | null {
if (!queue || !queue.queued_at || !LAUNCH_QUEUE_REASONS.has(queue.reason)) return null;
const capacity = validCapacity(queue.capacity);
const now = options.now ?? Date.now();
const isConnected = options.isConnected ?? true;
let capacityFreshness: LaunchQueueCapacityFreshness = "unavailable";
if (capacity) {
const observedAt = Date.parse(capacity.observedAt);
const age = now - observedAt;
capacityFreshness =
!isConnected || age >= LAUNCH_QUEUE_CAPACITY_STALE_AFTER_MS ? "stale" : "current";
}
return {
// Session IDs are ownership keys, not user-facing destination labels. A
// missing profile projection must fall back to the translated generic
// label instead of exposing an opaque UUID.
destinationId: queue.agent_profile_id?.trim() || null,
queuedAt: queue.queued_at,
reason: queue.reason,
retrying: queue.retrying,
capacityFreshness,
capacity,
};
}