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

@rxova/journey-core

v1.0.0-rc.2

Published

Journey core state machine.

Downloads

67

Readme

@rxova/journey-core

Typed state machine for multi-step UI flows.

@rxova/journey-core is approaching a 1.0.0-rc contract freeze. The current public model is:

  • JSON-only runtime context
  • static step meta
  • transition-side state updates through updateContext(...)
  • documented lifecycle callbacks and events as the supported extension surface

Install

npm i @rxova/journey-core

Quickstart

import { createJourneyMachine } from "@rxova/journey-core";

const machine = createJourneyMachine({
  initial: "account",
  context: { name: "" },
  steps: { account: {}, details: {}, review: {} },
  transitions: ["account", "details", "review"]
});

machine.startJourney();

await machine.send({ type: "goToNextStep" }); // account → details
await machine.send({ type: "goToNextStep" }); // details → review
await machine.goToPreviousStep(); // review → details

const snap = machine.getSnapshot();
console.log(snap.currentStepId); // "details"
console.log(snap.history.timeline); // ["account", "details"]
console.log(snap.status); // "running"

Three Transition Modes

Reserved step ids: *, global, COMPLETE, and TERMINATED. They are used by the runtime and cannot be used as real step names.

Linear

Array shorthand for sequential flows. Steps can carry synchronous updateContext transitions and timeouts.

transitions: [
  "account",
  {
    step: "details",
    updateContext: ({ context }) => ({ ...context, step: 2 }),
    timeoutMs: 5000
  },
  "payment",
  "review"
];

Graph

Step-keyed transitions with guards, updateContext, and branching.

transitions: {
  login: {
    goToNextStep: [
      { to: "admin", when: ({ context }) => context.role === "admin" },
      { to: "dashboard" }
    ]
  },
  admin: { completeJourney: true },
  dashboard: { completeJourney: true }
}

Graph Builder

For larger flows, createJourneyBuilder lets each step declare its own transitions co-located with its component. It compiles to the same JourneyDefinition — no new runtime concepts.

// builder.ts — typed singleton
const { createStep, to, build } = createJourneyBuilder<Context, StepId, EventMap>();

// steps/login.step.ts — co-located with Login.tsx
export const loginStep = createStep("login", {
  on: {
    submit: [to("admin").when(({ context }) => context.role === "admin"), to("dashboard")]
  }
});

// journey.ts — one-screen assembly
const definition = build({
  initial: "login",
  context: { role: "user" },
  steps: [loginStep, adminStep, dashboardStep]
});

Use the factory form when you need event.payload narrowed to the specific event type:

submit: ({ to }) => [to("admin").when(({ context, event }) => event.payload?.username !== "")];

Each transition modifier is single-use at the type level. Calling .when(), .updateContext(), .onEnter(), .onLeave(), .label(), or .timeoutMs() twice on the same builder is a TypeScript error. If you bypass the type system, runtime keeps the existing last-call-wins behavior.

Headless

Omit transitions entirely. The machine holds state, history, and context, but the caller decides where to go. Useful for custom renderers, server-driven flows, or when the navigation logic lives outside the definition.

const machine = createJourneyMachine({
  initial: "start",
  context: {},
  steps: { start: {}, configure: {}, confirm: {} }
  // no transitions — the caller drives the flow
});

machine.startJourney();
await machine.goToStepById("configure");
await machine.goToStepById("confirm");
await machine.goToPreviousStep(); // back to configure
await machine.completeJourney();

In headless mode, goToStepById(...) is the navigation primitive. goToNextStep() and custom events are explicit no-ops until you define transitions and move into linear or graph mode.

After dispose(), send-style APIs resolve with transitioned: false and error: JourneyDisposedError. Control APIs such as startJourney() and updateContext() stay no-op and emit a development warning.

updateContext() is the ordered context-write API. It runs through the same queue as send(), so it applies against the latest committed snapshot when it executes.

Step Lifecycle

Run side effects when a step is entered or left by attaching onEnter / onLeave directly to the step definition. Both callbacks receive the current context and are observational. They do not change transition selection or mutate context. Use transition updateContext for synchronous state commits that belong to the transition itself.

const machine = createJourneyMachine({
  context: { username: "" },
  steps: {
    login: {
      onLeave: ({ context }) => analytics.track("login_left", { user: context.username })
    },
    dashboard: {
      onEnter: ({ context }) => analytics.track("dashboard_entered"),
      onLeave: ({ context }) => console.log("leaving dashboard")
    }
  },
  transitions: ["login", "dashboard"]
});

With the graph builder, callbacks sit alongside transitions on the step:

const dashboardStep = createStep("dashboard", {
  onEnter: ({ context }) => analytics.track("dashboard_entered"),
  onLeave: ({ context }) => console.log("leaving dashboard"),
  on: { submit: [to("review")] }
});

In React, use useJourneyStepLifecycle when the callback needs access to component state or React context:

const { useJourneyStepLifecycle } = createJourney(definition);

function Dashboard() {
  useJourneyStepLifecycle("dashboard", {
    onEnter: ({ context }) => analytics.track("dashboard_entered"),
    onLeave: ({ context }) => console.log("leaving dashboard")
  });
  // ...
}

The hook always calls the latest version of your callbacks without re-subscribing.

Features

  • Async guardswhen can be async, with per-transition timeoutMs and AbortSignal-based cancellation. Step async state is tracked in snapshot.async.byStep[stepId] (idle, evaluating-when, error)
  • Timeline historygoToPreviousStep(), goToLastVisitedStep(), deterministic back/forward. Snapshot exposes history.timeline and history.index so you always know where the user has been
  • Computed stategetComputed() returns derived flags: isFirstStep, isLastStep, isLoading, isComplete, isTerminated, activeStepIndex, stepCount, mode (linear | graph | headless)
  • ObservabilitysubscribeEvent() streams typed lifecycle events: journey.start, transition.start, transition.success, step.enter, step.exit, journey.completed, journey.terminated, and more
  • Step metadata — attach typed metadata per step and read it with getStepMeta(stepId). Metadata is definition data, not mutable runtime state
  • Terminal eventscompleteJourney() and terminateJourney() with typed payloads. Once terminal, all navigation no-ops until resetJourney()
  • Snapshot — one object holds everything that changes at runtime: currentStepId, context, history, visited, status (idled, running, completed, terminated), and async
  • Global transitions — cross-cutting handlers via the global key, useful for close-confirmation or abort flows

Persistence

import { createPersistencePlugin } from "@rxova/journey-core/persistence";

const machine = createJourneyMachine(definition, {
  plugins: [
    createPersistencePlugin({
      key: "checkout",
      version: 2,
      blockList: ["payment.cardNumber"]
    })
  ]
});

Versioned snapshot storage with migrations, context allow/block lists, and configurable reset behavior. Defaults to localStorage when available.

If migrate(...) returns data that no longer matches a valid snapshot, Journey reports the error through onError (or a development warning when onError is omitted) and falls back to the initial snapshot.

Autosave

import { createAutosavePlugin } from "@rxova/journey-core/autosave";

const machine = createJourneyMachine(definition, {
  plugins: [
    createAutosavePlugin({
      key: "checkout-draft",
      debounceMs: 300,
      allowList: ["profile", "shipping"]
    })
  ]
});

await machine.flushAutosave();

Autosave adds debounced draft persistence, hydration, getAutosaveState(), flushAutosave(), and clearAutosave() without requiring the persistence plugin as a separate public dependency.

Analytics

import { createAnalyticsPlugin } from "@rxova/journey-core/analytics";

const machine = createJourneyMachine(definition, {
  plugins: [
    createAnalyticsPlugin({
      machineId: "checkout",
      track: (event) => analytics.track(event.name, event.payload)
    })
  ]
});

Analytics normalizes Journey lifecycle events into a stable event envelope and adds trackAnalyticsEvent(...) for custom markers.

DevTools

import { attachJourneyDevtools } from "@rxova/journey-devtools-bridge";

const detach = attachJourneyDevtools(machine, { label: "Checkout" });

Documentation

License

MIT