mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
4cd6a7191d
* feat(sftp): add cancel all to transfer center * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix: address Codex review on PR #3574 * fix(sftp): settle cancelled walks before directory recovery * fix(sftp): report failed cancel attempts without blocking retries * fix(sftp): batch pending cancel cleanup for large queues * fix(sftp): preserve settled children during cancel recovery * fix(sftp): release skipped cancel-all pre-latches * fix(sftp): satisfy cancellation result and fixture types * fix(sftp): dispatch cancel batches before walk settlement * fix(sftp): distinguish live compression recovery from restored attention --------- Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
96 lines
4.2 KiB
TypeScript
96 lines
4.2 KiB
TypeScript
import { useCallback, useMemo, useRef, useState } from "react";
|
|
|
|
import type { TransferTask } from "../../domain/models";
|
|
import {
|
|
getGlobalTransferBatchEligibility,
|
|
listGloballyCancellableTransferIds,
|
|
listGloballyPausableTransferIds,
|
|
listGloballyResumableTransferIds,
|
|
} from "../../domain/sftpTransferActions";
|
|
import { sftpTransferCenterStore } from "./sftpTransferCenterStore";
|
|
import { globalSftpTransferScheduler } from "./sftp/globalTransferScheduler";
|
|
import { clearTransferCancelledTree, markTransferCancelledTree } from "./sftp/transferCancelLatch";
|
|
import { transferRuntime } from "./sftp/transferRuntime";
|
|
|
|
export function useGlobalSftpTransferActions(tasks: readonly TransferTask[]) {
|
|
const cancellingRef = useRef(false);
|
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
const batchEligibility = useMemo(
|
|
() => getGlobalTransferBatchEligibility(tasks),
|
|
[tasks],
|
|
);
|
|
const pauseAll = useCallback(() => {
|
|
// Ignore clicks racing a cancelAll batch: re-enabling these tasks would
|
|
// restart work cancelAll has already pre-cancelled at the scheduler.
|
|
if (cancellingRef.current) return;
|
|
for (const taskId of listGloballyPausableTransferIds(tasks)) {
|
|
void sftpTransferCenterStore.pause(taskId);
|
|
}
|
|
}, [tasks]);
|
|
const resumeAll = useCallback(() => {
|
|
if (cancellingRef.current) return;
|
|
for (const taskId of listGloballyResumableTransferIds(tasks)) {
|
|
void sftpTransferCenterStore.resume(taskId);
|
|
}
|
|
}, [tasks]);
|
|
|
|
const cancelAll = useCallback(async () => {
|
|
if (cancellingRef.current) return;
|
|
cancellingRef.current = true;
|
|
setIsCancelling(true);
|
|
const currentTasks = transferRuntime.getSnapshot().tasks;
|
|
const ids = listGloballyCancellableTransferIds(currentTasks);
|
|
try {
|
|
// Stop queued work before yielding to IPC, including children of folder
|
|
// transfers. Otherwise later batches could start while earlier ones stop.
|
|
const selected = new Set(ids);
|
|
const children = new Map<string, string[]>();
|
|
for (const task of currentTasks) {
|
|
if (!task.parentTaskId || !selected.has(task.parentTaskId)
|
|
|| ["completed", "failed", "cancelled"].includes(task.status)) continue;
|
|
const idsForParent = children.get(task.parentTaskId) ?? [];
|
|
idsForParent.push(task.id);
|
|
children.set(task.parentTaskId, idsForParent);
|
|
globalSftpTransferScheduler.cancel(task.id);
|
|
}
|
|
for (const id of ids) {
|
|
markTransferCancelledTree(id, children.get(id));
|
|
globalSftpTransferScheduler.cancel(id);
|
|
}
|
|
// Bound IPC work for large queues while active transfers stop promptly.
|
|
for (let offset = 0; offset < ids.length; offset += 32) {
|
|
await Promise.all(ids.slice(offset, offset + 32).map(async (taskId) => {
|
|
const current = transferRuntime.getTask(taskId);
|
|
// "failed" is terminal here just like the initial eligibility filter:
|
|
// a queued task can fail naturally while its cancel batch awaits IPC,
|
|
// and cancel's owner/orphan recovery paths would repaint the row as
|
|
// cancelled, hiding the real failure and its diagnostic.
|
|
if (!current || ["completed", "failed", "cancelled"].includes(current.status)) {
|
|
// No cancel invocation will own this pre-latch. Release it here so
|
|
// Retry is not discarded while the finished walk is unwinding.
|
|
clearTransferCancelledTree(taskId, children.get(taskId));
|
|
return;
|
|
}
|
|
try {
|
|
await transferRuntime.cancel(taskId);
|
|
} catch {
|
|
// The row is kept in attention for recovery, so the pre-installed
|
|
// cancellation latch must be dropped or a later Resume is rejected
|
|
// outright by admitTaskRun's cancelled-root check.
|
|
clearTransferCancelledTree(taskId, children.get(taskId));
|
|
sftpTransferCenterStore.patchTask(taskId, {
|
|
status: "attention",
|
|
error: "Could not cancel transfer. Please try again.",
|
|
});
|
|
}
|
|
}));
|
|
}
|
|
} finally {
|
|
cancellingRef.current = false;
|
|
setIsCancelling(false);
|
|
}
|
|
}, []);
|
|
|
|
return { batchEligibility, pauseAll, resumeAll, cancelAll, isCancelling };
|
|
}
|