[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
202
application/state/sftp/transferHistoryRestoreMigration.ts
Normal file
202
application/state/sftp/transferHistoryRestoreMigration.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
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<TransferTask["status"]>(["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<void> {
|
||||
const scheduler = (globalThis as typeof globalThis & {
|
||||
scheduler?: { yield?: () => Promise<void> };
|
||||
}).scheduler;
|
||||
if (typeof scheduler?.yield === "function") {
|
||||
await scheduler.yield();
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((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<CooperativeTransferHistoryRestoreResult> {
|
||||
// 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<string, TransferTask[]>();
|
||||
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<string>();
|
||||
const parentUpdates = new Map<string, TransferTask>();
|
||||
const normalizedChildUpdates = new Map<string, TransferTask>();
|
||||
|
||||
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<number, TransferTask>();
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user