Files
skills/docs/foundation/flow/fsm/src/utils/context-helpers.md
2026-05-29 08:20:56 +01:00

3.6 KiB

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

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

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.