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) => {}
+ }
+ }
+ }
+ }
+ });
+ });
});