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
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
/**
|
|
* Process-level TransferRuntime — single control/execution surface for SFTP
|
|
* bulk transfers.
|
|
*
|
|
* Contract (one entry for all callers — Global Center, panel queue, tests):
|
|
* start / enqueue — register work in the store (no panel ownership required)
|
|
* pause / resume / cancel — process-global; soft control never needs a mounted owner
|
|
* subscribe / getSnapshot — observe runtime state
|
|
* runWalk — execute a directory/file walk that outlives React unmount
|
|
*
|
|
* Soft pause/resume of a live walk vs hard dedicated reconnect on dead/reconnect
|
|
* rows are internal strategies of `resume`. Callers do not pick owner vs orphan
|
|
* APIs. Panel hooks are enqueue + view only.
|
|
*/
|
|
|
|
import type { TransferTask } from "../../../domain/models";
|
|
import {
|
|
sftpTransferCenterStore,
|
|
type SftpTransferCenterSnapshot,
|
|
} from "../sftpTransferCenterStore";
|
|
import {
|
|
isTransferWalkInFlight,
|
|
registerTransferWalk,
|
|
unregisterTransferWalk,
|
|
} from "./transferWalkRegistry";
|
|
import { settleTransferCancelTree } from "./transferCancelLatch";
|
|
import { settleTransferControlEpochTree } from "./transferControlEpoch";
|
|
|
|
export type TransferRuntimeSnapshot = SftpTransferCenterSnapshot;
|
|
|
|
export type TransferWalkRunner = () => Promise<void>;
|
|
|
|
export interface TransferRuntime {
|
|
/** Observe store/runtime snapshot changes. */
|
|
subscribe(listener: () => void): () => void;
|
|
getSnapshot(): TransferRuntimeSnapshot;
|
|
getTask(taskId: string): TransferTask | undefined;
|
|
|
|
/**
|
|
* Insert or replace task rows (enqueue). Does not require a panel owner.
|
|
* ownerId on the task is an origin label only, not control authority.
|
|
*/
|
|
enqueue(tasks: readonly TransferTask[]): void;
|
|
/** Patch a single task from the runtime writer path (progress / lifecycle). */
|
|
patchTask(taskId: string, updates: Partial<TransferTask>): void;
|
|
|
|
/** Soft-pause a live walk (process-global; no owner required). */
|
|
pause(taskId: string): Promise<void>;
|
|
/**
|
|
* Resume: soft-unlatch when walk is in-flight; hard dedicated reconnect when
|
|
* walk is dead / reconnectRequired. Single external operation.
|
|
*/
|
|
resume(taskId: string): Promise<void>;
|
|
cancel(taskId: string): Promise<void>;
|
|
|
|
/**
|
|
* Run a transfer walk process-globally. Registers the walk before `runner`
|
|
* starts and always unregisters on settle — survives panel unmount.
|
|
* Concurrent starts on the same id no-op (existing walk wins).
|
|
*/
|
|
runWalk(rootTaskId: string, runner: TransferWalkRunner): Promise<void>;
|
|
isWalkInFlight(rootTaskId: string): boolean;
|
|
}
|
|
|
|
const inFlightRunPromises = new Map<string, Promise<void>>();
|
|
|
|
export function createTransferRuntime(
|
|
store: typeof sftpTransferCenterStore = sftpTransferCenterStore,
|
|
): TransferRuntime {
|
|
return {
|
|
subscribe(listener) {
|
|
return store.subscribe(listener);
|
|
},
|
|
getSnapshot() {
|
|
return store.getSnapshot();
|
|
},
|
|
getTask(taskId) {
|
|
return store.getSnapshot().tasks.find((task) => task.id === taskId);
|
|
},
|
|
enqueue(tasks) {
|
|
store.upsertTasks(tasks);
|
|
},
|
|
patchTask(taskId, updates) {
|
|
store.patchTask(taskId, updates);
|
|
},
|
|
pause(taskId) {
|
|
return store.pause(taskId);
|
|
},
|
|
resume(taskId) {
|
|
return store.resume(taskId);
|
|
},
|
|
cancel(taskId) {
|
|
return store.cancel(taskId);
|
|
},
|
|
async runWalk(rootTaskId, runner) {
|
|
const existing = inFlightRunPromises.get(rootTaskId);
|
|
if (existing || isTransferWalkInFlight(rootTaskId)) {
|
|
if (existing) await existing;
|
|
return;
|
|
}
|
|
registerTransferWalk(rootTaskId);
|
|
const run = (async () => {
|
|
try {
|
|
await runner();
|
|
} finally {
|
|
const childIds = store.getSnapshot().tasks
|
|
.filter((task) => task.parentTaskId === rootTaskId)
|
|
.map((task) => task.id);
|
|
const relatedChildIds = settleTransferCancelTree(rootTaskId, childIds);
|
|
settleTransferControlEpochTree(rootTaskId, relatedChildIds);
|
|
unregisterTransferWalk(rootTaskId);
|
|
if (inFlightRunPromises.get(rootTaskId) === run) {
|
|
inFlightRunPromises.delete(rootTaskId);
|
|
}
|
|
}
|
|
})();
|
|
inFlightRunPromises.set(rootTaskId, run);
|
|
await run;
|
|
},
|
|
isWalkInFlight(rootTaskId) {
|
|
return isTransferWalkInFlight(rootTaskId) || inFlightRunPromises.has(rootTaskId);
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Process-global singleton used by UI and panel hooks. */
|
|
export const transferRuntime = createTransferRuntime();
|
|
|
|
/** Test helper — clear in-flight run bookkeeping (walk registry is separate). */
|
|
export function resetTransferRuntimeRunsForTests(): void {
|
|
inFlightRunPromises.clear();
|
|
}
|