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] Actor status object #4199

Closed
wants to merge 9 commits into from
Closed
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
6 changes: 3 additions & 3 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,14 +406,14 @@
: new State(stateConfig, this);
}

public getStatus(

Check failure on line 409 in packages/core/src/StateMachine.ts

View workflow job for this annotation

GitHub Actions / build

Property 'getStatus' in type 'StateMachine<TContext, TEvent, TActor, TAction, TGuard, TDelay, TTag, TInput, TOutput, TResolvedTypesMeta>' is not assignable to the same property in base type 'ActorLogic<TEvent, State<TContext, TEvent, TActor, TTag, TOutput, TResolvedTypesMeta>, State<TContext, ... 4 more ..., TResolvedTypesMeta>, PersistedMachineState<...>, any, TInput, TOutput>'.
state: State<TContext, TEvent, TActor, TTag, TOutput, TResolvedTypesMeta>
) {
return state.error
? { status: 'error', data: state.error }
? { status: 'error' as const, error: state.error }
: state.done
? { status: 'done', data: state.output }
: { status: 'active' };
? { status: 'done' as const, output: state.output! }
: { status: 'active' as const };
}

public restoreState(
Expand Down
50 changes: 24 additions & 26 deletions packages/core/src/actors/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
EventObject,
Subscription,
AnyActorSystem,
ActorRefFrom
ActorRefFrom,
ActorStatusObject,
TODO
} from '../types';

export interface ObservableInternalState<T, TInput = unknown> {
export type ObservableInternalState<
T,
TInput = unknown
> = ActorStatusObject<T> & {
subscription: Subscription | undefined;
status: 'active' | 'done' | 'error' | 'canceled';
data: T | undefined;
input: TInput | undefined;
}
};

export type ObservablePersistedState<T, TInput = unknown> = Omit<
ObservableInternalState<T, TInput>,
Expand Down Expand Up @@ -47,7 +50,7 @@

return {
config: observableCreator,
transition: (state, event, { self, id, defer }) => {

Check failure on line 53 in packages/core/src/actors/observable.ts

View workflow job for this annotation

GitHub Actions / build

Type '(state: ObservableInternalState<T, TInput>, event: { [k: string]: unknown; type: string; }, { self, id, defer }: ActorContext<{ [k: string]: unknown; type: string; }, T | undefined, AnyActorSystem>) => ({ ...; } & { ...; }) | ... 5 more ... | { ...; }' is not assignable to type '(state: ObservableInternalState<T, TInput>, message: { [k: string]: unknown; type: string; }, ctx: ActorContext<{ [k: string]: unknown; type: string; }, T | undefined, AnyActorSystem>) => ObservableInternalState<...>'.
if (state.status !== 'active') {
return state;
}
Expand All @@ -64,14 +67,15 @@
});
return {
...state,
data: (event as any).data
output: (event as any).data
Copy link
Member

Choose a reason for hiding this comment

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

note: this is a mistake - nextEventType shouldn't assign to output. I'm working on this branch locally so I'll fix this.

};
case errorEventType:
return {
...state,
status: 'error',
error: (event as any).data,
input: undefined,
data: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
output: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
Copy link
Member

Choose a reason for hiding this comment

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

after the change we'll be able to drop this comment

subscription: undefined
};
case completeEventType:
Expand All @@ -97,8 +101,9 @@
return {
subscription: undefined,
status: 'active',
data: undefined,
input
output: undefined,
input,
error: undefined
};
},
start: (state, { self, system }) => {
Expand All @@ -122,14 +127,10 @@
}
});
},
getSnapshot: (state) => state.data,
getPersistedState: ({ status, data, input }) => ({
status,
data,
input
}),
getSnapshot: (state) => state.output,
getPersistedState: (state) => state,
getStatus: (state) => state,
restoreState: (state) => ({

Check failure on line 133 in packages/core/src/actors/observable.ts

View workflow job for this annotation

GitHub Actions / build

Type '(state: ObservablePersistedState<T, TInput>) => { subscription: undefined; output: T | undefined; error: unknown; input: TInput | undefined; status: "done" | ... 2 more ... | "active"; }' is not assignable to type '(persistedState: ObservablePersistedState<T, TInput>, actorCtx: ActorContext<{ [k: string]: unknown; type: string; }, T | undefined, ActorSystem<any>>) => ObservableInternalState<...>'.
...state,
subscription: undefined
})
Expand Down Expand Up @@ -161,7 +162,7 @@
// TODO: event types
return {
config: lazyObservable,
transition: (state, event) => {

Check failure on line 165 in packages/core/src/actors/observable.ts

View workflow job for this annotation

GitHub Actions / build

Type '(state: ObservableInternalState<T, TInput>, event: { [k: string]: unknown; type: string; }) => ({ status: "error"; output: undefined; error: unknown; } & { subscription: Subscription | undefined; input: TInput | undefined; }) | ... 5 more ... | { ...; }' is not assignable to type '(state: ObservableInternalState<T, TInput>, message: { [k: string]: unknown; type: string; }, ctx: ActorContext<{ [k: string]: unknown; type: string; }, T | undefined, AnyActorSystem>) => ObservableInternalState<...>'.
if (state.status !== 'active') {
return state;
}
Expand All @@ -172,7 +173,7 @@
...state,
status: 'error',
input: undefined,
data: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
error: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
subscription: undefined
};
case completeEventType:
Expand All @@ -194,11 +195,11 @@
return state;
}
},
getInitialState: (_, input) => {

Check failure on line 198 in packages/core/src/actors/observable.ts

View workflow job for this annotation

GitHub Actions / build

Type '(_: ActorContext<{ [k: string]: unknown; type: string; }, T | undefined, AnyActorSystem>, input: TInput) => { subscription: undefined; status: "active"; output: undefined; input: TInput; }' is not assignable to type '(actorCtx: ActorContext<{ [k: string]: unknown; type: string; }, T | undefined, AnyActorSystem>, input: TInput) => ObservableInternalState<T, TInput>'.
return {
subscription: undefined,
status: 'active',
data: undefined,
output: undefined,
input
};
},
Expand All @@ -225,15 +226,12 @@
});
},
getSnapshot: (_) => undefined,
getPersistedState: ({ status, data, input }) => ({
status,
data,
input
}),
getPersistedState: (state) => state,
getStatus: (state) => state,
restoreState: (state) => ({
...state,
subscription: undefined
})
restoreState: (state) =>
({
...state,
subscription: undefined
} as TODO)
};
}
19 changes: 10 additions & 9 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import {
ActorLogic,
ActorRefFrom,
ActorSystem,
AnyActorSystem
AnyActorSystem,
TODO
} from '../types';
import { XSTATE_STOP } from '../constants';

export interface PromiseInternalState<T, TInput = unknown> {
status: 'active' | 'error' | 'done' | 'canceled';
data: T | undefined;
status: 'active' | 'error' | 'done' | 'stopped';
output: T | undefined;
input: TInput | undefined;
}

Expand Down Expand Up @@ -64,20 +65,20 @@ export function fromPromise<T, TInput>(
return {
...state,
status: 'done',
data: (event as any).data,
output: (event as any).data,
input: undefined
};
case rejectEventType:
return {
...state,
status: 'error',
data: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
error: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
input: undefined
};
case XSTATE_STOP:
return {
...state,
status: 'canceled',
status: 'stopped',
input: undefined
};
default:
Expand Down Expand Up @@ -115,12 +116,12 @@ export function fromPromise<T, TInput>(
getInitialState: (_, input) => {
return {
status: 'active',
data: undefined,
output: undefined,
input
};
},
getSnapshot: (state) => state.data,
getStatus: (state) => state,
getSnapshot: (state) => state.output,
getStatus: (state) => state as TODO,
getPersistedState: (state) => state,
restoreState: (state) => state
};
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
PersistedStateFrom,
SnapshotFrom,
AnyActorRef,
OutputFrom,
DoneActorEvent
} from './types.ts';
import {
Expand Down Expand Up @@ -80,7 +81,7 @@ type InternalStateFrom<TLogic extends ActorLogic<any, any, any>> =
export class Actor<
TLogic extends AnyActorLogic,
TEvent extends EventObject = EventFromLogic<TLogic>
> implements ActorRef<TEvent, SnapshotFrom<TLogic>>
> implements ActorRef<TEvent, SnapshotFrom<TLogic>, OutputFrom<TLogic>>
{
/**
* The current internal state of the actor.
Expand Down Expand Up @@ -219,13 +220,13 @@ export class Actor<
case 'done':
this._stopProcedure();
this._complete();
this._doneEvent = createDoneActorEvent(this.id, status.data);
this._doneEvent = createDoneActorEvent(this.id, status.output);
this._parent?.send(this._doneEvent as any);
break;
case 'error':
this._stopProcedure();
this._error(status.data);
this._parent?.send(createErrorActorEvent(this.id, status.data));
this._error(status.error);
this._parent?.send(createErrorActorEvent(this.id, status.error));
break;
}
}
Expand Down Expand Up @@ -319,6 +320,10 @@ export class Actor<
return this;
}

public getStatus() {
return this.logic.getStatus!(this._state);
}

private _process(event: TEvent) {
// TODO: reexamine what happens when an action (or a guard or smth) throws
let nextState;
Expand Down
32 changes: 29 additions & 3 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1823,8 +1823,11 @@ export interface ActorLike<TCurrent, TEvent extends EventObject>
send: (event: TEvent) => void;
}

export interface ActorRef<TEvent extends EventObject, TSnapshot = any>
extends Subscribable<TSnapshot>,
export interface ActorRef<
TEvent extends EventObject,
TSnapshot = any,
TOutput = unknown
> extends Subscribable<TSnapshot>,
InteropObservable<TSnapshot> {
/**
* The unique identifier for this actor relative to its parent.
Expand All @@ -1837,6 +1840,7 @@ export interface ActorRef<TEvent extends EventObject, TSnapshot = any>
getSnapshot: () => TSnapshot;
// TODO: this should return some sort of TPersistedState, not any
getPersistedState?: () => any;
getStatus: () => ActorStatusObject<TOutput>;
stop: () => void;
toJSON?: () => any;
// TODO: figure out how to hide this externally as `sendTo(ctx => ctx.actorRef._parent._parent._parent._parent)` shouldn't be allowed
Expand Down Expand Up @@ -1988,6 +1992,28 @@ export interface ActorContext<

export type AnyActorContext = ActorContext<any, any, any>;

export type ActorStatusObject<TOutput> =
| {
status: 'done';
output: TOutput;
error: undefined;
}
| {
status: 'error';
output: undefined;
error: unknown;
}
| {
status: 'stopped';
output: TOutput | undefined;
error: unknown | undefined;
}
| {
status: 'active';
output: undefined;
error: undefined;
};

export interface ActorLogic<
TEvent extends EventObject,
TSnapshot = any,
Expand Down Expand Up @@ -2015,7 +2041,7 @@ export interface ActorLogic<
actorCtx: ActorContext<TEvent, TSnapshot>
) => TInternalState;
getSnapshot?: (state: TInternalState) => TSnapshot;
getStatus?: (state: TInternalState) => { status: string; data?: any };
getStatus?: (state: TInternalState) => ActorStatusObject<TOutput>;
start?: (
state: TInternalState,
actorCtx: ActorContext<TEvent, TSnapshot>
Expand Down
11 changes: 5 additions & 6 deletions packages/core/test/actorLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('promise logic (fromPromise)', () => {

expect(resolvedPersistedState).toEqual(
expect.objectContaining({
data: 42
output: 42
})
);

Expand All @@ -154,7 +154,7 @@ describe('promise logic (fromPromise)', () => {
const resolvedPersistedState = actor.getPersistedState();
expect(resolvedPersistedState).toEqual(
expect.objectContaining({
data: 1
output: 1
})
);
expect(createdPromises).toBe(1);
Expand Down Expand Up @@ -182,16 +182,15 @@ describe('promise logic (fromPromise)', () => {
const rejectedPersistedState = actor.getPersistedState();
expect(rejectedPersistedState).toEqual(
expect.objectContaining({
data: 1
error: 1
})
);
expect(createdPromises).toBe(1);

const restoredActor = createActor(promiseLogic, {
createActor(promiseLogic, {
state: rejectedPersistedState
}).start();

expect(restoredActor.getSnapshot()).toBe(1);
expect(createdPromises).toBe(1);
});

Expand Down Expand Up @@ -532,7 +531,7 @@ describe('machine logic', () => {
expect(persistedState.children.a.state).toEqual(
expect.objectContaining({
status: 'done',
data: 42
output: 42
})
);

Expand Down
12 changes: 8 additions & 4 deletions packages/xstate-react/test/useSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ describeEachReactMode('useSelector (%s)', ({ suiteKey, render }) => {
return { unsubscribe: () => {} };
},
getSnapshot: () => latestValue,
getInitialState: () => latestValue
getInitialState: () => latestValue,
getOutput: () => undefined
});

const parentMachine = createMachine({
Expand Down Expand Up @@ -485,7 +486,8 @@ describeEachReactMode('useSelector (%s)', ({ suiteKey, render }) => {
return { unsubscribe: () => {} };
},
getSnapshot: () => latestValue,
getInitialState: () => latestValue
getInitialState: () => latestValue,
getOutput: () => undefined
});

const parentMachine = createMachine({
Expand Down Expand Up @@ -522,7 +524,8 @@ describeEachReactMode('useSelector (%s)', ({ suiteKey, render }) => {
return { unsubscribe: () => {} };
},
getSnapshot: () => latestValue,
getInitialState: () => latestValue
getInitialState: () => latestValue,
getOutput: () => undefined
});

const actor1 = createCustomActor('foo');
Expand Down Expand Up @@ -553,7 +556,8 @@ describeEachReactMode('useSelector (%s)', ({ suiteKey, render }) => {
return { unsubscribe: () => {} };
},
getSnapshot: () => undefined,
getInitialState: () => undefined
getInitialState: () => undefined,
getOutput: () => undefined
});

const App = ({ selector }: { selector: any }) => {
Expand Down
6 changes: 4 additions & 2 deletions packages/xstate-solid/test/useActor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const createSimpleActor = <T extends unknown>(value: T) =>
createActor({
transition: (s) => s,
getSnapshot: () => value,
getInitialState: () => value
getInitialState: () => value,
getOutput: () => undefined
});

describe('useActor', () => {
Expand Down Expand Up @@ -651,7 +652,8 @@ describe('useActor', () => {
const simpleActor = createActor({
transition: (s) => s,
getSnapshot: () => 42,
getInitialState: () => 42
getInitialState: () => 42,
getOutput: () => undefined
});

const Test = () => {
Expand Down
Loading