first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
# context-helpers
## Purpose
Provides ergonomic factory helpers for building context-update and side-effect actions that can be slotted directly into `TransitionDefinition.actions` arrays. Without these helpers, every action must be written as an explicit arrow function returning a new context, which becomes verbose for simple partial-update or logging scenarios.
The two helpers mirror the `assign` and `pure` patterns popularised by XState, adapted to the `@suaveplan/fsm` action signature.
## Features
- `assign(patch)` — shallow-merges a static partial object into the context
- `assign(fn)` — delegates to a context-mapping function for complex derivations
- `pure(fn)` — wraps a side-effect function ensuring its return value never corrupts the context
- Both helpers are fully typed against the machine's `TContext` generic parameter
## Usage: Basic Example
```ts
import { assign, pure, createMachine } from '@suaveplan/fsm';
interface Ctx { count: number; lastEvent: string }
const machine = createMachine<Ctx>({
initial: 'idle',
context: { count: 0, lastEvent: '' },
states: {
idle: {
on: {
INCREMENT: {
target: 'idle',
actions: [
assign<Ctx>(ctx => ({ ...ctx, count: ctx.count + 1 })),
pure<Ctx>((ctx, evt) => { console.log('incremented', ctx, evt); }),
],
},
},
},
},
});
```
## Usage: Advanced Example
```ts
import { assign } from '@suaveplan/fsm';
interface WizardCtx {
step: number;
formData: Record<string, string>;
completed: boolean;
}
// Static patch (shallow merge)
const resetWizard = assign<WizardCtx>({ step: 1, completed: false });
// Functional update with derived logic
const advanceStep = assign<WizardCtx>(ctx => ({
...ctx,
step: ctx.step + 1,
completed: ctx.step + 1 >= 5,
}));
// Compose them in a transition
const machine = createMachine<WizardCtx>({
initial: 'wizard',
context: { step: 1, formData: {}, completed: false },
states: {
wizard: {
on: {
NEXT: { target: 'wizard', actions: [advanceStep] },
RESET: { target: 'wizard', actions: [resetWizard] },
},
},
},
});
```
## API Reference
### `assign<TContext>(updater): (context: TContext, event: FSMEvent) => TContext`
**Parameters:**
- `updater` — either a `Partial<TContext>` patch object or a function `(ctx: TContext) => TContext`
**Returns:** An action function compatible with `TransitionDefinition.actions`.
**Behaviour:**
- If `updater` is a plain object, it performs `{ ...context, ...updater }`.
- If `updater` is a function, it calls `updater(context)` and returns the result.
### `pure<TContext>(fn): (context: TContext, event: FSMEvent) => void`
**Parameters:**
- `fn` — a `(context: TContext, event: FSMEvent) => void` side-effect function
**Returns:** A wrapped action function whose return value is always `void`.
**Behaviour:** Calls `fn(context, event)` and discards any return value. This prevents accidental context mutations from side-effect functions that inadvertently return a value.
## Implementation Notes
`assign` accepts both object and function forms by checking `typeof updater === 'function'`. The cast `updater as (ctx: TContext) => TContext` is required because TypeScript cannot narrow a union of `Partial<TContext>` and `(ctx) => TContext` cleanly at the call site without the explicit cast.
`pure` exists as a safety wrapper rather than a direct alias because some developers write side-effect actions as `(ctx, evt) => doSomething()` where `doSomething` returns a non-void value. Without `pure`, that non-void value would be treated as a context replacement by the interpreter.

View File

@@ -0,0 +1,89 @@
# definition-validator
## Purpose
Validates an `FSMDefinition` object before the interpreter begins executing. Early structural validation prevents subtle runtime bugs such as transitions leading to undefined states or machines that can never leave their initial state due to a misconfigured `initial` key.
The validator is called automatically by `FSMInterpreter`'s constructor and by `createMachine()`. It can also be called standalone to check definitions at authoring time, in build pipelines, or within test suites that assert that certain configurations are correct.
## Features
- Verifies the top-level `initial` state exists in `states`
- Recursively collects all transition `target` references (including nested state trees) and confirms every target is a known state name
- Ensures any state node that declares child `states` also provides an `initial` child key
- Ensures that declared `initial` children actually exist within their parent's `states` map
- Throws `FSMError` with code `FSM.INVALID_DEFINITION` and rich `context` metadata on any failure
## Usage: Basic Example
```ts
import { validateDefinition } from '@suaveplan/fsm';
validateDefinition({
initial: 'idle',
context: {},
states: {
idle: { on: { START: { target: 'active' } } },
active: { on: { STOP: { target: 'idle' } } },
},
});
// No error — definition is valid
```
## Usage: Advanced Example
```ts
import { validateDefinition } from '@suaveplan/fsm';
import { FSMError, FSM_CODES } from '@suaveplan/fsm/errors';
// Catch and inspect validation errors
try {
validateDefinition({
initial: 'ghost', // "ghost" is not in states
context: {},
states: { idle: {} },
});
} catch (err) {
if (err instanceof FSMError && err.code === FSM_CODES.INVALID_DEFINITION) {
// err.context?.initial === 'ghost'
console.error('Bad definition:', err.message);
}
}
// Hierarchical definition with nested initial
validateDefinition({
initial: 'form',
context: { step: 1 },
states: {
form: {
initial: 'step1',
states: {
step1: { on: { NEXT: { target: 'step2' } } },
step2: { on: { BACK: { target: 'step1' } } },
},
},
},
});
```
## API Reference
### `validateDefinition<TContext>(def: FSMDefinition<TContext>): void`
**Parameters:**
- `def` — the complete FSM definition to validate
**Returns:** `void` (pure side-effect: throws on invalid input)
**Throws:** `FSMError` with code `FSM.INVALID_DEFINITION` and a `context` object containing the offending state/target names.
**Generic parameter:**
- `TContext` — the machine's extended context type; used only for correct typing of the definition shape.
## Implementation Notes
State name collection is done with a recursive depth-first traversal of the `states` map. Both fully-qualified dotted names (e.g. `"form.step1"`) and short names (e.g. `"step1"`) are added to the lookup set so that authors can use either convention in their `target` strings.
The validator deliberately does **not** check guard function arity or action return types — those are runtime concerns enforced by TypeScript at compile time. The focus here is purely structural integrity of the state graph.