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

@workflow-ts/core

v0.1.2

Published

Core workflow runtime and types

Downloads

36

Readme

@workflow-ts/core

Core runtime for workflow-ts - a TypeScript state machine architecture.

Installation

pnpm add @workflow-ts/core

API Reference

Workflow<P, S, O, R>

The main interface for defining a workflow.

interface Workflow<P, S, O, R> {
  // Create initial state from props
  initialState: (props: P, snapshot?: string) => S;

  // Optional: update state when props change (called before render)
  onPropsChanged?: (oldProps: P, newProps: P, state: S) => S;

  // Render current state into a rendering
  render: (props: P, state: S, context: RenderContext<S, O>) => R;

  // Optional: serialize state for persistence
  snapshot?: (state: S) => string;
}

Type Parameters:

  • P - Props (input from parent)
  • S - State (internal state machine)
  • O - Output (events to parent, or never/NoOutput if none)
  • R - Rendering (external representation)

Common aliases:

  • NoProps - Alias for void
  • NoOutput - Alias for never

createStatefulWorkflow(config)

Ergonomic builder for creating workflows with strong inference.

import { createStatefulWorkflow } from '@workflow-ts/core';

const counterWorkflow = createStatefulWorkflow({
  initialState: () => ({ count: 0 }),
  render: (_props, state, ctx) => ({
    count: state.count,
    increment: () => {
      ctx.actionSink.send((s) => ({ state: { count: s.count + 1 } }));
    },
  }),
});

createRuntime(workflow, props, config?)

Create a runtime to execute a workflow.

import { createRuntime } from '@workflow-ts/core';

// Simple usage
const runtime = createRuntime(workflow, props);

// With output handler
const runtime = createRuntime(workflow, props, {
  onOutput: (output) => console.log('Output:', output),
});

// With full config (snapshot restoration)
const runtime = createRuntime(workflow, props, {
  onOutput: (output) => console.log('Output:', output),
  initialState: { count: 5 },
  snapshot: savedSnapshot, // previously saved via runtime.snapshot()
});

// Enable debug logging
const runtime = createRuntime(workflow, props, {
  debug: true, // logs to console with [workflow-ts] prefix
});

// Or use custom logger
const runtime = createRuntime(workflow, props, {
  debug: (level, message, data) => {
    console.log(`[${level}]`, message, data);
  },
});

// Optional: value-based props equality (Kotlin-like == semantics)
const runtime = createRuntime(workflow, props, {
  propsEqual: (prev, next) => JSON.stringify(prev) === JSON.stringify(next),
});

// Legacy: still supports callback as third argument (backwards compatible)
const runtime = createRuntime(workflow, props, (output) => {
  console.log('Output:', output);
});

Parameters:

  • workflow - Workflow definition
  • props - Initial props
  • config? - Optional configuration object or output callback:
    • onOutput?: (output: O) => void - Callback for workflow outputs
    • initialState?: S - Initial state (for testing)
    • snapshot?: string - Serialized snapshot passed to initialState(props, snapshot)
    • debug?: boolean | DebugLogger - Enable debug logging
    • interceptors?: readonly Interceptor<S, O>[] - Observe action processing and state transitions
    • devTools?: DevTools<S, O, R> - Runtime inspection and event logging
    • propsEqual?: (prev: P, next: P) => boolean - Props equality comparator used by this runtime's updateProps and onPropsChanged (defaults to Object.is; not inherited by child runtimes)

For practical interceptor patterns (analytics/logging/debug/composition), see the Interceptors guide.

// Get current rendering
const rendering = runtime.getRendering();

// Subscribe to changes
const unsubscribe = runtime.subscribe((rendering) => {
  console.log('New rendering:', rendering);
});

// Clean up
runtime.dispose();

WorkflowRuntime<P, S, O, R>

The runtime class returned by createRuntime.

Methods:

| Method | Description | | --------------------- | --------------------------------------------------------------------------- | | getRendering() | Get current rendering | | getState() | Get current state (for debugging) | | getProps() | Get current props | | subscribe(listener) | Subscribe to rendering changes. Returns unsubscribe function. | | updateProps(props) | Update props and trigger re-render when propsEqual(prev, next) is false | | send(action) | Send an action directly | | on(type, handler) | Subscribe to a specific output type ({ type: string } outputs) | | off(type, handler?) | Unsubscribe typed output handlers | | snapshot() | Get serialized state string | | dispose() | Clean up runtime and all children | | isDisposed() | Check if disposed |

RenderContext<S, O>

Passed to render() for side effects.

interface RenderContext<S, O> {
  // Send an action to the runtime
  actionSink: Sink<Action<S, O>>;

  // Render a child workflow
  renderChild: <CP, CS, CO, CR>(
    workflow: Workflow<CP, CS, CO, CR>,
    props: CP,
    key?: string,
    handler?: (output: CO) => Action<S, O>,
  ) => CR;

  // Run a worker (async operation)
  runWorker: <W>(worker: Worker<W>, key: string, handler: (output: W) => Action<S, O>) => void;
}

runWorker key behavior:

  • key defines worker identity in the runtime.
  • Same key + still running: worker stays alive (no restart), handlers are updated.
  • Same key after completion: starts a fresh worker run.
  • If not called in a render pass: cancelled at end of render cycle.

For full lifecycle details and one-shot analytics/idempotency guidance, see Workers guide.

Action Types

Action<S, O>

A pure function that transforms state:

type Action<S, O = never> = (state: S) => ActionResult<S, O>;

interface ActionResult<S, O> {
  state: S; // New state (required)
  output?: O; // Event to parent (optional)
}

Action Helpers:

import { action, emit, noChange, compose, named, safeAction } from '@workflow-ts/core';

// Simple state update
const increment = action<{ count: number }>((s) => ({ count: s.count + 1 }));

// State update with output
const save = action<State, Output>((s) => s, { type: 'saved' });

// Only emit output (no state change)
const notify = emit({ type: 'done' });

// No-op action
const noop = noChange<State>();

// Compose actions
const resetAndNotify = compose(
  action<{ count: number }>((s) => ({ ...s, count: 0 })),
  action<{ count: number }, { type: 'reset' }>((s) => s, { type: 'reset' }),
);

// Named action (for debugging)
const namedIncrement = named('increment', increment);
// Runtime DevTools events include: actionName: 'increment'

type StateUnion = { type: 'idle' } | { type: 'loaded'; value: number };

// Guarded action for union states
const loadedOnly = safeAction<StateUnion, never, 'loaded'>('loaded', (s) => ({
  state: { ...s, value: s.value + 1 },
}));

Child Output Routing

import { routeChildOutput } from '@workflow-ts/core';

type ChildOutput = { type: 'success'; id: string } | { type: 'cancel' };
type ParentState = { step: 'idle' | 'done' };
type ParentOutput = { type: 'saved'; id: string };

const onChildOutput = routeChildOutput<ChildOutput, ParentState, ParentOutput>({
  success: (output) => () => ({
    state: { step: 'done' },
    output: { type: 'saved', id: output.id },
  }),
  cancel: () => () => ({ state: { step: 'idle' } }),
});

Result Helpers

import { matchResult, type Result } from '@workflow-ts/core';

type User = { id: string };
const result: Result<User, Error> = { type: 'success', data: { id: 'u1' } };
const action = matchResult(result, {
  success: (user) => () => ({ state: { status: 'loaded', user } }),
  error: (error) => () => ({ state: { status: 'error', message: error.message } }),
});

Worker Types

Worker<T>

An async operation that produces output.

interface Worker<T> {
  key: string;
  run: (signal: AbortSignal) => Promise<T>;
}

Worker Factories:

import { createWorker, fromPromise, fetchWorker, debounceWorker } from '@workflow-ts/core';

// From async function
const loadUser = createWorker('load-user', async (signal) => {
  const res = await fetch('/api/user', { signal });
  return res.json();
});

// From promise factory
const loadData = fromPromise('load-data', () => api.fetchData());

// Fetch JSON
const fetchTodos = fetchWorker<Todo[]>('fetch-todos', '/api/todos');

// Debounced worker
const debouncedSearch = debounceWorker('search', searchWorker, 300);

Snapshot Utilities

import { jsonSnapshot, versionedSnapshot } from '@workflow-ts/core';

// JSON serialization
const { snapshot, restore } = jsonSnapshot<{ count: number }>();

// Versioned snapshot (for migrations)
const { snapshot, restore } = versionedSnapshot<State>(
  1, // version
  (state) => JSON.stringify(state),
  (json, version) => {
    if (version === 0) {
      // migrate from v0
    }
    return JSON.parse(json);
  },
);

Example: HTTP Request Workflow

import { type Workflow, createRuntime, createWorker } from '@workflow-ts/core';

// State machine for HTTP request
type State =
  | { type: 'idle' }
  | { type: 'loading' }
  | { type: 'success'; data: unknown }
  | { type: 'error'; error: string };

interface Rendering {
  status: 'idle' | 'loading' | 'success' | 'error';
  data: unknown | null;
  error: string | null;
  fetch: () => void;
  reset: () => void;
}

// Worker for HTTP request
const fetchWorker = createWorker('fetch', async (signal) => {
  const res = await fetch('/api/data', { signal });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
});

const httpWorkflow: Workflow<void, State, never, Rendering> = {
  initialState: () => ({ type: 'idle' }),

  render: (_props, state, ctx) => {
    // Run worker when loading
    if (state.type === 'loading') {
      ctx.runWorker(fetchWorker, 'fetch', (result) => () => ({
        state: { type: 'success', data: result },
      }));
    }

    return {
      status: state.type,
      data: state.type === 'success' ? state.data : null,
      error: state.type === 'error' ? state.error : null,

      fetch: () => {
        ctx.actionSink.send(() => ({ state: { type: 'loading' } }));
      },

      reset: () => {
        ctx.actionSink.send(() => ({ state: { type: 'idle' } }));
      },
    };
  },
};

License

MIT