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

[core] Transition function #4954

Open
wants to merge 73 commits into
base: main
Choose a base branch
from
Open

[core] Transition function #4954

wants to merge 73 commits into from

Conversation

davidkpiano
Copy link
Member

This PR adds the transition(…) function for returning a [snapshot, actions] tuple given the machine, state, and event.

Its main use-case is for server-side workflows and a pure way of handling state transitions for actor logic.

Copy link

changeset-bot bot commented Jun 26, 2024

🦋 Changeset detected

Latest commit: 4516833

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
xstate Minor
@xstate/graph Major
@xstate/react Major
@xstate/solid Major
@xstate/svelte Major
@xstate/vue Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@with-heart
Copy link
Contributor

Just a reminder to remove commented lines before merging 💜

}
if (typeof action === 'function' && !('resolve' in action)) {
const type = `${this.id}|${kind}:${i}`;
this.machine.implementations.actions[type] = action as any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I don't particularly like that we are writing to this object because it might be shared among different machines in some way and we might potentially leak things between instances
  2. i'd be wary of the code here, I wonder if this gets "fixed" somehow but I think there might run into stale closure hazard by resolving this here and caching it

Comment on lines +314 to +316
if (typeof src !== 'string') {
this.machine.implementations.actors[sourceName] = src;
}
Copy link
Member

@Andarist Andarist Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaks in cases like this:

const sharedActors = {};

setup({ actors: sharedActors }).createMachine({
  invoke: {
    src: fromPromise(async () => ""),
  },
});

setup({ actors: sharedActors }).createMachine({
  invoke: {
    src: fromPromise(async () => 100),
  },
});

We should add a test case for this

actorScope.defer(() => {
actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId);
});
actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have some new test that shows this is a desired change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason to defer it - it's a clean-up

convertAction(
action as any,
snapshot.machine.root,
'enqueue' + Math.random(), // TODO: this should come from state node ID which isn't provided
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how hard would it be to fix it here?

Comment on lines 2653 to 2655
export type SpecialExecutableAction =
| ExecutableSpawnAction
| ExecutableRaiseAction;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt this contain sendTo action too? they can also be delayed

Comment on lines +2675 to +2676
| SpecialExecutableAction
| (string extends TAction['type'] ? never : ToExecutableAction<TAction>)
Copy link
Member

@Andarist Andarist Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| SpecialExecutableAction
| (string extends TAction['type'] ? never : ToExecutableAction<TAction>)
| SpecialExecutableAction
| (IsNever<TAction> extends true
? never
: ToExecutableAction<
string extends TAction['type']
? { type: string & {}; params: NonReducibleUnknown }
: TAction
>)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add type tests for both setup and bare createMachine

Comment on lines +1607 to 1617
actorScope.actionExecutor(
{
type: builtinAction.type,
info: actionArgs,
params,
exec: () => {} // noop
},
actorScope.self
);
if (actorScope.self._processingStatus === ProcessingStatus.Running) {
builtinAction.execute(actorScope, params);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens here is beyond convoluted:

  1. a noop exec gets passed into actorScope.actionExecutor
  2. then that gets mutated internally there for some actions (raise and sendTo)
  3. but then we can also learn that actorScope.actionExecutor isn't entirely responsible for the actions' execution because we continue to call builtinAction.execute here

It's very confusing and hard to reason about as the logic is spread across those 2 places weirdly

Comment on lines +134 to +143
const resolvedTarget =
typeof target === 'string'
? (source.getSnapshot() as AnyMachineSnapshot).children[target]
: target;

if (!resolvedTarget) {
throw new Error(
`Actor with id ${typeof target === 'string' ? target : target.sessionId} not found in the system.`
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this doesn't fit here. It's forcefully trying to make something pass without considering what area of the code was designed to deal with this kind of resolving. Scheduler works with actor refs so even assuming here that the source is a machine actor is already a big stretch. The target should always come pre-resolved to the Scheduler as it was before.

Comment on lines +3214 to +3222
expect(spy).toMatchMockCallsInlineSnapshot(`
[
[
{
"counter": 1,
},
],
]
`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed out this test case that shows, what I believe to be, a regression. Note that I pushed out 2 test cases here: ece423d

They both are identical, the only difference is that one uses raise(...) whereas the other one uses sendTo(({ self }) => self, ...). While the raise one doesn't make a lot of sense, it clearly shows that something is broken here because those 2 tests are exactly the same if we consider raise to be a sugar for the other one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants