diff --git a/.changeset/tall-flies-occur.md b/.changeset/tall-flies-occur.md new file mode 100644 index 0000000000..64541b28a9 --- /dev/null +++ b/.changeset/tall-flies-occur.md @@ -0,0 +1,7 @@ +--- +'@xstate/fsm': minor +--- + +This change adds support for using "\*" as a wildcard event type in machine configs. + +Because that event type previously held no special meaning, it was allowed as an event type both in configs and when transitioning and matched as any other would. As a result of changing it to be a wildcard, any code which uses "\*" as an ordinary event type will break, making this a major change. diff --git a/docs/packages/xstate-fsm/index.md b/docs/packages/xstate-fsm/index.md index f189e4c150..c39de8edc4 100644 --- a/docs/packages/xstate-fsm/index.md +++ b/docs/packages/xstate-fsm/index.md @@ -11,7 +11,10 @@

-The [@xstate/fsm package](https://github.com/statelyai/xstate/tree/main/packages/xstate-fsm) contains a minimal, 1kb implementation of [XState](https://github.com/statelyai/xstate) for **finite state machines**. +This package contains a minimal, 1kb implementation of [XState](https://github.com/statelyai/xstate) for **finite state machines**. + +- [Read the full documentation in the XState docs](https://xstate.js.org/docs/packages/xstate-fsm/). +- [Read our contribution guidelines](https://github.com/statelyai/xstate/blob/main/CONTRIBUTING.md). ## Features @@ -23,6 +26,7 @@ The [@xstate/fsm package](https://github.com/statelyai/xstate/tree/main/packages | Transitions (string target) | ✅ | ✅ | | Delayed transitions | ❌ | ✅ | | Eventless transitions | ❌ | ✅ | +| Wildcard transitions | ✅ | ✅ | | Nested states | ❌ | ✅ | | Parallel states | ❌ | ✅ | | History states | ❌ | ✅ | @@ -48,15 +52,15 @@ The [@xstate/fsm package](https://github.com/statelyai/xstate/tree/main/packages If you want to use statechart features such as nested states, parallel states, history states, activities, invoked services, delayed transitions, transient transitions, etc. please use [`XState`](https://github.com/statelyai/xstate). -## Super quick start +## Quick start -**Installation** +### Installation ```bash npm i @xstate/fsm ``` -**Usage (machine):** +### Usage (machine) ```js import { createMachine } from '@xstate/fsm'; @@ -79,7 +83,7 @@ untoggledState.value; // => 'inactive' ``` -**Usage (service):** +### Usage (service) ```js import { createMachine, interpret } from '@xstate/fsm'; @@ -100,9 +104,12 @@ toggleService.stop(); -- [Super quick start](#super-quick-start) +- [Quick start](#quick-start) + - [Installation](#installation) + - [Usage (machine)](#usage-machine) + - [Usage (service)](#usage-service) - [API](#api) - - [`createMachine(config)`](#createmachineconfig) + - [`createMachine(config, options)`](#createmachineconfig-options) - [Machine config](#machine-config) - [State config](#state-config) - [Transition config](#transition-config) @@ -149,7 +156,7 @@ The machine config has this schema: ### State config -- `on` (object) - an object mapping event types (keys) to [transitions](#transition-config) +- `on` (object) - an object mapping event types (keys) to [transitions](#transition-config); an event type of `"*"` is special, indicating a "wildcard" transition which occurs when no other transition applies ### Transition config diff --git a/packages/xstate-fsm/README.md b/packages/xstate-fsm/README.md index 1e32d508bb..ce67382695 100644 --- a/packages/xstate-fsm/README.md +++ b/packages/xstate-fsm/README.md @@ -26,7 +26,7 @@ This package contains a minimal, 1kb implementation of [XState](https://github.c | Transitions (string target) | ✅ | ✅ | | Delayed transitions | ❌ | ✅ | | Eventless transitions | ❌ | ✅ | -| Wildcard event descriptors | ❌ | ✅ | +| Wildcard transitions | ✅ | ✅ | | Nested states | ❌ | ✅ | | Parallel states | ❌ | ✅ | | History states | ❌ | ✅ | @@ -50,7 +50,7 @@ This package contains a minimal, 1kb implementation of [XState](https://github.c - Transition actions - `state.changed` -If you want to use statechart features such as nested states, parallel states, history states, activities, invoked services, delayed transitions, transient transitions, wildcard event descriptors, etc. please use [`XState`](https://github.com/statelyai/xstate). +If you want to use statechart features such as nested states, parallel states, history states, activities, invoked services, delayed transitions, transient transitions, etc. please use [`XState`](https://github.com/statelyai/xstate). ## Quick start diff --git a/packages/xstate-fsm/src/index.ts b/packages/xstate-fsm/src/index.ts index 594c48a26d..bfc1fe35d3 100644 --- a/packages/xstate-fsm/src/index.ts +++ b/packages/xstate-fsm/src/index.ts @@ -25,6 +25,7 @@ export { const INIT_EVENT: InitEvent = { type: 'xstate.init' }; const ASSIGN_ACTION: StateMachine.AssignAction = 'xstate.assign'; +const WILDCARD = '*'; function toArray(item: T | T[] | undefined): T[] { return item === undefined ? [] : ([] as T[]).concat(item); @@ -180,11 +181,21 @@ export function createMachine< ); } + if (!IS_PRODUCTION && eventObject.type === WILDCARD) { + throw new Error( + `An event cannot have the wildcard type ('${WILDCARD}')` + ); + } + if (stateConfig.on) { const transitions: Array< StateMachine.Transition > = toArray(stateConfig.on[eventObject.type]); + if (WILDCARD in stateConfig.on) { + transitions.push(...toArray(stateConfig.on[WILDCARD])); + } + for (const transition of transitions) { if (transition === undefined) { return createUnchangedState(value, context); diff --git a/packages/xstate-fsm/src/types.ts b/packages/xstate-fsm/src/types.ts index 8cca2eb137..9d1b54e533 100644 --- a/packages/xstate-fsm/src/types.ts +++ b/packages/xstate-fsm/src/types.ts @@ -136,12 +136,14 @@ export namespace StateMachine { states: { [key in TState['value']]: { on?: { - [K in TEvent['type']]?: SingleOrArray< - Transition< - TContext, - TEvent extends { type: K } ? TEvent : never, - TState['value'] - > + [K in TEvent['type'] | '*']?: SingleOrArray< + K extends '*' + ? Transition + : Transition< + TContext, + TEvent extends { type: K } ? TEvent : never, + TState['value'] + > >; }; exit?: SingleOrArray>; diff --git a/packages/xstate-fsm/test/fsm.test.ts b/packages/xstate-fsm/test/fsm.test.ts index 8d54d0b47b..4e761ea671 100644 --- a/packages/xstate-fsm/test/fsm.test.ts +++ b/packages/xstate-fsm/test/fsm.test.ts @@ -131,6 +131,68 @@ describe('@xstate/fsm', () => { expect(nextState.actions).toEqual([]); }); + describe('when a wildcard transition is defined', () => { + type Event = { type: 'event' }; + type State = + | { value: 'pass'; context: {} } + | { value: 'fail'; context: {} }; + it('should not use a wildcard when an unguarded transition matches', () => { + const machine = createMachine<{}, Event, State>({ + initial: 'fail', + states: { fail: { on: { event: 'pass', '*': 'fail' } }, pass: {} } + }); + const nextState = machine.transition(machine.initialState, 'event'); + expect(nextState.value).toBe('pass'); + }); + + it('should not use a wildcard when a guarded transition matches', () => { + const machine = createMachine<{}, Event, State>({ + initial: 'fail', + states: { + fail: { + on: { event: { target: 'pass', cond: () => true }, '*': 'fail' } + }, + pass: {} + } + }); + const nextState = machine.transition(machine.initialState, 'event'); + expect(nextState.value).toBe('pass'); + }); + + it('should use a wildcard when no guarded transition matches', () => { + const machine = createMachine<{}, Event, State>({ + initial: 'fail', + states: { + fail: { + on: { event: { target: 'fail', cond: () => false }, '*': 'pass' } + }, + pass: {} + } + }); + const nextState = machine.transition(machine.initialState, 'event'); + expect(nextState.value).toBe('pass'); + }); + + it('should use a wildcard when no transition matches', () => { + const machine = createMachine<{}, Event, State>({ + initial: 'fail', + states: { fail: { on: { event: 'fail', '*': 'pass' } }, pass: {} } + }); + const nextState = machine.transition(machine.initialState, 'FAKE' as any); + expect(nextState.value).toBe('pass'); + }); + + it("should throw an error when an event's type is the wildcard", () => { + const machine = createMachine<{}, Event, State>({ + initial: 'fail', + states: { pass: {}, fail: {} } + }); + expect(() => machine.transition('fail', '*' as any)).toThrow( + /wildcard type/ + ); + }); + }); + it('should throw an error for undefined states', () => { expect(() => { lightFSM.transition('unknown', 'TIMER'); diff --git a/packages/xstate-fsm/test/types.test.ts b/packages/xstate-fsm/test/types.test.ts index 4aa202c425..0b2628ae93 100644 --- a/packages/xstate-fsm/test/types.test.ts +++ b/packages/xstate-fsm/test/types.test.ts @@ -1,4 +1,5 @@ import { createMachine } from '../src'; +import { InitEvent } from '../src/types'; describe('matches', () => { it('should allow matches to be called multiple times in a single branch of code', () => { @@ -58,4 +59,33 @@ describe('matches', () => { ((_accept: string) => {})(state.context.count); } }); + + // This test only works if "strictFunctionTypes" is enabled. Once that has + // been done, the ts-expect-error comment below turned on. + it('should require actions on wildcard transitions to handle all event types', () => { + type Context = {}; + type FooEvent = { type: 'foo'; foo: string }; + type BarEvent = { type: 'bar'; bar: number }; + type Event = FooEvent | BarEvent; + type State = { value: 'one'; context: Context }; + createMachine({ + context: {}, + initial: 'one', + states: { + one: { + on: { + foo: { + target: 'one', + actions: (_context: Context, _event: InitEvent | FooEvent) => {} + }, + // @x-ts-expect-error + '*': { + target: 'one', + actions: (_context: Context, _event: InitEvent | FooEvent) => {} + } + } + } + } + }); + }); });