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

Add ability to spawn actor from any logic #4724

Open
wants to merge 18 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,15 @@ Read [📽 the slides](http://slides.com/davidkhourshid/finite-state-machines) (

## Packages

| Package | Description |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter |
| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState |
| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications |
| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications |
| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications |
| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications |
| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState |
| Package | Description |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter |
| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState |
| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications |
| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications |
| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications |
| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications |
| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState |

## Finite State Machines

Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/actors/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { XSTATE_STOP } from '../constants.ts';
import { AnyActorSystem } from '../system.ts';
import {
ActorLogic,
ActorRefFrom,
ActorScope,
ActorRefFromLogic,
AnyActorRef,
AnyEventObject,
Expand Down Expand Up @@ -92,6 +94,7 @@ type InvokeCallback<
self,
sendBack,
receive,
spawn,
emit
}: {
/**
Expand All @@ -111,6 +114,7 @@ type InvokeCallback<
* listener is then called whenever events are received by the callback actor
*/
receive: Receiver<TEvent>;
spawn: ActorScope<any, any>['spawnChild'];
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't all of those actor logics receive stopChild as well?

emit: (emitted: TEmitted) => void;
}) => (() => void) | void;

Expand Down Expand Up @@ -216,6 +220,7 @@ export function fromCallback<
callbackState.receivers ??= new Set();
callbackState.receivers.add(listener);
},
spawn: actorScope.spawnChild,
emit
});
},
Expand All @@ -241,6 +246,7 @@ export function fromCallback<
},
getInitialSnapshot: (_, input) => {
return {
context: undefined,
status: 'active',
output: undefined,
error: undefined,
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/actors/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { XSTATE_STOP } from '../constants';
import { AnyActorSystem } from '../system.ts';
import {
ActorLogic,
ActorScope,
ActorRefFromLogic,
EventObject,
NonReducibleUnknown,
Expand Down Expand Up @@ -128,6 +129,7 @@ export function fromObservable<
input: TInput;
system: AnyActorSystem;
self: ObservableActorRef<TContext>;
spawnChild: ActorScope<any, any>['spawnChild'];
emit: (emitted: TEmitted) => void;
}) => Subscribable<TContext>
): ObservableActorLogic<TContext, TInput, TEmitted> {
Expand Down Expand Up @@ -184,7 +186,7 @@ export function fromObservable<
_subscription: undefined
};
},
start: (state, { self, system, emit }) => {
start: (state, { self, system, spawnChild, emit }) => {
if (state.status === 'done') {
// Do not restart a completed observable
return;
Expand All @@ -193,6 +195,7 @@ export function fromObservable<
input: state.input!,
system,
self,
spawnChild,
emit
}).subscribe({
next: (value) => {
Expand Down
47 changes: 42 additions & 5 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { XSTATE_STOP } from '../constants.ts';
import { AnyActorSystem } from '../system.ts';
import {
ActorLogic,
ActorScope,
ActorRefFromLogic,
AnyActorRef,
EventObject,
Expand All @@ -11,10 +12,12 @@ import {

export type PromiseSnapshot<TOutput, TInput> = Snapshot<TOutput> & {
input: TInput | undefined;
children: Record<string, any>;
};

const XSTATE_PROMISE_RESOLVE = 'xstate.promise.resolve';
const XSTATE_PROMISE_REJECT = 'xstate.promise.reject';
const XSTATE_SPAWN_CHILD = 'xstate.spawn.child';

export type PromiseActorLogic<
TOutput,
Expand Down Expand Up @@ -135,19 +138,27 @@ export function fromPromise<
system: AnyActorSystem;
/** The parent actor of the promise actor */
self: PromiseActorRef<TOutput>;
spawnChild: ActorScope<any, any, any>['spawnChild'];
signal: AbortSignal;
emit: (emitted: TEmitted) => void;
}) => PromiseLike<TOutput>
): PromiseActorLogic<TOutput, TInput, TEmitted> {
const logic: PromiseActorLogic<TOutput, TInput, TEmitted> = {
config: promiseCreator,
transition: (state, event, scope) => {
transition: (state, event, actorScope) => {
if (state.status !== 'active') {
return state;
}

const stopChildren = () => {
for (const child of Object.values(state.children)) {
actorScope.stopChild?.(child);
}
};

switch (event.type) {
case XSTATE_PROMISE_RESOLVE: {
stopChildren();
const resolvedValue = (event as any).data;
return {
...state,
Expand All @@ -157,37 +168,61 @@ export function fromPromise<
};
}
case XSTATE_PROMISE_REJECT:
stopChildren();
return {
...state,
status: 'error',
error: (event as any).data,
input: undefined
};
case XSTATE_STOP: {
controllerMap.get(scope.self)?.abort();
case XSTATE_STOP:
stopChildren();
controllerMap.get(actorScope.self)?.abort();

return {
...state,
status: 'stopped',
input: undefined
};
case XSTATE_SPAWN_CHILD: {
return {
...state,
children: {
...state.children,
[(event as any).child.id]: (event as any).child
}
};
}
default:
return state;
}
},
start: (state, { self, system, emit }) => {
start: (state, { self, system, spawnChild, emit }) => {
// TODO: determine how to allow customizing this so that promises
// can be restarted if necessary
if (state.status !== 'active') {
return;
}

const innerSpawnChild: typeof spawnChild<any> = (logic, actorOptions) => {
const child = spawnChild?.(logic, actorOptions) as AnyActorRef;

self.send({
type: XSTATE_SPAWN_CHILD,
child
});

return child;
};

const controller = new AbortController();
controllerMap.set(self, controller);
const resolvedPromise = Promise.resolve(
promiseCreator({
input: state.input!,
system,
self,
spawnChild: innerSpawnChild as any,
signal: controller.signal,
emit
})
Expand Down Expand Up @@ -218,10 +253,12 @@ export function fromPromise<
},
getInitialSnapshot: (_, input) => {
return {
context: undefined,
status: 'active',
output: undefined,
error: undefined,
input
input,
children: {}
};
},
getPersistedSnapshot: (snapshot) => snapshot,
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/actors/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,12 @@ export function fromTransition<
| TContext
| (({
input,
self
self,
spawnChild
}: {
input: TInput;
self: TransitionActorRef<TContext, TEvent>;
spawnChild: ActorScope<any, any, any>['spawnChild'];
}) => TContext) // TODO: type
): TransitionActorLogic<TContext, TEvent, TInput, TEmitted> {
return {
Expand All @@ -203,14 +205,14 @@ export function fromTransition<
)
};
},
getInitialSnapshot: (_, input) => {
getInitialSnapshot: ({ self, spawnChild }, input) => {
return {
status: 'active',
output: undefined,
error: undefined,
context:
typeof initialContext === 'function'
? (initialContext as any)({ input })
? (initialContext as any)({ input, self, spawnChild })
: initialContext
};
},
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/createActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { symbolObservable } from './symbolObservable.ts';
import { AnyActorSystem, Clock, createSystem } from './system.ts';

import type {
ActorRefFrom,
ActorScope,
AnyActorLogic,
AnyActorRef,
Expand Down Expand Up @@ -177,6 +178,21 @@ export class Actor<TLogic extends AnyActorLogic>
}
(child as any)._stop();
},
spawnChild: <T extends AnyActorLogic>(
logic: T,
actorOptions?: ActorOptions<T>
Copy link
Member

Choose a reason for hiding this comment

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

this should reuse the type tricks from here:

...[options]: ConditionalRequired<
[
options?: ActorOptions<TLogic> & {
[K in RequiredOptions<TLogic>]: unknown;
}
],
IsNotNever<RequiredOptions<TLogic>>
>

) => {
const actor = createActor(logic, {
parent: this,
...actorOptions
});

if (this._processingStatus === ProcessingStatus.Running) {
actor.start();
}
Comment on lines +190 to +192
Copy link
Member

Choose a reason for hiding this comment

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

what about the actors spawned when this._processingStatus === ProcessingStatus.NotStarted?


return actor as ActorRefFrom<T>;
},
emit: (emittedEvent) => {
const listeners = this.eventListeners.get(emittedEvent.type);
const wildcardListener = this.eventListeners.get('*');
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/getNextSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export function createInertActorScope<T extends AnyActorLogic>(
sessionId: '',
stopChild: () => {},
system: self.system,
spawnChild: (logic) => {
const child = createActor(logic) as any;

return child;
},
emit: () => {}
};

Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2149,6 +2149,10 @@ export interface ActorScope<
emit: (event: TEmitted) => void;
system: TSystem;
stopChild: (child: AnyActorRef) => void;
spawnChild: <T extends AnyActorLogic>(
Copy link
Member

Choose a reason for hiding this comment

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

logic: T,
actorOptions?: ActorOptions<T>
) => ActorRefFrom<T>;
}

export type AnyActorScope = ActorScope<
Expand Down Expand Up @@ -2200,17 +2204,17 @@ export interface ActorLogic<
/** The initial setup/configuration used to create the actor logic. */
config?: unknown;
/**
* Transition function that processes the current state and an incoming
* message to produce a new state.
* Transition function that processes the current state and an incoming event
* to produce a new state.
*
* @param snapshot - The current state.
* @param message - The incoming message.
* @param event - The incoming event.
* @param actorScope - The actor scope.
* @returns The new state.
*/
transition: (
snapshot: TSnapshot,
message: TEvent,
event: TEvent,
actorScope: ActorScope<TSnapshot, TEvent, TSystem, TEmitted>
) => TSnapshot;
/**
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { isMachineSnapshot } from './State.ts';
import type { StateNode } from './StateNode.ts';
import { TARGETLESS_KEY } from './constants.ts';
import type {
AnyActorLogic,
AnyActorRef,
AnyEventObject,
AnyMachineSnapshot,
Expand Down Expand Up @@ -283,3 +282,7 @@ export function resolveReferencedActor(machine: AnyStateMachine, src: string) {
export function getAllOwnEventDescriptors(snapshot: AnyMachineSnapshot) {
return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])];
}

export function isActorRef(actorRef: unknown): actorRef is AnyActorRef {
return !!actorRef && typeof actorRef === 'object' && 'send' in actorRef;
}
Loading