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

effect-machine

v0.17.1

Published

Type-safe state machines for [Effect](https://effect.website).

Readme

effect-machine

Type-safe state machines for Effect.

Complex workflows usually fail the same way: one status field, a few side booleans, and effects scattered across callbacks. effect-machine gives you one typed model for state, events, and transitions, then runs it as a real actor.

Use it when a feature has:

  • multiple valid and invalid states
  • async work tied to state entry
  • retries, timeouts, cancellation, or backpressure
  • logic you want to reuse in-process, in tests, and in distributed systems

Install

bun add effect-machine effect

effect is a peer dependency. The repository validates both the v4 entrypoint and the effect-machine/v3 mirror with @effect/tsgo, the latest Effect beta, type-aware oxlint, and Bun tests.

Core Pattern

States and events are schemas. Types, validation, and serialization from one place.

import { Schema } from "effect";
import { Event, Machine, Slot, State } from "effect-machine";

const CheckoutState = State({
  ReviewingCart: { cartId: Schema.String, totalCents: Schema.Number },
  ChargingCard: { cartId: Schema.String, totalCents: Schema.Number },
  Confirmed: { cartId: Schema.String, receiptId: Schema.String },
  Failed: { cartId: Schema.String, reason: Schema.String },
});

const CheckoutEvent = Event({
  Submit: {},
  Charged: { receiptId: Schema.String },
  Declined: { reason: Schema.String },
  Cancel: {},
});

const CheckoutSlots = Slot.define({
  chargeCard: Slot.fn({ cartId: Schema.String, totalCents: Schema.Number }),
});

const checkoutMachine = Machine.make({
  state: CheckoutState,
  event: CheckoutEvent,
  slots: CheckoutSlots,
  initial: CheckoutState.ReviewingCart({ cartId: "cart_123", totalCents: 4200 }),
})
  .on(CheckoutState.ReviewingCart, CheckoutEvent.Submit, ({ state }) =>
    CheckoutState.ChargingCard.derive(state),
  )
  .on(CheckoutState.ChargingCard, CheckoutEvent.Charged, ({ state, event }) =>
    CheckoutState.Confirmed.derive(state, { receiptId: event.receiptId }),
  )
  .on(CheckoutState.ChargingCard, CheckoutEvent.Declined, ({ state, event }) =>
    CheckoutState.Failed.derive(state, { reason: event.reason }),
  )
  .onAny(CheckoutEvent.Cancel, ({ state }) =>
    CheckoutState.Failed.derive(state, { reason: "cancelled" }),
  )
  .spawn(CheckoutState.ChargingCard, ({ slots, state }) =>
    slots.chargeCard({ cartId: state.cartId, totalCents: state.totalCents }),
  )
  .final(CheckoutState.Confirmed)
  .final(CheckoutState.Failed);

A few things to notice:

  • Empty variants are values: State.Idle. Non-empty are constructors: State.Loading({ url }).
  • State.derive(source, overrides) carries overlapping fields forward without manual copying.
  • .onAny(...) is a fallback; a specific .on(...) wins.
  • .spawn(...) runs work on state entry and cancels it on state exit.

The builder also supports .timeout(state, { duration, event }), .postpone(state, event) for buffering, and .reenter(...) for re-running lifecycle on same-state transitions.

Slots

Slots separate what a machine needs from how the app provides it. Declare them on the machine, provide implementations where you run it.

const actor =
  yield *
  Machine.spawn(checkoutMachine, {
    slots: {
      chargeCard: ({ cartId, totalCents }) =>
        Effect.gen(function* () {
          const ctx = yield* checkoutMachine.Context;
          const result = yield* PaymentService.charge(cartId, totalCents);
          yield* ctx.self.send(
            result.ok
              ? CheckoutEvent.Charged({ receiptId: result.receiptId })
              : CheckoutEvent.Declined({ reason: result.error }),
          );
        }),
    },
  });
yield * actor.start;

The same machine can run with different slot implementations in tests, local apps, or production. Slots are accepted everywhere the machine runs:

  • Machine.spawn(machine, { slots })
  • Machine.replay(machine, events, { slots })
  • simulate(machine, events, { slots })
  • createTestHarness(machine, { slots })

Running Actors

Machine.spawn allocates an actor but does not start it. Call actor.start to fork the event loop, background effects, and spawn effects. Events sent before start() are queued.

const program = Effect.gen(function* () {
  const actor = yield* Machine.spawn(checkoutMachine, {
    slots: {
      chargeCard: ({ cartId }) =>
        checkoutMachine.Context.pipe(
          Effect.flatMap((ctx) =>
            ctx.self.send(CheckoutEvent.Charged({ receiptId: `rcpt_${cartId}` })),
          ),
        ),
    },
  });
  yield* actor.start;

  yield* actor.send(CheckoutEvent.Submit);
  const finalState = yield* actor.awaitFinal;
});

Effect.runPromise(Effect.scoped(program));

Key actor operations:

  • start forks the event loop (idempotent, required after Machine.spawn)
  • send(event) queues and returns immediately
  • call(event) returns full transition info
  • ask(event) returns a typed domain reply (requires Event.reply(...))
  • waitFor(...) / awaitFinal for coordination
  • stop interrupts now; drain processes the remaining queue first
  • watch(other) completes when another actor stops

For named actors or shared lookup, use an actor system. system.spawn auto-starts — no actor.start needed:

import { ActorSystemDefault, ActorSystemService } from "effect-machine";

const program = Effect.gen(function* () {
  const system = yield* ActorSystemService;
  const actor = yield* system.spawn("checkout-123", checkoutMachine);
  yield* actor.send(CheckoutEvent.Submit);
}).pipe(Effect.provide(ActorSystemDefault));

Typed Replies

Events can declare typed reply schemas:

const CartEvent = Event({
  GetTotal: Event.reply({}, Schema.Number),
});

machine.on(State.Active, CartEvent.GetTotal, ({ state }) => Machine.reply(state, state.totalCents));

const total = yield * actor.ask(CartEvent.GetTotal); // number

Testing

Test transitions without spawning actors:

import { simulate } from "effect-machine";

const result =
  yield *
  simulate(
    checkoutMachine,
    [CheckoutEvent.Submit, CheckoutEvent.Charged({ receiptId: "rcpt_123" })],
    { slots: { chargeCard: () => Effect.void } },
  );

expect(result.states.map((s) => s._tag)).toEqual(["ReviewingCart", "ChargingCard", "Confirmed"]);

simulate and createTestHarness test transition logic. They do not run .spawn() or .background() effects.

Cluster

When the same machine needs to run behind @effect/cluster, turn it into an entity:

import { EntityMachine, toEntity } from "effect-machine/cluster";

const CheckoutEntity = toEntity(checkoutMachine, { type: "Checkout" });

const CheckoutEntityLayer = EntityMachine.layer(CheckoutEntity, checkoutMachine, {
  initializeState: (entityId) => CheckoutState.ReviewingCart({ cartId: entityId, totalCents: 0 }),
  persistence: { strategy: "journal" },
});

Persistence strategies:

  • Snapshot — saves state periodically, restores on reactivation
  • Journal — appends events on each RPC, replays on reactivation

License

MIT