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] Add children to Snapshot interface for all logic types #4792

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/chilly-carpets-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': patch
---

Add `children` to `Snapshot` interface
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
2 changes: 1 addition & 1 deletion packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ export function getPersistedSnapshot<
const persisted = {
...jsonValues,
context: persistContext(context) as any,
children: childrenJson
children: childrenJson as Record<string, AnyActorRef | undefined>
};

return persisted;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/stopChild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ function executeStop(
// this allows us to prevent an actor from being started if it gets stopped within the same macrostep
// this can happen, for example, when the invoking state is being exited immediately by an always transition
if (actorRef._processingStatus !== ProcessingStatus.Running) {
actorScope.stopChild(actorRef);
actorScope.stopChild?.(actorRef);
return;
}
// stopping a child enqueues a stop event in the child actor's mailbox
// we need for all of the already enqueued events to be processed before we stop the child
// the parent itself might want to send some events to a child (for example from exit actions on the invoking state)
// and we don't want to ignore those events
actorScope.defer(() => {
actorScope.stopChild(actorRef);
actorScope.stopChild?.(actorRef);
});
}

Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/actors/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ActorLogic,
ActorRefFromLogic,
AnyActorRef,
AnyActorScope,
AnyEventObject,
EventObject,
NonReducibleUnknown,
Expand Down Expand Up @@ -92,6 +93,7 @@ type InvokeCallback<
self,
sendBack,
receive,
spawnChild,
emit
}: {
/**
Expand All @@ -111,6 +113,7 @@ type InvokeCallback<
* listener is then called whenever events are received by the callback actor
*/
receive: Receiver<TEvent>;
spawnChild: AnyActorScope['spawnChild'];
emit: (emitted: TEmitted) => void;
}) => (() => void) | void;

Expand Down Expand Up @@ -191,7 +194,7 @@ export function fromCallback<
const logic: CallbackActorLogic<TEvent, TInput, TEmitted> = {
config: invokeCallback,
start: (state, actorScope) => {
const { self, system, emit } = actorScope;
const { self, system, emit, spawnChild } = actorScope;

const callbackState: CallbackInstanceState<TEvent> = {
receivers: undefined,
Expand All @@ -216,6 +219,7 @@ export function fromCallback<
callbackState.receivers ??= new Set();
callbackState.receivers.add(listener);
},
spawnChild,
emit
});
},
Expand Down Expand Up @@ -244,7 +248,9 @@ export function fromCallback<
status: 'active',
output: undefined,
error: undefined,
input
input,
context: undefined,
children: {}
};
},
getPersistedSnapshot: (snapshot) => snapshot,
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/actors/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { XSTATE_STOP } from '../constants';
import { AnyActorSystem } from '../system.ts';
import {
ActorLogic,
ActorRefFrom,
AnyActorScope,
ActorRefFromLogic,
EventObject,
NonReducibleUnknown,
Expand Down Expand Up @@ -128,6 +130,7 @@ export function fromObservable<
input: TInput;
system: AnyActorSystem;
self: ObservableActorRef<TContext>;
spawnChild: AnyActorScope['spawnChild'];
emit: (emitted: TEmitted) => void;
}) => Subscribable<TContext>
): ObservableActorLogic<TContext, TInput, TEmitted> {
Expand Down Expand Up @@ -181,10 +184,11 @@ export function fromObservable<
error: undefined,
context: undefined,
input,
_subscription: undefined
_subscription: undefined,
children: {}
};
},
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 +197,7 @@ export function fromObservable<
input: state.input!,
system,
self,
spawnChild,
emit
}).subscribe({
next: (value) => {
Expand Down Expand Up @@ -333,7 +338,8 @@ export function fromEventObservable<
error: undefined,
context: undefined,
input,
_subscription: undefined
_subscription: undefined,
children: {}
};
},
start: (state, { self, system, emit }) => {
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/actors/promise.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,
AnyActorScope,
ActorRefFromLogic,
AnyActorRef,
EventObject,
Expand Down Expand Up @@ -135,6 +137,7 @@ export function fromPromise<
system: AnyActorSystem;
/** The parent actor of the promise actor */
self: PromiseActorRef<TOutput>;
spawnChild: AnyActorScope['spawnChild'];
signal: AbortSignal;
emit: (emitted: TEmitted) => void;
}) => PromiseLike<TOutput>
Expand Down Expand Up @@ -175,7 +178,7 @@ export function fromPromise<
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') {
Expand All @@ -189,6 +192,7 @@ export function fromPromise<
system,
self,
signal: controller.signal,
spawnChild,
emit
})
);
Expand Down Expand Up @@ -221,7 +225,9 @@ export function fromPromise<
status: 'active',
output: undefined,
error: undefined,
input
input,
context: undefined,
children: {}
};
},
getPersistedSnapshot: (snapshot) => snapshot,
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/actors/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import {
Snapshot
} from '../types.ts';

export type TransitionSnapshot<TContext> = Snapshot<undefined> & {
context: TContext;
};
export type TransitionSnapshot<TContext> = Snapshot<undefined, TContext>;

export type TransitionActorLogic<
TContext,
Expand Down Expand Up @@ -211,7 +209,8 @@ export function fromTransition<
context:
typeof initialContext === 'function'
? (initialContext as any)({ input })
: initialContext
: initialContext,
children: {}
};
},
getPersistedSnapshot: (snapshot) => snapshot,
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 @@ -169,6 +170,21 @@ export class Actor<TLogic extends AnyActorLogic>
this._deferred.push(fn);
},
system: this.system,
spawnChild: <T extends AnyActorLogic>(
logic: T,
actorOptions?: ActorOptions<T>
) => {
const actor = createActor(logic, {
parent: this,
...actorOptions
});

if (this._processingStatus === ProcessingStatus.Running) {
actor.start();
}

return actor as ActorRefFrom<T>;
},
stopChild: (child) => {
if (child._parent !== this) {
throw new Error(
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/getNextSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export function createInertActorScope<T extends AnyActorLogic>(
sessionId: '',
stopChild: () => {},
system: self.system,
emit: () => {}
emit: () => {},
spawnChild(logic) {
const child = createActor(logic) as any;

return child;
}
};

return inertActorScope;
Expand Down
16 changes: 14 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2148,7 +2148,11 @@ export interface ActorScope<
defer: (fn: () => void) => void;
emit: (event: TEmitted) => void;
system: TSystem;
stopChild: (child: AnyActorRef) => void;
stopChild?: (child: AnyActorRef) => void;
spawnChild?: <T extends AnyActorLogic>(
logic: T,
actorOptions?: ActorOptions<T>
) => ActorRefFrom<T>;
}

export type AnyActorScope = ActorScope<
Expand All @@ -2160,26 +2164,34 @@ export type AnyActorScope = ActorScope<

export type SnapshotStatus = 'active' | 'done' | 'error' | 'stopped';

export type Snapshot<TOutput> =
export type Snapshot<TOutput, TContext = unknown> =
| {
status: 'active';
output: undefined;
error: undefined;
context: TContext;
children?: Record<string, AnyActorRef | undefined>;
}
| {
status: 'done';
output: TOutput;
error: undefined;
context: TContext;
children?: Record<string, AnyActorRef | undefined>;
}
| {
status: 'error';
output: undefined;
error: unknown;
context: TContext;
children?: Record<string, AnyActorRef | undefined>;
}
| {
status: 'stopped';
output: undefined;
error: undefined;
context: TContext;
children?: Record<string, AnyActorRef | undefined>;
};

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/core/test/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1158,7 +1158,9 @@ describe('actors', () => {
getInitialSnapshot: () => ({
status: 'active',
output: undefined,
error: undefined
error: undefined,
context: undefined,
children: {}
}),
getPersistedSnapshot: (s) => s
};
Expand Down
Loading