[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:
204
application/state/useVaultGroupDeletion.ts
Normal file
204
application/state/useVaultGroupDeletion.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
import type { GroupConfig, Host, ManagedSource } from "../../domain/models";
|
||||
import { removeSnippetTargetGroupPaths } from "../../domain/hostGroupPathMutations";
|
||||
import { buildVaultGroupDeletion } from "../../domain/vaultGroupDeletion";
|
||||
import type {
|
||||
VaultGroupMutationResult,
|
||||
VaultGroupMutationState,
|
||||
} from "../../domain/vaultGroupMutation";
|
||||
import {
|
||||
type VaultLockHandle,
|
||||
withVaultImportLock,
|
||||
} from "./vaultManagedImportLock";
|
||||
|
||||
const RETRY_VAULT_GROUP_DELETION = Symbol("retry-vault-group-deletion");
|
||||
|
||||
const managedSourceSnapshotsMatch = (
|
||||
left: ManagedSource[],
|
||||
right: ManagedSource[],
|
||||
): boolean => {
|
||||
const serialize = (sources: ManagedSource[]) => JSON.stringify(
|
||||
[...sources].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
);
|
||||
return serialize(left) === serialize(right);
|
||||
};
|
||||
|
||||
export function useVaultGroupDeletion({
|
||||
customGroups,
|
||||
hosts,
|
||||
groupConfigs,
|
||||
managedSources,
|
||||
onReadPersistedHosts,
|
||||
onReadPersistedManagedSources,
|
||||
onCommitVaultGroupMutation,
|
||||
onClearAndRemoveManagedSource,
|
||||
onClearAndRemoveManagedSources,
|
||||
onDeletedPaths,
|
||||
}: {
|
||||
customGroups: string[];
|
||||
hosts: Host[];
|
||||
groupConfigs: GroupConfig[];
|
||||
managedSources: ManagedSource[];
|
||||
onReadPersistedHosts: () => Promise<Host[]>;
|
||||
onReadPersistedManagedSources: () => ManagedSource[];
|
||||
onCommitVaultGroupMutation: (
|
||||
mutate: (current: VaultGroupMutationState) => VaultGroupMutationResult,
|
||||
lock?: VaultLockHandle | null,
|
||||
) => Promise<VaultGroupMutationResult | { ok: false; superseded: true }>;
|
||||
onClearAndRemoveManagedSource?: (source: ManagedSource) => Promise<() => Promise<void>>;
|
||||
onClearAndRemoveManagedSources?: (sources: ManagedSource[]) => Promise<() => Promise<void>>;
|
||||
onDeletedPaths?: (selectedRoots: string[]) => void;
|
||||
}) {
|
||||
const latestRef = useRef({ customGroups, hosts, groupConfigs, managedSources });
|
||||
latestRef.current = { customGroups, hosts, groupConfigs, managedSources };
|
||||
|
||||
return useCallback(async (
|
||||
paths: Iterable<string>,
|
||||
deleteHosts: boolean = false,
|
||||
additionallyDeletedHostIds: ReadonlySet<string> = new Set(),
|
||||
) => {
|
||||
const selectedPaths = [...paths];
|
||||
let deletedRoots: string[] = [];
|
||||
while (true) {
|
||||
let restoreManagedFiles: (() => Promise<void>) | undefined;
|
||||
try {
|
||||
const outcome = await withVaultImportLock("vault", async (lock) => {
|
||||
const [latestHosts, latestManagedSources] = await Promise.all([
|
||||
onReadPersistedHosts(),
|
||||
Promise.resolve(onReadPersistedManagedSources()),
|
||||
]);
|
||||
latestRef.current = {
|
||||
...latestRef.current,
|
||||
hosts: latestHosts,
|
||||
managedSources: latestManagedSources,
|
||||
};
|
||||
let deletion = buildVaultGroupDeletion({
|
||||
selectedPaths,
|
||||
deleteHosts,
|
||||
...latestRef.current,
|
||||
});
|
||||
if (deletion.selectedRoots.length === 0) {
|
||||
return { status: "empty" as const };
|
||||
}
|
||||
|
||||
if (deletion.sourcesToRemove.length > 0) {
|
||||
if (onClearAndRemoveManagedSources) {
|
||||
restoreManagedFiles = await onClearAndRemoveManagedSources(deletion.sourcesToRemove);
|
||||
} else if (onClearAndRemoveManagedSource) {
|
||||
const restores = await Promise.all(
|
||||
deletion.sourcesToRemove.map((source) => onClearAndRemoveManagedSource(source)),
|
||||
);
|
||||
restoreManagedFiles = async () => {
|
||||
await Promise.all(restores.map((restore) => restore()));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
latestRef.current = {
|
||||
...latestRef.current,
|
||||
hosts: await onReadPersistedHosts(),
|
||||
managedSources: onReadPersistedManagedSources(),
|
||||
};
|
||||
const refreshedDeletion = buildVaultGroupDeletion({
|
||||
selectedPaths,
|
||||
deleteHosts,
|
||||
...latestRef.current,
|
||||
});
|
||||
if (!managedSourceSnapshotsMatch(
|
||||
deletion.sourcesToRemove,
|
||||
refreshedDeletion.sourcesToRemove,
|
||||
)) {
|
||||
throw RETRY_VAULT_GROUP_DELETION;
|
||||
}
|
||||
deletion = refreshedDeletion;
|
||||
|
||||
const expectedSources = deletion.sourcesToRemove;
|
||||
const transaction = await onCommitVaultGroupMutation(
|
||||
(current) => {
|
||||
const currentDeletion = buildVaultGroupDeletion({
|
||||
selectedPaths,
|
||||
deleteHosts,
|
||||
customGroups: current.groups,
|
||||
hosts: current.hosts,
|
||||
groupConfigs: current.configs,
|
||||
managedSources: current.managedSources,
|
||||
});
|
||||
if (!managedSourceSnapshotsMatch(
|
||||
expectedSources,
|
||||
currentDeletion.sourcesToRemove,
|
||||
)) {
|
||||
throw RETRY_VAULT_GROUP_DELETION;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
state: {
|
||||
groups: currentDeletion.customGroups,
|
||||
hosts: currentDeletion.hosts.filter(
|
||||
(host) => !additionallyDeletedHostIds.has(host.id),
|
||||
),
|
||||
configs: currentDeletion.groupConfigs,
|
||||
managedSources: current.managedSources.filter(
|
||||
(source) => !currentDeletion.sourcesToRemove.some(
|
||||
(removed) => removed.id === source.id,
|
||||
),
|
||||
),
|
||||
snippets: removeSnippetTargetGroupPaths(
|
||||
current.snippets,
|
||||
currentDeletion.selectedRoots,
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
lock,
|
||||
);
|
||||
if (!transaction.ok && "superseded" in transaction) {
|
||||
// Release the lock so concurrent saves can finish before retry.
|
||||
return { status: "retry" as const, restoreManagedFiles };
|
||||
}
|
||||
if ("error" in transaction) {
|
||||
throw new Error(transaction.error);
|
||||
}
|
||||
latestRef.current = {
|
||||
customGroups: transaction.state.groups,
|
||||
hosts: transaction.state.hosts,
|
||||
groupConfigs: transaction.state.configs,
|
||||
managedSources: transaction.state.managedSources,
|
||||
};
|
||||
return {
|
||||
status: "done" as const,
|
||||
deletedRoots: deletion.selectedRoots,
|
||||
};
|
||||
});
|
||||
|
||||
if (outcome.status === "empty") {
|
||||
onDeletedPaths?.([]);
|
||||
return;
|
||||
}
|
||||
if (outcome.status === "retry") {
|
||||
await outcome.restoreManagedFiles?.();
|
||||
// Drain concurrent locked writes outside the critical section.
|
||||
await onReadPersistedHosts();
|
||||
continue;
|
||||
}
|
||||
deletedRoots = outcome.deletedRoots;
|
||||
break;
|
||||
} catch (error) {
|
||||
await restoreManagedFiles?.();
|
||||
if (error === RETRY_VAULT_GROUP_DELETION) {
|
||||
await onReadPersistedHosts();
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
onDeletedPaths?.(deletedRoots);
|
||||
}, [
|
||||
onClearAndRemoveManagedSource,
|
||||
onClearAndRemoveManagedSources,
|
||||
onCommitVaultGroupMutation,
|
||||
onDeletedPaths,
|
||||
onReadPersistedHosts,
|
||||
onReadPersistedManagedSources,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user