Adapters

@stateledger/memory — in-memory adapter

Install

bash
yarn add -D @stateledger/memory

Install as a dev dependency | it’s intended for tests and local prototypes, not production.

When to use it

  • • Unit tests that exercise state-machine logic without needing a Postgres container.
  • • CI speed | no DB spin-up per test file, everything runs in the same Node process.
  • • Prototypes / demos where you want to sketch a state machine without setting up migrations.
Not for production. The memory adapter loses all data when the process exits, offers no concurrency safety across processes, and cannot join Prisma or Drizzle transactions.

Basic usage

typescript
import { defineMachine } from "@stateledger/core";
import { InMemoryAdapter } from "@stateledger/memory";

const invoiceMachine = defineMachine({
  name: "invoice",
  states: ["draft", "sent", "paid"],
  initialState: "draft",
  transitions: [
    { from: "draft", to: "sent" },
    { from: "sent",  to: "paid" },
  ] as const,
});

const adapter = new InMemoryAdapter();

const invoice = invoiceMachine.for("inv_1", { adapter, actor: "test" });

await invoice.transitionTo("sent");
await invoice.transitionTo("paid");

const state = await invoice.readCurrent();
// { state: "paid", actor: "test", at: <Date>, ... }

Resetting between tests

Each new InMemoryAdapter() call gives you an isolated store, so the simplest pattern is a fresh adapter per test:

typescript
import { beforeEach, describe, it, expect } from "vitest";
import { InMemoryAdapter } from "@stateledger/memory";
import { invoiceMachine } from "../src/machines";

describe("invoice lifecycle", () => {
  let adapter: InMemoryAdapter;

  beforeEach(() => {
    adapter = new InMemoryAdapter();
  });

  it("moves draft -> sent -> paid", async () => {
    const invoice = invoiceMachine.for("inv_1", { adapter });
    await invoice.transitionTo("sent");
    await invoice.transitionTo("paid");
    expect((await invoice.readCurrent())?.state).toBe("paid");
  });

  it("rejects illegal transitions", async () => {
    const invoice = invoiceMachine.for("inv_2", { adapter });
    await expect(invoice.transitionTo("paid")).rejects.toThrow();
  });
});

Seeding history

If your test needs an entity that’s already been through some transitions, replay them via the adapter before the assertion:

typescript
const invoice = invoiceMachine.for("inv_3", { adapter });
await invoice.transitionTo("sent");  // seed step 1
await invoice.transitionTo("paid");  // seed step 2

// Now the test scenario:
const history = await invoice.readHistory();
expect(history).toHaveLength(3); // initial + sent + paid

Limitations to remember

  • • No cross-process concurrency | acquiring an advisory lock is a no-op.
  • • No transactional join | you can’t wrap it in a Prisma transaction.
  • • Data lives only in the current Node process, gone on exit.
  • • Suitable for testing state-machine logic, not for testing your production adapter’s behaviour. For that, run integration tests against the real Prisma or Drizzle adapter with a disposable Postgres.