[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:
234
application/state/vaultImportProgress.ts
Normal file
234
application/state/vaultImportProgress.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
export type VaultImportProgressStage =
|
||||
| "reading"
|
||||
| "parsing"
|
||||
| "preparing"
|
||||
| "saving"
|
||||
| "complete"
|
||||
| "failed";
|
||||
|
||||
export interface VaultImportProgress {
|
||||
status: "running" | "complete" | "error";
|
||||
stage: VaultImportProgressStage;
|
||||
percent: number;
|
||||
formatLabel: string;
|
||||
fileName: string;
|
||||
completedFiles?: number;
|
||||
totalFiles?: number;
|
||||
currentFileName?: string;
|
||||
imported?: number;
|
||||
skipped?: number;
|
||||
duplicates?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type VaultHostPersistenceResult = boolean | void | "superseded";
|
||||
|
||||
export function countVaultImportDuplicates({
|
||||
importedHostCount,
|
||||
newHostCount,
|
||||
fileDuplicateCount,
|
||||
managed,
|
||||
}: {
|
||||
importedHostCount: number;
|
||||
newHostCount: number;
|
||||
fileDuplicateCount: number;
|
||||
managed: boolean;
|
||||
}): number {
|
||||
const existingDuplicateCount = managed
|
||||
? 0
|
||||
: Math.max(0, importedHostCount - newHostCount);
|
||||
return fileDuplicateCount + existingDuplicateCount;
|
||||
}
|
||||
|
||||
export async function ensureVaultImportPersisted(
|
||||
persisted: Exclude<VaultHostPersistenceResult, "superseded">,
|
||||
errorMessage: string,
|
||||
onPersisted?: () => unknown | Promise<unknown>,
|
||||
onPersistenceFailed?: () => unknown | Promise<unknown>,
|
||||
): Promise<void> {
|
||||
if (persisted === false) {
|
||||
try {
|
||||
await onPersistenceFailed?.();
|
||||
} catch {
|
||||
// Keep the original persistence error visible even if rollback also fails.
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
await onPersisted?.();
|
||||
}
|
||||
|
||||
const importFieldMatches = (left: unknown, right: unknown): boolean => (
|
||||
JSON.stringify(left) === JSON.stringify(right)
|
||||
);
|
||||
|
||||
const hostMatchesAppliedImport = (currentHost: Host, appliedHost: Host): boolean => {
|
||||
const { order: _currentOrder, ...currentComparable } = currentHost;
|
||||
const { order: _appliedOrder, ...appliedComparable } = appliedHost;
|
||||
return importFieldMatches(currentComparable, appliedComparable);
|
||||
};
|
||||
|
||||
export function rebaseVaultImportedHosts({
|
||||
currentHosts,
|
||||
baselineHosts,
|
||||
appliedHosts,
|
||||
}: {
|
||||
currentHosts: Host[];
|
||||
baselineHosts: Host[];
|
||||
appliedHosts: Host[];
|
||||
}): Host[] {
|
||||
const baselineById = new Map(baselineHosts.map((host) => [host.id, host]));
|
||||
const appliedById = new Map(appliedHosts.map((host) => [host.id, host]));
|
||||
const currentIds = new Set(currentHosts.map((host) => host.id));
|
||||
|
||||
const rebasedHosts = currentHosts.map((currentHost) => {
|
||||
const baselineHost = baselineById.get(currentHost.id);
|
||||
const appliedHost = appliedById.get(currentHost.id);
|
||||
if (!baselineHost || !appliedHost) return currentHost;
|
||||
|
||||
const fieldNames = new Set([
|
||||
...Object.keys(baselineHost),
|
||||
...Object.keys(appliedHost),
|
||||
] as Array<keyof Host>);
|
||||
const rebased = { ...currentHost } as Record<string, unknown>;
|
||||
for (const field of fieldNames) {
|
||||
if (importFieldMatches(baselineHost[field], appliedHost[field])) continue;
|
||||
if (!importFieldMatches(currentHost[field], baselineHost[field])) continue;
|
||||
if (Object.prototype.hasOwnProperty.call(appliedHost, field)) {
|
||||
rebased[field as string] = appliedHost[field];
|
||||
} else {
|
||||
delete rebased[field as string];
|
||||
}
|
||||
}
|
||||
return rebased as Host;
|
||||
});
|
||||
|
||||
for (const appliedHost of appliedHosts) {
|
||||
if (!baselineById.has(appliedHost.id) && !currentIds.has(appliedHost.id)) {
|
||||
rebasedHosts.push(appliedHost);
|
||||
}
|
||||
}
|
||||
return rebasedHosts;
|
||||
}
|
||||
|
||||
export function mergeVaultImportedGroups({
|
||||
currentGroups,
|
||||
baselineGroups,
|
||||
appliedGroups,
|
||||
}: {
|
||||
currentGroups: string[];
|
||||
baselineGroups: string[];
|
||||
appliedGroups: string[];
|
||||
}): string[] {
|
||||
const baseline = new Set(baselineGroups);
|
||||
const importedGroups = appliedGroups.filter((group) => !baseline.has(group));
|
||||
return Array.from(new Set([...currentGroups, ...importedGroups]));
|
||||
}
|
||||
|
||||
export function resolveUniqueManagedImportGroupName({
|
||||
baseName,
|
||||
customGroups,
|
||||
hosts,
|
||||
managedSources,
|
||||
ownerSourceId,
|
||||
}: {
|
||||
baseName: string;
|
||||
customGroups: string[];
|
||||
hosts: Host[];
|
||||
managedSources: ManagedSource[];
|
||||
ownerSourceId?: string;
|
||||
}): string {
|
||||
const existingGroupNames = new Set([
|
||||
...customGroups,
|
||||
...managedSources
|
||||
.filter((source) => source.id !== ownerSourceId)
|
||||
.map((source) => source.groupName),
|
||||
...hosts
|
||||
.filter((host) => host.managedSourceId !== ownerSourceId)
|
||||
.map((host) => host.group)
|
||||
.filter((group): group is string => Boolean(group)),
|
||||
]);
|
||||
const initialName = `${baseName} - Managed`;
|
||||
let groupName = initialName;
|
||||
let suffix = 1;
|
||||
while (existingGroupNames.has(groupName)) {
|
||||
groupName = `${initialName} (${suffix})`;
|
||||
suffix++;
|
||||
}
|
||||
return groupName;
|
||||
}
|
||||
|
||||
export function rollbackVaultImportedHosts({
|
||||
currentHosts,
|
||||
baselineHosts,
|
||||
appliedHosts,
|
||||
}: {
|
||||
currentHosts: Host[];
|
||||
baselineHosts: Host[];
|
||||
appliedHosts: Host[];
|
||||
}): Host[] {
|
||||
const baselineById = new Map(baselineHosts.map((host) => [host.id, host]));
|
||||
const appliedById = new Map(appliedHosts.map((host) => [host.id, host]));
|
||||
const addedHostIds = new Set(
|
||||
appliedHosts.filter((host) => !baselineById.has(host.id)).map((host) => host.id),
|
||||
);
|
||||
|
||||
return currentHosts.flatMap((currentHost) => {
|
||||
if (addedHostIds.has(currentHost.id)) {
|
||||
const appliedHost = appliedById.get(currentHost.id);
|
||||
return appliedHost && hostMatchesAppliedImport(currentHost, appliedHost)
|
||||
? []
|
||||
: [currentHost];
|
||||
}
|
||||
const baselineHost = baselineById.get(currentHost.id);
|
||||
const appliedHost = appliedById.get(currentHost.id);
|
||||
if (!baselineHost || !appliedHost) return [currentHost];
|
||||
|
||||
const fieldNames = new Set([
|
||||
...Object.keys(baselineHost),
|
||||
...Object.keys(appliedHost),
|
||||
] as Array<keyof Host>);
|
||||
const changedFields = [...fieldNames].filter((field) => (
|
||||
!importFieldMatches(baselineHost[field], appliedHost[field])
|
||||
));
|
||||
if (changedFields.length === 0) return [currentHost];
|
||||
const restored = { ...currentHost } as Record<string, unknown>;
|
||||
for (const field of changedFields) {
|
||||
if (!importFieldMatches(currentHost[field], appliedHost[field])) continue;
|
||||
if (Object.prototype.hasOwnProperty.call(baselineHost, field)) {
|
||||
restored[field as string] = baselineHost[field];
|
||||
} else {
|
||||
delete restored[field as string];
|
||||
}
|
||||
}
|
||||
return [restored as Host];
|
||||
});
|
||||
}
|
||||
|
||||
interface VaultImportPaintWaitOptions {
|
||||
requestFrame?: (callback: () => void) => unknown;
|
||||
setTimer?: (callback: () => void, delayMs: number) => unknown;
|
||||
clearTimer?: (timer: unknown) => void;
|
||||
}
|
||||
|
||||
export function waitForVaultImportProgressPaint({
|
||||
requestFrame = typeof requestAnimationFrame === "function"
|
||||
? (callback) => requestAnimationFrame(callback)
|
||||
: undefined,
|
||||
setTimer = (callback, delayMs) => setTimeout(callback, delayMs),
|
||||
clearTimer = (timer) => clearTimeout(timer as ReturnType<typeof setTimeout>),
|
||||
}: VaultImportPaintWaitOptions = {}): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let completed = false;
|
||||
let timer: unknown;
|
||||
const complete = () => {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
if (timer !== undefined) clearTimer(timer);
|
||||
resolve();
|
||||
};
|
||||
|
||||
timer = setTimer(complete, 100);
|
||||
requestFrame?.(complete);
|
||||
});
|
||||
}
|
||||
import type { Host, ManagedSource } from "../../domain/models";
|
||||
Reference in New Issue
Block a user