Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Call complete listeners exclusively when the actor reaches its done status #4609

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/famous-dingos-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'xstate': minor
---

An actor being stopped can now be observed:

```ts
const actor = createActor(machine);

actor.subscribe({
next: (snapshot) => {
if (snapshot.status === 'stopped') {
console.log('Actor stopped');
}
}
});

actor.start();

// ...

actor.stop();
```
49 changes: 28 additions & 21 deletions packages/core/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,19 @@ export class Actor<TLogic extends AnyActorLogic>
}
}

const notifyObservers = () => {
for (const observer of this.observers) {
try {
observer.next?.(snapshot);
} catch (err) {
reportUnhandledError(err);
}
}
};

switch ((this._snapshot as any).status) {
case 'active':
for (const observer of this.observers) {
try {
observer.next?.(snapshot);
} catch (err) {
reportUnhandledError(err);
}
}
notifyObservers();
break;
case 'done':
// next observers are meant to be notified about done snapshots
Expand All @@ -264,13 +268,7 @@ export class Actor<TLogic extends AnyActorLogic>
// it's more ergonomic for XState to treat a done snapshot as a "next" value
// and the completion event as something that is separate,
// something that merely follows emitting that done snapshot
for (const observer of this.observers) {
try {
observer.next?.(snapshot);
} catch (err) {
reportUnhandledError(err);
}
}
notifyObservers();

this._stopProcedure();
this._complete();
Expand All @@ -286,6 +284,9 @@ export class Actor<TLogic extends AnyActorLogic>
case 'error':
this._error((this._snapshot as any).error);
break;
case 'stopped':
notifyObservers();
break;
}
this.system._sendInspectionEvent({
type: '@xstate.snapshot',
Expand Down Expand Up @@ -513,22 +514,27 @@ export class Actor<TLogic extends AnyActorLogic>
this.update(nextState, event);
if (event.type === XSTATE_STOP) {
this._stopProcedure();
this._complete();
}
}

private _stop(): this {
private _stop(): void {
if (this._processingStatus === ProcessingStatus.Stopped) {
return this;
return;
}
this.mailbox.clear();
if (this._processingStatus === ProcessingStatus.NotStarted) {
this._processingStatus = ProcessingStatus.Stopped;
return this;
return;
}
this.mailbox.enqueue({ type: XSTATE_STOP } as any);

return this;
this._processingStatus = ProcessingStatus.Stopped;
// this.update(
// {
// ...(this._snapshot as any),
// status: 'stopped'
// },
// { event: 'xstate.stop' } as any
// );
}

/**
Expand All @@ -538,7 +544,8 @@ export class Actor<TLogic extends AnyActorLogic>
if (this._parent) {
throw new Error('A non-root actor cannot be stopped directly.');
}
return this._stop();
this._stop();
return this;
}
private _complete(): void {
for (const observer of this.observers) {
Expand Down
33 changes: 25 additions & 8 deletions packages/core/test/interpreter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1414,20 +1414,37 @@ describe('interpreter', () => {
expect(completeCb).toHaveBeenCalledTimes(1);
});

it('should call complete() once the interpreter is stopped', () => {
const completeCb = jest.fn();
it('should not call complete() once the actor is stopped', (done) => {
const spy = jest.fn();

const service = createActor(createMachine({})).start();
const actorRef = createActor(createMachine({})).start();

service.subscribe({
complete: () => {
completeCb();
actorRef.subscribe({
complete: spy,
next: (s) => {
if (s.status === 'stopped') {
done();
}
}
});

service.stop();
actorRef.stop();

expect(completeCb).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(0);
});

it('stopping an actor can be observed via snapshot.status', (done) => {
const actorRef = createActor(createMachine({})).start();

actorRef.subscribe({
next: (s) => {
if (s.status === 'stopped') {
done();
}
}
});

actorRef.stop();
});
});

Expand Down