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

[v5] System (receptionist) #3837

Merged
merged 33 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c947ecf
Introduce system
davidkpiano Feb 4, 2023
2717dd4
Add test file
davidkpiano Feb 6, 2023
a32f032
system poc
davidkpiano Feb 14, 2023
20fa153
Remvoe explicit createsystem in tests
davidkpiano Feb 15, 2023
4d48fa5
Remove comment
davidkpiano Feb 15, 2023
95b181b
What is this file doing here
davidkpiano Feb 15, 2023
8001447
Add changeset
davidkpiano Feb 15, 2023
2c5aa0e
Update .changeset/cool-grapes-suffer.md
davidkpiano Feb 20, 2023
bd766b7
Merge branch 'next' into v5/system-1
davidkpiano Feb 24, 2023
a416bbe
Update packages/core/src/types.ts
davidkpiano Mar 2, 2023
29649a6
Remove registry
davidkpiano Mar 4, 2023
1228142
Do not allow system as an interpret option (for now)
davidkpiano Mar 4, 2023
0491f35
Add test for accessing system externally
davidkpiano Mar 4, 2023
30664a4
Fix types
davidkpiano Mar 6, 2023
30df909
Change back to arrow
davidkpiano Mar 6, 2023
22dad30
Remove actor from receptionist when stopped
davidkpiano Mar 6, 2023
deb302a
Make methods, uh, "private"
davidkpiano Mar 6, 2023
dbab8f4
Merge branch 'next' into v5/system-1
davidkpiano Mar 14, 2023
1042f4d
Merge branch 'next' into v5/system-1
davidkpiano Mar 17, 2023
6bccd63
Merge branch 'next' into v5/system-1
davidkpiano Mar 17, 2023
5bd3054
Fix inspect test
davidkpiano Mar 17, 2023
623e4c6
Merge branch 'next' into v5/system-1
davidkpiano Mar 18, 2023
300659f
Merge branch 'next' into v5/system-1
davidkpiano Mar 24, 2023
d846ed1
Merge branch 'next' into v5/system-1
davidkpiano Mar 24, 2023
5e4c914
Fix id -> sessionId
davidkpiano Mar 28, 2023
a18fcdc
ActorSystem.set -> ._set
davidkpiano Mar 28, 2023
5feaa9e
key -> systemId
davidkpiano Mar 28, 2023
9cc70e4
More bikeshedding
davidkpiano Mar 28, 2023
ef1aa7b
systemId
davidkpiano Mar 28, 2023
4ce2ec8
Prevent overwriting actors
davidkpiano Mar 28, 2023
7fe343b
Fixed memory leak by separating sessionId from actor registration in …
Andarist Mar 28, 2023
b9efb29
add an extra test case
Andarist Mar 29, 2023
84af883
Re-register actors in the system when they start to account for resta…
Andarist Mar 29, 2023
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
49 changes: 49 additions & 0 deletions .changeset/cool-grapes-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
'xstate': major
---

Actors are now always part of a "system", which is a collection of actors that can communicate with each other. Systems are implicitly created, and can be used to get and set references to any actor in the system via the `key` prop:

```js
const machine = createMachine({
// ...
invoke: {
src: emailMachine,
// Registers `emailMachine` as `emailer` on the system
key: 'emailer'
}
});
```

```js
const machine = createMachine({
// ...
entry: assign({
emailer: (ctx, ev, { spawn }) => spawn(emailMachine, { key: 'emailer' })
})
});
```

Any invoked/spawned actor that is part of a system will be able to reference that actor:

```js
const anotherMachine = createMachine({
// ...
entry: sendTo(
(ctx, ev, { system }) => {
return system.get('emailer');
},
{ type: 'SEND_EMAIL', subject: 'Hello', body: 'World' }
)
});
```

Each top-level `interpret(...)` call creates a separate implicit system. In this example example, `actor1` and `actor2` are part of different systems and are unrelated:

```js
// Implicit system
const actor1 = interpret(machine).start();

// Another implicit system
const actor2 = interpret(machine).start();
```
5 changes: 3 additions & 2 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
StateMachineDefinition,
StateValue,
TransitionDefinition,
AnyActorContext,
PersistedMachineState
} from './types.js';
import {
Expand Down Expand Up @@ -266,7 +267,7 @@ export class StateMachine<
public microstep(
state: State<TContext, TEvent, TResolvedTypesMeta> = this.initialState,
event: TEvent | SCXML.Event<TEvent>,
actorCtx?: ActorContext<any, any> | undefined
actorCtx?: AnyActorContext | undefined
): Array<State<TContext, TEvent, TResolvedTypesMeta>> {
const scxmlEvent = toSCXMLEvent(event);

Expand All @@ -287,7 +288,7 @@ export class StateMachine<
* This "pre-initial" state is provided to initial actions executed in the initial state.
*/
private getPreInitialState(
actorCtx: ActorContext<any, any> | undefined,
actorCtx: AnyActorContext | undefined,
input: any
): State<TContext, TEvent, TResolvedTypesMeta> {
const [context, actions] = this.getContextAndActions(actorCtx, input);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export class StateNode<
const generatedId = createInvokeId(this.id, i);
const invokeConfig = toInvokeConfig(invocable, generatedId);
const resolvedId = invokeConfig.id || generatedId;
const { src } = invokeConfig;
const { src, key } = invokeConfig;

const resolvedSrc = isString(src)
? src
Expand All @@ -284,6 +284,7 @@ export class StateNode<
...invokeConfig,
src: resolvedSrc,
id: resolvedId,
key,
toJSON() {
const { onDone, onError, ...invokeDefValues } = invokeConfig;
return {
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,22 @@ export function toActionObject<
if (typeof action === 'function') {
const type = 'xstate.function';
return createDynamicAction({ type, params: {} }, (_event, { state }) => {
const a: BaseActionObject = {
const actionObject: BaseActionObject = {
type,
params: {
function: action
},
execute: (_actorCtx) => {
return action(state.context as TContext, _event.data as TEvent, {
action: a,
action: actionObject,
_event: _event as SCXML.Event<TEvent>,
state: state as AnyState
state: state as AnyState,
system: _actorCtx.system
});
}
};

return [state, a];
return [state, actionObject];
});
}

Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/actions/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function invoke<
id,
src,
parent: actorContext?.self,
key: invokeDef.key,
input:
typeof input === 'function'
? input(state.context, _event.data as any)
Expand All @@ -84,7 +85,7 @@ export function invoke<
});

resolvedInvokeAction.execute = (actorCtx) => {
const interpreter = actorCtx.self as AnyInterpreter;
const parent = actorCtx.self as AnyInterpreter;
const { id, ref } = resolvedInvokeAction.params;
if (!ref) {
if (!IS_PRODUCTION) {
Expand All @@ -102,7 +103,7 @@ export function invoke<
try {
actorRef.start?.();
} catch (err) {
interpreter.send(error(id, err));
parent.send(error(id, err));
return;
}
});
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { createDynamicAction } from '../../actions/dynamicAction.js';
import { stop as stopActionType } from '../actionTypes.js';
import { ActorStatus } from '../interpreter.js';
import {
ActorContext,
ActorRef,
AnyActorContext,
BaseDynamicActionObject,
DynamicStopActionObject,
EventObject,
Expand Down Expand Up @@ -58,7 +58,7 @@ export function stop<
{
type: 'xstate.stop',
params: { actor: actorRef },
execute: (actorCtx: ActorContext<any, any>) => {
execute: (actorCtx: AnyActorContext) => {
if (!actorRef) {
return;
}
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/actors/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ActorBehavior, ActorContext, EventObject } from '../types';
import {
ActorBehavior,
ActorContext,
ActorSystem,
EventObject
} from '../types';
import { isSCXMLEvent } from '../utils';

/**
Expand All @@ -9,11 +14,15 @@ import { isSCXMLEvent } from '../utils';
* @returns An actor behavior
*/

export function fromReducer<TState, TEvent extends EventObject>(
export function fromReducer<
TState,
TEvent extends EventObject,
TSystem extends ActorSystem<any>
>(
transition: (
state: TState,
event: TEvent,
actorContext: ActorContext<TEvent, TState>
actorContext: ActorContext<TEvent, TState, TSystem>
) => TState,
initialState: TState | (({ input }: { input: any }) => TState) // TODO: type
): ActorBehavior<TEvent, TState, TState> {
Expand Down
29 changes: 0 additions & 29 deletions packages/core/src/capturedState.ts

This file was deleted.

24 changes: 16 additions & 8 deletions packages/core/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type {
InterpreterFrom,
PersistedStateFrom,
SnapshotFrom,
ActorSystem,
AnyActorBehavior,
RaiseActionObject
} from './types.js';
import { stopSignalType } from './actors/index.js';
import { devToolsAdapter } from './dev/index.js';
import { IS_PRODUCTION } from './environment.js';
import { Mailbox } from './Mailbox.js';
import { registry } from './registry.js';
import { createSystem } from './system.js';
import { AreAllImplementationsAssumedToBeProvided } from './typegenTypes.js';
import {
ActorRef,
Expand Down Expand Up @@ -107,13 +108,15 @@ export class Interpreter<
// Actor Ref
public _parent?: ActorRef<any>;
public ref: ActorRef<TEvent>;
private _actorContext: ActorContext<TEvent, SnapshotFrom<TBehavior>>;
// TODO: add typings for system
private _actorContext: ActorContext<TEvent, SnapshotFrom<TBehavior>, any>;

/**
* The globally unique process ID for this invocation.
*/
public sessionId: string;

public system: ActorSystem<any>;
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved
private _doneEvent?: DoneEvent;

public src?: string;
Expand All @@ -133,11 +136,16 @@ export class Interpreter<
...options
};

const { clock, logger, parent, id } = resolvedOptions;
const { clock, logger, parent, id, key } = resolvedOptions;
const self = this;

// TODO: this should come from a "system"
this.sessionId = registry.bookId();
this.system = parent?.system ?? createSystem();

if (key) {
this.system.set(key, this);
Andarist marked this conversation as resolved.
Show resolved Hide resolved
}

this.sessionId = this.system._register(this);
this.id = id ?? this.sessionId;
this.logger = logger;
this.clock = clock;
Expand All @@ -152,7 +160,8 @@ export class Interpreter<
logger: this.logger,
defer: (fn) => {
this._deferred.push(fn);
}
},
system: this.system
};

// Ensure that the send method is bound to this interpreter instance
Expand Down Expand Up @@ -277,7 +286,6 @@ export class Interpreter<
return this;
}

registry.register(this.sessionId, this.ref);
this.status = ActorStatus.Running;

if (this.behavior.start) {
Expand Down Expand Up @@ -368,7 +376,7 @@ export class Interpreter<
this.mailbox = new Mailbox(this._process.bind(this));

this.status = ActorStatus.Stopped;
registry.free(this.sessionId);
this.system._unregister(this);

return this;
}
Expand Down
28 changes: 0 additions & 28 deletions packages/core/src/registry.ts

This file was deleted.

12 changes: 6 additions & 6 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { evaluateGuard, toGuardDefinition } from './guards.js';
import type { StateNode } from './StateNode.js';
import { isDynamicAction } from '../actions/dynamicAction.js';
import {
ActorContext,
AnyActorContext,
AnyEventObject,
AnyHistoryValue,
AnyState,
Expand Down Expand Up @@ -1038,7 +1038,7 @@ export function microstep<
>(
transitions: Array<TransitionDefinition<TContext, TEvent>>,
currentState: State<TContext, TEvent, any>,
actorCtx: ActorContext<any, any> | undefined,
actorCtx: AnyActorContext | undefined,
scxmlEvent: SCXML.Event<TEvent>
): State<TContext, TEvent, any> {
const { machine } = currentState;
Expand Down Expand Up @@ -1127,7 +1127,7 @@ function microstepProcedure(
currentState: AnyState,
mutConfiguration: Set<AnyStateNode>,
scxmlEvent: SCXML.Event<AnyEventObject>,
actorCtx: ActorContext<any, any> | undefined
actorCtx: AnyActorContext | undefined
): typeof currentState {
const { machine } = currentState;
const actions: BaseActionObject[] = [];
Expand Down Expand Up @@ -1490,7 +1490,7 @@ export function resolveActionsAndContext<
actions: BaseActionObject[],
scxmlEvent: SCXML.Event<TEvent>,
currentState: State<TContext, TEvent, any>,
actorCtx: ActorContext<any, any> | undefined
actorCtx: AnyActorContext | undefined
): {
nextState: AnyState;
} {
Expand Down Expand Up @@ -1564,7 +1564,7 @@ export function resolveActionsAndContext<
export function macrostep<TMachine extends AnyStateMachine>(
state: StateFromMachine<TMachine>,
scxmlEvent: SCXML.Event<TMachine['__TEvent']>,
actorCtx: ActorContext<any, any> | undefined
actorCtx: AnyActorContext | undefined
): {
state: typeof state;
microstates: Array<typeof state>;
Expand Down Expand Up @@ -1650,7 +1650,7 @@ export function macrostep<TMachine extends AnyStateMachine>(
function stopStep(
scxmlEvent: SCXML.Event<any>,
nextState: AnyState,
actorCtx: ActorContext<any, any> | undefined
actorCtx: AnyActorContext | undefined
): AnyState {
const actions: BaseActionObject[] = [];

Expand Down
Loading