npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bemedev/app-ts

v3.0.2

Published

A reprogramming method to code better

Readme

@bemedev/app-ts

[!CAUTION] Do not use version 2.1.0. This version contains build> configuration issues (rolldown.config.ts) and should be avoided. Please use version 2.2.0 or higher.

A TypeScript library for building finite state machines with a rich, type-safe API. Manages states, transitions, context, asynchronous operations, and reactive streams through a unified actors model.

Philosophy

The machine defines what can happen. The interpreter makes it happen.

A machine is purely declarative: it describes states, transitions, actions, and guards by name ('fetchUser', 'canSubmit'). It never calls external code directly. You wire real implementations in later via provideOptions or addOptions.

The interpreter takes a machine and executes it at runtime — it processes events, manages context, subscribes to actors, and drives transitions.

Actors — two kinds of external work

| Actor type | Shape | Trigger | Direction | | ---------- | ------------------- | ----------------- | -------------------------------- | | emitters | () => Pausable<T> | State entry | Source → Machine (read-only) | | children | () => Interpreter | Machine lifecycle | Bidirectional (via sendTo) |

Emitters importance is NOT touched during the flow

This is the single most important architectural choice to understand.

An emitter is a pausable stream source. It produces values on its own schedule. The machine only reacts to those values — it never sends events to the emitter, never modifies it, never controls its output.

┌─────────────┐    emissions    ┌──────────────┐
│  Pausable    │ ─────────────► │   Machine     │
│  (emitter)   │                │  next/error/  │
│              │  ◄── nothing   │  complete     │
└─────────────┘                 └──────────────┘
        ▲                              │
        │ subscribe on entry           │ stop on exit
        └──────────────────────────────┘

Emitter lifecycle:

  1. The machine config declares an emitter name and its handlers (next, error, complete).
  2. provideOptions wires the name to a factory: () => Pausable<T>.
  3. When the interpreter enters the state → the factory is called, the Pausable is subscribed and started.
  4. Each emission becomes an internal event routed to the matching handler (next → actions, error → actions, complete → actions or target).
  5. The machine never sends events to the Pausable. It is strictly one-way.
  6. When the interpreter exits the state (or stops) → the Pausable is stopped.
  7. Re-entering the state creates a new Pausable from scratch.

This differs fundamentally from:

  • Children — bidirectional; the parent can sendTo the child interpreter.

Installation

npm install @bemedev/app-ts
# or
pnpm add @bemedev/app-ts

Requirements: Node.js ≥ 22, TypeScript ≥ 5.x

Table of Contents

  1. Basic machine
  2. Typings utilities
  3. Machine interpretation
  4. Subscribe to state changes
  5. Actions
  6. Guards (predicates)
  7. Transitions: on, after, always
  8. Activities (recurring actions)
  9. Actors: emitters
  10. Actors: children
  11. Tags
  12. Legacy options (_legacy)
  13. API reference

1. Basic Machine

import { createMachine } from '@bemedev/app-ts';

const machine = createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: {
        START: 'running',
      },
    },
    running: {
      on: {
        STOP: 'idle',
      },
    },
  },
});

The config is pure data — no callbacks, no side-effects. You can serialise it, inspect it, or test it independently from runtime.

2. Typings Utilities

The library provides powerful typing utilities inspired by Valibot for defining complex types:

import { typings, inferT } from '@bemedev/app-ts';

// Literals
const status = typings.litterals('idle', 'pending', 'success', 'error');
type Status = inferT<typeof status>;
// 'idle' | 'pending' | 'success' | 'error'

// Union types
const value = typings.union('string', 'number', 'boolean');
type Value = inferT<typeof value>;
// string | number | boolean

// Arrays
const tags = typings.array('string');
type Tags = inferT<typeof tags>;
// string[]

// Tuples
const coordinates = typings.tuple('number', 'number');
type Coordinates = inferT<typeof coordinates>;
// [number, number]

// Objects
const user = typings.any({
  name: 'string',
  age: 'number',
  email: typings.maybe('string'), // optional field
});
type User = inferT<typeof user>;
// { name: string; age: number; email?: string }

// Records
const config = typings.record('string');
// Record<string, string>
const namedConfig = typings.record('number', 'width', 'height');
// { width: number; height: number }

// Intersection types
const person = typings.intersection(
  { name: 'string', age: 'number' },
  { email: 'string', phone: 'string' },
);
type Person = inferT<typeof person>;
// { name: string; age: number; email: string; phone: string }

// Discriminated unions
const shape = typings.discriminatedUnion(
  'type',
  { type: typings.litterals('circle'), radius: 'number' },
  {
    type: typings.litterals('rectangle'),
    width: 'number',
    height: 'number',
  },
);

// Partial objects
const optionalUser = typings.partial({
  name: 'string',
  age: 'number',
});
type OptionalUser = inferT<typeof optionalUser>;
// { name?: string; age?: number }

// Custom types
const customType = typings.custom<MyCustomType>();

// Single or Array (SoA)
const singleOrMany = typings.soa('string');
type SingleOrMany = inferT<typeof singleOrMany>;
// string | string[]

// StateValue type helper
const stateValue = typings.sv;
type MyStateValue = inferT<typeof stateValue>;
// StateValue

Using Typings with Machines

import { createMachine, typings } from '@bemedev/app-ts';

const machine = createMachine(
  {
    initial: 'idle',
    states: {
      idle: {
        on: { FETCH: 'loading' },
      },
      loading: {
        on: { SUCCESS: 'success', ERROR: 'error' },
      },
      success: {},
      error: {},
    },
  },
  typings({
    eventsMap: {
      FETCH: 'primitive',
      SUCCESS: { data: typings.array('string') },
      ERROR: { message: 'string' },
    },
    context: {
      items: typings.array('string'),
      error: typings.maybe('string'),
    },
  }),
);

3. Machine Interpretation

The interpreter brings a machine to life. It holds context, processes events, and manages actor subscriptions.

import { interpret } from '@bemedev/app-ts';

// Create an interpreter service
const service = interpret(machine, {
  context: { items: [], error: undefined },
  pContext: {}, // private context (invisible to subscribers)
});

// Start the service
service.start();

// Send events
service.send('FETCH');
service.send({
  type: 'SUCCESS',
  payload: { data: ['item1', 'item2'] },
});

// Read current state
console.log(service.value); // 'success'
console.log(service.context);
// { items: ['item1', 'item2'], error: undefined }

// Stop the service
await service[Symbol.asyncDispose]();

4. Subscribe to State Changes

import { interpret } from '@bemedev/app-ts';

const service = interpret(machine, {
  context: { items: [], error: undefined },
});

// Subscribe to all state changes
const subscription = service.subscribe((prevState, currentState) => {
  console.log('State changed:', {
    from: prevState.value,
    to: currentState.value,
  });
});

// Subscribe to specific events
const eventSubscription = service.subscribe({
  SUCCESS: ({ payload }) => console.log('Success:', payload.data),
  ERROR: ({ payload }) => console.log('Error:', payload.message),
  else: () => console.log('Other event'),
});

service.start();

// Later: unsubscribe
subscription.unsubscribe();
eventSubscription.close();

5. Actions

Actions are side-effects that run during transitions. They are provided by name in the config and implemented via provideOptions.

5.1 assign

Updates context values using decomposed paths.

.provideOptions(({ assign }) => ({
  actions: {
    // Update a nested field
    updateCount: assign(
      'context.count',
      ({ context }) => context.count + 1,
    ),

    // Replace the entire context
    reset: assign('context', () => ({
      count: 0,
      name: 'New',
    })),

    // Actor-scoped assign (for emitter payloads)
    insertData: assign('context.data', {
      'fetch::then': ({ payload, context }) => {
        context?.data?.push(...payload);
        return context?.data;
      },
    }),
  },
}))

5.2 voidAction

Side-effect only — does not modify context.

.provideOptions(({ voidAction }) => ({
  actions: {
    logState: voidAction(
      () => console.log('State changed'),
    ),

    // Actor-scoped void action (e.g. for emitter errors)
    signals: voidAction({
      'interval::error': ({ payload }) => {
        console.warn('Error received:', payload);
      },
    }),
  },
}))

5.3 batch

Groups multiple actions into one.

.provideOptions(({ batch, assign, erase }) => ({
  actions: {
    resetForm: batch(
      erase('context.name'),
      erase('context.email'),
      erase('context.age'),
    ),
  },
}))

5.4 filter & erase

filter — filters arrays, object arrays, or records in context:

.provideOptions(({ filter }) => ({
  actions: {
    // Filter array elements
    filterEven: filter(
      'context.numbers',
      (num: number) => num % 2 === 0,
    ),

    // Filter array of objects
    filterActive: filter(
      'context.people',
      ({ active }) => active,
    ),

    // Filter record by value
    filterHighScores: filter(
      'context.scores',
      (score) => score >= 80,
    ),
  },
}))

erase — sets a property to undefined:

.provideOptions(({ erase, batch }) => ({
  actions: {
    clearEmail: erase('context.user.email'),

    // Erase multiple with batch
    clearAll: batch(
      erase('context.name'),
      erase('context.email'),
      erase('context.age'),
    ),
  },
}))

5.5 resend & forceSend

Re-dispatch events from within actions.

  • resend(event) — sends the event only if the machine is not in a blocked state.
  • forceSend(event) — sends the event regardless of blocked state.
.provideOptions(({ resend, forceSend }) => ({
  actions: {
    retryFetch: resend('FETCH'),
    forceIncrement: forceSend('INCREMENT'),
  },
}))

5.6 Async actions & errorFn

As of v3.0.0, all action helpers accept async functions directly. The interpreter's action pipeline is fully async and sequentially awaited.

An optional third errorFn argument handles promise rejections inline:

.provideOptions(({ assign, voidAction }) => ({
  actions: {
    // Async assign — awaits the promise before updating context
    fetchUser: assign<'user', User, FetchError>(
      'context.user',
      async ({ event }) => (await fetch(`/u/${event.id}`)).json(),
      // errorFn: called with the rejection value; its result is merged
      (err, state) => ({
        context: { ...state.context, error: err.message },
      }),
    ),

    // Async void action — errorFn absent → error flows to _addError
    logActivity: voidAction(
      async ({ context }) => {
        await analytics.track(context.userId);
      },
    ),
  },
}))

When errorFn is absent and the action rejects, the error is routed to the internal _addError channel — no uncaught rejection.

6. Guards (Predicates)

Guards are pure predicates that decide whether a transition should fire.

.provideOptions(({ isValue, isNotValue }) => ({
  predicates: {
    // Built-in value check helpers
    isEmpty: isValue('context.items', []),
    hasToken: isNotValue('context.token', undefined),

    // Custom predicate
    isAuthenticated: ({ context }) =>
      context.token !== undefined,
  },
}))

Usage in config:

states: {
  idle: {
    on: {
      FETCH: {
        guards: 'canFetch',   // single guard
        target: 'loading',
      },
    },
    always: [
      { guards: 'isEmpty', target: '/empty' },
      '/default',             // fallback — no guard
    ],
  },
}

7. Transitions: on, after, always

on — event-driven transitions

states: {
  idle: {
    on: {
      // Simple target
      START: '/running',

      // With guard and actions
      FETCH: {
        guards: 'canFetch',
        target: '/loading',
        actions: 'setLoading',
      },

      // Multiple candidates — first matching guard wins
      SUBMIT: [
        { guards: 'isValid', target: '/success' },
        { guards: 'hasErrors', target: '/error' },
        '/fallback',
      ],
    },
  },
}

after — delayed transitions

Automatically transition after a named delay. If multiple delays are defined, the shortest one that passes its guard wins.

// Simple delay
const machine = createMachine(
  {
    initial: 'idle',
    states: {
      idle: { after: { DELAY: '/active' } },
      active: {},
    },
  },
  defaultT,
);

machine.addOptions(() => ({
  delays: { DELAY: 1000 },
}));
// After 1 s in 'idle' → automatically transition to 'active'
// Multiple delays — shortest wins
const machine2 = createMachine(
  {
    initial: 'idle',
    states: {
      idle: {
        after: {
          DELAY1: '/result1',
          DELAY2: '/result2',
        },
      },
      result1: {},
      result2: {},
    },
  },
  defaultT,
);

machine2.addOptions(() => ({
  delays: { DELAY1: 3000, DELAY2: 2000 },
}));
// DELAY2 (2 s) fires first → goes to result2
// Delayed with guard
states: {
  idle: {
    after: {
      DELAY: {
        guards: 'returnFalse',
        target: '/result1',
      },
      DELAY2: '/result2',
    },
  },
}
// DELAY fires first but guard prevents transition
// → DELAY2 wins

always — immediate (eventless) transitions

Evaluated every time the state is entered. First matching guard wins.

const machine = createMachine(
  {
    initial: 'idle',
    states: {
      idle: {
        always: [
          { guards: 'returnFalse', target: '/result1' },
          { guards: 'returnFalse', target: '/result3' },
          '/result2', // fallback — no guard
        ],
      },
      result1: {},
      result2: {},
      result3: {},
    },
  },
  defaultT,
);
// First two guards fail → goes to result2

8. Activities (Recurring Actions)

An activity is an action executed repeatedly on a named delay while the state is active. Activities support pause, resume, and stop controls.

const machine = createMachine(
  {
    initial: 'idle',
    states: {
      idle: {
        activities: { DELAY: 'inc' },
        on: {
          PAUSE: { actions: 'pause' },
          RESUME: { actions: 'resume' },
          STOP: { actions: 'stop' },
        },
      },
    },
  },
  typings({
    eventsMap: {
      PAUSE: 'primitive',
      RESUME: 'primitive',
      STOP: 'primitive',
    },
    context: { iterator: 'number' },
  }),
).provideOptions(
  ({ assign, pauseActivity, resumeActivity, stopActivity }) => ({
    actions: {
      inc: assign(
        'context.iterator',
        ({ context }) => context?.iterator + 1,
      ),
      pause: pauseActivity('/idle::DELAY'),
      resume: resumeActivity('/idle::DELAY'),
      stop: stopActivity('/idle::DELAY'),
    },
    delays: { DELAY: 100 },
  }),
);

The activity inc runs every 100 ms while in idle. Sending PAUSE freezes it, RESUME restarts it, and STOP terminates it permanently for that state visit.


9. Actors: Emitters

Key concept — emitters are NEVER touched during the flow.

Emitters are pausable stream sources. The machine subscribes to them on state entry and only reacts to their emissions. It never sends events to the Pausable, never modifies it, never controls its output.

9.1 How emitters work

  1. Config — declare the emitter name and its handlers:

    actors: {
      interval: {
        next: { actions: ['assigN'] },
        error: { actions: ['handleError'] },
        complete: { actions: ['onComplete'] },
      },
    }
  2. Implementation — provide a Pausable<T> factory:

    import { createPausable } from '@bemedev/rx-pausable'; // optional RxJS helper
    
    .provideOptions(() => ({
      actors: {
        emitters: {
          interval: () =>
            createPausable(
              interval(200).pipe(
                take(5),
                map(v => v + 1),
                map(v => v * 5),
              ),
            ),
        },
      },
    }))

    Pausable<T> is a framework-agnostic interface exported by this library. Any object satisfying { subscribe, start, stop, pause, resume } works. createPausable (from @bemedev/rx-pausable) is a convenience wrapper for RxJS observables — it is not a required dependency.

  3. Runtime — the interpreter manages the full lifecycle:

    • Enter state → factory called → subscribe() then start()
    • Each next emission → routed to next handler (actions/target)
    • An error emission → routed to error handler
    • A complete emission → routed to complete handler
    • Exit state (or interpreter stops) → stop()
    • Re-enter state → a new Pausable from scratch

9.2 Simple emitter — accumulating values

Derived from src/emitters/__tests__/data.ts and simple.test.ts

import { createMachine, typings, interpret } from '@bemedev/app-ts';
import { createPausable } from '@bemedev/rx-pausable';
import { interval, map, take } from 'rxjs';

const machine = createMachine(
  {
    initial: 'inactive',
    actors: {
      interval: {
        next: { actions: ['assigN'] },
        complete: { actions: ['mockCompleteAction'] },
      },
    },
    states: {
      inactive: { on: { NEXT: '/active' } },
      active: { on: { NEXT: '/inactive' } },
    },
  },
  typings({
    context: 'number',
    eventsMap: { NEXT: 'primitive' },
    actorsMap: {
      emitters: {
        interval: { next: 'number', error: 'primitive' },
      },
    },
  }),
).provideOptions(({ assign }) => ({
  actions: {
    assigN: assign('context', {
      'interval::next': ({ payload, context }) => notU(context) + payload,
    }),
  },
  actors: {
    emitters: {
      interval: () =>
        createPausable(
          interval(200).pipe(
            take(5),
            map(v => v + 1),
            map(v => v * 5),
          ),
        ),
    },
  },
}));

const service = interpret(machine, { context: 0 });
service.start();
// The interval emits autonomously every 200 ms:
//   emission 0 → (0+1)*5 = 5  → context: 0 + 5  = 5
//   emission 1 → (1+1)*5 = 10 → context: 5 + 10 = 15
//   emission 2 → (2+1)*5 = 15 → context: 15 + 15 = 30
//   emission 3 → (3+1)*5 = 20 → context: 30 + 20 = 50
//   emission 4 → (4+1)*5 = 25 → context: 50 + 25 = 75
//
// The machine NEVER told the interval what to emit.
// It only reacted to each value.

9.3 Emitter error handling

Derived from src/emitters/__tests__/error.test.ts

When the source errors, the error handler fires. The machine itself is not "broken" — it simply routes the error value to the declared actions.

import { createPausable } from '@bemedev/rx-pausable';
import { Subject } from 'rxjs';

const sub = new Subject<number>();

const machine = createMachine(
  {
    initial: 'idle',
    actors: {
      interval: {
        next: { actions: ['assigN'] },
        error: { actions: ['signals'] },
      },
    },
    states: { idle: {} },
  },
  typings({
    actorsMap: {
      emitters: {
        interval: { next: 'number', error: 'number' },
      },
    },
    context: 'number',
  }),
).provideOptions(({ assign, voidAction }) => ({
  actors: {
    emitters: { interval: () => createPausable(sub) },
  },
  actions: {
    assigN: assign('context', {
      'interval::next': ({ payload, context }) => context + payload,
    }),
    signals: voidAction({
      'interval::error': ({ payload }) => {
        console.warn('Error received:', payload);
      },
    }),
  },
}));

const service = interpret(machine, { context: 0 });
service.start();

// External code pushes values into the Subject:
sub.next(5); // → context becomes 5
sub.next(3); // → context becomes 8
sub.error(20); // → error handler fires, logs warning
//
// The machine didn't control the Subject.
// It only listened.

9.4 State-scoped emitters

Derived from src/emitters/__tests__/children.test.ts

When an emitter is defined inside a specific state (not at root), it only runs while that state is active. Exiting the state unsubscribes; re-entering creates a fresh subscription.

const machine = createMachine(
  {
    initial: 'inactive',
    states: {
      inactive: { on: { NEXT: '/active' } },
      active: {
        on: { NEXT: '/inactive' },
        actors: {
          interval1: {
            next: { actions: ['assigN'] },
            complete: { actions: ['mockCompleteAction'] },
          },
        },
      },
    },
  } /* typings... */,
);
Timeline:
  [inactive] ──NEXT──► [active]
                          │ subscribe to interval1
                          │ ... emissions arrive ...
                          │
              ◄──NEXT─── [active]
  [inactive]              │ unsubscribe from interval1
                          │
              ──NEXT──► [active]
                          │ NEW subscription to interval1

9.5 Emitters vs Children

| Aspect | Emitters | Children | | --------------- | --------------------------- | ------------------------------ | | Direction | Source → Machine only | Bidirectional | | Cardinality | 0..∞ emissions | Ongoing event exchange | | Machine control | None — read-only | sendTo sends events to child | | Subscription | subscribe / unsubscribe | interpret / stop | | Pause / Resume | Via @bemedev/rx-pausable | Via child interpreter |


10. Actors: Children

A child actor is a nested interpreter. The parent can send events to it via sendTo, and the child's events can bubble up to the parent via on handlers. Context can be mapped between parent and child.

10.1 Sending events to a child — sendTo

Derived from src/interpreters/__tests__/children.test.ts

const parent = createMachine(
  {
    actors: {
      child: {
        on: {
          NEXT: { actions: ['notify'] },
        },
      },
    },
    initial: 'idle',
    states: {
      idle: {
        on: {
          NEXT: { actions: ['sendChildNext'] },
        },
      },
    },
  },
  typings({
    eventsMap: { NEXT: 'primitive' },
    actorsMap: {
      children: { child: { NEXT: 'primitive' } },
    },
  }),
).provideOptions(({ sendTo, voidAction }) => ({
  actions: {
    notify: voidAction(() => {
      notify();
    }),
    sendChildNext: sendTo(child)(() => ({
      to: 'child',
      event: 'NEXT',
    })),
  },
  actors: {
    children: {
      child: () => interpret(child),
    },
  },
}));

When the parent receives NEXT, it forwards it to the child via sendTo. When the child processes NEXT, the parent's on.NEXT handler fires notify.

10.2 Context mapping between parent and child

const parent = createMachine(
  {
    actors: {
      child: {
        // Map child's entire context → parent.pContext.iterator
        contexts: { '.': 'iterator' },
      },
    },
    // ...
  },
  /* typings */
).provideOptions(() => ({
  actors: {
    children: {
      child: () => interpret(childMachine, { context: 0 }),
    },
  },
}));

When the child's context changes, it is automatically synced to the parent's private context (pContext) at the mapped key. This is one-way: the parent reads the child's context but does not write to it.


11. Tags

Tags are metadata labels on states. They allow UI code to query what category the current state belongs to without checking state names directly.

Derived from src/interpreters/__tests__/tags/tags.machine.ts

const machine = createMachine(
  {
    initial: 'idle',
    states: {
      idle: {
        tags: ['idle'],
        on: { NEXT: '/working' },
      },
      working: {
        tags: ['working', 'busy'],
        on: { NEXT: '/final', PREV: '/idle' },
      },
      final: {},
    },
  },
  typings({
    eventsMap: { NEXT: 'primitive', PREV: 'primitive' },
  }),
);

const service = interpret(machine);
service.start();

service.tags; // ['idle']
service.send('NEXT');
service.tags; // ['working', 'busy']

Tags in action callbacks

As of v2.5.0, tag literals are propagated into provideOptions callbacks as a typed union, enabling narrowing directly inside actions:

const machine = createMachine(
  {
    initial: 'idle',
    states: {
      idle: { on: { START: '/working' } },
      working: {
        tags: ['working', 'busy'],
        on: { DONE: '/idle' },
      },
    },
  },
  typings({ eventsMap: { START: 'primitive', DONE: 'primitive' } }),
).provideOptions(({ voidAction }) => ({
  actions: {
    log: voidAction(({ tags }) => {
      // tags: "working" | "busy" | undefined — not just string
      if (tags === 'busy') {
        /* handle busy state */
      }
    }),
  },
}));

12. Legacy Options (_legacy)

Both provideOptions and addOptions support accessing previously defined options through the _legacy parameter. This enables composition of existing actions, predicates, delays, and actors without manual tracking.

On a Machine

const machine = createMachine(config, types)
  .provideOptions(({ assign }) => ({
    actions: {
      increment: assign('context', ({ context }) => context + 1),
    },
  }))
  .provideOptions(({ batch }, { _legacy }) => ({
    actions: {
      doubleIncrement: batch(
        _legacy.actions.increment!,
        _legacy.actions.increment!,
      ),
    },
  }));

On an Interpreter

const service = interpret(machine, { context: 0 });

service.addOptions(({ assign }) => ({
  actions: {
    add: assign('context', ({ context }) => context + 5),
  },
}));

// Reuse previous action via _legacy
service.addOptions(({ batch }, { _legacy }) => ({
  actions: {
    addTwice: batch(_legacy.actions.add!, _legacy.actions.add!),
  },
}));

provideOptions on Interpreter (immutable)

const service1 = interpret(machine, { context: 0 });

const service2 = service1.provideOptions(({ assign }) => ({
  actions: {
    multiply: assign('context', ({ context }) => context * 2),
  },
}));

const service3 = service2.provideOptions(({ assign }, { _legacy }) => ({
  actions: {
    multiplyAndAdd: assign('context', ({ context }) => context * 2 + 10),
  },
}));
// service1, service2, service3 are independent instances

Properties available in _legacy:

| Property | Content | | -------------------- | ------------------------------- | | _legacy.actions | Previously defined actions | | _legacy.predicates | Previously defined guards | | _legacy.delays | Previously defined delays | | _legacy.machines | Previously defined child actors | | _legacy.emitters | Previously defined emitters |

Key features:

  • Immutable — the _legacy object is frozen; it cannot be mutated.
  • Cumulative — each call sees options from all previous calls.
  • Type-safe — fully typed for IntelliSense support.
  • Works with both addOptions (mutates) and provideOptions (returns new instance).

13. API Reference

Machine Creation

createMachine(config, types?)

Creates a new state machine.

Parameters:

  • config — Machine configuration object
    • initial — Initial state name
    • states — State definitions
    • actors? — Root-level actor declarations (emitters, children)
  • types — Type definitions (via typings(...))
    • context — Public context type
    • pContext? — Private context type
    • eventsMap — Event definitions
    • actorsMap? — Actor type maps (emitters, children)

Returns: Machine instance

createConfig(config)

Utility to create a typed configuration object without creating a full machine.

Machine Methods

machine.provideOptions(callback)

Provides implementations for actions, guards, delays, actors.

Parameters:

  • callback(helpers, options) — Function receiving:
    • helpersassign, voidAction, batch, filter, erase, sendTo, resend, forceSend, isValue, isNotValue, pauseActivity, resumeActivity, stopActivity
    • options{ _legacy } containing all previously defined options

Returns: New Machine instance (immutable)

machine.addOptions(callback)

Adds or overwrites options dynamically. Mutates the machine.

Returns: The added options object

Interpreter

interpret(machine, options?)

Creates an interpreter service for a machine.

Parameters:

  • machine — Machine instance
  • options:
    • context — Initial public context
    • pContext? — Initial private context
    • mode?'strict' | 'normal' (default: 'strict')
    • exact? — Use exact timing intervals (default: true)

Returns: Interpreter service

Interpreter Properties

| Property | Description | | ----------------- | --------------------------------------------------- | | service.value | Current state value (string or nested object) | | service.context | Current public context | | service.status | Service status ('idle', 'working', 'stopped') | | service.state | Complete state snapshot | | service.config | Current state configuration | | service.mode | Current mode ('strict' | 'normal') | | service.tags | Active tags for the current state |

Interpreter Methods

| Method | Description | | -------------------------------------- | ------------------------------------------------- | | service.start() | Starts the service and begins processing | | service.send(event) | Sends an event (string or { type, payload }) | | service.subscribe(subscriber) | Subscribes to state changes | | service.pause() | Pauses activities and timers | | service.resume() | Resumes after pausing | | service.stop() | Stops the service completely | | service.addOptions(callback) | Mutates the service with new options | | service.provideOptions(callback) | Returns a new service with additional options | | service.dispose() | Synchronously disposes of the service | | await service[Symbol.asyncDispose]() | Cleanly disposes (async) |

State Configuration

states: {
  stateName: {
    type?: 'atomic' | 'compound' | 'parallel' | 'final',
    initial?: string,
    tags?: string[],
    entry?: ActionConfig,
    exit?: ActionConfig,
    on?: { [event: string]: TransitionConfig },
    after?: { [delay: string]: TransitionConfig },
    always?: TransitionConfig | TransitionConfig[],
    activities?: { [delay: string]: ActionConfig },
    actors?: { [name: string]: ActorConfig },
    states?: { [state: string]: StateDefinition },
  }
}

Transition Configuration

type TransitionConfig =
  | string // Target state
  | {
      target?: string;
      guards?: GuardConfig;
      actions?: ActionConfig;
    }
  | TransitionConfig[]; // Multiple candidates (first match wins)

Emitter Types

The library exports a framework-agnostic Pausable<T> interface. Any object satisfying this shape can be used as an emitter factory return value — no RxJS dependency required.

import type { Pausable, EmitterObserver } from '@bemedev/app-ts';

// EmitterObserver<R>
type EmitterObserver<R> = {
  next: (value: R) => void;
  error: (err: any) => void;
  complete: () => void;
};

// Pausable<R>
type Pausable<R> = {
  subscribe: (observer: EmitterObserver<R>) => void;
  start: () => void; // begin consuming the source
  stop: () => void; // stop and clean up
  pause: () => void; // buffer incoming events
  resume: () => void; // replay buffer then resume
};

When using RxJS, wrap any Observable<T> with createPausable from @bemedev/rx-pausable (a separate, optional package).

Typings Utilities Reference

| Utility | Produces | | -------------------------------------- | -------------------------------- | | typings.litterals(...values) | Literal union types | | typings.union(...types) | Union types | | typings.array(type) | Array types | | typings.tuple(...types) | Tuple types | | typings.any(schema) | Object schemas | | typings.record(type, ...keys?) | Record types | | typings.intersection(...types) | Intersection types | | typings.discriminatedUnion(key, ...) | Discriminated unions | | typings.maybe(type) | Optional / undefined types | | typings.partial(schema) | All properties optional | | typings.custom<T>() | Custom TypeScript types | | typings.soa(type) | Single or Array type | | typings.sv | StateValue type helper | | inferT<T> | Infer TS type from typing schema |


Changelog

View CHANGELOG.md

NB

Don't use version 0.9.17, it doesn't export anything.

Contributing

Contributions are welcome! Please read our contribution guide for details.

License

MIT

Author

chlbri ([email protected])

My GitHub

Links