import type { TransferTask } from "../../../domain/models"; import { compareDirectoryTraversalPaths, createDirectoryManifestAccumulator, createDirectoryEntryIdentity, createEmptyDirectoryResumeCheckpoint, isValidDirectoryResumeCheckpoint, } from "../../../domain/sftpDirectoryCheckpoint"; import { pruneSftpTransferHistory, sanitizeSftpTransferTask, SFTP_TRANSFER_CENTER_VERSION, } from "../../../domain/sftpTransferCenter"; const RESTORE_SYNC_BUDGET_MS = 8; const RESTORE_CHECK_INTERVAL = 128; const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]); export interface CooperativeTransferHistoryRestoreResult { valid: boolean; tasks: TransferTask[]; } function monotonicNow(): number { return typeof globalThis.performance?.now === "function" ? globalThis.performance.now() : Date.now(); } async function yieldToEventLoop(): Promise { const scheduler = (globalThis as typeof globalThis & { scheduler?: { yield?: () => Promise }; }).scheduler; if (typeof scheduler?.yield === "function") { await scheduler.yield(); return; } await new Promise((resolve) => setTimeout(resolve, 0)); } function createRestoreYieldController() { let sliceStartedAt = monotonicNow(); return async (iteration: number, force = false) => { if (!force && iteration % RESTORE_CHECK_INTERVAL !== 0) return; if (!force && monotonicNow() - sliceStartedAt < RESTORE_SYNC_BUDGET_MS) return; await yieldToEventLoop(); sliceStartedAt = monotonicNow(); }; } /** * Upgrade a large pre-checkpoint transfer history without monopolizing the * renderer. The final bounded prune still uses the canonical domain function; * this function only performs its expensive legacy directory normalization * and manifest hashing in cooperative slices first. */ export async function restoreSftpTransferHistoryCooperatively( raw: string, now = Date.now(), ): Promise { // Let first paint and store subscribers attach before parsing a legacy blob. await yieldToEventLoop(); let parsed: { version?: unknown; tasks?: unknown }; try { parsed = JSON.parse(raw) as { version?: unknown; tasks?: unknown }; } catch { return { valid: false, tasks: [] }; } if (parsed.version !== SFTP_TRANSFER_CENTER_VERSION || !Array.isArray(parsed.tasks)) { return { valid: false, tasks: [] }; } const maybeYield = createRestoreYieldController(); const restored: TransferTask[] = []; for (let index = 0; index < parsed.tasks.length; index += 1) { const task = sanitizeSftpTransferTask(parsed.tasks[index]); if (task) restored.push(task); await maybeYield(index); } const unfinishedDirectoryParents: TransferTask[] = []; const childrenByParent = new Map(); for (let index = 0; index < restored.length; index += 1) { const task = restored[index]; if (task.isDirectory && !task.parentTaskId && !TERMINAL_STATUSES.has(task.status)) { unfinishedDirectoryParents.push(task); } if (task.parentTaskId) { const children = childrenByParent.get(task.parentTaskId) ?? []; children.push(task); childrenByParent.set(task.parentTaskId, children); } await maybeYield(index); } const compactedChildIds = new Set(); const parentUpdates = new Map(); const normalizedChildUpdates = new Map(); for (let parentIndex = 0; parentIndex < unfinishedDirectoryParents.length; parentIndex += 1) { const parent = unfinishedDirectoryParents[parentIndex]; let parentChildren = childrenByParent.get(parent.id) ?? []; const needsLegacyNormalization = !isValidDirectoryResumeCheckpoint(parent.directoryResumeCheckpoint) && parentChildren.some((child) => ( !Number.isSafeInteger(child.directoryEntryIndex) || !/^[a-f0-9]{64}$/.test(child.directoryEntryIdentity ?? "") )); if (needsLegacyNormalization) { parentChildren = [...parentChildren] .sort((left, right) => compareDirectoryTraversalPaths(left.sourcePath, right.sourcePath)); await maybeYield(parentIndex, parentChildren.length > RESTORE_CHECK_INTERVAL); for (let childIndex = 0; childIndex < parentChildren.length; childIndex += 1) { const child = parentChildren[childIndex]; const normalized: TransferTask = { ...child, directoryEntryIndex: childIndex, directoryEntryIdentity: createDirectoryEntryIdentity({ sourcePath: child.sourcePath, targetPath: child.targetPath, size: child.totalBytes, lastModified: child.sourceLastModified, }), }; parentChildren[childIndex] = normalized; normalizedChildUpdates.set(normalized.id, normalized); await maybeYield(childIndex); } } const checkpoint = isValidDirectoryResumeCheckpoint(parent.directoryResumeCheckpoint) ? { ...parent.directoryResumeCheckpoint } : createEmptyDirectoryResumeCheckpoint(); const initialCoveredEntries = checkpoint.coveredEntries; const childrenByIndex = new Map(); for (let childIndex = 0; childIndex < parentChildren.length; childIndex += 1) { const child = parentChildren[childIndex]; if ( Number.isSafeInteger(child.directoryEntryIndex) && (child.directoryEntryIndex ?? -1) >= 0 && /^[a-f0-9]{64}$/.test(child.directoryEntryIdentity ?? "") && !childrenByIndex.has(child.directoryEntryIndex!) ) { childrenByIndex.set(child.directoryEntryIndex!, child); } await maybeYield(childIndex); } let hashIterations = 0; const manifest = createDirectoryManifestAccumulator(checkpoint); while (childrenByIndex.has(checkpoint.coveredEntries)) { const child = childrenByIndex.get(checkpoint.coveredEntries)!; manifest.append(child.directoryEntryIdentity!); checkpoint.coveredEntries += 1; hashIterations += 1; await maybeYield(hashIterations); } checkpoint.manifestHash = manifest.digest(); let newlyCompacted = 0; for (let childIndex = 0; childIndex < parentChildren.length; childIndex += 1) { const child = parentChildren[childIndex]; if ( child.status === "completed" && Number.isSafeInteger(child.directoryEntryIndex) && (child.directoryEntryIndex ?? checkpoint.coveredEntries) < checkpoint.coveredEntries && /^[a-f0-9]{64}$/.test(child.directoryEntryIdentity ?? "") ) { compactedChildIds.add(child.id); newlyCompacted += 1; } await maybeYield(childIndex); } if (checkpoint.coveredEntries !== initialCoveredEntries || newlyCompacted > 0) { checkpoint.completedEntries = Math.min( checkpoint.coveredEntries, checkpoint.completedEntries + newlyCompacted, ); parentUpdates.set(parent.id, { ...parent, directoryResumeCheckpoint: checkpoint, transferredBytes: Math.max(parent.transferredBytes, checkpoint.completedEntries), }); } } const compacted: TransferTask[] = []; for (let index = 0; index < restored.length; index += 1) { const task = restored[index]; if (!compactedChildIds.has(task.id)) { compacted.push(parentUpdates.get(task.id) ?? normalizedChildUpdates.get(task.id) ?? task); } await maybeYield(index); } return { valid: true, tasks: pruneSftpTransferHistory(compacted, now), }; }