Skip to content

Commit 72f2c59

Browse files
committed
Fix systemId cleanup for nested children on stopChild
1 parent 54cd991 commit 72f2c59

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

packages/core/src/actions/stopChild.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ function resolveStop(
5252
undefined
5353
];
5454
}
55+
function unregisterRecursively(
56+
actorScope: AnyActorScope,
57+
actorRef: AnyActorRef
58+
) {
59+
// unregister children first (depth-first)
60+
const snapshot = actorRef.getSnapshot();
61+
if (snapshot && 'children' in snapshot) {
62+
for (const child of Object.values(
63+
snapshot.children as Record<string, AnyActorRef>
64+
)) {
65+
unregisterRecursively(actorScope, child);
66+
}
67+
}
68+
actorScope.system._unregister(actorRef);
69+
}
70+
5571
function executeStop(
5672
actorScope: AnyActorScope,
5773
actorRef: AnyActorRef | undefined
@@ -63,7 +79,8 @@ function executeStop(
6379
// we need to eagerly unregister it here so a new actor with the same systemId can be registered immediately
6480
// since we defer actual stopping of the actor but we don't defer actor creations (and we can't do that)
6581
// this could throw on `systemId` collision, for example, when dealing with reentering transitions
66-
actorScope.system._unregister(actorRef);
82+
// we also need to recursively unregister all nested children's systemIds
83+
unregisterRecursively(actorScope, actorRef);
6784

6885
// this allows us to prevent an actor from being started if it gets stopped within the same macrostep
6986
// this can happen, for example, when the invoking state is being exited immediately by an always transition

packages/core/test/system.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
fromPromise,
1616
fromTransition,
1717
sendTo,
18+
setup,
1819
spawnChild,
1920
stopChild
2021
} from '../src/index.ts';
@@ -584,4 +585,43 @@ describe('system', () => {
584585

585586
expect(actor.system.getAll()).toEqual({});
586587
});
588+
589+
it('should unregister nested child systemIds when stopping a parent actor', () => {
590+
const subchild = createMachine({});
591+
592+
const child = setup({
593+
actors: {
594+
subchild
595+
}
596+
}).createMachine({
597+
id: 'childSystem',
598+
invoke: {
599+
src: 'subchild',
600+
systemId: 'subchild'
601+
}
602+
});
603+
604+
const parent = setup({
605+
actors: { child }
606+
}).createMachine({
607+
entry: spawnChild('child', { id: 'childId' }),
608+
on: {
609+
restart: {
610+
actions: [
611+
stopChild('childId'),
612+
spawnChild('child', { id: 'childId' })
613+
]
614+
}
615+
}
616+
});
617+
618+
const root = createActor(parent).start();
619+
620+
expect(root.system.get('subchild')).toBeDefined();
621+
622+
// This should not throw "Actor with system ID 'subchild' already exists"
623+
expect(() => root.send({ type: 'restart' })).not.toThrow();
624+
625+
expect(root.system.get('subchild')).toBeDefined();
626+
});
587627
});

0 commit comments

Comments
 (0)