Files
kandev/apps/web/hooks/domains/azure-devops/use-azure-devops-task-pull-requests.ts
T
2026-07-21 08:40:26 +01:00

146 lines
5.4 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { useAppStore } from "@/components/state-provider";
import {
listWorkspaceAzureDevOpsTaskPullRequests,
syncAzureDevOpsTaskPullRequest,
} from "@/lib/api/domains/azure-devops-api";
import type { AzureDevOpsTaskPullRequest } from "@/lib/types/azure-devops";
type WorkspaceSnapshot = Record<string, AzureDevOpsTaskPullRequest[]>;
const TASK_PR_REFRESH_INTERVAL_MS = 90_000;
const EMPTY_TASK_PULL_REQUESTS: AzureDevOpsTaskPullRequest[] = [];
const pendingWorkspaces = new Map<string, Promise<WorkspaceSnapshot>>();
const pendingTaskPullRequests = new Map<string, Promise<AzureDevOpsTaskPullRequest>>();
const workspaceSnapshots = new Map<string, WorkspaceSnapshot>();
const workspaceUpdates = new Map<string, WorkspaceSnapshot>();
function withTaskPullRequest(
snapshot: WorkspaceSnapshot,
taskId: string,
pullRequest: AzureDevOpsTaskPullRequest,
): WorkspaceSnapshot {
const existing = snapshot[taskId] ?? [];
const index = existing.findIndex((item) => item.id === pullRequest.id);
const taskPullRequests = [...existing];
if (index >= 0) taskPullRequests[index] = pullRequest;
else taskPullRequests.push(pullRequest);
return { ...snapshot, [taskId]: taskPullRequests };
}
function mergeWorkspaceUpdates(workspaceId: string, snapshot: WorkspaceSnapshot) {
const updates = workspaceUpdates.get(workspaceId);
if (!updates) return snapshot;
let merged = snapshot;
for (const [taskId, pullRequests] of Object.entries(updates)) {
for (const pullRequest of pullRequests) {
merged = withTaskPullRequest(merged, taskId, pullRequest);
}
}
workspaceUpdates.delete(workspaceId);
return merged;
}
// Keep successful associations in the module snapshot so remounting a task
// chip cannot overwrite the Zustand update with an older workspace response.
export function cacheAzureDevOpsTaskPullRequest(
workspaceId: string,
taskId: string,
pullRequest: AzureDevOpsTaskPullRequest,
) {
const snapshot = workspaceSnapshots.get(workspaceId);
if (snapshot) {
workspaceSnapshots.set(workspaceId, withTaskPullRequest(snapshot, taskId, pullRequest));
return;
}
const updates = workspaceUpdates.get(workspaceId) ?? {};
workspaceUpdates.set(workspaceId, withTaskPullRequest(updates, taskId, pullRequest));
}
function loadWorkspace(workspaceId: string) {
const pending = pendingWorkspaces.get(workspaceId);
if (pending) return pending;
const request = listWorkspaceAzureDevOpsTaskPullRequests(workspaceId, { cache: "no-store" })
.then((result) => {
const snapshot = mergeWorkspaceUpdates(workspaceId, result.taskPrs ?? {});
workspaceSnapshots.set(workspaceId, snapshot);
return snapshot;
})
.finally(() => pendingWorkspaces.delete(workspaceId));
pendingWorkspaces.set(workspaceId, request);
return request;
}
function shouldRefresh(pullRequest: AzureDevOpsTaskPullRequest) {
const lastSyncedAt = Date.parse(pullRequest.lastSyncedAt ?? "");
return !Number.isFinite(lastSyncedAt) || Date.now() - lastSyncedAt >= TASK_PR_REFRESH_INTERVAL_MS;
}
function refreshTaskPullRequest(
workspaceId: string,
taskId: string,
pullRequest: AzureDevOpsTaskPullRequest,
) {
const key = `${workspaceId}:${pullRequest.id}`;
const pending = pendingTaskPullRequests.get(key);
if (pending) return pending;
const request = syncAzureDevOpsTaskPullRequest(workspaceId, taskId, {
repositoryId: pullRequest.repositoryId,
pullRequestId: pullRequest.pullRequestId,
}).finally(() => pendingTaskPullRequests.delete(key));
pendingTaskPullRequests.set(key, request);
return request;
}
export function useAzureDevOpsTaskPullRequests(workspaceId: string | null, taskId: string | null) {
const generation = useRef({ scope: workspaceId, value: 0 });
const loadedWorkspace = useRef<string | null>(null);
if (generation.current.scope !== workspaceId) {
generation.current = { scope: workspaceId, value: generation.current.value + 1 };
loadedWorkspace.current = null;
}
const pullRequests = useAppStore((state) =>
taskId
? (state.azureDevOpsTaskPullRequests.byTaskId[taskId] ?? EMPTY_TASK_PULL_REQUESTS)
: EMPTY_TASK_PULL_REQUESTS,
);
const setAll = useAppStore((state) => state.setAzureDevOpsTaskPullRequests);
const setOne = useAppStore((state) => state.setAzureDevOpsTaskPullRequest);
useEffect(() => {
if (!workspaceId) return;
const current = generation.current.value;
const applySnapshot = (snapshot: WorkspaceSnapshot) => {
if (current !== generation.current.value) return;
loadedWorkspace.current = workspaceId;
setAll(snapshot);
};
const snapshot = workspaceSnapshots.get(workspaceId);
if (snapshot) {
applySnapshot(snapshot);
return;
}
void loadWorkspace(workspaceId)
.then(applySnapshot)
.catch(() => undefined);
}, [generation, setAll, workspaceId]);
useEffect(() => {
if (!workspaceId || !taskId || loadedWorkspace.current !== workspaceId) return;
const current = generation.current.value;
for (const pullRequest of pullRequests) {
if (!shouldRefresh(pullRequest)) continue;
void refreshTaskPullRequest(workspaceId, taskId, pullRequest)
.then((refreshed) => {
cacheAzureDevOpsTaskPullRequest(workspaceId, taskId, refreshed);
if (current === generation.current.value) setOne(taskId, refreshed);
})
.catch(() => undefined);
}
}, [generation, pullRequests, setOne, taskId, workspaceId]);
return pullRequests;
}