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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@bemedev/app-ts

v1.6.3

Published

A reprogramming method to code better

Readme

@bemedev/app-ts

A TypeScript library for creating and managing state machines.

Installation

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

Features

  • Typed state machine creation
  • Public and private context management
  • Support for actions, guards and delays
  • Transition and event handling
  • Support for nested machines
  • Support for subscribables (e.g rxjs)
  • Comprehensive typings utilities with Valibot-like API

Usage

Basic Machine

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

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

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'),
    },
  }),
);

Machine Interpretation

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

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

// Start the service
service.start();

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

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

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

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();

Advanced Machine with Actions, Guards, and Delays

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

const fetchMachine = createMachine(
  {
    initial: 'idle',
    states: {
      idle: {
        on: {
          FETCH: {
            guards: 'canFetch',
            target: 'loading',
            actions: 'setLoading',
          },
        },
      },
      loading: {
        entry: 'logEntry',
        exit: 'logExit',
        promises: {
          src: 'fetchData',
          then: {
            actions: 'handleSuccess',
            target: 'success',
          },
          catch: {
            actions: 'handleError',
            target: 'error',
          },
        },
      },
      success: {
        after: {
          RESET_DELAY: { target: 'idle', actions: 'reset' },
        },
      },
      error: {
        on: {
          RETRY: 'loading',
        },
      },
    },
  },
  typings({
    eventsMap: {
      FETCH: { url: 'string' },
      RETRY: 'primitive',
    },
    context: {
      data: typings.maybe(typings.array('string')),
      error: typings.maybe('string'),
      loading: 'boolean',
    },
    pContext: {
      retryCount: 'number',
    },
    promiseesMap: {
      fetchData: {
        then: typings.array('string'),
        catch: { message: 'string' },
      },
    },
  }),
).provideOptions(({ assign, voidAction, isValue }) => ({
  actions: {
    setLoading: assign('context.loading', () => true),
    handleSuccess: assign('context', {
      'fetchData::then': ({ payload }) => ({
        data: payload,
        error: undefined,
        loading: false,
      }),
    }),
    handleError: assign('context', {
      'fetchData::catch': ({ payload }) => ({
        data: undefined,
        error: payload.message,
        loading: false,
      }),
    }),
    reset: assign('context', () => ({
      data: undefined,
      error: undefined,
      loading: false,
    })),
    logEntry: voidAction(() => console.log('Entering loading state')),
    logExit: voidAction(() => console.log('Exiting loading state')),
  },
  predicates: {
    canFetch: isValue('context.loading', false),
  },
  promises: {
    fetchData: async ({ event }) => {
      if (event.type !== 'FETCH') return [];
      const response = await fetch(event.payload.url);
      return response.json();
    },
  },
  delays: {
    RESET_DELAY: 3000, // 3 seconds
  },
}));

Nested Machines

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

const childMachine = createMachine(
  {
    initial: 'step1',
    states: {
      step1: { on: { NEXT: 'step2' } },
      step2: { on: { NEXT: 'step3' } },
      step3: { type: 'final' },
    },
  },
  typings({
    eventsMap: { NEXT: 'primitive' },
    context: { step: 'number' },
  }),
);

const parentMachine = createMachine(
  {
    machines: { child: 'childProcess' },
    initial: 'idle',
    states: {
      idle: {
        on: { START: 'working' },
      },
      working: {
        on: { COMPLETE: 'done' },
      },
      done: {},
    },
  },
  typings({
    eventsMap: { START: 'primitive', COMPLETE: 'primitive' },
    context: { status: 'string' },
  }),
).provideOptions(({ createChild }) => ({
  machines: {
    childProcess: createChild(
      childMachine,
      { context: { step: 0 } },
      { events: 'FULL' },
    ),
  },
}));

Activities (Recurring Actions)

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

const timerMachine = createMachine(
  {
    initial: 'running',
    states: {
      running: {
        activities: {
          TICK_DELAY: 'incrementCounter',
        },
        on: {
          PAUSE: 'paused',
        },
      },
      paused: {
        on: {
          RESUME: 'running',
        },
      },
    },
  },
  typings({
    eventsMap: { PAUSE: 'primitive', RESUME: 'primitive' },
    context: { counter: 'number' },
  }),
).provideOptions(({ assign }) => ({
  actions: {
    incrementCounter: assign(
      'context.counter',
      ({ context }) => context.counter + 1,
    ),
  },
  delays: {
    TICK_DELAY: 1000, // 1 second
  },
}));

API Reference

Machine Creation

createMachine(config, types)

Creates a new state machine.

Parameters:

  • config: Machine configuration object
    • initial: Initial state name
    • states: State definitions
    • machines?: Child machine definitions
    • emitters?: Observable emitters
  • types: Type definitions
    • context: Public context type
    • pContext?: Private context type
    • eventsMap: Event definitions
    • promiseesMap?: Promise definitions

Returns: Machine instance

createConfig(config)

Utility to create a typed configuration object.

Machine Methods

machine.provideOptions(callback)

Provides actions, guards, delays, promises, and child machines.

Parameters:

  • callback: Function receiving helper utilities:
    • assign: Update context values
    • voidAction: Create side-effect actions
    • isValue, isNotValue: Value comparison guards
    • createChild: Create child machine instances
    • sendTo: Send events to child machines
    • debounce, throttle: Rate limiting utilities

Returns: Machine instance

machine.addOptions(callback)

Adds or overwrites options dynamically.

Returns: The added options object.

Accessing Legacy Options with _legacy

Both provideOptions and addOptions now support accessing previously defined options through the _legacy parameter. This allows you to reuse and compose existing actions, predicates, delays, promises, machines, and emitters without manual tracking.

Example with Machine:

const machine = createMachine(config, types)
  .provideOptions(({ assign }) => ({
    actions: {
      increment: assign('context', ({ context }) => context + 1),
    },
  }))
  .provideOptions(({ batch }, { _legacy }) => {
    // Access previously defined increment action
    const previousIncrement = _legacy.actions?.increment;

    return {
      actions: {
        doubleIncrement: batch(previousIncrement!, previousIncrement!),
      },
    };
  });

Example with Interpreter:

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

// Define initial action
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!),
  },
}));

Example with provideOptions on Interpreter:

// Create new service instances with cumulative options
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 }) => {
      // Can access previous multiply action
      return context * 2 + 10;
    }),
  },
}));

// service1, service2, and service3 are independent instances

Properties available in _legacy:

  • _legacy.actions - Previously defined actions
  • _legacy.predicates - Previously defined guards/predicates
  • _legacy.delays - Previously defined delays
  • _legacy.promises - Previously defined promises
  • _legacy.machines - Previously defined child machines
  • _legacy.emitters - Previously defined emitters

Key features:

  • Immutable: _legacy object is frozen and cannot be modified
  • 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)

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

  • service.value - Current state value
  • service.context - Current public context
  • service.status - Service status ('idle', 'working', 'stopped', etc.)
  • service.state - Complete state snapshot
  • service.config - Current state configuration
  • service.mode - Current mode ('strict' | 'normal')

Interpreter Methods

service.start()

Starts the service and begins processing.

service.send(event)

Sends an event to the machine.

Parameters:

  • event: Event name (string) or event object { type, payload }

service.subscribe(subscriber, options?)

Subscribes to state changes.

Parameters:

  • subscriber: Callback function or event map
  • options?:
    • id?: Unique subscriber ID
    • equals?: Custom equality function

Returns: Subscription object with unsubscribe() or close() method

service.pause()

Pauses the service (stops activities and timers).

service.resume()

Resumes the service after pausing.

service.stop()

Stops the service completely.

service.addOptions(callback)

Dynamically adds or overwrites options on the running service. Mutates the current service instance.

Parameters:

  • callback: Same as machine.provideOptions callback, including _legacy parameter

Returns: The added options object.

Example:

service.addOptions(({ assign }, { _legacy }) => ({
  actions: {
    newAction: assign('context', ({ context }) => context + 1),
  },
}));

service.provideOptions(callback)

Creates a new service instance with additional options. Does not mutate the original service.

Parameters:

  • callback: Same as machine.provideOptions callback, including _legacy parameter

Returns: New Interpreter instance with preserved initial context

Example:

const service2 = service1.provideOptions(({ assign }) => ({
  actions: {
    customAction: assign('context', ({ context }) => context * 2),
  },
}));
// service1 and service2 are independent

await service[Symbol.asyncDispose]()

Cleanly disposes of the service (async).

service.dispose()

Synchronously disposes of the service.

State Configuration

State Definition

states: {
  stateName: {
    type?: 'atomic' | 'compound' | 'parallel' | 'final',
    initial?: string, // For compound states
    entry?: ActionConfig, // Actions on entry
    exit?: ActionConfig, // Actions on exit
    on?: { [event: string]: TransitionConfig }, // Event transitions
    after?: { [delay: string]: TransitionConfig }, // Delayed transitions
    always?: TransitionConfig, // Automatic transitions
    activities?: { [delay: string]: ActionConfig }, // Recurring actions
    promises?: PromiseConfig, // Async operations
    states?: { [state: string]: StateDefinition }, // Nested states
  }
}

Transition Configuration

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

Actions

assign(path, updater)

Updates context values.

actions: {
  updateCount: assign('context.count', ({ context }) => context.count + 1),
  updateMultiple: assign('context', () => ({ count: 0, name: 'New' })),
}

voidAction(callback)

Creates side-effect only actions.

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

batch(...actions)

Groups multiple actions.

actions: {
  initialize: batch('resetCount', 'clearError', 'logStart'),
}

filter(key, predicate)

Filters arrays or objects in context.

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

  // Filter array of objects
  filterActive: filter('context.users', (user) => user.active),

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

erase(path)

Sets a property to undefined. Uses full decomposed keys like assign.

actions: {
  // Erase single property
  clearEmail: erase('context.user.email'),

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

Guards (Predicates)

isValue(path, value)

Checks if a value equals expected value.

predicates: {
  isEmpty: isValue('context.items', []),
}

isNotValue(path, value)

Checks if a value differs from expected value.

Custom Guards

predicates: {
  isAuthenticated: ({ context }) => context.token !== undefined,
}

Delays

delays: {
  SHORT: 1000,
  LONG: ({ context }) => context.timeout,
}

Promises

promises: {
  fetchUser: async ({ event, context }) => {
    const response = await fetch(`/api/users/${event.payload.id}`);
    return response.json();
  },
}

Typings Utilities

  • typings.litterals(...values) - Create literal types
  • typings.union(...types) - Create union types
  • typings.array(type) - Create array types
  • typings.tuple(...types) - Create tuple types
  • typings.any(schema) - Create object schemas
  • typings.record(type, ...keys?) - Create record types
  • typings.intersection(...types) - Create intersection types
  • typings.discriminatedUnion(key, ...types) - Create discriminated unions
  • typings.maybe(type) - Create optional/undefined types
  • typings.partial(schema) - Make all properties optional
  • typings.custom<T>() - Use custom TypeScript types
  • typings.soa(type) - Single or Array type
  • typings.sv - StateValue type helper
  • inferT<T> - Infer TypeScript type from typing schema

CHANGE_LOG

NB

Don't use version 0.9.17, it doesn't exports anything

Contributing

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

License

MIT

Auteur

chlbri ([email protected])

My github

Liens