From 9d7a113b4f7d2c7ebfa934399a2e8f34decf2c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Thu, 9 Nov 2023 16:01:16 +0100 Subject: [PATCH 1/2] Declassify `State` --- packages/core/src/State.ts | 341 +++++++++++++----- packages/core/src/StateMachine.ts | 45 +-- packages/core/src/StateNode.ts | 4 +- packages/core/src/actions/assign.ts | 4 +- packages/core/src/actions/spawn.ts | 4 +- packages/core/src/actions/stop.ts | 4 +- packages/core/src/index.ts | 5 +- packages/core/src/stateUtils.ts | 38 +- packages/core/src/types.ts | 19 +- packages/core/test/interpreter.test.ts | 1 - packages/core/test/machine.test.ts | 1 - packages/core/test/scxml.test.ts | 2 +- packages/core/test/state.test.ts | 8 - packages/core/test/tags.test.ts | 2 +- packages/xstate-graph/test/graph.test.ts | 5 +- packages/xstate-inspect/src/serialize.ts | 2 +- packages/xstate-inspect/test/inspect.test.ts | 12 +- .../xstate-solid/src/deriveServiceState.ts | 47 +-- packages/xstate-solid/src/types.ts | 48 +-- packages/xstate-solid/src/useActor.ts | 10 +- packages/xstate-solid/src/useMachine.ts | 12 +- packages/xstate-solid/test/useActor.test.tsx | 2 +- .../xstate-solid/test/useMachine.test.tsx | 2 +- packages/xstate-test/src/types.ts | 10 +- 24 files changed, 349 insertions(+), 279 deletions(-) diff --git a/packages/core/src/State.ts b/packages/core/src/State.ts index e2c368a46f..f4c97fae6b 100644 --- a/packages/core/src/State.ts +++ b/packages/core/src/State.ts @@ -1,13 +1,9 @@ import isDevelopment from '#is-development'; import { $$ACTOR_TYPE } from './interpreter.ts'; import { memo } from './memo.ts'; -import { MachineSnapshot } from './StateMachine.ts'; import type { StateNode } from './StateNode.ts'; -import { - getConfiguration, - getStateNodes, - getStateValue -} from './stateUtils.ts'; +import type { StateMachine } from './StateMachine.ts'; +import { getStateValue } from './stateUtils.ts'; import { TypegenDisabled, TypegenEnabled } from './typegenTypes.ts'; import type { ProvidedActor, @@ -20,11 +16,11 @@ import type { Prop, StateConfig, StateValue, - TODO, AnyActorRef, Compute, EventDescriptor, - Snapshot + Snapshot, + ParameterizedObject } from './types.ts'; import { flatten, matchesState } from './utils.ts'; @@ -65,85 +61,70 @@ export function isStateConfig< } /** - * @deprecated Use `isStateConfig(object)` or `state instanceof State` instead. + * @deprecated Use `isStateConfig(object)` */ export const isState = isStateConfig; -export class State< + +interface MachineSnapshotBase< TContext extends MachineContext, TEvent extends EventObject, TActor extends ProvidedActor, TTag extends string, + TOutput, TResolvedTypesMeta = TypegenDisabled > { - public tags: Set; + machine: StateMachine< + TContext, + TEvent, + TActor, + ParameterizedObject, + ParameterizedObject, + string, + TTag, + unknown, + TOutput, + TResolvedTypesMeta + >; + tags: Set; + value: StateValue; + status: 'active' | 'done' | 'error' | 'stopped'; + error: unknown; + context: TContext; - public value: StateValue; - /** - * Indicates whether the state is a final state. - */ - public status: 'active' | 'done' | 'error' | 'stopped'; - /** - * The output data of the top-level finite state. - */ - public error: unknown; - public context: TContext; - public historyValue: Readonly> = {}; + historyValue: Readonly>; /** * The enabled state nodes representative of the state value. */ - public configuration: Array>; + configuration: Array>; /** * An object mapping actor names to spawned/invoked actors. */ - public children: ComputeChildren; + children: ComputeChildren; /** - * Creates a new `State` instance that represents the current state of a running machine. - * - * @param config + * The next events that will cause a transition from the current state. */ - constructor( - config: StateConfig, - public machine: AnyStateMachine - ) { - this.context = config.context; - this.historyValue = config.historyValue || {}; - this.matches = this.matches.bind(this); - this.configuration = config.configuration; - this.children = config.children as any; - - this.value = getStateValue(machine.root, this.configuration); - this.tags = new Set(flatten(this.configuration.map((sn) => sn.tags))); - this.status = config.status; - (this as any).output = config.output; - (this as any).error = config.error; - } + nextEvents: Array>; - public toJSON() { - const { configuration, tags, machine, ...jsonValues } = this; - - return { ...jsonValues, tags: Array.from(tags), meta: this.meta }; - } + meta: Record; /** * Whether the current state value is a subset of the given parent state value. * @param parentStateValue */ - public matches< + matches: < TSV extends TResolvedTypesMeta extends TypegenEnabled ? Prop, 'matchesStates'> : StateValue - >(parentStateValue: TSV): boolean { - return matchesState(parentStateValue as any, this.value); - } + >( + parentStateValue: TSV + ) => boolean; /** * Whether the current state configuration has a state node with the specified `tag`. * @param tag */ - public hasTag(tag: TTag): boolean { - return this.tags.has(tag as string); - } + hasTag: (tag: TTag) => boolean; /** * Determines whether sending the `event` will cause a non-forbidden transition @@ -153,48 +134,219 @@ export class State< * @param event The event to test * @returns Whether the event will cause a transition */ - public can(event: TEvent): boolean { - if (isDevelopment && !this.machine) { - console.warn( - `state.can(...) used outside of a machine-created State object; this will always return false.` - ); - } + can: (event: TEvent) => boolean; - const transitionData = this.machine.getTransitionData(this as any, event); + toJSON: () => unknown; +} - return ( - !!transitionData?.length && - // Check that at least one transition is not forbidden - transitionData.some((t) => t.target !== undefined || t.actions.length) - ); - } +interface ActiveMachineSnapshot< + TContext extends MachineContext, + TEvent extends EventObject, + TActor extends ProvidedActor, + TTag extends string, + TOutput, + TResolvedTypesMeta = TypegenDisabled +> extends MachineSnapshotBase< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + > { + status: 'active'; + output: undefined; + error: undefined; +} - /** - * The next events that will cause a transition from the current state. - */ - public get nextEvents(): Array> { - return memo(this, 'nextEvents', () => { - return [ - ...new Set(flatten([...this.configuration.map((sn) => sn.ownEvents)])) - ]; - }); - } +interface DoneMachineSnapshot< + TContext extends MachineContext, + TEvent extends EventObject, + TActor extends ProvidedActor, + TTag extends string, + TOutput, + TResolvedTypesMeta = TypegenDisabled +> extends MachineSnapshotBase< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + > { + status: 'done'; + output: TOutput; + error: undefined; +} + +interface ErrorMachineSnapshot< + TContext extends MachineContext, + TEvent extends EventObject, + TActor extends ProvidedActor, + TTag extends string, + TOutput, + TResolvedTypesMeta = TypegenDisabled +> extends MachineSnapshotBase< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + > { + status: 'error'; + output: undefined; + error: unknown; +} - public get meta(): Record { - return this.configuration.reduce((acc, stateNode) => { - if (stateNode.meta !== undefined) { - acc[stateNode.id] = stateNode.meta; +interface StoppedMachineSnapshot< + TContext extends MachineContext, + TEvent extends EventObject, + TActor extends ProvidedActor, + TTag extends string, + TOutput, + TResolvedTypesMeta = TypegenDisabled +> extends MachineSnapshotBase< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + > { + status: 'stopped'; + output: undefined; + error: undefined; +} + +export type MachineSnapshot< + TContext extends MachineContext, + TEvent extends EventObject, + TActor extends ProvidedActor, + TTag extends string, + TOutput, + TResolvedTypesMeta = TypegenDisabled +> = + | ActiveMachineSnapshot< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + > + | DoneMachineSnapshot< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + > + | ErrorMachineSnapshot< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + > + | StoppedMachineSnapshot< + TContext, + TEvent, + TActor, + TTag, + TOutput, + TResolvedTypesMeta + >; + +export function createMachineSnapshot< + TContext extends MachineContext, + TEvent extends EventObject, + TActor extends ProvidedActor, + TTag extends string, + TResolvedTypesMeta = TypegenDisabled +>( + config: StateConfig, + machine: AnyStateMachine +): MachineSnapshot< + TContext, + TEvent, + TActor, + TTag, + undefined, + TResolvedTypesMeta +> { + return { + status: config.status as any, + output: config.output, + error: config.error, + machine, + context: config.context, + configuration: config.configuration, + value: getStateValue(machine.root, config.configuration), + tags: new Set(flatten(config.configuration.map((sn) => sn.tags))), + children: config.children as any, + historyValue: config.historyValue || {}, + matches(parentStateValue) { + return matchesState(parentStateValue as any, this.value); + }, + hasTag(tag) { + return this.tags.has(tag); + }, + can(event) { + if (isDevelopment && !this.machine) { + console.warn( + `state.can(...) used outside of a machine-created State object; this will always return false.` + ); } - return acc; - }, {} as Record); - } + + const transitionData = this.machine.getTransitionData(this as any, event); + + return ( + !!transitionData?.length && + // Check that at least one transition is not forbidden + transitionData.some((t) => t.target !== undefined || t.actions.length) + ); + }, + get nextEvents() { + return memo(this, 'nextEvents', () => { + return [ + ...new Set(flatten([...this.configuration.map((sn) => sn.ownEvents)])) + ]; + }); + }, + get meta() { + return this.configuration.reduce((acc, stateNode) => { + if (stateNode.meta !== undefined) { + acc[stateNode.id] = stateNode.meta; + } + return acc; + }, {} as Record); + }, + toJSON() { + const { + configuration, + tags, + machine, + nextEvents, + toJSON, + can, + hasTag, + matches, + ...jsonValues + } = this; + return { ...jsonValues, tags: Array.from(tags) }; + } + }; } -export function cloneState( +export function cloneMachineSnapshot( state: TState, config: Partial> = {} ): TState { - return new State( + return createMachineSnapshot( + // TODO: it's wasteful that this spread triggers getters { ...state, ...config } as StateConfig, state.machine ) as TState; @@ -218,8 +370,19 @@ export function getPersistedState< >, options?: unknown ): Snapshot { - const { configuration, tags, machine, children, context, ...jsonValues } = - state; + const { + configuration, + tags, + machine, + children, + context, + can, + hasTag, + matches, + toJSON, + nextEvents, + ...jsonValues + } = state; const childrenJson: Record = {}; diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index c648746f66..c1560c3573 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -1,7 +1,12 @@ import { assign } from './actions.ts'; import { createInitEvent } from './eventUtils.ts'; import { STATE_DELIMITER } from './constants.ts'; -import { cloneState, getPersistedState, State } from './State.ts'; +import { + cloneMachineSnapshot, + createMachineSnapshot, + getPersistedState, + MachineSnapshot +} from './State.ts'; import { StateNode } from './StateNode.ts'; import { getConfiguration, @@ -32,7 +37,6 @@ import type { MachineImplementationsSimplified, MachineTypes, NoInfer, - StateConfig, StateMachineDefinition, StateValue, TransitionDefinition, @@ -55,35 +59,6 @@ import isDevelopment from '#is-development'; export const STATE_IDENTIFIER = '#'; export const WILDCARD = '*'; -export type MachineSnapshot< - TContext extends MachineContext, - TEvent extends EventObject, - TActor extends ProvidedActor, - TTag extends string, - TOutput, - TResolvedTypesMeta = TypegenDisabled -> = - | (State & { - status: 'active'; - output: undefined; - error: undefined; - }) - | (State & { - status: 'done'; - output: TOutput; - error: undefined; - }) - | (State & { - status: 'error'; - output: undefined; - error: unknown; - }) - | (State & { - status: 'stopped'; - output: undefined; - error: undefined; - }); - export class StateMachine< TContext extends MachineContext, TEvent extends EventObject, @@ -272,7 +247,7 @@ export class StateMachine< getStateNodes(this.root, resolvedStateValue) ); - return new State( + return createMachineSnapshot( { configuration: [...configurationSet], context: config.context || ({} as TContext), @@ -326,7 +301,7 @@ export class StateMachine< isErrorActorEvent(event) && !state.nextEvents.some((nextEvent) => nextEvent === event.type) ) { - return cloneState(state, { + return cloneMachineSnapshot(state, { status: 'error', error: event.data }); @@ -393,7 +368,7 @@ export class StateMachine< > { const { context } = this.config; - const preInitial = new State( + const preInitial = createMachineSnapshot( { context: typeof context !== 'function' && context ? context : ({} as TContext), @@ -592,7 +567,7 @@ export class StateMachine< children[actorId] = actorRef; }); - const restoredSnapshot = new State( + const restoredSnapshot = createMachineSnapshot( { ...(snapshot as any), children, diff --git a/packages/core/src/StateNode.ts b/packages/core/src/StateNode.ts index a5c950a25e..9d76b56e04 100644 --- a/packages/core/src/StateNode.ts +++ b/packages/core/src/StateNode.ts @@ -1,4 +1,4 @@ -import type { State } from './State.ts'; +import { MachineSnapshot } from './State.ts'; import type { StateMachine } from './StateMachine.ts'; import { NULL_EVENT, STATE_DELIMITER } from './constants.ts'; import { evaluateGuard } from './guards.ts'; @@ -362,7 +362,7 @@ export class StateNode< } public next( - state: State, + state: MachineSnapshot, event: TEvent ): TransitionDefinition[] | undefined { const eventType = event.type; diff --git a/packages/core/src/actions/assign.ts b/packages/core/src/actions/assign.ts index c8e894b256..8f771f50e5 100644 --- a/packages/core/src/actions/assign.ts +++ b/packages/core/src/actions/assign.ts @@ -1,5 +1,5 @@ import isDevelopment from '#is-development'; -import { cloneState } from '../State.ts'; +import { cloneMachineSnapshot } from '../State.ts'; import { Spawner, createSpawner } from '../spawn.ts'; import type { ActionArgs, @@ -68,7 +68,7 @@ function resolveAssign( const updatedContext = Object.assign({}, state.context, partialUpdate); return [ - cloneState(state, { + cloneMachineSnapshot(state, { context: updatedContext, children: Object.keys(spawnedChildren).length ? { diff --git a/packages/core/src/actions/spawn.ts b/packages/core/src/actions/spawn.ts index 5957356f61..0a7fee88eb 100644 --- a/packages/core/src/actions/spawn.ts +++ b/packages/core/src/actions/spawn.ts @@ -1,5 +1,5 @@ import isDevelopment from '#is-development'; -import { cloneState } from '../State.ts'; +import { cloneMachineSnapshot } from '../State.ts'; import { createErrorActorEvent } from '../eventUtils.ts'; import { ProcessingStatus, createActor } from '../interpreter.ts'; import { @@ -93,7 +93,7 @@ function resolveSpawn( ); } return [ - cloneState(state, { + cloneMachineSnapshot(state, { children: { ...state.children, [resolvedId]: actorRef! diff --git a/packages/core/src/actions/stop.ts b/packages/core/src/actions/stop.ts index 621896c23f..6119d6b7e8 100644 --- a/packages/core/src/actions/stop.ts +++ b/packages/core/src/actions/stop.ts @@ -1,5 +1,5 @@ import isDevelopment from '#is-development'; -import { cloneState } from '../State.ts'; +import { cloneMachineSnapshot } from '../State.ts'; import { ProcessingStatus } from '../interpreter.ts'; import { ActionArgs, @@ -44,7 +44,7 @@ function resolveStop( delete children[resolvedActorRef.id]; } return [ - cloneState(state, { + cloneMachineSnapshot(state, { children }), resolvedActorRef diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f535d1b9d0..4fec3d1687 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,7 @@ export * from './actions.ts'; export * from './actors/index.ts'; export { SimulatedClock } from './SimulatedClock.ts'; export { type Spawner } from './spawn.ts'; -export { StateMachine, type MachineSnapshot } from './StateMachine.ts'; +export { StateMachine } from './StateMachine.ts'; export { getStateNodes } from './stateUtils.ts'; export * from './typegenTypes.ts'; export * from './types.ts'; @@ -10,7 +10,7 @@ export { waitFor } from './waitFor.ts'; import { Actor, createActor, interpret, Interpreter } from './interpreter.ts'; import { createMachine } from './Machine.ts'; import { mapState } from './mapState.ts'; -import { State } from './State.ts'; +export { type MachineSnapshot } from './State.ts'; import { StateNode } from './StateNode.ts'; // TODO: decide from where those should be exported export { matchesState, pathToStateValue, toObserver } from './utils.ts'; @@ -20,7 +20,6 @@ export { createMachine, interpret, mapState, - State, StateNode, type Interpreter }; diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index f23144bcaf..c2b2ec3573 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -1,5 +1,5 @@ import isDevelopment from '#is-development'; -import { State, cloneState } from './State.ts'; +import { MachineSnapshot, cloneMachineSnapshot } from './State.ts'; import type { StateNode } from './StateNode.ts'; import { raise } from './actions.ts'; import { createAfterEvent, createDoneStateEvent } from './eventUtils.ts'; @@ -634,17 +634,12 @@ export function getStateNodeByPath( /** * Returns the state nodes represented by the current state value. * - * @param state The state value or State instance + * @param stateValue The state value or State instance */ export function getStateNodes< TContext extends MachineContext, TEvent extends EventObject ->( - stateNode: AnyStateNode, - state: StateValue | State -): Array { - const stateValue = state instanceof State ? state.value : toStateValue(state); - +>(stateNode: AnyStateNode, stateValue: StateValue): Array { if (typeof stateValue === 'string') { return [stateNode, stateNode.states[stateValue]]; } @@ -677,7 +672,7 @@ export function transitionAtomicNode< >( stateNode: AnyStateNode, stateValue: string, - state: State, + state: MachineSnapshot, event: TEvent ): Array> | undefined { const childStateNode = getStateNode(stateNode, stateValue); @@ -696,7 +691,7 @@ export function transitionCompoundNode< >( stateNode: AnyStateNode, stateValue: StateValueMap, - state: State, + state: MachineSnapshot, event: TEvent ): Array> | undefined { const subStateKeys = Object.keys(stateValue); @@ -722,7 +717,7 @@ export function transitionParallelNode< >( stateNode: AnyStateNode, stateValue: StateValueMap, - state: State, + state: MachineSnapshot, event: TEvent ): Array> | undefined { const allInnerTransitions: Array> = []; @@ -758,13 +753,7 @@ export function transitionNode< >( stateNode: AnyStateNode, stateValue: StateValue, - state: State< - TContext, - TEvent, - TODO, - TODO, - TODO // tags - >, + state: MachineSnapshot, event: TEvent ): Array> | undefined { // leaf node @@ -1065,7 +1054,7 @@ export function microstep< ) { return nextState; } - return cloneState(nextState, { + return cloneMachineSnapshot(nextState, { configuration: nextConfiguration, historyValue }); @@ -1203,7 +1192,7 @@ function enterStates( continue; } - nextState = cloneState(nextState, { + nextState = cloneMachineSnapshot(nextState, { status: 'done', output: getMachineOutput( nextState, @@ -1634,9 +1623,12 @@ export function macrostep( // Handle stop event if (event.type === XSTATE_STOP) { - nextState = cloneState(stopChildren(nextState, event, actorScope), { - status: 'stopped' - }); + nextState = cloneMachineSnapshot( + stopChildren(nextState, event, actorScope), + { + status: 'stopped' + } + ); states.push(nextState); return { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2ae4a39ec1..bc920d99c5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,7 +1,7 @@ import type { StateNode } from './StateNode.ts'; -import type { State } from './State.ts'; +import type { MachineSnapshot } from './State.ts'; import type { Clock, Actor, ProcessingStatus } from './interpreter.ts'; -import type { MachineSnapshot, StateMachine } from './StateMachine.ts'; +import type { StateMachine } from './StateMachine.ts'; import { TypegenDisabled, ResolveTypegenMeta, @@ -926,13 +926,8 @@ export type AnyStateNode = StateNode; export type AnyStateNodeDefinition = StateNodeDefinition; -export type AnyState = State< - any, // context - any, // event - any, // actor - any, // tags - any // typegen ->; +// TODO: replace with AnyMachineSnapshot +export type AnyState = MachineSnapshot; export type AnyStateMachine = StateMachine< any, @@ -2139,10 +2134,11 @@ type ResolveEventType = ReturnTypeOrValue extends infer R infer _TResolvedTypesMeta > ? TEvent - : R extends State< + : R extends MachineSnapshot< infer _TContext, infer TEvent, infer _TActor, + infer _TTag, infer _TOutput, infer _TResolvedTypesMeta > @@ -2172,10 +2168,11 @@ export type ContextFrom = ReturnTypeOrValue extends infer R infer _TTypesMeta > ? TContext - : R extends State< + : R extends MachineSnapshot< infer TContext, infer _TEvent, infer _TActor, + infer _TTag, infer _TOutput, infer _TResolvedTypesMeta > diff --git a/packages/core/test/interpreter.test.ts b/packages/core/test/interpreter.test.ts index 0538e8c7aa..c5da52e7fd 100644 --- a/packages/core/test/interpreter.test.ts +++ b/packages/core/test/interpreter.test.ts @@ -12,7 +12,6 @@ import { stop, log } from '../src/index.ts'; -import { State } from '../src/State'; import { isObservable } from '../src/utils'; import { interval, from } from 'rxjs'; import { fromObservable } from '../src/actors/observable'; diff --git a/packages/core/test/machine.test.ts b/packages/core/test/machine.test.ts index 81fc43bb6b..d87e98fc39 100644 --- a/packages/core/test/machine.test.ts +++ b/packages/core/test/machine.test.ts @@ -1,5 +1,4 @@ import { createActor, createMachine, assign } from '../src/index.ts'; -import { State } from '../src/State.ts'; const pedestrianStates = { initial: 'walk', diff --git a/packages/core/test/scxml.test.ts b/packages/core/test/scxml.test.ts index cc15e00e44..ae6090d7fc 100644 --- a/packages/core/test/scxml.test.ts +++ b/packages/core/test/scxml.test.ts @@ -422,7 +422,7 @@ async function runTestToCompletion( } service.send({ type: event.name }); - const stateIds = getStateNodes(machine.root, nextState).map( + const stateIds = getStateNodes(machine.root, nextState.value).map( (stateNode) => stateNode.id ); diff --git a/packages/core/test/state.test.ts b/packages/core/test/state.test.ts index 69b6fd6a66..aed7e61913 100644 --- a/packages/core/test/state.test.ts +++ b/packages/core/test/state.test.ts @@ -176,14 +176,6 @@ describe('State', () => { }); }); - describe('State.prototype.matches', () => { - it('should keep reference to state instance after destructuring', () => { - const { matches } = createActor(exampleMachine).getSnapshot(); - - expect(matches('one')).toBe(true); - }); - }); - describe('status', () => { it('should show that a machine has not reached its final state', () => { expect(createActor(exampleMachine).getSnapshot().status).not.toBe('done'); diff --git a/packages/core/test/tags.test.ts b/packages/core/test/tags.test.ts index d66b4e80af..34b75d181e 100644 --- a/packages/core/test/tags.test.ts +++ b/packages/core/test/tags.test.ts @@ -143,6 +143,6 @@ describe('tags', () => { const jsonState = createActor(machine).getSnapshot().toJSON(); - expect(jsonState.tags).toEqual(['go', 'light']); + expect((jsonState as any).tags).toEqual(['go', 'light']); }); }); diff --git a/packages/xstate-graph/test/graph.test.ts b/packages/xstate-graph/test/graph.test.ts index 4c0f6d07d5..8c3ecbc2df 100644 --- a/packages/xstate-graph/test/graph.test.ts +++ b/packages/xstate-graph/test/graph.test.ts @@ -3,7 +3,6 @@ import { createMachine, EventObject, assign, - State, fromTransition, Snapshot } from 'xstate'; @@ -30,14 +29,14 @@ function getPathSnapshot(path: StatePath, any>): { } { return { state: - path.state instanceof State + 'machine' in path.state && 'value' in path.state ? path.state.value : 'context' in path.state ? path.state.context : path.state, steps: path.steps.map((step) => ({ state: - step.state instanceof State + 'machine' in step.state && 'value' in step.state ? step.state.value : 'context' in step.state ? step.state.context diff --git a/packages/xstate-inspect/src/serialize.ts b/packages/xstate-inspect/src/serialize.ts index 3bae20417a..80aaee723f 100644 --- a/packages/xstate-inspect/src/serialize.ts +++ b/packages/xstate-inspect/src/serialize.ts @@ -21,7 +21,7 @@ export function selectivelyStringify( } export function stringifyState(state: AnyState, replacer?: Replacer): string { - const { machine, configuration, tags, ...stateToStringify } = state; + const { machine, configuration, tags, meta, ...stateToStringify } = state; return selectivelyStringify( { ...stateToStringify, tags: Array.from(tags) }, ['context'], diff --git a/packages/xstate-inspect/test/inspect.test.ts b/packages/xstate-inspect/test/inspect.test.ts index 67845843cd..5bebc7191f 100644 --- a/packages/xstate-inspect/test/inspect.test.ts +++ b/packages/xstate-inspect/test/inspect.test.ts @@ -97,7 +97,7 @@ describe('@xstate/inspect', () => { "machine": "{"id":"whatever","key":"whatever","type":"compound","initial":{"target":["#whatever.active"],"source":"#whatever","actions":[],"eventType":null},"history":false,"states":{"active":{"id":"whatever.active","key":"active","type":"atomic","initial":{"target":[],"source":"#whatever.active","actions":[],"eventType":null},"history":false,"states":{},"on":{},"transitions":[],"entry":[],"exit":[],"order":1,"invoke":[],"tags":[]}},"on":{},"transitions":[],"entry":[],"exit":[],"order":-1,"invoke":[],"tags":[]}", "parent": undefined, "sessionId": "x:1", - "state": "{"value":"active","status":"active","context":{"cycle":"[Circular]"},"historyValue":{},"children":{},"tags":[]}", + "state": "{"status":"active","context":{"cycle":"[Circular]"},"value":"active","children":{},"historyValue":{},"tags":[]}", "type": "service.register", }, ] @@ -148,7 +148,7 @@ describe('@xstate/inspect', () => { }, { "sessionId": "x:3", - "state": "{"value":"active","status":"active","context":{},"historyValue":{},"children":{},"tags":[]}", + "state": "{"status":"active","context":{},"value":"active","children":{},"historyValue":{},"tags":[]}", "type": "service.state", }, ] @@ -205,7 +205,7 @@ describe('@xstate/inspect', () => { "machine": "{"id":"(machine)","key":"(machine)","type":"compound","initial":{"target":["#(machine).active"],"source":"#(machine)","actions":[],"eventType":null},"history":false,"states":{"active":{"id":"(machine).active","key":"active","type":"atomic","initial":{"target":[],"source":"#(machine).active","actions":[],"eventType":null},"history":false,"states":{},"on":{},"transitions":[],"entry":[],"exit":[],"order":1,"invoke":[],"tags":[]}},"on":{},"transitions":[],"entry":[],"exit":[],"order":-1,"invoke":[],"tags":[]}", "parent": undefined, "sessionId": "x:5", - "state": "{"value":"active","status":"active","context":{"map":"map","deep":{"map":"map"}},"historyValue":{},"children":{},"tags":[]}", + "state": "{"status":"active","context":{"map":"map","deep":{"map":"map"}},"value":"active","children":{},"historyValue":{},"tags":[]}", "type": "service.register", }, { @@ -215,7 +215,7 @@ describe('@xstate/inspect', () => { }, { "sessionId": "x:5", - "state": "{"value":"active","status":"active","context":{"map":"map","deep":{"map":"map"}},"historyValue":{},"children":{},"tags":[]}", + "state": "{"status":"active","context":{"map":"map","deep":{"map":"map"}},"value":"active","children":{},"historyValue":{},"tags":[]}", "type": "service.state", }, ] @@ -324,7 +324,7 @@ describe('@xstate/inspect', () => { }, { "sessionId": "x:7", - "state": "{"value":{},"status":"active","context":{"value":{"unsafe":"[unsafe]"}},"historyValue":{},"children":{},"tags":[]}", + "state": "{"status":"active","context":{"value":{"unsafe":"[unsafe]"}},"value":{},"children":{},"historyValue":{},"tags":[]}", "type": "service.state", }, ] @@ -346,7 +346,7 @@ describe('@xstate/inspect', () => { }, { "sessionId": "x:7", - "state": "{"value":{},"status":"active","context":{"value":{"unsafe":"[unsafe]"}},"historyValue":{},"children":{},"tags":[]}", + "state": "{"status":"active","context":{"value":{"unsafe":"[unsafe]"}},"value":{},"children":{},"historyValue":{},"tags":[]}", "type": "service.state", }, ] diff --git a/packages/xstate-solid/src/deriveServiceState.ts b/packages/xstate-solid/src/deriveServiceState.ts index c97feeddfb..32c6dc035f 100644 --- a/packages/xstate-solid/src/deriveServiceState.ts +++ b/packages/xstate-solid/src/deriveServiceState.ts @@ -1,5 +1,4 @@ -import { AnyState, matchesState } from 'xstate'; -import type { CheckSnapshot } from './types.ts'; +import { AnyState, Snapshot } from 'xstate'; function isState(state: any): state is AnyState { return ( @@ -12,34 +11,38 @@ function isState(state: any): state is AnyState { ); } +function reactiveMethod( + method: (this: T, ...args: Args) => R +) { + return function (this: T, ...args: Args) { + return method.apply(this, args); + }; +} + /** * Takes in an interpreter or actor ref and returns a State object with reactive * methods or if not State, the initial value passed in * @param state {AnyState | unknown} * @param prevState {AnyState | unknown} */ -export const deriveServiceState = < - StateSnapshot extends AnyState, - StateReturnType = CheckSnapshot ->( - state: StateSnapshot | unknown, - prevState?: StateSnapshot | unknown -): StateReturnType => { +export const deriveServiceState = ( + state: Snapshot, + prevState?: Snapshot +) => { if (isState(state)) { + const shouldKeepReactiveMethods = prevState && isState(prevState); return { ...state, - toJSON: state.toJSON, - can: state.can, - hasTag: state.hasTag, - nextEvents: state.nextEvents, - matches: - prevState && isState(prevState) - ? prevState.matches - : function (this: AnyState, parentStateValue: string) { - return matchesState(parentStateValue, this.value); - } - } as StateReturnType; - } else { - return state as StateReturnType; + can: shouldKeepReactiveMethods + ? prevState.can + : reactiveMethod(state.can), + hasTag: shouldKeepReactiveMethods + ? prevState.hasTag + : reactiveMethod(state.hasTag), + matches: shouldKeepReactiveMethods + ? prevState.matches + : reactiveMethod(state.matches) + }; } + return state; }; diff --git a/packages/xstate-solid/src/types.ts b/packages/xstate-solid/src/types.ts index a8721fe0f8..3c79178987 100644 --- a/packages/xstate-solid/src/types.ts +++ b/packages/xstate-solid/src/types.ts @@ -1,56 +1,10 @@ import type { - AnyState, AnyStateMachine, AreAllImplementationsAssumedToBeProvided, - EventObject, InternalMachineImplementations, - ActorOptions, - MachineContext, - ProvidedActor, - TypegenDisabled, - HomomorphicPick, - MachineSnapshot + ActorOptions } from 'xstate'; -type MachineSnapshotPOJO< - TContext extends MachineContext, - TEvent extends EventObject = EventObject, - TActor extends ProvidedActor = ProvidedActor, - TTag extends string = string, - TOutput = unknown, - TResolvedTypesMeta = TypegenDisabled -> = HomomorphicPick< - MachineSnapshot, - keyof MachineSnapshot< - TContext, - TEvent, - TActor, - TTag, - TOutput, - TResolvedTypesMeta - > ->; - -// Converts a State class type to a POJO State type. This reflects that the state -// is being spread into a new object for reactive tracking in SolidJS -export type CheckSnapshot = Snapshot extends MachineSnapshot< - infer TContext, - infer TEvents, - infer TActor, - infer TTag, - infer TOutput, - infer TResolvedTypesMeta -> - ? MachineSnapshotPOJO< - TContext, - TEvents, - TActor, - TTag, - TOutput, - TResolvedTypesMeta - > - : Snapshot; - type InternalMachineOpts< TMachine extends AnyStateMachine, RequireMissing extends boolean = false diff --git a/packages/xstate-solid/src/useActor.ts b/packages/xstate-solid/src/useActor.ts index 1915e9b6aa..c423bb3c3c 100644 --- a/packages/xstate-solid/src/useActor.ts +++ b/packages/xstate-solid/src/useActor.ts @@ -3,7 +3,6 @@ import type { Accessor } from 'solid-js'; import { createEffect, createMemo, onCleanup } from 'solid-js'; import { deriveServiceState } from './deriveServiceState.ts'; import { createImmutable } from './createImmutable.ts'; -import type { CheckSnapshot } from './types.ts'; import { unwrap } from 'solid-js/store'; const noop = () => { @@ -14,13 +13,13 @@ type Sender = (event: TEvent) => void; export function useActor>( actorRef: Accessor | TActor -): [Accessor>>, TActor['send']]; +): [Accessor>, TActor['send']]; export function useActor< TSnapshot extends Snapshot, TEvent extends EventObject >( actorRef: Accessor> | ActorRef -): [Accessor>, Sender]; +): [Accessor, Sender]; export function useActor( actorRef: | Accessor>> @@ -34,7 +33,10 @@ export function useActor( snapshot: deriveServiceState(actorMemo().getSnapshot?.()) }); - const setActorState = (actorState: unknown, prevState?: unknown) => { + const setActorState = ( + actorState: Snapshot, + prevState?: Snapshot + ) => { setState({ snapshot: deriveServiceState(actorState, prevState) }); diff --git a/packages/xstate-solid/src/useMachine.ts b/packages/xstate-solid/src/useMachine.ts index db8aeb7f58..6c997542c4 100644 --- a/packages/xstate-solid/src/useMachine.ts +++ b/packages/xstate-solid/src/useMachine.ts @@ -1,5 +1,5 @@ -import type { AnyStateMachine, StateFrom, Actor, Prop } from 'xstate'; -import type { CheckSnapshot, RestParams } from './types.ts'; +import type { AnyStateMachine, Actor, Prop, SnapshotFrom } from 'xstate'; +import type { RestParams } from './types.ts'; import { createService } from './createService.ts'; import { onCleanup, onMount } from 'solid-js'; import { deriveServiceState } from './deriveServiceState.ts'; @@ -9,11 +9,7 @@ import { unwrap } from 'solid-js/store'; type UseMachineReturn< TMachine extends AnyStateMachine, TInterpreter = Actor -> = [ - CheckSnapshot>, - Prop, - TInterpreter -]; +> = [SnapshotFrom, Prop, TInterpreter]; export function useMachine( machine: TMachine, @@ -28,7 +24,7 @@ export function useMachine( onMount(() => { const { unsubscribe } = service.subscribe((nextState) => { setState( - deriveServiceState(nextState, unwrap(state)) as StateFrom + deriveServiceState(nextState, unwrap(state)) as SnapshotFrom ); }); diff --git a/packages/xstate-solid/test/useActor.test.tsx b/packages/xstate-solid/test/useActor.test.tsx index 1468954bfa..86de7b86ef 100644 --- a/packages/xstate-solid/test/useActor.test.tsx +++ b/packages/xstate-solid/test/useActor.test.tsx @@ -374,7 +374,7 @@ describe('useActor', () => { data-testid="transition-button" onclick={() => send({ type: 'TRANSITION' })} /> -
{toJson().value.toString()}
+
{(toJson() as any).value.toString()}
); }; diff --git a/packages/xstate-solid/test/useMachine.test.tsx b/packages/xstate-solid/test/useMachine.test.tsx index 88a37192f3..9e6ed611b2 100644 --- a/packages/xstate-solid/test/useMachine.test.tsx +++ b/packages/xstate-solid/test/useMachine.test.tsx @@ -752,7 +752,7 @@ describe('useMachine hook', () => { data-testid="transition-button" onclick={() => send({ type: 'TRANSITION' })} /> -
{toJson().value.toString()}
+
{(toJson() as any).value.toString()}
); }; diff --git a/packages/xstate-test/src/types.ts b/packages/xstate-test/src/types.ts index 47cb36a0d4..22876a0d3a 100644 --- a/packages/xstate-test/src/types.ts +++ b/packages/xstate-test/src/types.ts @@ -3,7 +3,6 @@ import { EventObject, MachineConfig, MachineTypes, - State, StateNodeConfig, TransitionConfig, TypegenConstraint, @@ -13,7 +12,8 @@ import { MachineContext, ActorLogic, ParameterizedObject, - Snapshot + Snapshot, + MachineSnapshot } from 'xstate'; type TODO = any; @@ -102,11 +102,11 @@ export type TestMachineOptions< export interface TestMeta { test?: ( testContext: T, - state: State + state: MachineSnapshot ) => Promise | void; description?: | string - | ((state: State) => string); + | ((state: MachineSnapshot) => string); skip?: boolean; } interface TestStateResult { @@ -184,7 +184,7 @@ export interface TestTransitionConfig< TTestContext > extends TransitionConfig { test?: ( - state: State, + state: MachineSnapshot, testContext: TTestContext ) => void; } From 7f11bf57d8ad597296986aa067b308cdd511edbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 10 Nov 2023 12:54:22 +0100 Subject: [PATCH 2/2] add changeset --- .changeset/polite-pens-lie.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/polite-pens-lie.md diff --git a/.changeset/polite-pens-lie.md b/.changeset/polite-pens-lie.md new file mode 100644 index 0000000000..f07ebfc20b --- /dev/null +++ b/.changeset/polite-pens-lie.md @@ -0,0 +1,5 @@ +--- +'xstate': major +--- + +`State` class has been removed and replaced by `MachineSnapshot` object. They largely have the same properties and methods. On of the main noticeable results of this change is that you can no longer check `state instanceof State`.