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

whatnow

v2.0.1

Published

**A lightweight, type-safe state machine for managing complex async workflows.**

Readme

WhatNow

A lightweight, type-safe state machine for managing complex async workflows.

github license npm version

Features

  • 🪶 Lightweight: Zero dependencies, minimal overhead
  • 🔒 Immutable: Predictable state updates that are always immutable
  • 🎯 Type-safe: Built-in type safety for your state machines
  • 📋 Queue-based: Keeps async operations ordered and sequential
  • 🛑 Abortable: Reset or abort operations mid-flight

AI Disclosure

This library was co-created with AI, which means it was thoughtfully designed and reviewed by humans.

Installation

Using npm:

npm install whatnow

Using yarn:

yarn add whatnow

Quick Start

Simple Counter Example

import { WhatNow } from 'whatnow'

// Define your steps
type Step = 'START' | 'INCREMENTING' | 'DONE'

// Define your state
interface State {
  count: number
}

// Create a simple counter machine
const counter = new WhatNow<Step, State>({
  initialState: { count: 0 },
  steps: {
    // Initial step
    START: async (_, { act }) => {
      return { step: 'INCREMENTING' }
    },
    // Increment the counter
    INCREMENTING: async ({ state }) => {
      return {
        step: 'DONE',
        state: { count: state.count + 1 },
      }
    },
    // Terminal state
    DONE: null,
  },
  onChange: () => {
    console.log('Count:', counter.state.count)
  },
  onError: (error) => {
    console.error('Error:', error)
  },
})

// Start counting
counter.act('START')

Advanced Example: Data Processing

import { WhatNow } from 'whatnow'

// Define your steps
type Step =
  | 'UNLOADED'
  | 'INITIALIZING'
  | 'LOADED'
  | 'PROCESSINGDATA'
  | 'UNLOADING'

// Define your state shape (external state)
interface State {
  data: string[]
  processedCount: number
  loaded: boolean
}

// Define internal context (machine-only state)
interface Context {
  unsubscribe?: () => void // Cleanup function for subscription
}

// Define any payload types
interface Payload {
  newData?: string[]
}

// Simulate a data source
const dataSource = {
  subscribe: (callback: (data: string[]) => void) => {
    // Simulate async data arrival
    setTimeout(() => {
      callback(['item1', 'item2', 'item3'])
    }, 100)

    return () => {
      // Cleanup subscription
      console.log('Unsubscribed from data source')
    }
  },
}

// Create your state machine
const machine = new WhatNow<Step, State, Payload, Context>({
  initialState: {
    data: [],
    processedCount: 0,
    loaded: false,
  },
  initialContext: {
    unsubscribe: undefined,
  },
  steps: {
    // Terminal state - initial state before initialization
    UNLOADED: null,

    // Initialize the system by subscribing to data source
    INITIALIZING: async ({ context }, { act }) => {
      const unsubscribe = dataSource.subscribe((newData) => {
        act('PROCESSINGDATA', { newData })
      })

      return {
        step: 'LOADED',
        state: {
          loaded: true,
          data: [],
          processedCount: 0,
        },
        context: {
          unsubscribe,
        },
      }
    },

    // Process items in batch
    PROCESSINGDATA: async ({ state, payload }, { act }) => {
      // If the system is not loaded, we can skip the processing
      if (!state.loaded) {
        return { step: 'UNLOADED' }
      }

      if (payload.newData) {
        // Add new data to existing data and update processed count
        return {
          step: 'LOADED',
          state: {
            data: [...state.data, ...payload.newData],
            processedCount: state.processedCount + payload.newData.length,
            loaded: true,
          },
        }
      }

      return { step: 'PROCESSINGDATA' }
    },

    // Terminal state - system is ready for new data
    LOADED: null,

    // Cleanup state - unsubscribe and reset
    UNLOADING: async ({ state, context }, { act }) => {
      // If the system is not loaded, we can skip the cleanup
      if (!state.loaded) {
        return { step: 'UNLOADED' }
      }

      // Cleanup subscription using stored cleanup function
      context.unsubscribe?.()

      // Reset to initial state and transition to UNLOADED
      return {
        step: 'UNLOADED',
        state: {
          data: [],
          processedCount: 0,
          loaded: false,
        },
        context: {
          unsubscribe: undefined,
        },
      }
    },
  },
  onChange: () => {
    console.log('State updated:', machine.state)
  },
  onError: (error) => {
    console.error('Error occurred:', error)
  },
})

// Example usage:
// Will subscribe and wait for data
machine.act('INITIALIZING')

// Data will arrive via subscription, triggering PROCESSINGDATA and moving to LOADED

// Cleanup and return to UNLOADED state
machine.act('UNLOADING')

API Reference

WhatNow<TStep, TState, TPayload, TContext>

The main class for creating a state machine.

Constructor Options

interface WhatNowConfig<TStep, TState, TPayload, TContext> {
  steps: StepHandlers<TStep, TState, TPayload, TContext>
  initialState: TState
  initialContext?: TContext
  onChange: () => void
  onError: (error: Error) => void
}
  • steps: Map of step names to their handler functions
  • initialState: Initial state object (external state)
  • initialContext: Optional initial context object (internal state)
  • onChange: Callback triggered when state changes
  • onError: Error handler for async operations

Methods

  • act(step: TStep, payload?: Partial<TPayload>): Enqueue a new step transition
  • reset(resetStep: TStep): Clear the queue and schedule a new step. The currently executing step will complete, but any steps it triggers or that were previously queued will be discarded. The specified reset step will run after the current step completes.
  • state: Getter that returns the current state (readonly)

Step Handlers

Step handlers are async functions that process the current state and return the next step:

type StepHandler<TStep, TState, TPayload, TContext> = (
  params: Readonly<{
    state: TState
    context: TContext
    step: TStep
    payload: Partial<TPayload>
  }>,
  actions: {
    act: (step: TStep, payload?: Partial<TPayload>) => void
    reset: (step: TStep) => void
  },
) => Promise<Readonly<StepHandlerReturn<TStep, TState, TContext>>>

Return value:

interface StepHandlerReturn<TStep, TState, TContext> {
  step: TStep // The next step to transition to
  state?: TState // Optional state updates (must provide complete state object)
  context?: TContext // Optional context updates (must provide complete context object)
}

Best Practices

  1. Step Names

    • Define step types as string literals for type safety
    • Use -ing suffix for active steps (e.g., 'LOADING')
    • Use -ed suffix for terminal states (e.g., 'LOADED')
  2. State Management

    • Keep state updates immutable
    • Use context for internal machine state
    • Provide complete objects when updating state/context
    • Avoid optional properties in state/context objects
  3. Step Handlers

    • Define terminal states as null
    • Use payloads to pass data between steps
    • Handle errors in the onError callback

API Reference

See docs