fix(spaces): unlink one space-child edge instead of over-deleting (COR-1)
When a single m.space.child was removed (unlinking child C from space P), the
roomToParents reducer fired the whole-room DELETE action, which wiped C's
entire parent set, stripped C as a parent from every other room, and orphaned
C's own descendants until a full resync. So removing C from space A also
dropped C's other parent B, and C's children lost C.
Add a targeted UNLINK {parent, child} action that removes only that one
parent->child edge and prunes the child entry only when its parent set
empties (matching the map's build-time invariant that zero-parent rooms have
no entry). Point the invalid-child branch of handleStateChange at it; DELETE
is unchanged for genuine room leave/delete. Unit-tested (keeps other parents,
prunes on last parent, does NOT orphan descendants, unknown pair no-op).
Verified correct + consumer-safe by two review passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,11 @@ export type RoomToParentsAction =
|
||||
| {
|
||||
type: 'DELETE';
|
||||
roomId: string;
|
||||
}
|
||||
| {
|
||||
type: 'UNLINK';
|
||||
parent: string;
|
||||
child: string;
|
||||
};
|
||||
|
||||
const baseRoomToParents = atom<RoomToParents>(new Map());
|
||||
@@ -63,6 +68,23 @@ export const roomToParentsAtom = atom<RoomToParents, [RoomToParentsAction], unde
|
||||
noParentRooms.forEach((room) => draftRoomToParents.delete(room));
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (action.type === 'UNLINK') {
|
||||
// Remove ONE parent from ONE child (a single `m.space.child` was removed).
|
||||
// Unlike DELETE (used when a room is left/deleted), this must NOT touch the
|
||||
// child's other parents, nor strip the child as a parent elsewhere, nor
|
||||
// orphan the child's own descendants. Only prune the child's entry if this
|
||||
// was its last parent.
|
||||
set(
|
||||
baseRoomToParents,
|
||||
produce(get(baseRoomToParents), (draftRoomToParents) => {
|
||||
const parents = draftRoomToParents.get(action.child);
|
||||
if (!parents) return;
|
||||
parents.delete(action.parent);
|
||||
if (parents.size === 0) draftRoomToParents.delete(action.child);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -100,7 +122,10 @@ export const useBindRoomToParentsAtom = (
|
||||
if (isValidChild(mEvent)) {
|
||||
setRoomToParents({ type: 'PUT', parent: roomId, children: [childId] });
|
||||
} else {
|
||||
setRoomToParents({ type: 'DELETE', roomId: childId });
|
||||
// A single m.space.child was removed: unlink only THIS parent→child
|
||||
// edge. DELETE here wiped the child's other parents and orphaned its
|
||||
// descendants until a full resync (COR-1).
|
||||
setRoomToParents({ type: 'UNLINK', parent: roomId, child: childId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user