mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(client): strip removed backends from remaining mirror replicas — no phantom refs
This commit is contained in:
@@ -354,7 +354,7 @@ export default function AdminStoragePanel(): React.ReactElement {
|
||||
admin.setDraft(
|
||||
confirmRemove.degenerate
|
||||
? removeBackend(draft, confirmRemove.name)
|
||||
: removeBackendAndMirrors(draft, confirmRemove.name),
|
||||
: removeBackendAndMirrors(state, draft, confirmRemove.name),
|
||||
)
|
||||
}
|
||||
setConfirmRemove(null)
|
||||
|
||||
@@ -210,11 +210,26 @@ describe('mirror fold/synthesize (replicas-on-primary)', () => {
|
||||
it('FE-ADMIN-STORM-013: removeBackendAndMirrors cascades; replicaOfPrimaries phrases in primary names', () => {
|
||||
const draft = mirroredDraft();
|
||||
expect(replicaOfPrimaries(draft, 'off-box')).toEqual(['backups-local']);
|
||||
const removed = removeBackendAndMirrors(draft, 'backups-local');
|
||||
const removed = removeBackendAndMirrors(MIRRORED_STATE, draft, 'backups-local');
|
||||
expect(removed.backends.some((b) => b.name === 'mirror')).toBe(false);
|
||||
expect(removed.backends.some((b) => b.name === 'backups-local')).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STORM-015: removing a replica strips it from remaining mirrors; a mirror stripped empty dissolves', () => {
|
||||
// Single-replica mirror: stripping off-box empties it → dissolve + re-point
|
||||
// (backups is settings-sourced at the mirror in MIRRORED_STATE, so it re-points, not drops).
|
||||
const dissolved = removeBackendAndMirrors(MIRRORED_STATE, mirroredDraft(), 'off-box');
|
||||
expect(dissolved.backends.some((b) => b.name === 'off-box')).toBe(false);
|
||||
expect(dissolved.backends.some((b) => b.type === 'mirror')).toBe(false);
|
||||
expect(dissolved.categories.backups).toBe('backups-local');
|
||||
// Multi-replica mirror: stripping one target keeps the mirror alive with the rest.
|
||||
const twoTargets = setMirrorTargets(MIRRORED_STATE, mirroredDraft(), 'backups-local', ['off-box', 'uploads-local']);
|
||||
const survived = removeBackendAndMirrors(MIRRORED_STATE, twoTargets, 'off-box');
|
||||
const mirror = survived.backends.find((b) => b.type === 'mirror')!;
|
||||
expect(mirror.options).toEqual({ primary: 'backups-local', replicas: ['uploads-local'] });
|
||||
expect(survived.categories.backups).toBe('mirror'); // still routed via the surviving mirror
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STORM-014: primaryNameOf maps mirror names (draft or state) to their primary; others pass through', () => {
|
||||
expect(primaryNameOf(MIRRORED_STATE, mirroredDraft(), 'mirror')).toBe('backups-local');
|
||||
expect(primaryNameOf(MIRRORED_STATE, { backends: [], categories: {} }, 'mirror')).toBe('backups-local'); // state fallback
|
||||
|
||||
@@ -246,12 +246,40 @@ export function renameBackendRefs(draft: StorageConfig, oldName: string, newName
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove a backend AND every mirror wrapping it as primary (they would dangle). */
|
||||
export function removeBackendAndMirrors(draft: StorageConfig, name: string): StorageConfig {
|
||||
return {
|
||||
...draft,
|
||||
backends: draft.backends.filter((b) => b.name !== name && !(isMirror(b) && b.options.primary === name)),
|
||||
/**
|
||||
* Remove a backend, every mirror wrapping it as primary, and every replica
|
||||
* reference to it in the remaining mirrors — a mirror stripped of its last
|
||||
* replica dissolves (with the same category re-pointing as an explicit
|
||||
* dissolve), so the draft never carries a phantom reference the composer
|
||||
* cannot render or repair.
|
||||
*/
|
||||
export function removeBackendAndMirrors(
|
||||
state: StorageAdminState,
|
||||
draft: StorageConfig,
|
||||
name: string,
|
||||
): StorageConfig {
|
||||
const stripped = draft.backends
|
||||
.filter((b) => b.name !== name && !(isMirror(b) && b.options.primary === name))
|
||||
.map((b) =>
|
||||
isMirror(b) && b.options.replicas.includes(name)
|
||||
? { ...b, options: { ...b.options, replicas: b.options.replicas.filter((r) => r !== name) } }
|
||||
: b,
|
||||
)
|
||||
let next: StorageConfig = { ...draft, backends: stripped }
|
||||
for (const mirror of stripped.filter(isMirror)) {
|
||||
if (mirror.options.replicas.length > 0) continue
|
||||
if (adoptedMirrorFor(next, mirror.options.primary)?.name === mirror.name) {
|
||||
next = setMirrorTargets(state, next, mirror.options.primary, [])
|
||||
} else {
|
||||
// A degenerate duplicate stripped empty: drop it and re-point its categories.
|
||||
const categories = { ...next.categories }
|
||||
for (const category of STORAGE_CATEGORIES) {
|
||||
if (categories[category] === mirror.name) categories[category] = mirror.options.primary
|
||||
}
|
||||
next = { backends: next.backends.filter((b) => b.name !== mirror.name), categories }
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
/** Primaries whose mirrors use the named backend as a replica — pre-check copy stays in primary names. */
|
||||
|
||||
@@ -355,6 +355,8 @@ export default function MAdminStoragePanel(): React.ReactElement {
|
||||
{rows.map((row) => {
|
||||
const resultKey = row.mirrorName ?? row.name
|
||||
const result = admin.testResults[resultKey]
|
||||
// row.mirrorName only exists when foldBackends adopted a draft mirror
|
||||
// for this row, so the draft lookup below cannot miss.
|
||||
const testCandidate = row.mirrorName
|
||||
? draft.backends.find((b) => b.name === row.mirrorName)!
|
||||
: row.backend
|
||||
@@ -575,7 +577,7 @@ export default function MAdminStoragePanel(): React.ReactElement {
|
||||
admin.setDraft(
|
||||
confirmRemove.degenerate
|
||||
? removeBackend(draft, confirmRemove.name)
|
||||
: removeBackendAndMirrors(draft, confirmRemove.name),
|
||||
: removeBackendAndMirrors(state, draft, confirmRemove.name),
|
||||
)
|
||||
}
|
||||
setConfirmRemove(null)
|
||||
|
||||
Reference in New Issue
Block a user